完善菜单功能

This commit is contained in:
X1627315083@163.com
2026-03-05 10:37:41 +08:00
5 changed files with 113 additions and 76 deletions

View File

@@ -5,8 +5,8 @@
<span <span
v-else v-else
class="icon" class="icon"
@click="onClickTool(v.name)" @click="onClickTool(v)"
:class="{ active: v.name === tool }" :class="{ active: v.name === tool, disabled: v.disabled }"
> >
<svg-icon :name="v.icon" :size="v.iconSize" /> <svg-icon :name="v.icon" :size="v.iconSize" />
</span> </span>
@@ -32,16 +32,37 @@
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
const toolManager = inject('toolManager') as any const toolManager = inject('toolManager') as any
const tool = computed(() => stateManager.tool.value) const tool = computed(() => stateManager.tool.value)
const historyIndex = computed(() => stateManager.historyIndex.value)
const historyList = computed(() => stateManager.historyList.value)
const isUndo = computed(() => !historyList.value[historyIndex.value - 1])
const isRedo = computed(() => !historyList.value[historyIndex.value + 1])
const tools = ref([ const tools = ref([
{ name: TOOLS.SELECT, icon: 'c-select', iconSize: 16 }, { name: TOOLS.SELECT, icon: 'c-select', iconSize: 16, disabled: ref(false) },
{ name: TOOLS.MOVE, icon: 'c-move', iconSize: 18 }, { name: TOOLS.MOVE, icon: 'c-move', iconSize: 18, disabled: ref(false) },
{ name: TOOLS.TEXT, icon: 'c-text', iconSize: 18 }, { name: TOOLS.TEXT, icon: 'c-text', iconSize: 18, disabled: ref(false) },
{ type: 'line' }, { type: 'line' },
{ name: TOOLS.UNDO, icon: 'c-undo', iconSize: 18 }, {
{ name: TOOLS.REDO, icon: 'c-redo', iconSize: 18 } name: TOOLS.UNDO,
icon: 'c-undo',
iconSize: 18,
disabled: isUndo,
on: () => stateManager.undoState()
},
{
name: TOOLS.REDO,
icon: 'c-redo',
iconSize: 18,
disabled: isRedo,
on: () => stateManager.redoState()
}
]) ])
const onClickTool = (tool: any) => { const onClickTool = (tool: any) => {
toolManager.setTool(tool) if (tool.disabled?.value) return
if (tool.on) {
tool.on()
} else {
toolManager.setTool(tool.name)
}
} }
</script> </script>
@@ -79,10 +100,14 @@
height: 3rem; height: 3rem;
--svg-icon-color: #000; --svg-icon-color: #000;
border-radius: 0.4rem; border-radius: 0.4rem;
&.active, &:not(.disabled).active,
&:hover { &:not(.disabled):hover {
background-color: #dfdfdf; background-color: #dfdfdf;
} }
&.disabled {
opacity: 0.5;
cursor: not-allowed;
}
} }
> button { > button {
width: 10rem; width: 10rem;

View File

@@ -16,7 +16,7 @@
<span class="text">Edit</span> <span class="text">Edit</span>
</button> </button>
</div> </div>
<img class="image" :src="data.url" /> <img class="image" :src="data.url" :style="{transform: `scale(${data?.scale?.x || 1}, ${data?.scale?.y || 1})`}" />
<div class="more" @click="showMenu = !showMenu" @mousedown.stop> <div class="more" @click="showMenu = !showMenu" @mousedown.stop>
<svg-icon name="more" size="24" size-unit="px" color="#C9C9C9" /> <svg-icon name="more" size="24" size-unit="px" color="#C9C9C9" />
</div> </div>
@@ -24,7 +24,7 @@
<div <div
v-for="(v, i) in menus" v-for="(v, i) in menus"
:key="i" :key="i"
:class="[v.isDivide ? 'divide' : 'item']" :class="[v.isDivide ? 'divide' : 'item', { disabled: v.disabled }]"
@click="onMenuItem(v)" @click="onMenuItem(v)"
> >
<template v-if="!v.isDivide"> <template v-if="!v.isDivide">
@@ -39,17 +39,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch } from 'vue' import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch } from 'vue'
const props = defineProps({ const props = defineProps({
config: {
type: Object,
default: () => ({})
},
data: { data: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
} }
}) })
const emit = defineEmits(['delete-node', 'copy-node', 'bring-to-font', 'send-to-back', 'flip-horizontal', 'flip-vertical']) const emit = defineEmits(['delete-node', 'copy-node', 'bring-to-font', 'send-to-back', 'update-data'])
const attrs = useAttrs() const attrs = useAttrs()
const showHeader = ref(!!attrs.node?.data?.isHeader) const showHeader = ref(!!attrs.node?.data?.isHeader)
const showMenu = ref(false) const showMenu = ref(false)
const data = reactive({ const data = reactive({
url: props.data?.url || '' url: props.data?.url || '',
scale: props.data?.scale || { x:1,y:1 }
}) })
watch( watch(
() => props.data.url, () => props.data.url,
@@ -59,17 +64,21 @@
) )
const menus = ref([ const menus = ref([
{ label: 'Copy', tip: 'Ctrl+C', on: () => emit('copy-node') }, { label: 'Copy', tip: 'Ctrl+C', on: () => emit('copy-node') },
// { label: 'Paste', tip: 'Ctrl+V', on: () => {} }, {
// { label: 'Duplicate', tip: 'Ctrl+D', on: () => {} }, label: 'Delete',
{ label: 'Delete', tip: 'Del', on: () => emit('delete-node') }, tip: 'Del',
on: () => emit('delete-node'),
disabled: !!props.config?.disableDelete
},
{ isDivide: true }, { isDivide: true },
{ label: 'Bring to font', tip: '', on: () => {emit('bring-to-font')} }, { label: 'Bring to font', tip: '', on: () => {} },
{ label: 'Send to back', tip: '', on: () => {emit('send-to-back')} }, { label: 'Send to back', tip: '', on: () => {} },
{ isDivide: true }, { isDivide: true },
{ label: 'Flip horizontal', tip: '', on: () => {emit('flip-horizontal')} }, { label: 'Flip horizontal', tip: '', on: () => {data.scale.x = -data.scale.x; emit('update-data',data)} },
{ label: 'Flip vertical', tip: '', on: () => {emit('flip-vertical')} } { label: 'Flip vertical', tip: '', on: () => {data.scale.y = -data.scale.y; emit('update-data',data)} }
]) ])
const onMenuItem = (v) => { const onMenuItem = (v) => {
if (v.disabled) return
v.on && v.on() v.on && v.on()
hideMenu() hideMenu()
} }
@@ -181,7 +190,14 @@
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
&:hover { &.disabled {
cursor: not-allowed;
> .tip,
> .label {
color: rgba(0, 0, 0, 0.2);
}
}
&:not(.disabled):hover {
background: #f3f3f3; background: #f3f3f3;
} }
> .label { > .label {

View File

@@ -11,7 +11,7 @@
tabindex="0" tabindex="0"
class="input" class="input"
ref="inputRef" ref="inputRef"
contenteditable="true" :contenteditable="edit"
@input="onInput" @input="onInput"
@blur="onBlur" @blur="onBlur"
@paste.prevent @paste.prevent
@@ -66,6 +66,7 @@
<style lang="less" scoped> <style lang="less" scoped>
.text { .text {
user-select: none;
&.edit { &.edit {
> .input { > .input {
cursor: text; cursor: text;

View File

@@ -28,6 +28,7 @@
<component <component
:is="components[node.data.component]" :is="components[node.data.component]"
:node="node" :node="node"
:config="node.data"
:data="node.data.data" :data="node.data.data"
v-bind="node.data" v-bind="node.data"
@delete-node="deleteNode(node.id)" @delete-node="deleteNode(node.id)"
@@ -35,8 +36,6 @@
@update-data="(v) => (node.data.data = v)" @update-data="(v) => (node.data.data = v)"
@bring-to-font="bringToFont(node.id)" @bring-to-font="bringToFont(node.id)"
@send-to-back="sendToBack(node.id)" @send-to-back="sendToBack(node.id)"
@flip-horizontal="flipHorizontal(node.id)"
@flip-vertical="flipVertical(node.id)"
/> />
</node> </node>
</template> </template>
@@ -152,15 +151,6 @@
const sendToBack = (id) => { const sendToBack = (id) => {
stateManager.sendToBack(id) stateManager.sendToBack(id)
} }
/** 水平翻转 */
const flipHorizontal = (id) => {
stateManager.setNodeFlip(id,'X')
}
/** 垂直翻转 */
const flipVertical = (id) => {
stateManager.setNodeFlip(id,'Y')
}
// 导出流程 // 导出流程
const exportFlow = () => { const exportFlow = () => {
// flowManager.exportFlow() // flowManager.exportFlow()
@@ -188,9 +178,9 @@
onMounted(() => { onMounted(() => {
// window['vueFlow'] = vueFlow // window['vueFlow'] = vueFlow
// window['nodes'] = nodes // window['nodes'] = nodes
console.log(props.config)
nodeManager.createResultNode({ nodeManager.createResultNode({
data: { data: {
disableDelete: true,
isHeader: false, isHeader: false,
data: { data: {
url: props.config.url url: props.config.url

View File

@@ -16,9 +16,16 @@ export class StateManager {
zoom: any zoom: any
tool: any tool: any
cursor: any cursor: any
// 节点是否可拖动
nodesDraggable: any nodesDraggable: any
// 拖动时是否可以平移画布
panOnDrag: any panOnDrag: any
// 历史记录-撤回/重做
mxHistory: any
historyList: any
historyIndex: any
// 管理器 // 管理器
eventManager: any eventManager: any
flowManager: any flowManager: any
@@ -38,17 +45,14 @@ export class StateManager {
this.cursor = ref("") this.cursor = ref("")
this.nodesDraggable = ref(false) this.nodesDraggable = ref(false)
this.panOnDrag = ref(false) this.panOnDrag = ref(false)
this.mxHistory = ref(50)
this.historyList = ref([])
this.historyIndex = ref(0)
this.nodes = ref<NodesItem[]>([]); this.nodes = ref<NodesItem[]>([]);
this.nodes_ = computed(() => { this.nodes_ = computed(() => {
return this.nodes.value.map((node, index) => { return this.nodes.value.map((node, index) => {
const obj = node; const obj = node;
// if (index === 0) {
// obj.type = NODE_TYPE.INPUT;
// } else if (index === this.nodes.value.length - 1) {
// obj.type = NODE_TYPE.OUTPUT;
// } else {
// obj.type = NODE_TYPE.SECONDARY;
// }
const superiorID = node.data.superiorID; const superiorID = node.data.superiorID;
const isSuperior = this.nodes.value.some((v) => v.id === superiorID) const isSuperior = this.nodes.value.some((v) => v.id === superiorID)
const isSubord = this.nodes.value.some((v) => v.data.superiorID === node.id) const isSubord = this.nodes.value.some((v) => v.data.superiorID === node.id)
@@ -68,17 +72,6 @@ export class StateManager {
this.edges = computed(() => { this.edges = computed(() => {
const arr = [] const arr = []
this.nodes.value.forEach((node, index) => { this.nodes.value.forEach((node, index) => {
// if (index < this.nodes.value.length - 1) {
// const source = node.id
// const target = this.nodes.value[index + 1].id
// arr.push({
// id: `el-${source}-${target}`,
// source: source,
// target: target,
// selectable: false,
// type: 'default'
// })
// }
const superiorID = node.data.superiorID; const superiorID = node.data.superiorID;
const isSuperior = this.nodes.value.some((v) => v.id === superiorID) const isSuperior = this.nodes.value.some((v) => v.id === superiorID)
if (superiorID && isSuperior) { if (superiorID && isSuperior) {
@@ -99,28 +92,21 @@ export class StateManager {
} }
/** 添加节点 */ /** 添加节点 */
addNode(node: NodesItem) { addNode(node: NodesItem) {
this.nodes.value.push(node) this.nodes.value.push(node);
this.recordState()
} }
/** 删除节点 */ /** 删除节点 */
deleteNode(id: string) { deleteNode(id: string) {
this.nodes.value = this.nodes.value.filter((node: NodesItem) => node.id !== id) this.nodes.value = this.nodes.value.filter((node: NodesItem) => node.id !== id)
this.recordState()
} }
/** 获取节点 */ /** 获取节点 */
getNodeById(id: string) { getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) }
return this.nodes.value.find((node: NodesItem) => node.id === id)
}
/** 获取下级节点 */ /** 获取下级节点 */
getSubordNodeByID(id: string) { getSubordNodeByID(id: string) { return this.nodes.value.find((node: NodesItem) => node.data.superiorID === id) }
return this.nodes.value.find((node: NodesItem) => node.data.superiorID === id) getLastNode() { return this.nodes.value[this.nodes.value.length - 1] }
}
getLastNode() {
return this.nodes.value[this.nodes.value.length - 1]
}
/** 设置工具 */ /** 设置工具 */
setTool(tool: string) { setTool(tool: string) { this.tool.value = tool }
this.tool.value = tool
}
/** 设置光标 */ /** 设置光标 */
setCursor(v: string) { this.cursor.value = v } setCursor(v: string) { this.cursor.value = v }
/** 设置节点是否可拖动 */ /** 设置节点是否可拖动 */
@@ -139,15 +125,34 @@ export class StateManager {
if (fromIndex === -1) return console.warn(`没有找到指定id:${id}`) if (fromIndex === -1) return console.warn(`没有找到指定id:${id}`)
this.nodes.value.splice(0, 0, ...this.nodes.value.splice(fromIndex, 1)) this.nodes.value.splice(0, 0, ...this.nodes.value.splice(fromIndex, 1))
} }
/** 设置水平或者垂直翻转 */ /** 记录状态 */
setNodeFlip(id,direction) { recordState() {
const node = this.getNodeById(id) if (this.historyIndex.value < this.historyList.value.length - 1) {
if (!node) return console.warn(`没有找到指定id:${id}`) this.historyList.value.splice(this.historyIndex.value)
if (direction === 'X') {
node.data.scale.x = -node.data.scale.x
} else if (direction === 'Y') {
node.data.scale.y = -node.data.scale.y
} }
console.log(node,direction) const state = {
nodes: JSON.stringify(this.nodes.value)
}
this.historyList.value.push(state)
const size = this.historyList.value.length - this.mxHistory.value
if (size > 0) this.historyList.value.splice(0, size)
this.historyIndex.value = this.historyList.value.length - 1
} }
/** 撤回状态 */
undoState() {
var index = this.historyIndex.value - 1
const state = this.historyList.value[index]
if (!state) return
this.historyIndex.value = index
this.nodes.value = JSON.parse(state.nodes)
}
/** 重做状态 */
redoState() {
var index = this.historyIndex.value + 1
const state = this.historyList.value[index]
if (!state) return
this.historyIndex.value = index
this.nodes.value = JSON.parse(state.nodes)
}
} }