refactor: 初始化项目

Re-1.0
咬轮猫 3 years ago
parent 4a68c51102
commit ecac75bac0

@ -0,0 +1,5 @@
*.md
.vscode
.idea
dist
node_modules

@ -0,0 +1,59 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 'latest',
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true
}
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended' // 一定要放在最后。因为 extends 中后引入的规则会覆盖前面的规则。
],
rules: {
// @typescript-eslint
'@typescript-eslint/explicit-function-return-type': 'off', // 需要函数和类方法的显式返回类型
'@typescript-eslint/no-explicit-any': 'off', // 禁止使用该 any 类型
'@typescript-eslint/no-var-requires': 'off', // 不允许使用 require 语句,除了在 import 语句中
'@typescript-eslint/no-empty-function': 'off', // 禁止空函数
'@typescript-eslint/no-use-before-define': 'off', // 在定义之前禁止使用变量
'@typescript-eslint/ban-ts-comment': 'off', // 禁止 @ts-<directive> 使用评论或在指令后要求描述
'@typescript-eslint/ban-types': 'off', // 禁止使用特定类型
'@typescript-eslint/no-non-null-assertion': 'off', // '!'不允许使用后缀运算符的非空断言
'@typescript-eslint/explicit-module-boundary-types': 'off', // 需要导出函数和类的公共类方法的显式返回和参数类型
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
], // 禁止未使用的变量
// vue
'vue/custom-event-name-casing': 'off', // 为自定义事件名称强制使用特定大小写
'vue/attributes-order': 'off', // 强制执行属性顺序
'vue/one-component-per-file': 'off', // 强制每个组件都应该在自己的文件中
'vue/html-closing-bracket-newline': 'off', // 在标签的右括号之前要求或禁止换行
'vue/multiline-html-element-content-newline': 'off', // 在多行元素的内容之前和之后需要换行符
'vue/singleline-html-element-content-newline': 'off', // 在单行元素的内容之前和之后需要换行符
'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制执行属性命名样式
'vue/require-default-prop': 'off', // 需要 props 的默认值
'vue/html-indent': ['error', 2], // 在<template>中强制一致缩进
'vue/html-self-closing': 'off', // 执行自闭合的风格
'vue/max-attributes-per-line': 'off', // 强制每行属性的最大数量
'vue/multi-word-component-names': 'off', // 是否开启组件命名规则校验(强制多个单词以驼峰或'-'链接的命名规则)
// ESLint
'no-use-before-define': 'off', // 禁止在变量定义之前使用它们
'space-before-function-paren': 'off' // 强制在 function的左括号之前使用一致的空格
}
};

21
.gitignore vendored

@ -1,5 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
.husky/.gitignore vendored

@ -0,0 +1 @@
_

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no -- commitlint --edit $1

@ -0,0 +1,9 @@
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Yarn
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
npx lint-staged

@ -0,0 +1,3 @@
/node_modules/**
/dist*
/public/*

@ -1,3 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
"recommendations": ["Vue.volar"]
}

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 咬轮猫
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue-webtopo-svgeditor</title>
<title>Vite App</title>
</head>
<body>
<div id="app"></div>

1587
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,39 +1,40 @@
{
"name": "vue-webtopo-svgeditor",
"version": "0.0.4",
"files": [
"dist"
],
"module": "./dist/vue-webtopo-svgeditor.es.ts",
"main": "./dist/vue-webtopo-svgeditor.umd.ts",
"exports": {
".": {
"import": "./dist/vue-webtopo-svgeditor.es.ts",
"require": "./dist/vue-webtopo-svgeditor.umd.ts"
},
"./dist/style.css": {
"import": "./dist/style.css",
"require": "./dist/style.css"
}
},
"version": "0.0.5",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
"preview": "vite preview",
"lint:eslint": "eslint --fix --ext .js,.ts,.vue,.tsx ./src",
"preinstall": "npx only-allow pnpm",
"postinstall": "husky install"
},
"dependencies": {
"axios": "^0.22.0",
"vue": "^3.2.6"
"vue": "^3.2.25",
"vue-router": "4"
},
"devDependencies": {
"@vicons/fluent": "^0.11.0",
"@vitejs/plugin-vue": "^1.6.1",
"@vue/compiler-sfc": "^3.2.6",
"naive-ui": "^2.18.2",
"typescript": "^4.3.2",
"vite": "^2.5.4",
"echarts": "^5.2.1",
"vue-echarts": "^6.0.0",
"vue-tsc": "^0.2.2"
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.5",
"@vitejs/plugin-vue": "^2.3.3",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-define-config": "^1.5.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.2.0",
"husky": "^8.0.1",
"prettier": "^2.7.1",
"typescript": "^4.5.4",
"vite": "^2.9.9",
"vite-plugin-eslint": "^1.6.1",
"vue-eslint-parser": "^9.0.3",
"vue-tsc": "^0.34.7"
},
"lint-staged": {
"*.{js,jsx,vue,ts,tsx}": [
"pnpm run lint:eslint"
]
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,20 @@
module.exports = {
printWidth: 100, // 最大行长规则通常设置为 100 或 120
tabWidth: 2, // 指定每个标签缩进级别的空格数。
useTabs: false, // 使用制表符而不是空格缩进行。
semi: true, // true默认: 在每条语句的末尾添加一个分号。false仅在可能导致 ASI 失败的行的开头添加分号。
vueIndentScriptAndStyle: true, // Vue 文件脚本和样式标签缩进
singleQuote: true, // 使用单引号而不是双引号
quoteProps: 'as-needed', // 引用对象中的属性时,仅在需要时在对象属性周围添加引号。
bracketSpacing: true, // 在对象文字中的括号之间打印空格。
trailingComma: 'none', // "none":没有尾随逗号。"es5": 在 ES5 中有效的尾随逗号对象、数组等TypeScript 中的类型参数中没有尾随逗号。"all"- 尽可能使用尾随逗号。
bracketSameLine: false, // 将>多行 HTMLHTML、JSX、Vue、Angular元素放在最后一行的末尾而不是单独放在下一行不适用于自闭合元素
jsxSingleQuote: false, // 在 JSX 中使用单引号而不是双引号。
arrowParens: 'always', // 在唯一的箭头函数参数周围始终包含括号。
insertPragma: false, // 插入编译指示
requirePragma: false, // 需要编译指示
proseWrap: 'never', // 如果散文超过打印宽度,则换行
htmlWhitespaceSensitivity: 'strict', // 所有标签周围的空格(或缺少空格)被认为是重要的。
endOfLine: 'lf', // 确保在文本文件中仅使用 ( \n)换行,常见于 Linux 和 macOS 以及 git repos 内部。
rangeStart: 0 // 格式化文件时,回到包含所选语句的第一行的开头。
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="">
<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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

@ -1,159 +1,19 @@
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import SvgEditor from './components/SvgEditor.vue';
import SvgPrview from './components/SvgPrview.vue';
import axios from "axios";
import { ref } from '@vue/reactivity';
const interval = ref<any>(null);
const component_infos = ref([]);
const ref_svgedit = ref();
//
const prview_data = ref<any>([]);
//
axios.get("InterfaceReturn.json").then(res => {
component_infos.value = res.data;
});
// 0 1
const displaymode = ref(0);
//svg
const savesvg_data = ref([]);
//svgdom
const savesvg_dom_data: any = ref(null);
//
const saveSvgInfo = (svg_data: any, svg_dom: any) => {
savesvg_data.value = svg_data;
savesvg_dom_data.value = svg_dom;
}
const downloadSvgData = () => {
if (savesvg_data.value == null) {
alert('请先保存绘制数据');
return;
}
let datastr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(savesvg_data.value))
let download = document.createElement('a')
download.setAttribute('href', datastr)
download.setAttribute('download', `${new Date().getTime()}.json`)
download.click()
download.remove();
}
const downloadSvgDomData = () => {
if (savesvg_dom_data.value == null) {
alert('请先保存绘制数据');
return;
}
console.log(savesvg_dom_data.value);
let datastr = 'data:text;charset=utf-8,' + encodeURIComponent(savesvg_dom_data.value.outerHTML);
let download = document.createElement('a')
download.setAttribute('href', datastr)
download.setAttribute('download', `${new Date().getTime()}.html`)
download.click()
download.remove()
}
const switchMode = () => {
displaymode.value = displaymode.value == 0 ? 1 : 0;
if (displaymode.value == 1) {
prview_data.value = [];
setTimeout(() => {
prview_data.value = savesvg_data.value;
}, 500);
}
}
const loadExampleData = () => {
axios.get("example.json").then(res => {
clearInterval(interval.value)
prview_data.value = res.data;
let temp: Array<any> = [...res.data];
//
//
let anyCircuitBreakerList = temp.filter(f => f.type == 'CircuitBreakerSvg');
//线
const anyWireBreakList = temp.filter(f => f.type == 'WireBreakSvg');
//
const anyEchartsPieList = temp.filter(f => f.type == 'EchartsPieSvg');
//
const anyEchartsBasicBarSvgList = temp.filter(f => f.type == 'EchartsBasicBarSvg');
interval.value = setInterval(function () {
anyCircuitBreakerList.forEach(anyCircuitBreaker => {
//
let random = Math.round(Math.random() * 10);
if (random < 5) {
(anyCircuitBreaker as any).extend_attr.switch.val.selectval = '{\"fill\":\"#FF0000\"}';
}
else {
(anyCircuitBreaker as any).extend_attr.switch.val.selectval = '{\"fill\":\"#00FF00\"}';
}
});
anyWireBreakList.forEach(anyWireBreak => {
//
let random = Math.round(Math.random() * 10);
if (random < 5) {
(anyWireBreak as any).extend_attr.switch.val.selectval = '{\"x2\":-10}';
}
else {
(anyWireBreak as any).extend_attr.switch.val.selectval = '{\"x2\":0}';
}
});
anyEchartsPieList.forEach(anyEchartsPie => {
const temp_val = JSON.parse(anyEchartsPie.extend_attr.echarts_option.val);
(temp_val.series[0].data as Array<any>).forEach(f => {
//
let random = Math.round(Math.random() * 100);
f.value = random;
});
anyEchartsPie.extend_attr.echarts_option.val = JSON.stringify(temp_val);
})
anyEchartsBasicBarSvgList.forEach(anyEchartsBasicBar => {
let data_arr = [Math.round(Math.random() * 300), Math.round(Math.random() * 300), Math.round(Math.random() * 300), Math.round(Math.random() * 300), Math.round(Math.random() * 300), Math.round(Math.random() * 300), Math.round(Math.random() * 300)];
const temp_val = JSON.parse(anyEchartsBasicBar.extend_attr.echarts_option.val);
temp_val.series[0].data = data_arr;
anyEchartsBasicBar.extend_attr.echarts_option.val = JSON.stringify(temp_val);
})
const tempa = JSON.stringify(temp)
prview_data.value = JSON.parse(tempa);
}, 2000)
});
}
const loadExample = (file_name: string) => {
axios.get(file_name).then(res => {
console.log(ref_svgedit.value);
(ref_svgedit.value as any).setSvgLists(res.data);
})
}
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
</script>
<template>
<div style="text-align: center;">
<span>此处为测试演示用 组件不包含此元素</span>
<button class="btn" @click="loadExample('example.json')" v-if="displaymode == 0"></button>
<button class="btn" @click="loadExample('example2.json')" v-if="displaymode == 0"></button>
<button class="btn" @click="switchMode">{{ displaymode == 0 ? '' : '' }}</button>
<button class="btn" @click="downloadSvgData" v-if="displaymode == 0"></button>
<button class="btn" @click="downloadSvgDomData" v-if="displaymode == 0">svg</button>
<button class="btn" @click="loadExampleData" v-if="displaymode == 1"></button>
</div>
<div style="width:1500px;height:650px;margin:0 auto">
<div v-show="displaymode == 0">
<svg-editor
ref="ref_svgedit"
:component_infos="component_infos"
@saveSvgInfo="saveSvgInfo"
:svgCanvas="{ width: 1920, height: 1080 }"
></svg-editor>
</div>
<div v-show="displaymode != 0">
<svg-prview
:component_infos="component_infos"
:svg_data="prview_data"
:svgCanvas="{ width: 1920, height: 1080 }"
></svg-prview>
</div>
</div>
<router-view></router-view>
</template>
<style>
.btn {
margin-left: 1rem;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

@ -1,77 +0,0 @@
/**
* @description:
* @param {*} commonComponentList
* @param {*} drawComponentList
* @param {*} chartComponentList
* @return {*}
*/
export interface ILeftImgLists {
commonComponentList?: Array<IComponentInfo>,
drawComponentList?: Array<IComponentInfo>,
chartComponentList?: Array<IComponentInfo>
}
/**
* @description:
* @param {*}
* @return {*}
*/
export interface ISvgDataLists {
id: string,
type?: string,
title?: string,
svgPositionX: number,
svgPositionY: number,
angle?: number,
size?: number,
extend_attr?: any
}
export interface ISvgCanvas {
width: number,
height: number
}
/**
* @description:
* @param {*} type
* @param {*} title
* @param {*} panel_class
* @param {*} template
* @param {*} props
* @param {*} extend_attr
* @param {*} create_type
* @param {*} priview_img
* @return {*}
*/
export interface IComponentInfo {
type?: string,
title?: string,
panel_class?: string,
template?: string,//
props?: Array<string>,
extend_attr?: any,
create_type?: string,
priview_img?: string
}
/**
* @description:
* @param {*} status 1 0
* @param {*} mPositionX x
* @param {*} mPositionY y
* @return {*}
*/
export interface IMouseInfo {
status: number,
mPositionX: number
mPositionY: number
}
/**
* @description: svg
* @param {*}
* @return {*}
*/
export interface ISelectSvg {
id: string,
index: number,
sPositionX: number,
sPositionY: number,
create_type: string,
}

@ -1,111 +0,0 @@
/* 正向流动效果 */
.svg_ani_flow {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: ani_flow 10s linear infinite;
animation-fill-mode: forwards;
-webkit-animation: ani_flow 10s linear infinite;
-webkit-animation-fill-mode: forwards;
}
@keyframes ani_flow {
from {
stroke-dasharray: 10, 5;
}
to {
stroke-dasharray: 13, 5;
}
}
@-webkit-keyframes ani_flow {
from {
stroke-dasharray: 10, 5;
}
to {
stroke-dasharray: 13, 5;
}
}
/* 停止流动效果 */
.svg_ani_flow_stop {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: ani_flow_stop 10s linear infinite;
animation-fill-mode: forwards;
-webkit-animation: ani_flow_stop 10s linear infinite;
-webkit-animation-fill-mode: forwards;
}
@keyframes ani_flow_stop {
from {
stroke-dasharray: 10, 5;
}
to {
stroke-dasharray: 10, 5;
}
}
@-webkit-keyframes ani_flow_stop {
from {
stroke-dasharray: 10, 5;
}
to {
stroke-dasharray: 10, 5;
}
}
/* 反向流动效果 */
.svg_ani_flow_back {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: ani_flow_back 10s linear infinite;
animation-fill-mode: forwards;
-webkit-animation: ani_flow_back 10s linear infinite;
-webkit-animation-fill-mode: forwards;
}
@keyframes ani_flow_back {
from {
stroke-dasharray: 13, 5;
}
to {
stroke-dasharray: 10, 5;
}
}
@-webkit-keyframes ani_flow_stop {
from {
stroke-dasharray: 10, 5;
}
to {
stroke-dasharray: 10, 5;
}
}
/* 以最大40高度填充 */
.svg_ani_fill_h40 {
animation: ani_fill_h40 5s linear infinite;
animation-fill-mode: forwards;
-webkit-animation: ani_fill_h40 5s linear infinite;
-webkit-animation-fill-mode: forwards;
}
@keyframes ani_fill_h40 {
from {
height: 0px;
}
to {
height: 40px;
}
}
@-webkit-keyframes ani_flow_stop {
from {
stroke-dasharray: 10, 5;
}
to {
stroke-dasharray: 10, 5;
}
}

@ -1,20 +0,0 @@
<template>
<n-card title="操作说明" embedded :bordered="false">
<ul>
<li>常规组件和图表组件用鼠标左键选中并按住可拖动至画布</li>
<li>绘制组件需先选中 再将鼠标移至画布中按住左键开始绘制 松开左键结束绘制</li>
<li>在画布上单击左键并按住可以拖动组件改变位置</li>
<li>当组件选中时会有选中效果 此时右侧面板弹出 可使用快捷键和编辑右侧面板属性更改组件</li>
<li>鼠标右键点击组件可以进行一些快捷操作</li>
<li>键盘可移动选中组件 ctrl+c复制当前选中组件 deleted删除当前选中组件</li>
<li>ctrl+键盘可移动组件图层</li>
<li>点击画布空白处可以取消选中组件 并关闭属性面板</li>
<li>点击 符号可以显示或隐藏左侧面板</li>
<li>如果使用我的在线体验地址 请先保存绘制再切换到预览界面</li>
<li>更多使用帮助请参考<a href="https://github.com/yaolunmao/vue-webtopo-svgeditor">github</a><a href="https://gitee.com/yaolunmao/vue-webtopo-svgeditor">gitee</a></li>
</ul>
</n-card>
</template>
<script setup lang="ts">
import { NCard } from "naive-ui";
</script>

@ -1,155 +0,0 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { ChevronDoubleLeft20Regular, ChevronDoubleRight20Regular } from '@vicons/fluent';
import { NIcon, NCollapse, NCollapseItem, useMessage } from "naive-ui";
import { ILeftImgLists, IComponentInfo } from "../Model";
const emit = defineEmits(['setCreatSvgInfo'])
const message = useMessage();
const props = defineProps({
left_imglists: {
type: Object as () => ILeftImgLists,
default: []
},
//
select_toolbar: {
type: String,
default: ''
}
});
const leftnav = reactive({
navclass: 'leftnavDisplay',
navopen: true
});
const clickHandleIcon = () => {
leftnav.navopen = !leftnav.navopen;
leftnav.navclass = leftnav.navclass == 'leftnavDisplay' ? 'leftnavNone' : 'leftnavDisplay'
};
const dragStartEvent = (leftImgItem: IComponentInfo, e: DragEvent) => {
//svg
emit('setCreatSvgInfo', leftImgItem);
}
const dragEndEvent = (leftImgItem: IComponentInfo, e: DragEvent) => {
//svg
if (e.dataTransfer?.dropEffect !== 'copy') {
message.error('请将组件拖到画布中!');
//
emit('setCreatSvgInfo', {});
return;
}
}
</script>
<template>
<div :class="leftnav.navclass">
<div class="svgimg" v-show="leftnav.navopen">
<n-collapse default-expanded-names="1" accordion>
<n-collapse-item title="常规组件" name="1" style="margin-top: 15px;">
<ul class="leftImgUl">
<li v-for="leftImgItem in props.left_imglists.commonComponentList">
<img
:title="leftImgItem.title"
:src="leftImgItem.priview_img"
draggable="true"
@dragstart="dragStartEvent(leftImgItem, $event)"
@dragend="dragEndEvent(leftImgItem, $event)"
/>
</li>
</ul>
</n-collapse-item>
<n-collapse-item title="绘制组件" name="2">
<ul class="leftImgUl">
<li v-for="leftImgItem in props.left_imglists.drawComponentList">
<img
:class="props.select_toolbar == leftImgItem.type ? 'svg-selected' : ''"
:title="leftImgItem.title"
:src="leftImgItem.priview_img"
@click="() => { emit('setCreatSvgInfo', leftImgItem) }"
/>
</li>
</ul>
</n-collapse-item>
<n-collapse-item title="图表组件" name="3">
<ul class="leftImgUl">
<li v-for="leftImgItem in props.left_imglists.chartComponentList">
<img
:title="leftImgItem.title"
:src="leftImgItem.priview_img"
draggable="true"
@dragstart="dragStartEvent(leftImgItem, $event)"
@dragend="dragEndEvent(leftImgItem, $event)"
/>
</li>
</ul>
</n-collapse-item>
</n-collapse>
</div>
<div class="handlehidden">
<n-icon class="handleicon" size="10" @click="clickHandleIcon">
<chevron-double-left20-regular v-if="leftnav.navopen" />
<chevron-double-right20-regular v-else />
</n-icon>
</div>
</div>
</template>
<style scoped>
.leftnavDisplay,
.leftnavNone {
height: 100%;
display: flex;
}
.leftnavDisplay {
width: 210px;
}
.leftnavNone {
width: 10px;
}
.svgimg {
/* background-color: green; */
height: 100%;
width: 100%;
display: flex;
flex-wrap: wrap;
overflow: overlay;
}
.svgimg::-webkit-scrollbar {
display: none;
}
.handlehidden {
height: 100%;
width: 10px;
background-color: grey;
}
.handleicon {
top: 50%;
cursor: pointer;
position: relative;
}
.leftImgUl {
display: flex;
flex-wrap: wrap;
list-style: none;
margin: 0;
padding: 0;
}
.leftImgUl li {
width: calc(33.33% - 30px);
margin: 0 15px 15px 15px;
padding: 0;
border-radius: 50%;
box-shadow: 1px 1px 5px #ddd;
cursor: pointer;
align-items: center;
justify-content: center;
display: flex;
}
.leftImgUl li :hover {
box-shadow: 1px 1px 10px #ccc;
border-radius: 50%;
}
.leftImgUl img {
width: 100%;
}
.svg-selected {
outline: 1px solid #0cf;
}
</style>

@ -1,81 +0,0 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { NForm, NFormItem, NInput, NInputNumber, NColorPicker, NRadioGroup, NSpace, NRadio,NSelect } from "naive-ui";
import { ISvgDataLists } from "../Model";
const props = defineProps({
set_svg_info: {
type: Object as () => ISvgDataLists,
default: {}
}
});
</script>
<template>
<div class="rightnav">
<div style="margin-left: 2rem;margin-top: 1rem;">
<n-form size="small" label-placement="left">
<n-form-item label="标识">
<span>{{ props.set_svg_info.id }}</span>
</n-form-item>
<n-form-item label="类型">
<span>{{ props.set_svg_info.type }}</span>
</n-form-item>
<n-form-item label="名称">
<n-input placeholder="请输入名称" v-model:value="props.set_svg_info.title" />
</n-form-item>
<n-form-item label="x轴坐标">
<n-input-number v-model:value="props.set_svg_info.svgPositionX" :min="0" />
</n-form-item>
<n-form-item label="y轴坐标">
<n-input-number v-model:value="props.set_svg_info.svgPositionY" :min="0" />
</n-form-item>
<n-form-item label="大小">
<n-input-number v-model:value="props.set_svg_info.size" :min="1" :step="0.1" />
</n-form-item>
<n-form-item label="旋转">
<n-input-number v-model:value="props.set_svg_info.angle" />
</n-form-item>
<n-form-item v-for="item in props.set_svg_info.extend_attr" :label="item.title">
<n-input-number v-if="item.type == 'numberinputbox'" v-model:value="item.val" />
<n-color-picker
v-else-if="item.type == 'colorinputbox'"
:modes="['hex']"
v-model:value="item.val"
/>
<n-input
v-else-if="item.type == 'textinputbox'"
:placeholder="`请输入${item.title}`"
v-model:value="item.val"
/>
<n-input
v-else-if="item.type == 'textareainputbox'"
:placeholder="`请输入${item.title}`"
type="textarea"
v-model:value="item.val"
/>
<n-radio-group v-model:value="item.val.selectval" v-else-if="item.type == 'radiogroup'">
<n-space>
<n-radio
v-for="ridioitem in item.val.ridiogroup"
:key="ridioitem.value"
:value="ridioitem.value"
>{{ ridioitem.label }}</n-radio>
</n-space>
</n-radio-group>
<n-select v-else-if="item.type == 'select'" v-model:value="item.val.selectval" :options="item.val.selectgroup" />
</n-form-item>
</n-form>
</div>
</div>
</template>
<style scoped>
.rightnav {
width: 250px;
height: 100%;
overflow: auto;
}
.rightnav::-webkit-scrollbar {
display: none;
}
</style>

@ -1,50 +0,0 @@
<script setup lang="ts">
import { reactive, ref, createApp, markRaw,provide } from "vue";
import { use } from "echarts/core";
import { SVGRenderer } from "echarts/renderers";
import { PieChart,BarChart } from "echarts/charts";
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from "echarts/components";
import { THEME_KEY } from "vue-echarts";
use([
SVGRenderer,
PieChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
BarChart
]);
provide(THEME_KEY, "default")
const props = defineProps({
component_type: {
type: String as () => any,
default: ''
},
component_template: {
type: String as () => any,
default: ''
},
component_props: {
type: Array as () => Array<any>,
default: ''
},
component_attr: {
type: Object as () => any,
default: {}
},
});
const component_info = markRaw({
template: props.component_template,
props: ['prop_data']
});
</script>
<template>
<component :is="component_info" :prop_data="props.component_attr"></component>
</template>

@ -1,555 +0,0 @@
<script setup lang="ts">
import { computed, reactive, Ref, ref, watch } from 'vue';
import TopToolBar from './TopBar.vue';
import LeftToolBar from './LeftToolBar.vue';
import RightToolBar from './RightToolBar.vue';
import BottomBar from './BottomBar.vue';
import { NMessageProvider } from "naive-ui";
import { IComponentInfo, ISvgDataLists, ISvgCanvas, ILeftImgLists, IMouseInfo, ISelectSvg } from "../Model";
import SvgDynamic from "./SvgDynamic.vue";
import "../assets/css/svgAnimation/index.css";
import { moveUp, moveDown, moveLeft, moveRight, hotkeyCopy, hotkeyDel, hotkeyPutOnTop, hotkeyPutOnButtom, hotkeyPutOnUp, hotkeyPutOnDown } from "../func/HotkeyFunc";
const emit = defineEmits(['saveSvgInfo']);
const props = defineProps({
//json
component_infos: {
type: Array as () => Array<IComponentInfo>,
required: true,
default: []
},
svgCanvas: {
type: Object as () => ISvgCanvas,
default: { width: 1520, height: 720 }
}
});
const svg_dom_ref = ref<null | HTMLElement>(null);
const contextMenuRef = ref<HTMLElement>();
const svgLists: ISvgDataLists[] = reactive([]);
const topbar_dom_ref = ref(null);
//
const display_contextmenu = ref(false);
//
const contextmenu_data = reactive([{
name: "复制",
hotkey: "Ctrl+C",
enable: true,
fun: function () {
if (!this.enable) {
return;
}
hotkeyCopy(svgLists, select_svg);
display_contextmenu.value = false;
}
}, {
name: "删除",
hotkey: "Delete",
enable: false,
fun: function () {
if (!this.enable) {
return;
}
hotkeyDel(svgLists, select_svg);
display_contextmenu.value = false;
}
}, {
name: "置于顶层",
hotkey: "Ctrl+→",
enable: true,
fun: function () {
if (!this.enable) {
return;
}
hotkeyPutOnTop(svgLists, select_svg);
display_contextmenu.value = false;
}
}, {
name: "置于底层",
hotkey: "Ctrl+←",
enable: true,
fun: function () {
if (!this.enable) {
return;
}
hotkeyPutOnButtom(svgLists, select_svg);
display_contextmenu.value = false;
}
}, {
name: "置于上一层",
hotkey: "Ctrl+↑",
enable: true,
fun: function () {
if (!this.enable) {
return;
}
hotkeyPutOnUp(svgLists, select_svg);
display_contextmenu.value = false;
}
}, {
name: "置于下一层",
hotkey: "Ctrl+↓",
enable: true,
fun: function () {
if (!this.enable) {
return;
}
hotkeyPutOnDown(svgLists, select_svg);
display_contextmenu.value = false;
}
}]);
const set_svg_info: Ref<ISvgDataLists> = ref({
id: '', title: '', svgPositionX: 0, svgPositionY: 0
});
//svg
const select_svg: ISelectSvg = reactive({
id: '',
index: 0,
sPositionX: 0,
sPositionY: 0,
create_type: ''
});
//svg
const select_lefttool: Ref<IComponentInfo> = ref({});
//svg
const leftimg_lists: Ref<ILeftImgLists> = ref({
commonComponentList: [],
drawComponentList: [],
chartComponentList: []
});
//
const select_toolbar: Ref<string | undefined> = ref('');
//
const mouseInfo: IMouseInfo = reactive({
status: 0,
mPositionX: 0,
mPositionY: 0
});
const rightnav_open = ref(false);
/**
* @description: 从左侧工具栏拖动组件到画布触发的事件
* @param {*}
* @return {*}
*/
const dropEvent = (e: DragEvent) => {
//
if (Object.keys(select_lefttool.value).length < 1) {
//
return;
}
//
const create_svg: ISvgDataLists = {
id: `${new Date().getTime()}`,
type: select_lefttool.value.type,
title: select_lefttool.value.title,
svgPositionX: e.offsetX,
svgPositionY: e.offsetY,
angle: 0,
size: 1,
extend_attr: JSON.parse(JSON.stringify(select_lefttool.value.extend_attr))
}
svgLists.push(create_svg);
//
select_lefttool.value = {};
}
const dragEnterEvent = (e: DragEvent) => {
//dragenterdragover drop
rightnav_open.value = false;
e.preventDefault();
}
const dragOverEvent = (e: DragEvent) => {
//dragenterdragover drop
e.preventDefault();
}
/**
* @description: 设置要创建组件的信息
* @param {*} createsvg_info 选中的组件信息
* @return {*}
*/
const setCreatSvgInfo = (createsvg_info: IComponentInfo) => {
select_lefttool.value = createsvg_info;
select_toolbar.value = createsvg_info.type;
}
/**
* @description: 保存绘制组件后的数据和svgdom
* @param {*}
* @return {*}
*/
const saveSvgInfo = () => {
if (svgLists.length == 0) {
(topbar_dom_ref.value as any).saveSvgInfoRes({ code: 2, msg: '请先绘制图像!' });
return;
}
(topbar_dom_ref.value as any).saveSvgInfoRes(svgLists.length > 0 ? { code: 1, msg: '保存成功!' } : { code: 0, msg: '保存失败!' });
emit('saveSvgInfo', svgLists, svg_dom_ref.value)
}
/**
* @description: 鼠标点击画布上svg触发事件
* @param {*}
* @return {*}
*/
const mouseDownEvent = (selectsvg: ISvgDataLists, index: number, e: MouseEvent) => {
e.preventDefault();
e.cancelBubble = true;
//
select_lefttool.value = {};
//
({ id: select_svg.id, svgPositionX: select_svg.sPositionX, svgPositionY: select_svg.sPositionY } = selectsvg);
rightnav_open.value = false;
select_svg.index = index;
select_svg.create_type = "draggable"
mouseInfo.status = 1;
mouseInfo.mPositionX = e.clientX;
mouseInfo.mPositionY = e.clientY;
}
const mouseMoveEvent = (e: MouseEvent) => {
//
if (mouseInfo.status != 1 || !select_svg.id) {
return;
}
const { clientX, clientY } = e;
if (select_svg.create_type == "draggable") {
let new_select_svg = { ...select_svg };
new_select_svg.sPositionX += clientX - mouseInfo.mPositionX;
new_select_svg.sPositionY += clientY - mouseInfo.mPositionY;
//
({ sPositionX: svgLists[select_svg.index].svgPositionX, sPositionY: svgLists[select_svg.index].svgPositionY } = new_select_svg);
}
else if (select_svg.create_type == "draw") {
//
if (svgLists[select_svg.index].extend_attr?.startpoint_x?.val != null) {
svgLists[select_svg.index].extend_attr.startpoint_x.val = 0;
}
if (svgLists[select_svg.index].extend_attr?.startpoint_y?.val != null) {
svgLists[select_svg.index].extend_attr.startpoint_y.val = 0;
}
if (svgLists[select_svg.index].extend_attr?.endpoint_x?.val != null) {
svgLists[select_svg.index].extend_attr.endpoint_x.val = clientX - mouseInfo.mPositionX;
}
if (svgLists[select_svg.index].extend_attr?.endpoint_y?.val != null) {
svgLists[select_svg.index].extend_attr.endpoint_y.val = clientY - mouseInfo.mPositionY;
}
}
}
const mouseUpEvent = (e: MouseEvent) => {
//
if (mouseInfo.status != 1 || !select_svg.id) {
return;
}
mouseInfo.status = 0;
rightnav_open.value = true;
set_svg_info.value = svgLists[select_svg.index];
//
select_lefttool.value = {};
select_toolbar.value = '';
}
/**
* @description: 鼠标点击画布
* @param {*}
* @return {*}
*/
const mouseDownCanvasEvent = (e: MouseEvent) => {
//
if (Object.keys(select_lefttool.value).length < 1) {
rightnav_open.value = false;
select_svg.id = '';
return;
}
//
const create_svg: ISvgDataLists = {
id: `${new Date().getTime()}`,
type: select_lefttool.value.type,
title: select_lefttool.value.title,
svgPositionX: e.offsetX,
svgPositionY: e.offsetY,
angle: 0,
size: 1,
extend_attr: JSON.parse(JSON.stringify(select_lefttool.value.extend_attr))//
}
svgLists.push(create_svg);
//
({ id: select_svg.id, svgPositionX: select_svg.sPositionX, svgPositionY: select_svg.sPositionY } = create_svg);
rightnav_open.value = false;
select_svg.index = svgLists.length - 1;
select_svg.create_type = "draw"
mouseInfo.status = 1;
mouseInfo.mPositionX = e.clientX;
mouseInfo.mPositionY = e.clientY;
}
/**
* @description: 鼠标右键
* @param {*}
* @return {*}
*/
const contextmenuEvent = (e: MouseEvent) => {
e.preventDefault();
display_contextmenu.value = true;
(contextMenuRef.value as any).style.left = e.pageX + 'px';
(contextMenuRef.value as any).style.top = e.pageY + 'px';
contextmenu_data.map(m => m.enable = true);
//index
if (svgLists.length === 1) {
//
contextmenu_data[3].enable = false;
contextmenu_data[5].enable = false;
//
contextmenu_data[2].enable = false;
contextmenu_data[4].enable = false;
}
else if (select_svg.index === 0) {
//
contextmenu_data[3].enable = false;
contextmenu_data[5].enable = false;
}
else if (select_svg.index === svgLists.length - 1) {
//
contextmenu_data[2].enable = false;
contextmenu_data[4].enable = false;
}
}
/**
* @description: 点击页面其他位置隐藏右键菜单
* @param {*}
* @return {*}
*/
const documentClickEvent = (e: MouseEvent) => {
if (e.button !== 2) {
display_contextmenu.value = false;
}
}
watch(() => [...props.component_infos], (newval, oldval) => {
leftimg_lists.value = {
commonComponentList: newval.filter(f => f.panel_class == 'common'),
drawComponentList: newval.filter(f => f.panel_class == 'draw'),
chartComponentList: newval.filter(f => f.panel_class == 'chart')
};
});
//
document.onkeydown = function (e) {
//
if (!select_svg.id) {
return;
}
if (!e.ctrlKey && e.key == 'ArrowUp') {
e.preventDefault();
moveUp(svgLists, select_svg);
} else if (!e.ctrlKey && e.key == 'ArrowDown') {
e.preventDefault();
moveDown(svgLists, select_svg);
} else if (!e.ctrlKey && e.key == 'ArrowLeft') {
e.preventDefault();
moveLeft(svgLists, select_svg);
} else if (!e.ctrlKey && e.key == 'ArrowRight') {
e.preventDefault();
moveRight(svgLists, select_svg);
}
//ctrl c
else if (e.ctrlKey && e.key.toLowerCase() == 'c') {
e.preventDefault();
hotkeyCopy(svgLists, select_svg);
}
//deleted
else if (e.key == 'Delete') {
e.preventDefault();
hotkeyDel(svgLists, select_svg);
rightnav_open.value = false;
}
//
else if (e.ctrlKey && e.key == 'ArrowUp') {
e.preventDefault();
hotkeyPutOnUp(svgLists, select_svg);
}
//
else if (e.ctrlKey && e.key == 'ArrowDown') {
e.preventDefault();
hotkeyPutOnDown(svgLists, select_svg);
}
//
else if (e.ctrlKey && e.key == 'ArrowLeft') {
e.preventDefault();
hotkeyPutOnButtom(svgLists, select_svg);
}
//
else if (e.ctrlKey && e.key == 'ArrowRight') {
e.preventDefault();
hotkeyPutOnTop(svgLists, select_svg);
}
}
const setSvgLists = (new_val: ISvgDataLists[]) => {
svgLists.length = 0;
svgLists.push(...new_val);
emit('saveSvgInfo', svgLists, svg_dom_ref.value)
}
defineExpose({
setSvgLists
})
</script>
<template>
<div class="navtop">
<n-message-provider>
<top-tool-bar @saveSvgInfo="saveSvgInfo" ref="topbar_dom_ref"></top-tool-bar>
</n-message-provider>
</div>
<div class="ancestors" @mousedown="documentClickEvent">
<div class="navleft">
<n-message-provider>
<left-tool-bar
:left_imglists="leftimg_lists"
:select_toolbar="select_toolbar"
@setCreatSvgInfo="setCreatSvgInfo"
></left-tool-bar>
</n-message-provider>
</div>
<div
class="warp"
@drop="dropEvent"
@dragenter="dragEnterEvent"
@dragover="dragOverEvent"
@mousemove="mouseMoveEvent"
@mouseup="mouseUpEvent"
@mousedown="mouseDownCanvasEvent"
>
<svg
xmlns="http://www.w3.org/2000/svg"
style="background-color:#000000;"
:width="svgCanvas.width"
:height="svgCanvas.height"
:viewBox="`0 0 ${svgCanvas.width} ${svgCanvas.height}`"
ref="svg_dom_ref"
>
<defs />
<filter x="0" y="0" width="1" height="1" id="solid">
<feFlood flood-color="rgb(255,255,255)" />
<feComposite in="SourceGraphic" />
</filter>
<g
style="cursor:pointer"
:class="item.id == select_svg.id ? 'svg-selected' : ''"
v-for="(item,index) in svgLists"
:key="item.id"
:id="item.id"
:transform="'translate(' + (item.svgPositionX) + ',' + (item.svgPositionY) + ')' + 'rotate(' + item.angle + ')' + 'scale(' + item.size + ')'"
@mousedown="mouseDownEvent(item, index, $event)"
@contextmenu.stop="contextmenuEvent"
>
<svg-dynamic
:component_type="item.type"
:component_template="props.component_infos.filter(f => f.type == item.type)[0].template"
:component_props="props.component_infos.filter(f => f.type == item.type)[0].props"
:component_attr="item"
/>
</g>
</svg>
</div>
<div class="navright" v-show="rightnav_open">
<right-tool-bar :set_svg_info="set_svg_info"></right-tool-bar>
</div>
</div>
<div class="navbuttom">
<bottom-bar></bottom-bar>
</div>
<!-- 右键菜单 -->
<ul ref="contextMenuRef" class="contextMenu" v-show="display_contextmenu">
<li v-for="(item,index) in contextmenu_data" :key="index" @click="item.fun()">
<p :class="item.enable ? '' : 'disabled'">
{{ item.name }}
<span class="shortcut">{{ item.hotkey }}</span>
</p>
</li>
</ul>
</template>
<style scoped>
.contextMenu {
position: absolute;
z-index: 99999;
background: #ffffff;
padding: 5px 0;
margin: 0px;
display: block;
border-radius: 5px;
box-shadow: 2px 5px 10px rgba(0, 0, 0, 0.3);
}
.contextMenu li {
list-style: none;
padding: 0px;
margin: 0px;
}
.contextMenu .shortcut {
width: 115px;
text-align: right;
float: right;
}
.contextMenu p {
text-decoration: none;
display: block;
padding: 0px 15px 1px 20px;
margin: 0;
user-select: none;
-webkit-user-select: none;
}
.contextMenu p:hover {
background-color: #0cf;
color: #ffffff;
cursor: default;
}
.contextMenu .disabled {
color: #999;
}
.contextMenu .disabled:hover {
color: #999;
background-color: transparent;
}
.contextMenu li.separator {
border-top: solid 1px #e3e3e3;
padding-top: 5px;
margin-top: 5px;
}
.ancestors {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.ancestors .navleft {
width: auto;
height: 100%;
/* border-left: solid 1px rgb(239, 239, 245); */
}
.ancestors .warp {
overflow: auto;
width: 100%;
height: 100%;
}
.ancestors .navright {
width: auto;
height: 100%;
/*background-color: aqua;*/
}
.navtop {
width: 100%;
height: 60px;
border-bottom: solid 1px rgb(239, 239, 245);
}
.navbuttom {
width: 100%;
height: 60px;
}
.svg-selected {
outline: 1px solid #0cf;
}
.warp::-webkit-scrollbar {
display: none;
}
</style>

@ -1,46 +0,0 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
style="background-color:#000000;"
width="100%"
height="100%"
:viewBox="`0 0 ${props.svgCanvas.width} ${props.svgCanvas.height}`"
>
<g
v-for="(item,index) in props.svg_data"
:key="item.id"
:id="item.id"
:transform="'translate(' + (item.svgPositionX) + ',' + (item.svgPositionY) + ')' + 'rotate(' + item.angle + ')' + 'scale(' + item.size + ')'"
>
<svg-dynamic
:component_type="item.type"
:component_template="props.component_infos.filter(f => f.type == item.type)[0].template"
:component_props="props.component_infos.filter(f => f.type == item.type)[0].props"
:component_attr="item"
/>
</g>
</svg>
</template>
<script setup lang="ts">
import SvgDynamic from "./SvgDynamic.vue";
import { ISvgDataLists, ISvgCanvas, IComponentInfo } from "../Model";
import "../assets/css/svgAnimation/index.css";
const props = defineProps({
//json
component_infos: {
type: Array as () => Array<IComponentInfo>,
required: true,
default: []
},
//
svg_data: {
type: Array as () => Array<ISvgDataLists>,
required: true,
default: []
},
svgCanvas: {
type: Object as () => ISvgCanvas,
default: { width: 1520, height: 720 }
}
});
</script>

@ -1,68 +0,0 @@
<template>
<div style="padding: 10px;display: flex;">
<img title="基于vue3+ts+svg的web组件编辑器" class="logoimg" src="../assets/logo.png" />
<span class="logotext">vue-webtopo-svgeditor</span>
<div style="display: flex;width: 100%;flex-direction: row-reverse;">
<a href="https://www.cnblogs.com/Hero-/p/14784744.html" target="_blank" class="a-link">
<n-button text>帮助</n-button>
</a>
<a href="https://github.com/yaolunmao/vue-webtopo-svgeditor" target="_blank" class="a-link">
<n-button text>Github</n-button>
</a>
<n-popover trigger="hover">
<template #trigger>
<n-button text class="button-class">v3.0</n-button>
</template>
<div>这个点了没什么用 不过会变色</div>
<div>说实话 这个版本号就是瞎写的 一点都不严谨</div>
</n-popover>
<n-button type="primary" class="button-class" @click="() => { emit('saveSvgInfo') }">保存</n-button>
</div>
</div>
</template>
<script setup lang="ts">
import { NButton, useMessage, NPopover } from "naive-ui";
const message = useMessage();
const emit = defineEmits(['saveSvgInfo']);
defineExpose({
/**
* @description: 保存svg结果 父组件调用用
* @param {*}type 0失败 1成功 2警告
* @return {*}
*/
saveSvgInfoRes(type: any) {
if (type?.code == 0) {
message.error(type?.msg);
}
else if (type?.code == 1) {
message.success(type?.msg);
}
else if (type?.code == 2) {
message.warning(type?.msg);
}
}
})
</script>
<style>
.logoimg {
height: 2rem;
min-width: 2rem;
margin-right: 0.1rem;
margin-top: 0.2rem;
}
.logotext {
font-size: 0.8rem;
font-weight: 700;
color: #2c3e50;
margin-top: 0.7rem;
width: 30%;
}
.a-link {
text-decoration: none;
margin-right: 1.5rem;
align-self: center;
}
.button-class {
margin-right: 1.5rem;
}
</style>

6
src/env.d.ts vendored

@ -1,8 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
import type { DefineComponent } from 'vue';
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
const component: DefineComponent<{}, {}, any>;
export default component;
}

@ -1,4 +0,0 @@
import SvgEditor from './components/SvgEditor.vue';
import SvgPrview from './components/SvgPrview.vue';
import ECharts from 'vue-echarts';
export { SvgEditor,SvgPrview,ECharts as VChart }

@ -1,105 +0,0 @@
import { IComponentInfo, ISvgDataLists, ISvgCanvas, ILeftImgLists, IMouseInfo, ISelectSvg } from "../Model";
/**
*
* @param svgLists
* @param select_svg
*/
export function moveUp(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
svgLists[select_svg.index].svgPositionY -= 1;
}
/**
*
* @param svgLists
* @param select_svg
*/
export function moveDown(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
svgLists[select_svg.index].svgPositionY += 1;
}
/**
*
* @param svgLists
* @param select_svg
*/
export function moveLeft(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
svgLists[select_svg.index].svgPositionX -= 1;
}
/**
*
* @param svgLists
* @param select_svg
*/
export function moveRight(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
svgLists[select_svg.index].svgPositionX += 1;
}
/**
*
* @param svgLists
* @param select_svg
*/
export function hotkeyCopy(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
svgLists.push({
...(JSON.parse(JSON.stringify(svgLists[select_svg.index]))),
id: `${new Date().getTime()}`,
svgPositionX: svgLists[select_svg.index].svgPositionX + 10,
svgPositionY: svgLists[select_svg.index].svgPositionY + 10,
title: svgLists[select_svg.index].title + `-copy`
})
}
/**
*
* @param svgLists
* @param select_svg
*/
export function hotkeyDel(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
svgLists.splice(select_svg.index, 1);
}
/**
*
* @param svgLists
* @param select_svg
*/
export function hotkeyPutOnTop(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
const temp = svgLists[select_svg.index];
svgLists.splice(select_svg.index, 1);
svgLists.push(temp);
select_svg.index = svgLists.length - 1;
}
/**
*
* @param svgLists
* @param select_svg
*/
export function hotkeyPutOnButtom(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
const temp = svgLists[select_svg.index];
svgLists.splice(select_svg.index, 1);
svgLists.unshift(temp);
select_svg.index = 0;
}
/**
*
* @param svgLists
* @param select_svg
*/
export function hotkeyPutOnUp(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
if (svgLists.length === 1 || select_svg.index === svgLists.length - 1) {
return;
}
const temp = svgLists[select_svg.index];
svgLists[select_svg.index] = svgLists[select_svg.index + 1];
svgLists[select_svg.index + 1] = temp;
select_svg.index += 1;
}
/**
*
* @param svgLists
* @param select_svg
*/
export function hotkeyPutOnDown(svgLists: ISvgDataLists[], select_svg: ISelectSvg) {
if (svgLists.length === 1 || select_svg.index === 0) {
return;
}
const temp = svgLists[select_svg.index];
svgLists[select_svg.index] = svgLists[select_svg.index - 1];
svgLists[select_svg.index - 1] = temp;
select_svg.index -= 1;
}

@ -1,6 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import ECharts from 'vue-echarts';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router/index';
const app = createApp(App);
app.component('v-chart', ECharts);
app.use(router);
app.mount('#app');

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router';
export const constantRoutes = [
{
path: '/',
redirect: '/edit'
},
{
name: 'edit',
path: '/edit',
component: () => import('../views/edit/index.vue')
},
{
name: 'preview',
path: '/preview',
component: () => import('../views/preview/index.vue')
}
];
const router = createRouter({
history: createWebHistory(), // hash模式createWebHashHistoryhistory模式createWebHistory
routes: constantRoutes
});
export default router;

@ -0,0 +1,3 @@
<template>
<div>编辑页</div>
</template>

@ -0,0 +1,3 @@
<template>
<div>预览页</div>
</template>

@ -8,8 +8,11 @@
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
"lib": ["esnext", "dom"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

@ -1,28 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import eslintPlugin from 'vite-plugin-eslint';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
alias: {
'vue': 'vue/dist/vue.esm-bundler.js' // 定义vue的别名如果使用其他的插件可能会用到别名
},
build: {
lib: {
entry: path.resolve(__dirname, 'src/export.ts'),
name: 'vue-webtopo-svgeditor',
fileName: (format) => `vue-webtopo-svgeditor.${format}.ts`
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ['vue'],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue'
}
}
}
}
})
plugins: [
vue(),
eslintPlugin({
cache: false,
include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
})
]
});

Loading…
Cancel
Save