This commit is contained in:
lzp
2026-03-09 16:45:30 +08:00
parent 40dfb93406
commit 9189b5387f
9 changed files with 199 additions and 41 deletions

View File

@@ -1,28 +1,64 @@
<template>
<div class="layer-item">
<div class="layer-item" @click="onClickLayer">
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
<div class="thumb"></div>
<div class="name">
<div @dblclick="editName = true" v-if="!editName">{{ layer.name }}</div>
<input type="text" v-model="layer.name" v-else @blur="editName = false" />
<div @dblclick="onClickEditName" v-if="!editName">
{{ layer.info.name || '未命名图层' }}
</div>
<input
v-else
type="text"
ref="nameInputRef"
:value="layer.info.name"
@blur="onChangeName"
@keyup.enter="onChangeName"
/>
</div>
<div class="icons">
<span><svg-icon name="dc-show" size="15" /></span>
<span><svg-icon name="dc-delete" size="13" /></span>
<span @click="onClickShowHide"
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
/></span>
<span @click="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
<span><svg-icon name="dc-down_arrow" size="11" /></span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, inject, computed } from 'vue'
import { ref, inject, nextTick } from 'vue'
const layerManager = inject('layerManager') as any
const editName = ref(false)
const props = defineProps({
layer: {
type: Object,
default: () => ({})
}
})
const editName = ref(false)
const nameInputRef = ref(null)
const onClickEditName = () => {
editName.value = true
nextTick(() => {
nameInputRef.value.focus()
})
}
const onChangeName = (e) => {
const value = e.target.value
editName.value = false
const name = props.layer.info.name
if (name !== value) {
layerManager.setLayerNameByID(props.layer.info.id, value)
}
}
const onClickShowHide = () => {
layerManager.setLayerVisibleByID(props.layer.info.id, !props.layer.visible)
}
const onClickDelete = () => {
layerManager.deleteLayerByID(props.layer.info.id)
}
const onClickLayer = () => {
layerManager.setActiveID(props.layer.info.id)
}
</script>
<style lang="less" scoped>
@@ -39,11 +75,11 @@
&:last-child {
border-bottom: none;
}
&:not([draging='true']) {
&:hover,
&.active {
background-color: #ededed;
}
&:not([draging='true']):hover {
background-color: #f5f5f5;
}
&.active {
background-color: #ededed !important;
}
&.drag {
opacity: 0;
@@ -51,7 +87,7 @@
&.ghost,
&.chosen {
box-shadow: inset 0 0 5px #aaa;
background-color: #ededed;
background-color: #ededed !important;
}
> .drag {
padding: 0.3rem;

View File

@@ -11,7 +11,7 @@
</div>
<div class="content">
<VueDraggable
:model-value="sortableRootLayers"
:model-value="list"
@start="handleDragStart"
@end="handleDragEnd"
class="sortable-layers"
@@ -36,10 +36,11 @@
:scroll-speed="10"
>
<layer-item
v-for="layer in sortableRootLayers"
:key="layer.id"
v-for="layer in list"
:key="layer.info.id"
:layer="layer"
:draging="draging"
:class="{ active: layer.info.id === layerManager.activeID.value }"
/>
</VueDraggable>
</div>
@@ -51,20 +52,16 @@
import { ref, inject, computed } from 'vue'
import layerItem from './layer-item.vue'
const draging = ref(false)
const sortableRootLayers = ref([
{ id: 1, name: 'layer1' },
{ id: 2, name: 'layer2' },
{ id: 3, name: 'layer3' }
])
const layerManager = inject('layerManager') as any
const list = computed(() => layerManager.layers.value)
const handleDragStart = () => {
draging.value = true
}
const handleDragEnd = (event) => {
draging.value = false
const { from, to, oldIndex, newIndex, item } = event
console.log('🔄 拖拽结束事件:', event)
sortableRootLayers.value.splice(newIndex, 0, sortableRootLayers.value.splice(oldIndex, 1)[0])
const { from, to, oldIndex, newIndex, data } = event
console.log('oldIndex', data)
layerManager.dragSort(data.info.id, newIndex)
}
</script>
@@ -107,6 +104,7 @@
> .content {
flex: 1;
overflow-y: auto;
min-height: 20rem;
&::-webkit-scrollbar {
width: 4px;
}

View File

@@ -5,7 +5,7 @@
</div>
<layer-panel />
<details-panel />
<header-tools />
<header-tools @export="exportCanvas" />
<zoom
:zoom="canvasManager.currentZoom.value / 100"
:step="0.1"
@@ -27,6 +27,7 @@
// 管理器
import { StateManager } from './manager/StateManager'
import { LayerManager } from './manager/LayerManager'
import { EventManager } from './manager/EventManager'
import { CanvasManager } from './manager/CanvasManager'
import { ToolManager } from './manager/ToolManager'
@@ -50,6 +51,11 @@
stateManager.setManager({ canvasManager, canvasRef })
provide('canvasManager', canvasManager)
// 图层管理器
const layerManager = new LayerManager({ stateManager, canvasManager })
stateManager.setManager({ layerManager })
provide('layerManager', layerManager)
// 事件管理器
const eventManager = new EventManager({ stateManager })
stateManager.setManager({ eventManager })
@@ -90,6 +96,9 @@
})
canvasManager.resetZoom()
}
const exportCanvas = () => {
console.log(canvasManager.getBitObjects())
}
</script>
<style lang="less">
@import '@vue-flow/core/dist/style.css';

View File

@@ -13,7 +13,7 @@ export class AnimationManager {
constructor(canvas, options = {}) {
this.canvasManager = options.canvasManager;
this.canvas = canvas;
this.currentZoom = options.currentZoom || { value: 100 };
this.currentZoom = options.currentZoom;
// 动画相关属性
this._zoomAnimation = null;

View File

@@ -15,6 +15,9 @@ interface CanvasInitOptions {
}
export class CanvasManager {
stateManager: any
layerManager: any
animationManager: any
eventManager: any
deviceInfo: any
canvas: any
canvasViewWidth: number
@@ -22,8 +25,6 @@ export class CanvasManager {
canvasWidth: number
canvasHeight: number
currentZoom: any
animationManager: any
eventManager: any
constructor(options) {
this.stateManager = options.stateManager;
this.deviceInfo = detectDeviceType();
@@ -34,6 +35,7 @@ export class CanvasManager {
this.canvasViewHeight = options.canvasViewHeight || 1080
}
initCanvas(options: CanvasInitOptions) {
this.layerManager = this.stateManager.layerManager
this.setCanvasViewSize(options)
this.canvasWidth = options.canvasWidth || 750
this.canvasHeight = options.canvasHeight || 600
@@ -62,7 +64,11 @@ export class CanvasManager {
top: 20,
width: 100,
height: 100,
fill: '#f00'
fill: '#f00',
info: {
id: 'rect1',
name: 'rect1',
}
})
this.canvas.add(rect)
//创建圆形
@@ -70,9 +76,37 @@ export class CanvasManager {
left: 200,
top: 200,
radius: 50,
fill: '#0f0'
fill: '#0f0',
info: {
id: 'circle',
name: 'circle',
}
})
this.canvas.add(circle)
// 文字
const text = new fabric.Text('Hello World', {
left: 300,
top: 300,
fontSize: 24,
fill: '#000',
info: {
id: 'text1',
name: 'text',
}
})
this.canvas.add(text)
// 文字
const text2 = new fabric.Text('Hello World', {
left: 300,
top: 300,
fontSize: 24,
fill: '#000',
info: {
id: 'text2',
name: 'tex2t',
}
})
this.canvas.add(text2)
this.animationManager = new AnimationManager(this.canvas, {
currentZoom: this.currentZoom,
canvasManager: this,
@@ -81,9 +115,9 @@ export class CanvasManager {
defaultDuration: 0.3, // 缩短默认动画时间
});
this.setupCanvasEvents()
this.stateManager.toolManager.setTool(OperationType.PAN)
this.stateManager.toolManager.setTool(OperationType.SELECT)
this.layerManager.updateLayers()
this.layerManager.setActiveID(text2.info.id)
}
setupCanvasEvents() {
// 创建画布事件管理器
@@ -98,7 +132,36 @@ export class CanvasManager {
resetZoom() {
this.animationManager.resetZoom()
}
getObjects() {
return this.canvas.getObjects() || []
}
getObjectById(id: string) {
return this.getObjects().find((item: any) => item.info.id === id)
}
renderAll() {
this.canvas.renderAll()
}
deleteObjectById(id: string) {
const object = this.getObjectById(id)
if (object) {
this.canvas.remove(object)
this.layerManager.updateLayers()
this.renderAll()
}
}
// 拖拽排序
dragSort(id, newIndex) {
this.canvas.moveTo(this.getObjectById(id), newIndex)
this.layerManager.updateLayers()
this.renderAll()
}
getBitObjects() {
return this.getObjects().map(v => {
const object = v.toJSON("info");
return object
})
}
}

View File

@@ -0,0 +1,46 @@
import { ref } from 'vue'
export class LayerManager {
stateManager: any
canvasManager: any
layers: any
activeID: any
constructor(options) {
this.stateManager = options.stateManager;
this.canvasManager = options.canvasManager;
this.layers = ref([])
this.activeID = ref("")
}
setActiveID(id: string) { this.activeID.value = id }
getLayerByID(id) {
return this.layers.value.find((item: any) => item.info.id === id)
}
setLayerNameByID(id, name: string) {
const layer = this.getLayerByID(id)
if (layer) {
layer.info.name = name
this.canvasManager.renderAll()
}
}
setLayerVisibleByID(id, visible: boolean) {
const layer = this.getLayerByID(id)
if (layer) {
layer.set({
visible: visible
})
this.canvasManager.renderAll()
}
}
deleteLayerByID(id) {
this.canvasManager.deleteObjectById(id)
}
// 拖拽排序
dragSort(id, newIndex) {
const index = Math.abs(this.layers.value.length - newIndex - 1)
this.canvasManager.dragSort(id, index)
}
// 更新图层列表
updateLayers() {
this.layers.value = this.canvasManager.getObjects().reverse()
}
}

View File

@@ -12,13 +12,15 @@ export class StateManager {
historyIndex: any
// 管理器
eventManager: any
canvasManager: any
layerManager: any
eventManager: any
toolManager: any
// 设置管理器
setManager(options) {
options.eventManager && (this.eventManager = options.eventManager)
options.canvasManager && (this.canvasManager = options.canvasManager)
options.layerManager && (this.layerManager = options.layerManager)
options.toolManager && (this.toolManager = options.toolManager)
}
constructor(options) {

View File

@@ -603,12 +603,12 @@ export class CanvasEventManager {
handleDragEnd(opt, isTouch = false) {
if (this.canvas.isDragging) {
// 使用动画管理器处理惯性效果
if (this.lastMousePositions.length > 1 && opt && opt.e) {
this.animationManager.applyInertiaEffect(
this.lastMousePositions,
isTouch
);
}
// if (this.lastMousePositions.length > 1 && opt && opt.e) {
// this.animationManager.applyInertiaEffect(
// this.lastMousePositions,
// isTouch
// );
// }
}
this.canvas.isDragging = false;