You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vue-webtopo-svgeditor/src/components/SvgEditor.vue

556 lines
15 KiB
Vue

<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) => {
//dragenter和dragover一定要阻止浏览器默认行为 不然不会触发drop
rightnav_open.value = false;
e.preventDefault();
}
const dragOverEvent = (e: DragEvent) => {
//dragenter和dragover一定要阻止浏览器默认行为 不然不会触发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>