feat: 基础连线功能以及连线动画

Re-1.0
咬轮猫 3 years ago
parent 8db0925c4a
commit 40f981864a

@ -39,10 +39,17 @@
@mouseenter="onSvgMouseEnter(item, index, $event)"
@mouseleave="onSvgMouseLeave(item, index, $event)"
>
<connection-line
v-if="item.type === EDoneJsonType.ConnectionLine"
:item-info="item"
:point-visiable="
visiable_info.connection_line && visiable_info.select_item.info?.id == item.id
"
></connection-line>
<use
v-if="item.type === EDoneJsonType.File"
v-else-if="item.type === EDoneJsonType.File"
:xlink:href="`#svg-${item.name}`"
fill="#ff0000"
:fill="item.props.fill.val"
width="100"
height="100"
:id="item.id"
@ -64,15 +71,8 @@
stroke="#FF0000"
stroke-width="2"
></line>
<path
v-else-if="item.type === EDoneJsonType.ConnectionLine"
:id="item.id"
:d="positionArrarToPath(item.props.point_position.val)"
fill-opacity="0"
stroke="#FF0000"
stroke-width="2"
></path>
<rect
v-if="item.config.actual_rect"
:id="`rect${item.id}`"
fill="black"
fill-opacity="0"
@ -89,7 +89,12 @@
:width="item.actual_bound.width * item.scale_x"
:height="item.actual_bound.height * item.scale_y"
style="stroke: none; stroke-width: 2; stroke-miterlimit: 10"
:class="`${globalStore.intention == EGlobalStoreIntention.None ? 'svg-item-none' : ''}
:class="`${
globalStore.intention == EGlobalStoreIntention.None ||
globalStore.intention == EGlobalStoreIntention.Select
? 'svg-item-none'
: ''
}
${
globalStore.intention == EGlobalStoreIntention.Move &&
globalStore.handle_svg_info?.info.id == item.id
@ -112,10 +117,14 @@
></handle-panel>
<connection-panel
v-if="
(globalStore.handle_svg_info?.info.id !== item.id ||
globalStore.intention == EGlobalStoreIntention.None) &&
visiable_info.select_item.info?.id == item.id &&
visiable_info.connection_panel &&
item.config.have_anchor
item.config.have_anchor &&
(globalStore.intention === EGlobalStoreIntention.Select
? item.id !== globalStore.handle_svg_info?.info.id
? true
: false
: true)
"
:item-info="item"
></connection-panel>
@ -136,13 +145,7 @@
IDoneJson
} from '@/store/global/types';
import { useSvgEditLayoutStore } from '@/store/svgedit-layout';
import {
getCenterPoint,
randomString,
positionArrarToPath,
getSvgNowPosition,
setSvgActualInfo
} from '@/utils';
import { getCenterPoint, randomString, getSvgNowPosition, setSvgActualInfo } from '@/utils';
import {
calculateBottom,
calculateLeft,
@ -156,6 +159,8 @@
import HandlePanel from '@/components/webtopo-svgedit/components/handle-panel/index.vue';
import ConnectionPanel from '@/components/webtopo-svgedit/components/connection-panel/index.vue';
import { EDoneJsonType } from '@/config-center/types';
import ConnectionLine from '@/components/webtopo-svgedit/components/connection-line/index.vue';
import { IVisiableInfo } from './types';
// import HandlePanel from '../handle-panel/index.vue';
const globalStore = useGlobalStore();
const configStore = useConfigStore();
@ -167,14 +172,19 @@
? "url('/src/assets/icons/rotate.svg') 12 12, auto"
: 'default'
);
const visiable_info = reactive({
const visiable_info: IVisiableInfo = reactive({
handle_panel: computed(
() =>
globalStore.intention === EGlobalStoreIntention.Select ||
globalStore.intention === EGlobalStoreIntention.Zoom ||
globalStore.intention === EGlobalStoreIntention.Rotate
),
connection_panel: false
connection_panel: false,
connection_line: false,
select_item: {
info: null,
index: -1
}
});
const dropEvent = (e: DragEvent) => {
if (globalStore.intention == EGlobalStoreIntention.None) {
@ -251,12 +261,12 @@
e.preventDefault();
};
const onSvgMouseDown = (select_item: IDoneJson, index: number, e: MouseEvent) => {
console.log(172, e);
console.log('触发选中', e);
if (globalStore.intention === EGlobalStoreIntention.Connection) {
return;
}
e.preventDefault();
e.cancelBubble = true;
e.stopPropagation();
//
globalStore.intention = EGlobalStoreIntention.Select;
globalStore.setHandleSvgInfo(select_item, index);
@ -272,13 +282,33 @@
};
const onSvgMouseEnter = (select_item: IDoneJson, index: number, e: MouseEvent) => {
e.preventDefault();
e.cancelBubble = true;
e.stopPropagation();
visiable_info.connection_panel = true;
visiable_info.connection_line = true;
if (
(globalStore.intention === EGlobalStoreIntention.Connection ||
globalStore.intention === EGlobalStoreIntention.SetConnectionLineNode) &&
select_item.type === EDoneJsonType.ConnectionLine
) {
return;
}
visiable_info.select_item.info = select_item;
visiable_info.select_item.index = index;
};
const onSvgMouseLeave = (select_item: IDoneJson, index: number, e: MouseEvent) => {
e.preventDefault();
e.cancelBubble = true;
e.stopPropagation();
if (
(globalStore.intention === EGlobalStoreIntention.Connection ||
globalStore.intention === EGlobalStoreIntention.SetConnectionLineNode) &&
select_item.type === EDoneJsonType.ConnectionLine
) {
return;
}
visiable_info.connection_panel = false;
visiable_info.connection_line = false;
visiable_info.select_item.info = null;
visiable_info.select_item.index = -1;
};
const onCanvasMouseMove = (e: MouseEvent) => {
// 线
@ -471,6 +501,24 @@
)
};
// console.log('线', start_x, start_y, end_x, end_y, clientX, clientY);
} else if (
globalStore.intention === EGlobalStoreIntention.SetConnectionLineNode &&
globalStore.handle_svg_info
) {
globalStore.handle_svg_info.info.props.point_position.val[
globalStore.connection_line_node_info.point_index
] = {
x: getSvgNowPosition(
globalStore.mouse_info.position_x,
clientX,
globalStore.connection_line_node_info.init_pos.x
),
y: getSvgNowPosition(
globalStore.mouse_info.position_y,
clientY,
globalStore.connection_line_node_info.init_pos.y
)
};
}
};
const onCanvasMouseUp = (e: MouseEvent) => {

@ -0,0 +1,11 @@
import { IDoneJson } from '@/store/global/types';
export interface IVisiableInfo {
handle_panel: boolean;
connection_panel: boolean;
connection_line: boolean;
select_item: {
info: null | IDoneJson;
index: number;
};
}

@ -0,0 +1,127 @@
<!-- eslint-disable vue/html-indent -->
<template>
<g>
<path
:id="props.itemInfo.id"
:d="positionArrarToPath(props.itemInfo.props.point_position.val)"
fill="none"
:stroke="
props.itemInfo.animations?.type.val === EConfigAnimationsType.Electricity
? props.itemInfo.animations.color.val
: props.itemInfo.props.stroke.val
"
stroke-width="2"
style="cursor: move"
stroke-dashoffset="0"
:stroke-dasharray="
props.itemInfo.animations?.type.val === EConfigAnimationsType.Electricity ? 6 : 0
"
>
<animate
v-if="props.itemInfo.animations?.type.val === EConfigAnimationsType.Electricity"
attributeName="stroke-dashoffset"
:from="props.itemInfo.animations.reverse.val ? 0 : 1000"
:to="props.itemInfo.animations.reverse.val ? 1000 : 0"
:dur="`${props.itemInfo.animations.dur.val}s`"
:repeatCount="props.itemInfo.animations.repeatCount.val"
/>
</path>
<!-- 水珠 -->
<path
v-if="props.itemInfo.animations?.type.val === EConfigAnimationsType.WaterDrop"
:d="positionArrarToPath(props.itemInfo.props.point_position.val)"
fill="none"
fill-opacity="0"
:stroke="props.itemInfo.animations.color.val"
stroke-width="2"
stroke-dasharray="6"
stroke-dashoffset="0"
stroke-linecap="round"
>
<animate
attributeName="stroke-dashoffset"
:from="props.itemInfo.animations.reverse.val ? 0 : 1000"
:to="props.itemInfo.animations.reverse.val ? 1000 : 0"
:dur="`${props.itemInfo.animations.dur.val}s`"
:repeatCount="props.itemInfo.animations.repeatCount.val"
fill="freeze"
/>
</path>
<!-- 轨迹 -->
<circle
v-else-if="props.itemInfo.animations?.type.val === EConfigAnimationsType.Track"
cx="0"
cy="0"
r="5"
:fill="props.itemInfo.animations.color.val"
>
<animateMotion
:path="
positionArrarToPath(
props.itemInfo.animations.reverse.val
? [...props.itemInfo.props.point_position.val].reverse()
: props.itemInfo.props.point_position.val
)
"
:dur="`${props.itemInfo.animations.dur.val}s`"
:repeatCount="props.itemInfo.animations.repeatCount.val"
>
</animateMotion>
</circle>
<!-- 更改线段 -->
<g v-if="props.pointVisiable">
<circle
v-for="(item, index) in props.itemInfo.props.point_position.val"
:key="index"
:cx="item.x"
:cy="item.y"
r="4"
stroke-width="1"
:stroke="props.itemInfo.props.stroke.val"
fill="#fff"
style="cursor: pointer"
@mousedown="setConnectionLineNode(index, $event, item.x, item.y)"
/></g>
</g>
</template>
<script setup lang="ts">
import { EMouseInfoState, IDoneJson } from '@/store/global/types';
import { EGlobalStoreIntention } from '@/store/global/types';
import { PropType } from 'vue';
import { positionArrarToPath } from '@/utils';
import { useGlobalStore } from '@/store/global';
import { EConfigAnimationsType } from '@/config-center/types';
const props = defineProps({
itemInfo: {
type: Object as PropType<IDoneJson>,
default: () => {}
},
pointVisiable: {
type: Boolean,
default: false
}
});
const globalStore = useGlobalStore();
const setConnectionLineNode = (point_index: number, e: MouseEvent, x: number, y: number) => {
if (globalStore.intention === EGlobalStoreIntention.Connection) {
return;
}
globalStore.setHandleSvgInfo(props.itemInfo);
const { clientX, clientY } = e;
e.stopPropagation();
globalStore.connection_line_node_info = {
init_pos: { x, y },
point_index: point_index
};
globalStore.intention = EGlobalStoreIntention.SetConnectionLineNode;
globalStore.setMouseInfo({
state: EMouseInfoState.Down,
position_x: clientX,
position_y: clientY,
now_position_x: clientX,
now_position_y: clientY,
new_position_x: 0,
new_position_y: 0
});
};
</script>

@ -10,7 +10,7 @@
stroke="rgba(0,0,0,0)"
stroke-width="2"
:fill="fill"
:style="{ 'vector-effect': 'non-scaling-stroke'}"
:style="{ 'vector-effect': 'non-scaling-stroke','cursor': 'pointer'}"
pointer-events="all"
@mousedown="onConnectionMouseDown(ELineBindAnchors.TopCenter,$event)"/>
<circle
@ -21,7 +21,7 @@
stroke="rgba(0,0,0,0)"
stroke-width="2"
:fill="fill"
:style="{ 'vector-effect': 'non-scaling-stroke'}"
:style="{ 'vector-effect': 'non-scaling-stroke','cursor': 'pointer'}"
pointer-events="all"
@mousedown="onConnectionMouseDown(ELineBindAnchors.Left,$event)"/>
<circle
@ -32,7 +32,7 @@
stroke="rgba(0,0,0,0)"
stroke-width="2"
:fill="fill"
:style="{ 'vector-effect': 'non-scaling-stroke'}"
:style="{ 'vector-effect': 'non-scaling-stroke','cursor': 'pointer'}"
pointer-events="all"
@mousedown="onConnectionMouseDown(ELineBindAnchors.Right,$event)"/>
<circle
@ -43,7 +43,7 @@
stroke="rgba(0,0,0,0)"
stroke-width="2"
:fill="fill"
:style="{ 'vector-effect': 'non-scaling-stroke'}"
:style="{ 'vector-effect': 'non-scaling-stroke','cursor': 'pointer'}"
pointer-events="all"
@mousedown="onConnectionMouseDown(ELineBindAnchors.BottomCenter,$event)"/>
@ -148,19 +148,10 @@
},
...create_line_info
};
done_item_json.props.point_position.val.push(
{
x: configStore.svg.svg_position_center.x,
y: configStore.svg.svg_position_center.y
},
//push
{
x: configStore.svg.svg_position_center.x,
y: configStore.svg.svg_position_center.y
}
);
done_item_json.props.point_position.val.push();
done_item_json.props.point_position.val.push({
x: configStore.svg.svg_position_center.x,
y: configStore.svg.svg_position_center.y
});
globalStore.setHandleSvgInfo(done_item_json, globalStore.done_json.length);
globalStore.setDoneJson(done_item_json);

@ -144,7 +144,7 @@
const onHandleMouseDown = (type: EScaleInfoType, e: MouseEvent) => {
console.log('onHandleMouseDown', e);
const { clientX, clientY } = e;
e.cancelBubble = true;
e.stopPropagation();
globalStore.intention = EGlobalStoreIntention.Zoom;
globalStore.setMouseInfo({
state: EMouseInfoState.Down,
@ -178,7 +178,7 @@
};
const onRotateCircleMouseDown = (e: MouseEvent) => {
const { clientX, clientY } = e;
e.cancelBubble = true;
e.stopPropagation();
globalStore.intention = EGlobalStoreIntention.Rotate;
globalStore.rotate_info = {
angle: props.itemInfo.rotate

@ -0,0 +1,62 @@
<template>
<el-form-item
v-for="(attr_item, key) in props.objInfo"
:key="key"
:label="attr_item.title"
size="small"
>
<el-select
v-if="attr_item.type === EConfigItemPropsType.Select"
v-model="attr_item.val"
placeholder="Select"
size="small"
:disabled="attr_item?.disabled"
>
<el-option
v-for="item in attr_item.options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input-number
v-else-if="attr_item.type === EConfigItemPropsType.InputNumber"
v-model="attr_item.val"
:disabled="attr_item?.disabled"
></el-input-number>
<el-input
v-else-if="attr_item.type === EConfigItemPropsType.Input"
v-model="attr_item.val"
:disabled="attr_item?.disabled"
></el-input>
<el-color-picker
v-else-if="attr_item.type === EConfigItemPropsType.Color"
v-model="attr_item.val"
:disabled="attr_item?.disabled"
></el-color-picker>
<el-switch
v-else-if="attr_item.type === EConfigItemPropsType.Switch"
v-model="attr_item.val"
:disabled="attr_item?.disabled"
></el-switch>
</el-form-item>
</template>
<script setup lang="ts">
import { EConfigItemPropsType, IConfigItemProps } from '@/config-center/types';
import {
ElFormItem,
ElColorPicker,
ElInputNumber,
ElInput,
ElSelect,
ElOption,
ElSwitch
} from 'element-plus';
import { PropType } from 'vue';
const props = defineProps({
objInfo: {
type: Object as PropType<IConfigItemProps>,
default: () => {}
}
});
</script>

@ -42,9 +42,22 @@
<el-form-item label="y轴坐标" size="small">
<el-input-number v-model="globalStore.handle_svg_info.info.y"></el-input-number>
</el-form-item>
<dynamic-el-form-item
:obj-info="globalStore.handle_svg_info.info.props"
></dynamic-el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="动画" name="animation">
<el-form
label-width="90px"
label-position="left"
v-if="globalStore.handle_svg_info.info.animations"
>
<dynamic-el-form-item
:obj-info="globalStore.handle_svg_info.info.animations"
></dynamic-el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="动画" name="animation"></el-tab-pane>
<el-tab-pane label="事件" name="event"></el-tab-pane>
</el-tabs>
</div>
@ -63,14 +76,14 @@
ElInput
} from 'element-plus';
import { ref } from 'vue';
import { useConfigStore } from '../../../../store/config';
import { useGlobalStore } from '../../../../store/global';
import { EGlobalStoreIntention } from '../../../../store/global/types';
import { useConfigStore } from '@/store/config';
import { useGlobalStore } from '@/store/global';
import { EGlobalStoreIntention } from '@/store/global/types';
import DynamicElFormItem from './dynamic-el-form-item.vue';
const configStore = useConfigStore();
const globalStore = useGlobalStore();
const activeName = ref('style');
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event);
};

@ -6,12 +6,13 @@ export const circuit_breaker_svg_file: IConfigItem = {
type: EDoneJsonType.File,
config: {
can_zoom: true,
have_anchor: true
have_anchor: true,
actual_rect: true
},
props: {
fill: {
title: '填充色',
type: EConfigItemPropsType.Switch,
title: '开关',
type: EConfigItemPropsType.Select,
val: '#ff0000',
options: [
{

@ -6,7 +6,8 @@ export const alternator_svg_file: IConfigItem = {
type: EDoneJsonType.File,
config: {
can_zoom: true,
have_anchor: true
have_anchor: true,
actual_rect: true
},
props: {
fill: {

@ -6,7 +6,8 @@ export const traction_transformer_svg_file: IConfigItem = {
type: EDoneJsonType.File,
config: {
can_zoom: true,
have_anchor: true
have_anchor: true,
actual_rect: true
},
props: {
fill: {

@ -1,4 +1,4 @@
import { EConfigItemPropsType, EDoneJsonType } from '@/config-center/types';
import { EConfigAnimationsType, EConfigItemPropsType, EDoneJsonType } from '@/config-center/types';
import type { ISystemStraightLine } from './types';
export const straight_line_system: ISystemStraightLine = Object.seal({
@ -7,7 +7,8 @@ export const straight_line_system: ISystemStraightLine = Object.seal({
type: EDoneJsonType.StraightLine,
config: {
can_zoom: false,
have_anchor: false
have_anchor: false,
actual_rect: false
},
props: {
fill: {
@ -47,11 +48,12 @@ export const connection_line_system: ISystemStraightLine = Object.freeze({
type: EDoneJsonType.ConnectionLine,
config: {
can_zoom: false,
have_anchor: false
have_anchor: false,
actual_rect: false
},
props: {
fill: {
title: '填充色',
stroke: {
title: '线条颜色',
type: EConfigItemPropsType.Color,
val: '#ff0000'
},
@ -61,6 +63,40 @@ export const connection_line_system: ISystemStraightLine = Object.freeze({
val: []
}
},
animations: {
type: {
title: '动画类型',
type: EConfigItemPropsType.Select,
val: EConfigAnimationsType.None,
options: [
{
label: '无',
value: EConfigAnimationsType.None
},
{
label: '电流',
value: EConfigAnimationsType.Electricity
},
{
label: '轨迹',
value: EConfigAnimationsType.Track
},
{
label: '水珠',
value: EConfigAnimationsType.WaterDrop
}
]
},
dur: { title: '持续时间', type: EConfigItemPropsType.InputNumber, val: 20 },
repeatCount: {
title: '循环次数',
type: EConfigItemPropsType.Input,
val: 'indefinite',
disabled: true
},
color: { title: '颜色', type: EConfigItemPropsType.Color, val: '#0a7ae2' },
reverse: { title: '反转动画', type: EConfigItemPropsType.Switch, val: false }
},
bind_anchors: {
start: null,
end: null

@ -21,6 +21,7 @@ export interface IConfigItem {
props: IConfigItemProps;
type: EDoneJsonType;
config: IDoneJsonConfig;
animations?: IConfigItemProps;
}
export interface IConfigItemProps {
[key: string]: {
@ -28,14 +29,23 @@ export interface IConfigItemProps {
type: EConfigItemPropsType;
val: any;
options?: { value: any; label: string }[];
disabled?: boolean;
};
}
export enum EConfigAnimationsType {
None = 'None',
Electricity = 'Electricity', //电流效果
WaterDrop = 'WaterDrop', //水珠
Track = 'Track' //轨迹
}
export enum EConfigItemPropsType {
Input = 'Input',
Color = 'Color',
InputNumber = 'InputNumber',
Switch = 'Switch', //此类型option默认索引0为关闭
JsonEdit = 'JsonEdit'
JsonEdit = 'JsonEdit',
Select = 'Select'
}
export enum EDoneJsonType {
File = 'File',
@ -45,4 +55,5 @@ export enum EDoneJsonType {
interface IDoneJsonConfig {
can_zoom: boolean;
have_anchor: boolean;
actual_rect: boolean;
}

@ -49,6 +49,13 @@ export const useGlobalStore = defineStore('global-store', {
},
rotate_info: {
angle: 0
},
connection_line_node_info: {
init_pos: {
x: 0,
y: 0
},
point_index: 0
}
};
},
@ -72,11 +79,16 @@ export const useGlobalStore = defineStore('global-store', {
setMouseInfo(mouse_info: IMouseInfo) {
this.mouse_info = mouse_info;
},
setHandleSvgInfo(info: IDoneJson | null, index: number) {
setHandleSvgInfo(info: IDoneJson | null, index?: number) {
let current_index = index;
if (info) {
if (!current_index && current_index != 0) {
//如果索引没传 在这根据id查出索引
current_index = this.done_json.findIndex((f) => f.id === info.id) ?? -1;
}
this.handle_svg_info = {
info: info,
index: index
index: current_index
};
} else {
this.handle_svg_info = info;

@ -9,6 +9,7 @@ export interface IGlobalStore {
handle_svg_info: IHandleSvgInfo | null;
scale_info: IScaleInfo;
rotate_info: IRotateInfo;
connection_line_node_info: IConnectionLineNodeInfo;
}
export interface IDoneJson extends IConfigItem {
id: string;
@ -43,7 +44,8 @@ export enum EGlobalStoreIntention {
Select = 'Select',
Zoom = 'Zoom',
Rotate = 'Rotate',
Connection = 'Connection'
Connection = 'Connection',
SetConnectionLineNode = 'SetConnectionLineNode'
}
export interface IMouseInfo {
state: EMouseInfoState;
@ -103,3 +105,10 @@ export interface ICoordinate {
x: number;
y: number;
}
export interface IConnectionLineNodeInfo {
init_pos: {
x: number;
y: number;
};
point_index: number;
}

Loading…
Cancel
Save