Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
2026-03-10 16:28:17 +08:00
15 changed files with 269 additions and 42 deletions

12
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"pinia": "^2.0.32",
"pinia-persistedstate-plugin": "^0.1.0",
"pinia-plugin-persistedstate": "^3.1.0",
"three": "^0.148.0",
"vue": "^3.2.47",
"vue-draggable-plus": "^0.6.1",
"vue-i18n": "^11.2.8",
@@ -8556,6 +8557,12 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/three": {
"version": "0.148.0",
"resolved": "https://registry.npmmirror.com/three/-/three-0.148.0.tgz",
"integrity": "sha512-8uzVV+qhTPi0bOFs/3te3RW6hb3urL8jYEl6irjCWo/l6sr8MPNMcClFev/MMYeIxr0gmDcoXTy/8LXh/LXkfw==",
"license": "MIT"
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz",
@@ -16065,6 +16072,11 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"three": {
"version": "0.148.0",
"resolved": "https://registry.npmmirror.com/three/-/three-0.148.0.tgz",
"integrity": "sha512-8uzVV+qhTPi0bOFs/3te3RW6hb3urL8jYEl6irjCWo/l6sr8MPNMcClFev/MMYeIxr0gmDcoXTy/8LXh/LXkfw=="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz",

View File

@@ -27,6 +27,7 @@
"pinia": "^2.0.32",
"pinia-persistedstate-plugin": "^0.1.0",
"pinia-plugin-persistedstate": "^3.1.0",
"three": "^0.148.0",
"vue": "^3.2.47",
"vue-draggable-plus": "^0.6.1",
"vue-i18n": "^11.2.8",

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.52856 1.52876C1.78891 1.26841 2.21102 1.26841 2.47137 1.52876L14.4714 13.5288C14.7317 13.7891 14.7317 14.2112 14.4714 14.4716C14.211 14.7319 13.7889 14.7319 13.5286 14.4716L11.8523 12.7953C10.8026 13.4938 9.51597 14.0002 7.99997 14.0002C5.89718 14.0002 4.24037 13.0275 3.03903 11.9196C1.84104 10.8148 1.0565 9.5415 0.674858 8.83761C0.390009 8.31223 0.390478 7.68717 0.675086 7.1623C1.06023 6.45201 1.86133 5.15114 3.09055 4.03355L1.52856 2.47157C1.26822 2.21122 1.26822 1.78911 1.52856 1.52876ZM4.03457 4.97757C2.93812 5.95924 2.20475 7.13846 1.8472 7.79786C1.77728 7.92679 1.77735 8.07364 1.84699 8.20209C2.19241 8.83919 2.89458 9.97267 3.94294 10.9395C4.98794 11.9032 6.33712 12.6668 7.99997 12.6668C9.09957 12.6668 10.0599 12.3336 10.8868 11.8298L9.59732 10.5403C8.43827 11.2706 6.8882 11.131 5.87864 10.1215C4.86909 9.11193 4.72948 7.56186 5.45981 6.40281L4.03457 4.97757ZM6.44811 7.39112L8.60901 9.55201C8.01093 9.78639 7.30472 9.66194 6.82145 9.17867C6.33819 8.69541 6.21374 7.9892 6.44811 7.39112Z" fill="#0D0D0D"/>
<path d="M6.81556 3.46675C7.18897 3.38117 7.58347 3.3335 7.99997 3.3335C9.66282 3.3335 11.012 4.09716 12.057 5.06087C13.1054 6.02766 13.8075 7.16114 14.153 7.79823C14.2223 7.92613 14.2225 8.07385 14.1523 8.2033C13.9632 8.55189 13.664 9.05544 13.2557 9.60051C13.035 9.89521 13.095 10.313 13.3897 10.5338C13.6844 10.7545 14.1022 10.6945 14.3229 10.3998C14.7784 9.79162 15.1118 9.23078 15.3243 8.83897C15.6087 8.31464 15.6102 7.68865 15.3251 7.16272C14.9434 6.45883 14.1589 5.18548 12.9609 4.0807C11.7596 2.97282 10.1028 2.00016 7.99997 2.00016C7.48067 2.00016 6.98628 2.05972 6.51772 2.16711C6.15883 2.24935 5.93457 2.60696 6.01682 2.96585C6.09907 3.32473 6.45668 3.54899 6.81556 3.46675Z" fill="#0D0D0D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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;

View File

@@ -53,6 +53,9 @@
@home="() => fitView({ maxZoom: 1 })"
/>
<image-preview ref="imagePreviewRef" />
<baseModal ref="three">
</baseModal>
</template>
<script setup lang="ts">
@@ -64,6 +67,7 @@
import headerTools from './components/header-tools.vue'
import zoom from '../components/zoom.vue'
import imagePreview from '../components/image-preview.vue'
import baseModal from '../components/base-modal.vue'
// 节点
import node from './components/node.vue'
import resultImage from './components/nodes/result-image.vue'

View File

@@ -0,0 +1,51 @@
<template>
<el-dialog
class="base-modal"
v-model="showDialog"
align-center
:show-close="false"
width="70vw"
style="border-radius: 20px; padding: 0; --el-dialog-padding-primary: 0"
>
<template #header="{ close }">
<div class="header-close" @click="close">
<svg-icon name="close" size="23" size-unit="px" />
</div>
</template>
<div class="modal-box">
<slot></slot>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
const showDialog = ref(false)
const open = (url_: any) => {
showDialog.value = true
}
const close = () => {
showDialog.value = false
}
defineExpose({
open,
close
})
</script>
<style lang="less" scoped>
.header-close {
position: absolute;
top: 30px;
right: 40px;
width: 40px;
height: 40px;
cursor: pointer;
}
.modal-box {
width: 100%;
height: 70vh;
padding: 67px;
}
</style>

View File

@@ -69,6 +69,7 @@
position: absolute;
top: 3rem;
right: 3rem;
--my-info-bgColor: #fff;
}
> .close-btn {
cursor: pointer;

View File

@@ -72,9 +72,9 @@
min-width: 18rem;
height: 4.3rem;
margin-right: 1rem;
background-color: rgba(255, 252, 244, 1);
border: 1px solid #ffcf90;
border-radius: 0.8rem;
background-color: var(--my-info-bgColor, rgba(255, 252, 244, 1));
> .credits {
flex: 1;
font-size: 1.3rem;