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

This commit is contained in:
2026-03-24 13:59:19 +08:00
37 changed files with 817 additions and 678 deletions

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.157122 13.4725L0.526449 10.3737C0.550847 10.1699 0.642783 9.98015 0.787096 9.83413L9.86461 0.654119C10.1812 0.334063 10.6118 0.151974 11.062 0.149342C11.5123 0.146814 11.9457 0.324063 12.2659 0.640637L13.8892 2.24581C14.2094 2.56248 14.3915 2.99386 14.394 3.44416C14.3964 3.89432 14.2192 4.32698 13.9027 4.64714L4.8252 13.8272C4.68081 13.9731 4.49215 14.0672 4.28861 14.0938L1.19415 14.4979C1.05466 14.5161 0.912174 14.5022 0.778872 14.4573C0.645754 14.4124 0.525008 14.3374 0.425099 14.2386C0.325191 14.1398 0.248858 14.0199 0.202421 13.8873C0.167635 13.7878 0.14946 13.6829 0.149902 13.578L0.157122 13.4725ZM11.752 5.29496L13.1397 3.89164C13.2557 3.77408 13.3206 3.61539 13.3198 3.45019C13.3189 3.28482 13.2522 3.12618 13.1347 3.00982L11.5104 1.40368C11.3928 1.28748 11.2334 1.22262 11.068 1.22354C10.9028 1.22458 10.7449 1.29127 10.6286 1.40863L9.24097 2.81195L11.752 5.29496ZM1.84678 13.3302L4.09809 13.0363L10.9965 6.05996L8.4845 3.57598L1.58606 10.5523L1.31747 12.8068L1.24565 13.4088L1.84678 13.3302Z" fill="#0D0D0D" stroke="black" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.26 0.555138L15.4449 4.74003C15.6209 4.91603 15.7605 5.12497 15.8557 5.35492C15.951 5.58488 16 5.83134 16 6.08024C16 6.32914 15.951 6.5756 15.8557 6.80556C15.7605 7.03551 15.6209 7.24445 15.4449 7.42045L8.13225 14.7322L13.2682 14.7331C13.421 14.7329 13.5687 14.7881 13.6839 14.8884C13.7992 14.9888 13.8741 15.1275 13.8949 15.2789L13.9 15.3648C13.9 15.5175 13.8447 15.665 13.7443 15.7801C13.644 15.8951 13.5054 15.97 13.3541 15.9907L13.2682 15.9966L6.18385 15.9975C5.91791 16.0124 5.65181 15.9709 5.403 15.8758C5.15418 15.7807 4.92826 15.6342 4.74003 15.4457L0.555138 11.26C0.379138 11.084 0.239526 10.875 0.144276 10.6451C0.0490248 10.4151 0 10.1687 0 9.91976C0 9.67086 0.0490248 9.4244 0.144276 9.19444C0.239526 8.96449 0.379138 8.75555 0.555138 8.57955L8.57955 0.555138C8.75555 0.379138 8.96449 0.239527 9.19444 0.144276C9.4244 0.049025 9.67086 0 9.91976 0C10.1687 0 10.4151 0.049025 10.6451 0.144276C10.875 0.239527 11.084 0.379138 11.26 0.555138ZM2.70065 8.21986L1.44805 9.4733C1.32974 9.59176 1.26328 9.75234 1.26328 9.91976C1.26328 10.0872 1.32974 10.2478 1.44805 10.3662L5.63378 14.5519C5.75677 14.6749 5.9185 14.7373 6.08024 14.7373L6.10467 14.7331L6.13668 14.7347C6.28414 14.7216 6.42224 14.6569 6.5267 14.5519L7.7793 13.2993L2.69981 8.21986H2.70065ZM9.4733 1.44805L3.59356 7.3261L8.67305 12.4056L14.552 6.52754C14.6107 6.46887 14.6572 6.3992 14.689 6.32253C14.7208 6.24585 14.7371 6.16366 14.7371 6.08066C14.7371 5.99766 14.7208 5.91547 14.689 5.8388C14.6572 5.76212 14.6107 5.69245 14.552 5.63378L10.3662 1.44805C10.2478 1.32974 10.0872 1.26328 9.91976 1.26328C9.75234 1.26328 9.59176 1.32974 9.4733 1.44805Z" fill="#212121"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 962 B

After

Width:  |  Height:  |  Size: 962 B

View File

@@ -1,23 +1,25 @@
<template> <template>
<transition name="fade"> <transition name="fade">
<div v-if="show" class="ai-selectbox-panel"> <div v-if="show" class="ai-selectbox-panel">
<div> <div
<span class="icon"><svg-icon name="dc-add" size="16" /></span> v-for="item in list"
<span class="label">Add</span> :key="item.type"
</div> :class="{ active: item.name === props.currentTool }"
<div> >
<span class="icon"><svg-icon name="dc-remove" size="16" /></span> <span class="icon"><svg-icon :name="item.name" size="16" /></span>
<span class="label">Remove</span> <span class="label">{{ item.label }}</span>
</div> </div>
<button>创建</button> <button>创建</button>
</div> </div>
</transition> </transition>
<brush-control-panel :currentTool="show ? 'draw' : ''" style="top: 14rem" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject, computed, watch } from 'vue' import { ref, inject, computed, watch } from 'vue'
import depthSlider from './tools/depth-slider.vue' import depthSlider from './tools/depth-slider.vue'
import { OperationType } from '../tools/layerHelper' import { OperationType, AI_SELECTBOX_TYPE } from '../tools/layerHelper'
const props = defineProps({ const props = defineProps({
currentTool: { required: true, type: [String, null] } currentTool: { required: true, type: [String, null] }
}) })
@@ -25,6 +27,28 @@
const toolManager = inject('toolManager') as any const toolManager = inject('toolManager') as any
const showTools = [OperationType.SELECTBOX] const showTools = [OperationType.SELECTBOX]
const show = computed(() => showTools.includes(props.currentTool)) const show = computed(() => showTools.includes(props.currentTool))
const list = ref([
{
type: AI_SELECTBOX_TYPE.ADD,
name: 'dc-add_sb',
label: 'Add'
},
{
type: AI_SELECTBOX_TYPE.REMOVE,
name: 'dc-remove_sb',
label: 'Remove'
},
{
type: AI_SELECTBOX_TYPE.DRAW,
name: 'dc-brush_sb',
label: 'Brush'
},
{
type: AI_SELECTBOX_TYPE.ERASER,
name: 'dc-erase_sb',
label: 'Erase'
}
])
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
// 淡入淡出动画 // 淡入淡出动画

View File

@@ -46,6 +46,10 @@
watch(brushState, (value) => { watch(brushState, (value) => {
if (value) updateBrushState() if (value) updateBrushState()
}) })
watch(
() => props.currentTool,
(value) => updateBrushState()
)
const brushSize = ref(40) const brushSize = ref(40)
const brushOpacity = ref(100) const brushOpacity = ref(100)
const brushColor = ref('#000000') const brushColor = ref('#000000')

View File

@@ -33,8 +33,8 @@
const isShow = computed(() => isRepeat.value) const isShow = computed(() => isRepeat.value)
const updateActiveObject = () => { const updateActiveObject = () => {
const obj = layers.value.find((v: any) => v.info.id === activeID.value) const layer = layerManager.getActiveLayer()
activeObject.value = obj ? JSON.parse(JSON.stringify(obj)) : null activeObject.value = layer ? JSON.parse(JSON.stringify(layer)) : null
} }
watch(layers, () => updateActiveObject()) watch(layers, () => updateActiveObject())
watch(activeID, () => updateActiveObject()) watch(activeID, () => updateActiveObject())

View File

@@ -1,33 +1,43 @@
<template> <template>
<div class="layer-item" @click="onClickLayer"> <div class="layer-item">
<div class="drag"><svg-icon name="dc-drag" size="18" /></div> <div class="item" @click="onClickLayer">
<div class="thumb"> <div class="drag"><svg-icon name="dc-drag" size="18" /></div>
<img v-if="layer.thumbnail" :src="layer.thumbnail" /> <div class="thumb">
</div> <img v-if="layer.thumbnail" :src="layer.thumbnail" />
<div class="name"> </div>
<div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name"> <div class="name">
{{ layer.info.name || '未命名图层' }} <div @dblclick="onClickEditName" v-if="!editName" :title="layer.info.name">
{{ 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 @click.stop="onClickLock">
<svg-icon v-show="!layer.info.lock" name="dc-lock_0" size="15" />
<svg-icon v-show="layer.info.lock" name="dc-lock_1" size="15" color="#1890ff" />
</span>
<span @click.stop="onClickShowHide"
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
/></span>
<span @click.stop="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
<span
v-if="isGroup"
@click.stop="onShowGroup"
class="show-group"
:class="{ active: layer.info.showChildren }"
>
<svg-icon name="dc-down_arrow" size="11" />
</span>
</div> </div>
<input
v-else
type="text"
ref="nameInputRef"
:value="layer.info.name"
@blur="onChangeName"
@keyup.enter="onChangeName"
/>
</div>
<div class="icons">
<span @click.stop="onClickLock">
<svg-icon v-show="!layer.info.lock" name="dc-lock_0" size="15" />
<svg-icon v-show="layer.info.lock" name="dc-lock_1" size="15" color="#1890ff" />
</span>
<span @click.stop="onClickShowHide"
><svg-icon :name="layer.visible ? 'dc-show' : 'dc-hide'" size="15"
/></span>
<span @click.stop="onClickDelete"><svg-icon name="dc-delete" size="13" /></span>
<!-- <span><svg-icon name="dc-down_arrow" size="11" /></span> -->
</div> </div>
<slot></slot>
</div> </div>
</template> </template>
@@ -42,6 +52,10 @@
layer: { layer: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
isGroup: {
type: Boolean,
default: false
} }
}) })
const nameInputRef = ref(null) const nameInputRef = ref(null)
@@ -72,82 +86,98 @@
const info = props.layer.info const info = props.layer.info
layerManager.setLayerLockById(info.id, !info.lock) layerManager.setLayerLockById(info.id, !info.lock)
} }
const onShowGroup = () => {
props.layer.info.showChildren = !props.layer.info.showChildren
// layerManager.setLayerGroupVisibleById(props.layer.info.id, !props.layer.visible)
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.layer-item { .layer-item {
width: 100%; &:last-child > .item {
height: 9.5rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1.4rem;
background-color: #fafafa;
gap: 1rem;
border-bottom: 0.2rem solid #ededed;
&:last-child {
border-bottom: none; border-bottom: none;
} }
&:not([draging='true']):hover { &:not([draging='true']) > .item:hover {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
&.active { &.active > .item {
background-color: #ededed !important; background-color: #ededed !important;
} }
&.drag { &.drag {
opacity: 0; opacity: 0;
} }
&.ghost, &.ghost > .item,
&.chosen { &.chosen > .item {
box-shadow: inset 0 0 5px #aaa; box-shadow: inset 0 0 5px #aaa;
background-color: #ededed !important; background-color: #ededed !important;
} }
> .drag { > .item {
padding: 0.3rem; width: 100%;
cursor: move; height: 9.5rem;
}
> .thumb {
width: 5.6rem;
height: 5.6rem;
border-radius: 0.5rem;
overflow: hidden;
border: 0.2rem solid #ebebeb;
background-color: #fff;
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
> .name {
flex: 1;
font-size: 1.4rem;
overflow: hidden;
> div {
cursor: pointer;
height: 3rem;
line-height: 3rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
> input {
width: 100%;
height: 3rem;
}
}
> .icons {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.8rem; padding: 0 1.4rem;
> span { background-color: #fafafa;
cursor: pointer; gap: 1rem;
width: 1.6rem; border-bottom: 0.2rem solid #ededed;
height: 1.6rem;
> .drag {
padding: 0.3rem;
cursor: move;
}
> .thumb {
width: 5.6rem;
height: 5.6rem;
border-radius: 0.5rem;
overflow: hidden;
border: 0.2rem solid #ebebeb;
background-color: #fff;
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
> .name {
flex: 1;
font-size: 1.4rem;
overflow: hidden;
> div {
cursor: pointer;
height: 3rem;
line-height: 3rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
> input {
width: 100%;
height: 3rem;
}
}
> .icons {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.8rem;
> span {
cursor: pointer;
width: 1.6rem;
height: 1.6rem;
display: flex;
align-items: center;
justify-content: center;
}
> .show-group {
> .c-svg {
transition: transform 0.2s;
transform: rotate(-90deg);
}
&.active > .c-svg {
transform: rotate(0deg);
}
}
} }
} }
} }

View File

@@ -12,28 +12,16 @@
<div class="content"> <div class="content">
<VueDraggable <VueDraggable
:model-value="list" :model-value="list"
@start="handleDragStart" @start="(e) => handleDragStart(e)"
@end="handleDragEnd" @end="(e) => handleDragEnd(e)"
@add="(e) => handleAdd(e)"
class="sortable-layers" class="sortable-layers"
:data-container-type="'root'" v-bind="config"
:data-parent-id="null"
:animation="250"
:disabled="false"
handle=".drag"
ghost-class="ghost"
chosen-class="chosen"
drag-class="drag"
:group="{ :group="{
name: 'groupName', name: 'group',
pull: true, pull: true,
put: true put: true
}" }"
:swap-threshold="0.5"
:empty-insert-threshold="5"
:force-fallback="false"
:fallback-tolerance="3"
:scroll-sensitivity="100"
:scroll-speed="10"
> >
<layer-item <layer-item
v-for="layer in list" v-for="layer in list"
@@ -41,7 +29,34 @@
:layer="layer" :layer="layer"
:draging="draging" :draging="draging"
:class="{ active: layer.info.id === layerManager.activeID.value }" :class="{ active: layer.info.id === layerManager.activeID.value }"
/> :is-group="layer.type === 'group'"
>
<VueDraggable
v-if="layer.type === 'group'"
v-show="layer.info.showChildren"
:model-value="layer.children"
@start="(e) => handleDragStart(e, layer)"
@end="(e) => handleDragEnd(e, layer)"
@add="(e) => handleAdd(e, layer)"
class="sortable-layers-child"
v-bind="config"
:group="{
name: 'child_' + layer.info.id,
pull: true,
put: true
}"
>
<layer-item
v-for="child in layer.children"
:key="child.info.id"
:layer="child"
:draging="draging"
:class="{
active: child.info.id === layerManager.activeID.value
}"
/>
</VueDraggable>
</layer-item>
</VueDraggable> </VueDraggable>
</div> </div>
</div> </div>
@@ -53,14 +68,65 @@
import layerItem from './layer-item.vue' import layerItem from './layer-item.vue'
const draging = ref(false) const draging = ref(false)
const layerManager = inject('layerManager') as any const layerManager = inject('layerManager') as any
const canvasManager = inject('canvasManager') as any
const list = computed(() => layerManager.layers.value) const list = computed(() => layerManager.layers.value)
const handleDragStart = () => { const config = ref({
draging.value = true 'data-container-type': 'root',
'data-parent-id': 'null',
animation: 250,
disabled: false,
handle: '.drag',
'ghost-class': 'ghost',
'chosen-class': 'chosen',
'drag-class': 'drag',
'swap-threshold': 0.5,
'empty-insert-threshold': 5,
'force-fallback': false,
'fallback-tolerance': 3,
'scroll-sensitivity': 100,
'scroll-speed': 10
})
const startParent = ref(null)
const clearData = () => {
startParent.value = null
} }
const handleDragEnd = (event) => { const handleDragStart = (event, parent?) => {
draging.value = true
startParent.value = parent
}
const moveElementInPlace = (arr, oldIndex, newIndex) => {
if (oldIndex === newIndex) return arr
const movedItem = arr[oldIndex]
// 移除元素
arr.splice(oldIndex, 1)
// 插入到新位置
arr.splice(newIndex, 0, movedItem)
return arr
}
const handleDragEnd = (event, parent?) => {
draging.value = false draging.value = false
const { from, to, oldIndex, newIndex, data } = event const { from, to, oldIndex, newIndex, data } = event
layerManager.dragSort(data.info.id, newIndex) if (from !== to) return
const arr = parent ? parent.children : layerManager.layers.value
moveElementInPlace(arr, oldIndex, newIndex)
clearData()
layerManager.sortLayers(true)
}
const handleAdd = (event, parent?) => {
const { from, to, oldIndex, newIndex, data } = event
if (data.type === 'group') return
console.log('跨级拖动', startParent.value, oldIndex, parent, newIndex)
const oldArr = startParent.value ? startParent.value.children : layerManager.layers.value
const arr = parent ? parent.children : layerManager.layers.value
const layer = oldArr.splice(oldIndex, 1)[0]
if (parent) {
layer.info.parentId = parent.info.id
} else {
delete layer.info.parentId
}
arr.splice(newIndex, 0, layer)
clearData()
layerManager.sortLayers(true)
} }
const addLayer = () => { const addLayer = () => {
layerManager.createEmptyLayer(true, true) layerManager.createEmptyLayer(true, true)
@@ -116,4 +182,12 @@
} }
} }
} }
.sortable-layers-child {
border-left: 2.5rem solid #e8e8e8;
border-bottom: 0.2rem solid #ededed;
// min-height: 5rem;
&:last-child {
border-bottom: none;
}
}
</style> </style>

View File

@@ -1,5 +1,6 @@
import { fabric } from 'fabric-with-all' import { fabric } from 'fabric-with-all'
import { createId } from '../../tools/tools' import { OperationType, AI_SELECTBOX_TYPE } from '../tools/layerHelper'
import { getObjectAlphaToCanvas, traceImageContour } from '../tools/canvasMethod'
/** 智能框选工具管理器 */ /** 智能框选工具管理器 */
export class AISelectboxToolManager { export class AISelectboxToolManager {
@@ -7,6 +8,7 @@ export class AISelectboxToolManager {
canvasManager: any canvasManager: any
stateManager: any stateManager: any
layerManager: any layerManager: any
toolManager: any
isDragging: boolean = false isDragging: boolean = false
startX: number = 0 startX: number = 0
@@ -16,7 +18,16 @@ export class AISelectboxToolManager {
this.canvasManager = options.canvasManager this.canvasManager = options.canvasManager
this.stateManager = options.stateManager this.stateManager = options.stateManager
this.layerManager = options.layerManager this.layerManager = options.layerManager
this.toolManager = options.toolManager
}
/** 处理切换工具 */
handleToolChange(oldTool: string, newTool: string) {
if (newTool === OperationType.SELECTBOX) {
// 切换到智能框选工具
} else {
// 切换到普通框选工具
}
} }
mouseDownEvent(e) { mouseDownEvent(e) {
this.isDragging = true this.isDragging = true
@@ -63,7 +74,7 @@ export class AISelectboxToolManager {
this.canvasManager.canvas.remove(this.demoObject) this.canvasManager.canvas.remove(this.demoObject)
this.canvasManager.canvas.renderAll() this.canvasManager.canvas.renderAll()
this.createSelectbox() // this.createSelectbox()
} }
@@ -81,15 +92,11 @@ export class AISelectboxToolManager {
stroke: "rgba(255, 77, 71, 1)", stroke: "rgba(255, 77, 71, 1)",
strokeWidth: 1.5, strokeWidth: 1.5,
strokeDashArray: [4, 4], strokeDashArray: [4, 4],
fill: "rgba(255, 186, 186, 0.5)", fill: "transparent",
strokeUniform: true, // 保持描边宽度不随缩放改变 strokeUniform: true, // 保持描边宽度不随缩放改变
// strokeLineCap: "round",// 折线端点样式 selectable: false,
// strokeLineJoin: "bevel", // 折线连接样式 evented: false,
// selectable: false, absolutePositioned: true,
// evented: false,
excludeFromExport: true,
hoverCursor: "default",
moveCursor: "default",
}; };
async createSelectbox() { async createSelectbox() {
const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png" const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png"
@@ -112,200 +119,31 @@ export class AISelectboxToolManager {
}).join(" L "); }).join(" L ");
const path = new fabric.Path(`M ${str} z`); const path = new fabric.Path(`M ${str} z`);
path.set({ path.set({
left: left + minX * scaleX, left: left + minX,
top: top + minY * scaleY, top: top + minY,
scaleX: scaleX, scaleX: scaleX,
scaleY: scaleY, scaleY: scaleY,
...this.selectionStyle, ...this.selectionStyle,
}); });
const rect1 = new fabric.Rect({ const group = await this.layerManager.createGroupLayer({
left: 0, clipPath: path,
top: 0, }, false, false)
width: 100, const rect = await this.layerManager.createRectLayer({
height: 100, width: path.width,
fill: '#f00', height: path.height,
info: { left: left + minX,
id: createId("rect"), top: top + minY,
name: '矩形图层', fill: "rgba(255, 186, 186, 0.5)",
} info: { parentId: group.info.id },
}) }, false, true)
const rect2 = new fabric.Rect({ await this.canvasManager.updateSubLayerClipPath()
left: 200, await this.layerManager.updateLayerThumbnailsById(rect.info.id, "", false)
top: 200, await this.layerManager.updateLayerThumbnailsById(group.info.id, rect.thumbnail)
width: 100, this.stateManager.recordState()
height: 100, this.toolManager.setTool(OperationType.SELECT)
fill: '#ff0',
info: {
id: createId("rect"),
name: '矩形图层',
}
})
this.layerManager.createGroupLayer({
child: [rect1, rect2],
})
// this.canvasManager.canvas.add(path)
// this.canvasManager.canvas.renderAll()
} }
dispose() { } dispose() { }
} }
/**
* 获取对象黑白通道画布
* @param {fabric.Object} object - 要处理的 fabric 对象
* @param {ImageData} revData - 相反的ImageData白通道的相同位置是否为透明revData为白色为透明黑色为不透明
* @param {number} diff - 差值,默认 25
* @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 }
* @param {boolean} isMerge - 是否合并true=合并revDatafalse=反转revData
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
*/
export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) {
const image = object.getElement();
if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") {
console.warn("对象不是图片");
return null;
}
const { width, height } = image;
if (!width || !height) {
console.warn("对象没有元素");
return null;
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, width, height);
const data = ctx.getImageData(0, 0, width, height);
for (let i = 0; i < data.data.length; i += 4) {
const r = data.data[i + 0];
const g = data.data[i + 1];
const b = data.data[i + 2];
const a = data.data[i + 3];
const revR = revData?.data[i + 0] || 0;
const revG = revData?.data[i + 1] || 0;
const revB = revData?.data[i + 2] || 0;
const revA = revData?.data[i + 3] || 0;
let isHave = false;
if (r || g || b || a) {
if (revR > diff || revG > diff || revB > diff || revA > diff) {
isHave = false;
} else {
isHave = true;
}
}
if (isMerge && (revR || revG || revB || revA)) isHave = true;
if (isHave) {
data.data[i + 0] = rgba.r;
data.data[i + 1] = rgba.g;
data.data[i + 2] = rgba.b;
data.data[i + 3] = rgba.a;
} else {
data.data[i + 0] = 0;
data.data[i + 1] = 0;
data.data[i + 2] = 0;
data.data[i + 3] = 0;
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(data, 0, 0);
return canvas;
}
/**
* 图片边界跟踪算法(透明底)
* @param {HTMLCanvasElement} canvas - canvas元素
* @param {Number} scale - 缩放比例
* @returns {Array} 边界点数组 [{x, y}, ...]
*/
export function traceImageContour(canvas) {
const ctx = canvas.getContext("2d", { willReadFrequently: true });
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
// 查找起始点(第一个不透明像素)
let startX = -1;
let startY = -1;
outer: for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
if (data[index + 3] > 0) {
startX = x;
startY = y;
break outer;
}
}
}
if (startX === -1) return []; // 没有不透明像素
// Moore-Neighbor边界跟踪算法
const contour = [];
const visited = new Set();
const directions = [
[-1, 0],
[-1, -1],
[0, -1],
[1, -1],
[1, 0],
[1, 1],
[0, 1],
[-1, 1],
];
let currentX = startX;
let currentY = startY;
let backtrackDir = 4; // 起始方向:右
do {
const pointKey = `${currentX},${currentY}`;
if (!visited.has(pointKey)) {
contour.push({ x: currentX, y: currentY });
visited.add(pointKey);
}
// 从右方向开始顺时针查找
let found = false;
for (let i = 0; i < 8; i++) {
const dir = (backtrackDir + i) % 8;
const dx = directions[dir][0];
const dy = directions[dir][1];
const checkX = currentX + dx;
const checkY = currentY + dy;
if (
checkX >= 0 &&
checkX < width &&
checkY >= 0 &&
checkY < height
) {
const index = (checkY * width + checkX) * 4;
if (data[index + 3] > 0) {
currentX = checkX;
currentY = checkY;
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
found = true;
break;
}
}
}
if (!found) break;
} while (
!(currentX === startX && currentY === startY) &&
visited.size < width * height
);
return contour;
}

View File

@@ -5,11 +5,12 @@ import { AnimationManager } from './AnimationManager'
import { detectDeviceType } from '../tools/index' import { detectDeviceType } from '../tools/index'
import { CanvasEventManager } from "./events/CanvasEventManager"; import { CanvasEventManager } from "./events/CanvasEventManager";
import { OperationType } from '../tools/layerHelper' import { OperationType } from '../tools/layerHelper'
import { cloneObjects } from '../tools/canvasMethod'
import { createId } from '../../tools/tools' import { createId } from '../../tools/tools'
import md5 from 'md5' import md5 from 'md5'
// 自定义画布转对象属性 // 自定义画布转对象属性
fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail"]; fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail", "absolutePositioned"];
fabric.Object.prototype.toObject_ = fabric.Object.prototype.toObject fabric.Object.prototype.toObject_ = fabric.Object.prototype.toObject
fabric.Object.prototype.toObject = function () { fabric.Object.prototype.toObject = function () {
const args = [...arguments] const args = [...arguments]
@@ -129,25 +130,28 @@ export class CanvasManager {
/** 测试-开始 */ /** 测试-开始 */
// this.stateManager.setIsRecord(false) // this.stateManager.setIsRecord(false)
// const rect = await this.layerManager.createRectLayer({ left: 200 }) // const groupObject = await this.layerManager.createGroupLayer()
// await this.layerManager.createStarLayer({ left: 400 }) // const parentId = groupObject.info.id
// await this.layerManager.createArrowLayer({ left: 600 }) // const rect = await this.layerManager.createRectLayer({ left: 200, info: { parentId } })
// const star = await this.layerManager.createStarLayer({ left: 400, info: { parentId } })
// const arrow = await this.layerManager.createArrowLayer({ left: 600, info: { parentId } })
// await this.layerManager.createGroupLayer()
// this.layerManager.setActiveID(rect.info.id) // this.layerManager.setActiveID(rect.info.id)
// this.stateManager.setIsRecord(true) // this.stateManager.setIsRecord(true)
/** 测试-结束 */ /** 测试-结束 */
this.resetZoom(false, true)// 画布居中 this.resetZoom(false, true)// 画布居中
this.stateManager.toolManager.setTool(OperationType.SELECT)
this.layerManager.updateLayers() this.layerManager.updateLayers()
this.stateManager.recordState() this.stateManager.recordState()
// this.stateManager.toolManager.setTool(OperationType.RECTANGLE)
} }
/** 画布添加对象 */ /** 画布添加对象 */
async add(obj: any, isRecord = true) { async add(obj: any, isRecord = true) {
this.canvas.add(obj) this.canvas.add(obj)
const id = obj?.info?.id || "" const id = obj?.info?.id || ""
if (id) { if (id) {
this.layerManager.updateLayers() await this.layerManager.updateLayers(!!obj.info.parentId)
this.renderAll() this.renderAll()
await this.layerManager.updateLayerThumbnailsById(id) await this.layerManager.updateLayerThumbnailsById(id)
} }
@@ -162,6 +166,35 @@ export class CanvasManager {
} }
} }
/** 更新子图层裁剪区域 */
async updateSubLayerClipPath() {
const objects = this.getObjects().filter((v: any) => v.type !== "group" && !!v.info?.id);
for (let i = 0; i < objects.length; i++) {
let object = objects[i]
if (object.clipPath) object.set({ clipPath: null })
let group = this.getObjectById(object.info.parentId)
if (!group) continue
let path = group.clipPath
if (!path) continue
let clipPath = await cloneObjects([path]).then((v) => v[0])
clipPath.set({
absolutePositioned: true,
})
object.set({ clipPath })
}
this.renderAll()
}
/** 排序画布对象 */
async sortObjectByIds(ids: string[], isRecord?: boolean) {
ids.forEach((id, index) => {
this.canvas.moveTo(this.getObjectById(id), index)
})
await this.updateSubLayerClipPath()
this.renderAll()
if (isRecord) this.stateManager.recordState()
}
/** 设置画布事件 */ /** 设置画布事件 */
setupCanvasEvents() { setupCanvasEvents() {
// 创建画布事件管理器 // 创建画布事件管理器
@@ -177,8 +210,18 @@ export class CanvasManager {
} }
/** 设置激活对象 */ /** 设置激活对象 */
setActiveObjectById(id: string) { setActiveObjectById(id: string) {
this.discardActiveObject()
const obj = this.getObjectById(id) const obj = this.getObjectById(id)
if (obj && obj.evented) this.canvas.setActiveObject(obj) if (!obj) return
if (obj.type === "group") {
const objects = [];
this.getObjects().forEach((item: any) => {
if (item?.info?.parentId === id) objects.push(item)
})
if (objects.length > 0) this.canvas.setActiveObject(new fabric.ActiveSelection(objects, { canvas: this.canvas }));
} else {
if (obj.evented) this.canvas.setActiveObject(obj)
}
this.renderAll() this.renderAll()
} }
resetZoom(animated = true, adaptive = true) { resetZoom(animated = true, adaptive = true) {
@@ -222,11 +265,11 @@ export class CanvasManager {
renderAll() { renderAll() {
this.canvas.renderAll() this.canvas.renderAll()
} }
deleteObjectById(id: string) { deleteObjectById(id: string, isUpdate = true) {
const object = this.getObjectById(id) const object = this.getObjectById(id)
if (object) { if (object) {
this.canvas.remove(object) this.canvas.remove(object)
this.layerManager.updateLayers() if (isUpdate) this.layerManager.updateLayers()
this.renderAll() this.renderAll()
} }
} }
@@ -235,13 +278,6 @@ export class CanvasManager {
this.canvas.discardActiveObject() this.canvas.discardActiveObject()
this.renderAll() this.renderAll()
} }
// 拖拽排序
dragSort(id, newIndex) {
this.canvas.moveTo(this.getObjectById(id), newIndex)
this.layerManager.updateLayers()
this.renderAll()
this.stateManager.recordState()
}
/** 画笔事件 */ /** 画笔事件 */
setupBrushEvents() { setupBrushEvents() {
@@ -254,13 +290,13 @@ export class CanvasManager {
}; };
} }
/** 处理绘制图像 */ /** 处理绘制图像 */
handleDrawImage(fabricImage: fabric.Object) { async handleDrawImage(fabricImage: fabric.Object) {
const activeID = this.stateManager.layerManager.activeID.value const activeID = this.stateManager.layerManager.activeID.value
const activeLayer = this.getObjectById(activeID) const activeLayer = this.getObjectById(activeID)
if (activeLayer) { if (activeLayer && activeLayer.fill?.repeat !== "repeat") {
this.layerManager.imageMergeToLayer(activeLayer, fabricImage) this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
} else { } else {
const emptyLayer = this.layerManager.createEmptyLayer(false); const emptyLayer = await this.layerManager.createEmptyLayer(false);
this.layerManager.setActiveID(emptyLayer.info.id, false) this.layerManager.setActiveID(emptyLayer.info.id, false)
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage) this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
} }

View File

@@ -2,7 +2,7 @@ import { ref } from 'vue'
import { fabric } from 'fabric-with-all' import { fabric } from 'fabric-with-all'
import { createId } from '../../tools/tools' import { createId } from '../../tools/tools'
import { exportObjectsToImage, exportObjectToThumbnail } from '../tools/exportMethod' import { exportObjectsToImage, exportObjectToThumbnail } from '../tools/exportMethod'
import { OperationType } from '../tools/layerHelper' import { OperationType, BlendMode } from '../tools/layerHelper'
import { getArrowPath, cloneObjects, getStarArr } from '../tools/canvasMethod' import { getArrowPath, cloneObjects, getStarArr } from '../tools/canvasMethod'
export class LayerManager { export class LayerManager {
@@ -18,7 +18,12 @@ export class LayerManager {
} }
onMounted() { } onMounted() { }
setActiveID(id: string, isActive = true) { setActiveID(id: string, isActive = true) {
this.activeID.value = id const layer = this.getLayerById(id)
if (layer?.type === "group") {
this.activeID.value = ""
} else {
this.activeID.value = id
}
if (isActive) { if (isActive) {
this.canvasManager.setActiveObjectById(id) this.canvasManager.setActiveObjectById(id)
this.stateManager.toolManager.setTool(OperationType.SELECT) this.stateManager.toolManager.setTool(OperationType.SELECT)
@@ -28,7 +33,18 @@ export class LayerManager {
return this.getLayerById(this.activeID.value) return this.getLayerById(this.activeID.value)
} }
getLayerById(id) { getLayerById(id) {
return this.layers.value.find((item: any) => item.info.id === id) function call(arr) {
for (let i = 0; i < arr.length; i++) {
let v = arr[i]
if (v.info.id === id) return v
if (v.children) {
let layer = call(v.children)
if (layer) return layer
}
}
return null
}
return call(this.layers.value)
} }
setLayerNameById(id, name: string) { setLayerNameById(id, name: string) {
const layer = this.getLayerById(id) const layer = this.getLayerById(id)
@@ -82,6 +98,10 @@ export class LayerManager {
/** 删除指定图层 */ /** 删除指定图层 */
deleteLayerById(id, isActive = true) { deleteLayerById(id, isActive = true) {
const layer = this.getLayerById(id)
if (layer.children) {
layer.children.forEach(v => this.canvasManager.deleteObjectById(v.info.id, false))
}
this.canvasManager.deleteObjectById(id) this.canvasManager.deleteObjectById(id)
if (id === this.activeID.value && isActive) { if (id === this.activeID.value && isActive) {
this.setActiveID(this.layers.value[0]?.info?.id || "") this.setActiveID(this.layers.value[0]?.info?.id || "")
@@ -106,17 +126,38 @@ export class LayerManager {
this.setActiveID(newObject.info.id) this.setActiveID(newObject.info.id)
}) })
} }
// 拖拽排序 /** 根据layers排序图层 */
dragSort(id, newIndex) { async sortLayers(isRecord?: boolean) {
const index = Math.abs(this.layers.value.length - newIndex - 1) const ids = [];
this.canvasManager.dragSort(id, index) call(this.layers.value)
await this.canvasManager.sortObjectByIds(ids.reverse(), isRecord)
function call(arr) {
arr.forEach(v => {
ids.push(v.info.id)
if (v.children) call(v.children)
})
}
} }
// 更新图层列表 // 更新图层列表
updateLayers() { async updateLayers(isSort = false) {
this.layers.value = this.canvasManager.getObjects() const objects = this.canvasManager.getObjects().map(v => v.toObject()).filter(v => !!v.info?.id).reverse()
.filter((v: any) => !!v?.info?.id) objects.forEach(v => {
.reverse() if (v.type === "group") {
.map(v => v.toObject()) if (!v.children) v.children = []
return;
}
const parentId = v.info?.parentId
if (!parentId) return
objects.forEach((obj: any) => {
if (obj.info?.id !== parentId) return
if (!obj.children) obj.children = []
obj.children.push(v)
})
})
const layers = objects.filter(v => !v.info?.parentId)
this.layers.value = layers
if (isSort) await this.sortLayers()
} }
/** 设置图层位置-不设置默认居中 */ /** 设置图层位置-不设置默认居中 */
@@ -136,7 +177,7 @@ export class LayerManager {
} }
} }
/** 创建空图层 */ /** 创建空图层 */
createEmptyLayer(isRecord = true, isActive = false) { async createEmptyLayer(isRecord = true, isActive = false) {
const emptyObject = new fabric.Rect({ const emptyObject = new fabric.Rect({
width: 0, width: 0,
height: 0, height: 0,
@@ -147,37 +188,28 @@ export class LayerManager {
} }
}) })
this.setLayerPosition(emptyObject) this.setLayerPosition(emptyObject)
this.canvasManager.add(emptyObject, isRecord) await this.canvasManager.add(emptyObject, isRecord)
if (isActive) this.setActiveID(emptyObject.info.id, false) if (isActive) this.setActiveID(emptyObject.info.id, false)
return emptyObject return emptyObject
} }
/** 创建组图层 */ /** 创建组图层 */
createGroupLayer(options?: any, isRecord = true, isActive = false) { async createGroupLayer(options?: any, isRecord = true, isActive = false) {
const child = options?.child || [] const children = options?.children || []
delete options.child delete options.children
const groupObject = new fabric.Group(child, { const groupObject = new fabric.Group(children, {
// subTargetCheck: true, // 关键:检测子对象 ...(options || {}),
// interactive: true, // 启用交互 hasControls: false, // 不显示控制点
// hasControls: true, hasBorders: false, // 不显示边框
// hasBorders: true, selectable: false, // 不可选中(可选)
// // 子对象样式
// cornerColor: 'blue',
// cornerSize: 8,
// borderColor: 'green',
// // 允许子对象独立变换
// lockScalingX: false,
// lockScalingY: false,
// lockRotation: false,
info: { info: {
id: createId("group"), id: createId("group"),
name: '组图层', name: '智能选区组',
showChildren: true,
...(options?.info || {}), ...(options?.info || {}),
} }
}) })
// this.setLayerPosition(groupObject) this.setLayerPosition(groupObject, options)
this.canvasManager.add(groupObject, isRecord) await this.canvasManager.add(groupObject, isRecord)
if (isActive) this.setActiveID(groupObject.info.id, false) if (isActive) this.setActiveID(groupObject.info.id, false)
return groupObject return groupObject
} }
@@ -199,7 +231,7 @@ export class LayerManager {
return textObject return textObject
} }
/** 创建矩形图层 */ /** 创建矩形图层 */
async createRectLayer(options?: any, isActive = false) { async createRectLayer(options?: any, isRecord = true, isActive = true) {
const rectObject = new fabric.Rect({ const rectObject = new fabric.Rect({
width: 100, width: 100,
height: 100, height: 100,
@@ -213,12 +245,12 @@ export class LayerManager {
} }
}) })
this.setLayerPosition(rectObject, options) this.setLayerPosition(rectObject, options)
await this.canvasManager.add(rectObject) await this.canvasManager.add(rectObject, isRecord)
if (isActive) this.setActiveID(rectObject.info.id) if (isActive) this.setActiveID(rectObject.info.id)
return rectObject return rectObject
} }
/** 创建直线图层 */ /** 创建直线图层 */
async createLineLayer(options?: any, isActive = false) { async createLineLayer(options?: any, isRecord = true, isActive = true) {
const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0] const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0]
delete options.x1 delete options.x1
delete options.y1 delete options.y1
@@ -235,14 +267,13 @@ export class LayerManager {
} }
}) })
this.setLayerPosition(lineObject, options) this.setLayerPosition(lineObject, options)
await this.canvasManager.add(lineObject) await this.canvasManager.add(lineObject, isRecord)
if (isActive) this.setActiveID(lineObject.info.id) if (isActive) this.setActiveID(lineObject.info.id)
return lineObject return lineObject
} }
/** 创建椭圆图层 */ /** 创建椭圆图层 */
async createEllipseLayer(options?: any, isActive = false) { async createEllipseLayer(options?: any, isRecord = true, isActive = true) {
const ellipseObject = new fabric.Ellipse({ const ellipseObject = new fabric.Ellipse({
radius: 50,
fill: '#000', fill: '#000',
strokeWidth: 0, strokeWidth: 0,
...(options || {}), ...(options || {}),
@@ -258,7 +289,7 @@ export class LayerManager {
return ellipseObject return ellipseObject
} }
/** 创建三角形图层 */ /** 创建三角形图层 */
async createTriangleLayer(options?: any, isActive = false) { async createTriangleLayer(options?: any, isRecord = true, isActive = true) {
const triangleObject = new fabric.Triangle({ const triangleObject = new fabric.Triangle({
width: 100, width: 100,
height: 100, height: 100,
@@ -272,12 +303,12 @@ export class LayerManager {
} }
}) })
this.setLayerPosition(triangleObject, options) this.setLayerPosition(triangleObject, options)
await this.canvasManager.add(triangleObject) await this.canvasManager.add(triangleObject, isRecord)
if (isActive) this.setActiveID(triangleObject.info.id) if (isActive) this.setActiveID(triangleObject.info.id)
return triangleObject return triangleObject
} }
/** 创建五角星图层 */ /** 创建五角星图层 */
async createStarLayer(options?: any, isActive = false) { async createStarLayer(options?: any, isRecord = true, isActive = true) {
const width = options?.width || 100 const width = options?.width || 100
const height = options?.height || 100 const height = options?.height || 100
delete options.points delete options.points
@@ -292,12 +323,12 @@ export class LayerManager {
} }
}) })
this.setLayerPosition(starObject, options) this.setLayerPosition(starObject, options)
await this.canvasManager.add(starObject) await this.canvasManager.add(starObject, isRecord)
if (isActive) this.setActiveID(starObject.info.id) if (isActive) this.setActiveID(starObject.info.id)
return starObject return starObject
} }
/** 创建箭头图层 */ /** 创建箭头图层 */
async createArrowLayer(options?: any, isActive = false) { async createArrowLayer(options?: any, isRecord = true, isActive = true) {
const width = options?.width || 100 const width = options?.width || 100
const height = options?.height || 10 const height = options?.height || 10
delete options.width delete options.width
@@ -316,7 +347,7 @@ export class LayerManager {
} }
}); });
this.setLayerPosition(arrowObject, options) this.setLayerPosition(arrowObject, options)
this.canvasManager.add(arrowObject) await this.canvasManager.add(arrowObject, isRecord)
if (isActive) this.setActiveID(arrowObject.info.id) if (isActive) this.setActiveID(arrowObject.info.id)
return arrowObject return arrowObject
} }
@@ -324,7 +355,7 @@ export class LayerManager {
/** 创建图片图层 */ /** 创建图片图层 */
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true) { async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true, isActive = true) {
const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize(); const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize();
const imageObject = await new Promise((resolve) => { const imageObject = await new Promise((resolve) => {
@@ -350,7 +381,7 @@ export class LayerManager {
}) as fabric.Object }) as fabric.Object
this.setLayerPosition(imageObject, options) this.setLayerPosition(imageObject, options)
await this.canvasManager.add(imageObject, isRecord) await this.canvasManager.add(imageObject, isRecord)
this.setActiveID(imageObject.info.id) if (isActive) this.setActiveID(imageObject.info.id)
return imageObject return imageObject
} }
@@ -363,6 +394,7 @@ export class LayerManager {
left: info.left, left: info.left,
top: info.top, top: info.top,
info: { info: {
...(targetLayer?.info || {}),
id: createId("image"), id: createId("image"),
name: targetLayer?.info?.name || "合并图层", name: targetLayer?.info?.name || "合并图层",
} }
@@ -370,12 +402,17 @@ export class LayerManager {
resolve(img) resolve(img)
}, { crossOrigin: 'anonymous' }) }, { crossOrigin: 'anonymous' })
}) })
// console.log(mergedImage)
const index = this.canvasManager.getObjects().indexOf(targetLayer); const index = this.canvasManager.getObjects().indexOf(targetLayer);
this.deleteLayerById(targetLayer.info.id, false) this.deleteLayerById(targetLayer.info.id, false)
this.setActiveID(mergedImage.info.id, false)
const nid = mergedImage.info.id
await this.canvasManager.add(mergedImage, false); await this.canvasManager.add(mergedImage, false);
this.setActiveID(nid, false)
this.canvasManager.canvas.moveTo(mergedImage, index); this.canvasManager.canvas.moveTo(mergedImage, index);
// this.stateManager.objectManager.setBlendMode(nid, BlendMode.MULTIPLY)
// this.stateManager.objectManager.setFillRepeat(nid, false)
this.canvasManager.renderAll() this.canvasManager.renderAll()
this.updateLayers() this.updateLayers()
this.stateManager.recordState() this.stateManager.recordState()
@@ -391,12 +428,12 @@ export class LayerManager {
}) })
} }
/** 更新图层缩略图 */ /** 更新图层缩略图 */
async updateLayerThumbnailsById(id: string) { async updateLayerThumbnailsById(id: string, thumbnail?: string, isUpdate = true) {
const object = this.canvasManager.getObjectById(id); const object = this.canvasManager.getObjectById(id);
if (!object) return; if (!object) return;
const url = await exportObjectToThumbnail(object); const url = thumbnail || await exportObjectToThumbnail(object);
object.thumbnail = url object.thumbnail = url
this.updateLayers() if (isUpdate) this.updateLayers()
} }
dispose() { } dispose() { }

View File

@@ -87,7 +87,7 @@ export class ObjectManager {
} }
/** 设置平铺状态 */ /** 设置平铺状态 */
setFillRepeat(id: string) { setFillRepeat(id: string, isRecord = true) {
const object = this.canvasManager.getObjectById(id) const object = this.canvasManager.getObjectById(id)
if (!object) return console.warn('设置平铺状态失败对象不存在ID:', id) if (!object) return console.warn('设置平铺状态失败对象不存在ID:', id)
if (object.type !== 'image') return console.warn('设置平铺状态失败,对象不是图片类型:', id) if (object.type !== 'image') return console.warn('设置平铺状态失败,对象不是图片类型:', id)
@@ -133,7 +133,7 @@ export class ObjectManager {
}); });
rect.set("fill", pattern) rect.set("fill", pattern)
this.canvasManager.canvas.remove(object) this.canvasManager.canvas.remove(object)
this.canvasManager.add(rect) this.canvasManager.add(rect, isRecord)
} }
/** 获取填充对象 */ /** 获取填充对象 */
getFillRepeatObject(id: string) { getFillRepeatObject(id: string) {

View File

@@ -131,7 +131,7 @@ export class ShapeToolManager {
upRectangle(object) { upRectangle(object) {
if (object.width === 0) object.width = 100 if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100 if (object.height === 0) object.height = 100
this.layerManager.createRectLayer(object, true) this.layerManager.createRectLayer(object)
} }
/** 绘制直线 */ /** 绘制直线 */
@@ -151,7 +151,7 @@ export class ShapeToolManager {
}) })
} }
upLine(object) { upLine(object) {
this.layerManager.createLineLayer(object, true) this.layerManager.createLineLayer(object)
} }
/** 绘制椭圆 */ /** 绘制椭圆 */
@@ -170,7 +170,7 @@ export class ShapeToolManager {
upEllipse(object) { upEllipse(object) {
if (object.rx === 0) object.rx = 50 if (object.rx === 0) object.rx = 50
if (object.ry === 0) object.ry = 50 if (object.ry === 0) object.ry = 50
this.layerManager.createEllipseLayer(object, true) this.layerManager.createEllipseLayer(object)
} }
@@ -192,7 +192,7 @@ export class ShapeToolManager {
upTriangle(object) { upTriangle(object) {
if (object.width === 0) object.width = 100 if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100 if (object.height === 0) object.height = 100
this.layerManager.createTriangleLayer(object, true) this.layerManager.createTriangleLayer(object)
} }
@@ -217,7 +217,7 @@ export class ShapeToolManager {
upStar(object) { upStar(object) {
if (object.width === 0) object.width = 100 if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100 if (object.height === 0) object.height = 100
this.layerManager.createStarLayer(object, true) this.layerManager.createStarLayer(object)
} }
/** 绘制箭头 */ /** 绘制箭头 */
@@ -249,7 +249,7 @@ export class ShapeToolManager {
top: this.startY, top: this.startY,
}, true) }, true)
} else { } else {
this.layerManager.createArrowLayer(object, true) this.layerManager.createArrowLayer(object)
} }
} }

View File

@@ -38,6 +38,7 @@ export class StateManager {
brushManager: any brushManager: any
keyEventManager: any keyEventManager: any
objectManager: any objectManager: any
aiSelectboxToolManager: any
// 设置管理器 // 设置管理器
setManager(options) { setManager(options) {
options.eventManager && (this.eventManager = options.eventManager) options.eventManager && (this.eventManager = options.eventManager)
@@ -47,6 +48,7 @@ export class StateManager {
options.brushManager && (this.brushManager = options.brushManager) options.brushManager && (this.brushManager = options.brushManager)
options.keyEventManager && (this.keyEventManager = options.keyEventManager) options.keyEventManager && (this.keyEventManager = options.keyEventManager)
options.objectManager && (this.objectManager = options.objectManager) options.objectManager && (this.objectManager = options.objectManager)
options.aiSelectboxToolManager && (this.aiSelectboxToolManager = options.aiSelectboxToolManager)
} }
constructor(options) { constructor(options) {
this.mxHistory = ref(50) this.mxHistory = ref(50)
@@ -71,6 +73,7 @@ export class StateManager {
/** 记录状态 */ /** 记录状态 */
recordState() { recordState() {
if (this.running.value) return if (this.running.value) return
console.log("recordState")
this.running.value = true this.running.value = true
if (this.historyIndex.value < this.historyList.value.length - 1) { if (this.historyIndex.value < this.historyList.value.length - 1) {
this.historyList.value.splice(this.historyIndex.value + 1) this.historyList.value.splice(this.historyIndex.value + 1)

View File

@@ -101,6 +101,7 @@ export class ToolManager {
setTool(value: string) { setTool(value: string) {
const tool = this.tools.find((t) => t.name === value) const tool = this.tools.find((t) => t.name === value)
if (!tool) return console.warn(`工具${tool}不存在`) if (!tool) return console.warn(`工具${tool}不存在`)
const oldTool = this.currentTool.value
this.currentTool.value = tool.name this.currentTool.value = tool.name
this.canvasManager.canvas.defaultCursor = tool.cursor this.canvasManager.canvas.defaultCursor = tool.cursor
this.setCanvasEvented(!!tool.selection) this.setCanvasEvented(!!tool.selection)
@@ -110,6 +111,7 @@ export class ToolManager {
if (tool.setup) tool.setup() if (tool.setup) tool.setup()
this.stateManager?.aiSelectboxToolManager?.handleToolChange?.(oldTool, tool.name)
setTimeout(() => { setTimeout(() => {
this.canvasManager.renderAll() this.canvasManager.renderAll()
}); });
@@ -142,13 +144,16 @@ export class ToolManager {
const brushStore = this.brushManager?.brushStore const brushStore = this.brushManager?.brushStore
if (brushStore) { if (brushStore) {
// 同步基本属性 // 同步基本属性
this.brushManager.setBrushSize(brushStore.state.size); // this.brushManager.setBrushSize(brushStore.state.size);
this.brushManager.setBrushColor(brushStore.state.color); // this.brushManager.setBrushColor(brushStore.state.color);
this.brushManager.setBrushOpacity(brushStore.state.opacity); // this.brushManager.setBrushOpacity(brushStore.state.opacity);
// 同步笔刷类型 - 修复方法名使用正确的setBrushType方法 // 同步笔刷类型 - 修复方法名使用正确的setBrushType方法
this.brushManager.setBrushType("pencil"); this.brushManager.setBrushType("pencil");
} }
this.brushManager.setBrushSize(5);
this.brushManager.setBrushColor("#000");
this.brushManager.setBrushOpacity(1);
// 更新应用到画布 // 更新应用到画布
this.brushManager.updateBrush(); this.brushManager.updateBrush();
@@ -168,6 +173,7 @@ export class ToolManager {
this.brushManager.createEraser(); this.brushManager.createEraser();
} }
this.brushManager.setBrushSize(5);
this.stateManager.layerManager.setActiveObjectErasable() this.stateManager.layerManager.setActiveObjectErasable()
// 启用笔刷指示器 // 启用笔刷指示器
this._enableBrushIndicator(); this._enableBrushIndicator();

View File

@@ -34,6 +34,7 @@ export class CanvasEventManager {
} }
this.shapeToolManager = new ShapeToolManager(managers) this.shapeToolManager = new ShapeToolManager(managers)
this.aiSelectboxToolManager = new AISelectboxToolManager(managers) this.aiSelectboxToolManager = new AISelectboxToolManager(managers)
this.stateManager.setManager({ aiSelectboxToolManager: this.aiSelectboxToolManager })
// 初始化所有事件 // 初始化所有事件
this.initEvents(); this.initEvents();
@@ -59,9 +60,8 @@ export class CanvasEventManager {
// 共享事件 // 共享事件
this.setupSelectionEvents(); this.setupSelectionEvents();
this.setupObjectEvents(); this.setupObjectEvents();
// this.setupDoubleClickEvents(); this.setupDoubleClickEvents();
// this.setupHandlePathCreated();
} }
setupZoomEvents() { setupZoomEvents() {
@@ -730,8 +730,10 @@ export class CanvasEventManager {
}); });
this.canvas.on("object:modified", async (e) => { this.canvas.on("object:modified", async (e) => {
// updateLayers(e); // updateLayers(e);
const id = e.target?.info?.id; const target = e.target;
const id = target?.info?.id;
if (id) await this.layerManager.updateLayerThumbnailsById(id) if (id) await this.layerManager.updateLayerThumbnailsById(id)
if (target.type === "group") await this.canvasManager.updateSubLayerClipPath()
this.stateManager.recordState(); this.stateManager.recordState();
}); });
this.canvas.on("object:removed", (e) => { this.canvas.on("object:removed", (e) => {
@@ -752,113 +754,6 @@ export class CanvasEventManager {
}); });
} }
setupLongPress(callback) {
this.canvas.on("mouse:down", (opt) => {
if (!opt.target) return;
this.longPressTimer = setTimeout(() => {
callback(opt);
}, this.longPressThreshold);
});
this.canvas.on("mouse:up", () => {
clearTimeout(this.longPressTimer);
});
this.canvas.on("mouse:move", () => {
clearTimeout(this.longPressTimer);
});
}
// 设置路径创建事件
setupHandlePathCreated() {
// 在 CanvasEventManager 的构造函数或初始化方法中
// this.canvas.on("path:created", this._handlePathCreated.bind(this));
}
_handlePathCreated(e) {
// // 获取新创建的路径对象
// const path = e.path;
// // 设置路径的ID和其他属性
// path.id = generateId(); // 生成唯一ID
// // 获取当前活动图层
// const activeLayer = this.layerManager.getActiveLayer();
// // 将路径对象绑定到当前活动图层
// if (activeLayer) {
// // 设置路径的图层ID
// path.layerId = activeLayer.id;
// // 更新图层对象列表
// if (!activeLayer.fabricObjects) activeLayer.fabricObjects = [];
// activeLayer.fabricObjects.push(path);
// // 更新图层缩略图
// if (this.thumbnailManager) {
// this.thumbnailManager.generateLayerThumbnail(activeLayer.id);
// }
// }
}
/**
* 合并图层中的对象为组以提高性能
* @param {Object} options 合并选项
* @param {fabric.Image} options.fabricImage 新的图像对象
* @param {Object} options.activeLayer 当前活动图层
* @private
*/
async mergeLayerObjectsForPerformance({ fabricImage, activeLayer, options }) {
// 确保有命令管理器
if (!this.layerManager || !this.layerManager.commandManager) {
console.warn("合并对象失败:没有命令管理器");
return;
}
// 确保有活动图层
if (!activeLayer) {
console.warn("合并对象失败:没有活动图层");
return;
}
// 验证是否需要合并
const hasExistingObjects =
Array.isArray(activeLayer.fabricObjects) &&
activeLayer.fabricObjects.length > 0;
const hasNewImage = !!fabricImage;
if (!hasExistingObjects && !hasNewImage) {
// console.log("没有对象需要合并");
return;
}
// 如果只有一个新图像且图层为空,直接添加到图层
if (hasNewImage && !hasExistingObjects) {
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id, options);
return;
}
// 执行高保真合并操作
try {
// console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`);
const command = await this.layerManager.LayerObjectsToGroup(
activeLayer,
fabricImage
);
// 设置命令的撤销状态
if (isBoolean(options.undoable)) command.undoable = options.undoable; // 是否撤销
this.layerManager?.commandManager?.execute?.(command, {
name: `合并图层 ${activeLayer.name} 中的对象为组`,
});
} catch (error) {
console.error("合并图层对象时发生错误:", error);
// 降级处理:如果合并失败,至少保证新图像能添加到图层
if (fabricImage && this.layerManager) {
// console.log("执行降级处理:直接添加图像到图层");
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
}
}
}
updateSelectedLayer(opt) { updateSelectedLayer(opt) {
const selected = opt.selected[0]; const selected = opt.selected[0];
if (selected && opt.selected.length === 1) { if (selected && opt.selected.length === 1) {
@@ -866,31 +761,6 @@ export class CanvasEventManager {
} }
} }
// 更新图层缩略图
updateLayerThumbnail(layerId) {
if (!this.thumbnailManager || !layerId || !this.layers) return;
const layer = this.layers.value.find((l) => l.id === layerId);
if (layer) {
this.thumbnailManager.generateLayerThumbnail(layer);
}
}
// 更新子元素组合缩略图
updateLayerChidrenThumbnail(layerId, fabricObject) {
if (!this.thumbnailManager || !fabricObject || !this.layers) return;
// 查找对应的图层(现在元素就是图层)
const layer = this.layers.value.find(
(l) => l.fabricObjects && l.fabricObjects?.[0]?.id === layerId
);
if (layer) {
// 生成图层缩略图
this.thumbnailManager.generateLayerThumbnail(layer);
}
}
/** /**
* 精确检测设备类型,区分 PC、Mac、平板和移动设备 * 精确检测设备类型,区分 PC、Mac、平板和移动设备

View File

@@ -93,3 +93,157 @@ export function angleBetweenPointsDegrees(x1, y1, x2, y2) {
return deg; return deg;
} }
/**
* 获取对象黑白通道画布
* @param {fabric.Object} object - 要处理的 fabric 对象
* @param {ImageData} revData - 相反的ImageData白通道的相同位置是否为透明revData为白色为透明黑色为不透明
* @param {number} diff - 差值,默认 25
* @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 }
* @param {boolean} isMerge - 是否合并true=合并revDatafalse=反转revData
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
*/
export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) {
const image = object.getElement();
if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") {
console.warn("对象不是图片");
return null;
}
const { width, height } = image;
if (!width || !height) {
console.warn("对象没有元素");
return null;
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d", { willReadFrequently: true });
ctx.drawImage(image, 0, 0, width, height);
const data = ctx.getImageData(0, 0, width, height);
for (let i = 0; i < data.data.length; i += 4) {
const r = data.data[i + 0];
const g = data.data[i + 1];
const b = data.data[i + 2];
const a = data.data[i + 3];
const revR = revData?.data[i + 0] || 0;
const revG = revData?.data[i + 1] || 0;
const revB = revData?.data[i + 2] || 0;
const revA = revData?.data[i + 3] || 0;
let isHave = false;
if (r || g || b || a) {
if (revR > diff || revG > diff || revB > diff || revA > diff) {
isHave = false;
} else {
isHave = true;
}
}
if (isMerge && (revR || revG || revB || revA)) isHave = true;
if (isHave) {
data.data[i + 0] = rgba.r;
data.data[i + 1] = rgba.g;
data.data[i + 2] = rgba.b;
data.data[i + 3] = rgba.a;
} else {
data.data[i + 0] = 0;
data.data[i + 1] = 0;
data.data[i + 2] = 0;
data.data[i + 3] = 0;
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(data, 0, 0);
return canvas;
}
/**
* 图片边界跟踪算法(透明底)
* @param {HTMLCanvasElement} canvas - canvas元素
* @param {Number} scale - 缩放比例
* @returns {Array} 边界点数组 [{x, y}, ...]
*/
export function traceImageContour(canvas) {
const ctx = canvas.getContext("2d", { willReadFrequently: true });
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const width = canvas.width;
const height = canvas.height;
// 查找起始点(第一个不透明像素)
let startX = -1;
let startY = -1;
outer: for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
if (data[index + 3] > 0) {
startX = x;
startY = y;
break outer;
}
}
}
if (startX === -1) return []; // 没有不透明像素
// Moore-Neighbor边界跟踪算法
const contour = [];
const visited = new Set();
const directions = [
[-1, 0],
[-1, -1],
[0, -1],
[1, -1],
[1, 0],
[1, 1],
[0, 1],
[-1, 1],
];
let currentX = startX;
let currentY = startY;
let backtrackDir = 4; // 起始方向:右
do {
const pointKey = `${currentX},${currentY}`;
if (!visited.has(pointKey)) {
contour.push({ x: currentX, y: currentY });
visited.add(pointKey);
}
// 从右方向开始顺时针查找
let found = false;
for (let i = 0; i < 8; i++) {
const dir = (backtrackDir + i) % 8;
const dx = directions[dir][0];
const dy = directions[dir][1];
const checkX = currentX + dx;
const checkY = currentY + dy;
if (
checkX >= 0 &&
checkX < width &&
checkY >= 0 &&
checkY < height
) {
const index = (checkY * width + checkX) * 4;
if (data[index + 3] > 0) {
currentX = checkX;
currentY = checkY;
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
found = true;
break;
}
}
}
if (!found) break;
} while (
!(currentX === startX && currentY === startY) &&
visited.size < width * height
);
return contour;
}

View File

@@ -29,6 +29,12 @@ export async function exportObjectsToImage(objects = [], isDetails = false) {
left: obj.left - boundingBox.left, left: obj.left - boundingBox.left,
top: obj.top - boundingBox.top, top: obj.top - boundingBox.top,
}) })
if (obj.clipPath && obj.clipPath.absolutePositioned) {
obj.clipPath.set({
left: obj.clipPath.left - boundingBox.left,
top: obj.clipPath.top - boundingBox.top,
})
}
staticCanvas.add(obj) staticCanvas.add(obj)
}) })
// 导出图片 // 导出图片

View File

@@ -68,3 +68,11 @@ export const BlendMode = {
DESTINATION_IN: "destination-in", // 目标内 DESTINATION_IN: "destination-in", // 目标内
DESTINATION_OUT: "destination-out", // 目标外 DESTINATION_OUT: "destination-out", // 目标外
}; };
/** 智能框选工具类型枚举 */
export const AI_SELECTBOX_TYPE = {
ADD: "add", // 添加模式
REMOVE: "remove", // 删除模式
DRAW: "draw", // 绘画模式
ERASER: "eraser", // 橡皮擦模式
}

View File

@@ -11,15 +11,12 @@
<div class="item" @mousedown="(e) => stateManager.setActiveNodeID(node.id)"> <div class="item" @mousedown="(e) => stateManager.setActiveNodeID(node.id)">
<slot></slot> <slot></slot>
</div> </div>
<div class="add" @mousedown.stop v-if="isAdd" @click="onAdd">
<svg-icon name="add" size="14" size-unit="px" />
</div>
<div class="mask" v-show="mask"></div> <div class="mask" v-show="mask"></div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core' import { Handle, Position } from '@vue-flow/core'
import { NODE_DATATYPE, NODE_DATATIER, NODE_TYPE } from '../tools/index.d' import { NODE_DATATYPE, NODE_TYPE } from '../tools/index.d'
import { computed, ref, inject } from 'vue' import { computed, ref, inject } from 'vue'
const handles = ref({ const handles = ref({
[NODE_TYPE.INPUT]: [{ id: 'Right', type: 'source', position: Position.Right }], [NODE_TYPE.INPUT]: [{ id: 'Right', type: 'source', position: Position.Right }],
@@ -48,39 +45,6 @@
default: false default: false
} }
}) })
const stateManager = inject('stateManager') as any
const nodes = computed(() => props.stateManager.nodes.value)
const isSubord = computed(() => nodes.value.some((v) => v.data.superiorID === props.node.id))
const tier = computed(() => Number(props.node?.data?.tier || 0))
//只有3d模型才有三级菜单,目前三级菜单内容少直接禁用按钮
const isAdd3d = computed(() => (tier.value === 2 && props.node?.data?.superiorNodeType === NODE_DATATYPE.TO_3D_MODEL) || props.node?.data?.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL)
const isReturned = computed(() => {
return (
props.node.data.type == NODE_DATATYPE.RESULT_IMAGE &&
props.node.data.data.imageProcessTasks[0].status == 'RETURNED'
)
})
const isAdd = computed(
() =>
!isSubord.value &&
NODE_DATATYPE.RESULT_IMAGE === props.node.data.type &&
!(tier.value === NODE_DATATIER.TO_3VIEW) &&
isReturned.value &&
isAdd3d.value
)
const onAdd = () => {
const tier_ = tier.value + 1
// 从data中获取originalImage
let nodeData = props.node?.data?.data.imageProcessTasks.filter(
(v) => v.taskId === props.node?.data?.data.selectTaskId
)
const originalImage = nodeData[0]?.url
if (!originalImage) console.log('originalImage 找不到原始图片')
props.stateManager.nodeManager.createCardsSelect({
data: { tier: tier_, superiorID: props.node.id, originalImage }
})
stateManager.setActiveNodeID('')
}
const posCenter = computed(() => { const posCenter = computed(() => {
const arr = [NODE_DATATYPE.RESULT_IMAGE] const arr = [NODE_DATATYPE.RESULT_IMAGE]
return arr.includes(props.node?.data?.type) return arr.includes(props.node?.data?.type)
@@ -106,25 +70,6 @@
> .item { > .item {
position: relative; position: relative;
} }
> .add {
position: absolute;
width: 32px;
height: 32px;
border: 2px solid #fff;
top: var(--top);
right: -16px;
transform: translateY(-50%);
background-color: #ed8936;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 25px;
box-shadow: 0 8px 20px 0 #71809633;
cursor: pointer;
z-index: 20;
}
&.center { &.center {
--top: 50%; --top: 50%;
} }

View File

@@ -69,25 +69,26 @@
const onClickItem = (v) => { const onClickItem = (v) => {
const id = props.node.id const id = props.node.id
if (!id) return if (!id) return
const superiorID = props.node.data.superiorID
stateManager.deleteNode(id) stateManager.deleteNode(id)
console.log(props.node)
if(v.secondaryMenu){ if(v.secondaryMenu){
nodeManager.createCardsSelect({ nodeManager.createCardsSelect({
data: { data: {
tier: props.node.data?.tier, tier: props.node.data?.tier,
superiorID: props.node.data?.superiorID, superiorID,
originalImage: props.node.data?.originalImage, isActive: props.node.data?.isActive,
secondaryMenu: v.secondaryMenu secondaryMenu: v.secondaryMenu,
createIndexPosition: props.node.data.createIndexPosition,
} }
}) })
}else{ }else{
const superiorID = props.node.data.superiorID
nodeManager.createCardNode({ nodeManager.createCardNode({
data: { data: {
tier: v.tier, tier: v.tier,
type: v.type, type: v.type,
superiorID, superiorID,
originalImage: props.node.data?.originalImage, isActive: props.node.data?.isActive,
createIndexPosition: props.node.data.createIndexPosition,
} }
}) })
} }

View File

@@ -9,24 +9,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, inject, useAttrs } from 'vue' import { reactive, inject, useAttrs, computed } from 'vue'
import myEvent from '@/utils/myEvent' import myEvent from '@/utils/myEvent'
import { getCurrentTime } from '../../../../tools/tools.ts' import { getCurrentTime } from '../../../../tools/tools.ts'
import { NODE_DATATIER } from '../../../tools/index.d' import { NODE_DATATIER } from '../../../tools/index.d'
const attrs = useAttrs() const attrs = useAttrs()
const data = reactive({
url: attrs.node?.data?.originalImage,
})
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
const nodeManager = inject('nodeManager') as any const nodeManager = inject('nodeManager') as any
const eventManager = inject('eventManager') as any const eventManager = inject('eventManager') as any
const data = reactive({
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
})
const getApiData = ()=>{ const getApiData = ()=>{
return { return {
} }
} }
const opCanvas = ()=>{ const opCanvas = ()=>{
const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs?.node?.data?.superiorID || null)
if (!superiorNodeUrl) console.log('superiorNodeUrl 找不到原始图片')
const data = { const data = {
url:attrs?.node?.data?.originalImage, url:superiorNodeUrl,
canvasId: attrs?.node?.data?.canvasId || null, canvasId: attrs?.node?.data?.canvasId || null,
sketchId: stateManager.sketchId.value, sketchId: stateManager.sketchId.value,
onWorkbench:(options)=>{ onWorkbench:(options)=>{

View File

@@ -42,12 +42,15 @@
import ColorPalette from './color-palette.vue' import ColorPalette from './color-palette.vue'
import To3View from './to-3view.vue' import To3View from './to-3view.vue'
import To3DModel from './to-3d-model.vue' import To3DModel from './to-3d-model.vue'
import { useI18n } from 'vue-i18n'
import { ElMessageBox } from 'element-plus'
import { toRealStyleApi, toColorPaletteApi, toSceneCompositionApi, sketchAddPrintApi, sketchToThreeApi, threeToThreeViewsApi } from '@/api/flow-canvas' import { toRealStyleApi, toColorPaletteApi, toSceneCompositionApi, sketchAddPrintApi, sketchToThreeApi, threeToThreeViewsApi } from '@/api/flow-canvas'
// import ToVideo from './to-video.vue' // import ToVideo from './to-video.vue'
// import AddPrint from './add-print.vue' // import AddPrint from './add-print.vue'
// import ToCAD from './to-cad.vue' // import ToCAD from './to-cad.vue'
const { t } = useI18n()
const attrs = useAttrs() const attrs = useAttrs()
const componentRef = ref(null) const componentRef = ref(null)
const components = [ const components = [
@@ -143,14 +146,13 @@
const onGenerateClick = async () => { const onGenerateClick = async () => {
const data = componentRef.value?.getApiData?.() || {} const data = componentRef.value?.getApiData?.() || {}
const subordNode = stateManager.getSubordNodeById(attrs.node.id)
const subordNodes = stateManager.getSubordNodes(attrs.node.id) const subordNodes = stateManager.getSubordNodes(attrs.node.id)
const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs.node.data.superiorID)
if(!superiorNodeUrl)return console.log('superiorNodeUrl 找不到原始图片')
emit('update-data', componentRef.value?.data) emit('update-data', componentRef.value?.data)
if(!attrs.node?.data?.originalImage)console.log('originalImage 找不到原始图片')
const apiData = { const apiData = {
sketchId: props.sketchId, sketchId: props.sketchId,
imageUrl: attrs.node?.data?.originalImage, imageUrl: superiorNodeUrl,
...data, ...data,
} }
const taskList = await currentComponent.value.api(apiData).then((rv)=>{ const taskList = await currentComponent.value.api(apiData).then((rv)=>{
@@ -178,8 +180,9 @@
}) })
} }
//删除功能卡片 //删除功能卡片
const onDeleteClick = ()=>{ const onDeleteClick = async ()=>{
stateManager.deleteNode(attrs.node.id,{isElMessageBox:true}) console.log(stateManager.nodes)
stateManager.getSubordinateAllNodes(attrs.node.id,{ isElMessageBox: true })
} }
const setDate = () => { const setDate = () => {
for (const key in props.data) { for (const key in props.data) {

View File

@@ -9,12 +9,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, onMounted, useAttrs } from 'vue' import { reactive, inject, useAttrs, computed } from 'vue'
import uploadFile from '../../tools/upload-file.vue' import uploadFile from '../../tools/upload-file.vue'
const attrs = useAttrs() const attrs = useAttrs()
const stateManager = inject('stateManager') as any
const data = reactive({ const data = reactive({
url: attrs.node?.data?.originalImage, url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
}) })
const getApiData = ()=>{ const getApiData = ()=>{
return { return {

View File

@@ -13,8 +13,8 @@
const attrs = useAttrs() const attrs = useAttrs()
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
const data = reactive({ const data = reactive({
url: attrs.node.data.originalImage, url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
}) })
const getApiData = ()=>{ const getApiData = ()=>{
let glbUrl = null let glbUrl = null
const superiorNode = stateManager.nodes.value.filter((item:any)=>item.id === attrs.node?.data?.superiorID)[0] const superiorNode = stateManager.nodes.value.filter((item:any)=>item.id === attrs.node?.data?.superiorID)[0]

View File

@@ -53,6 +53,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="add" @mousedown.stop v-if="isAdd" @click="onAdd">
<svg-icon name="add" size="14" size-unit="px" />
</div>
</div> </div>
</template> </template>
@@ -61,7 +64,7 @@
import { downloadImage } from '../../../tools/tools' import { downloadImage } from '../../../tools/tools'
import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch, computed, onMounted } from 'vue' import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch, computed, onMounted } from 'vue'
import HighlightAdmin from '@/components/highlightAdmin.vue' import HighlightAdmin from '@/components/highlightAdmin.vue'
import { NODE_DATATYPE } from '../../tools/index.d' import { NODE_DATATIER, NODE_DATATYPE } from '../../tools/index.d'
const openImagePreview = inject('openImagePreview') as (url: string) => void const openImagePreview = inject('openImagePreview') as (url: string) => void
const openThreeModelPreview = inject('openThreeModelPreview') as (url: string) => void const openThreeModelPreview = inject('openThreeModelPreview') as (url: string) => void
const props = defineProps({ const props = defineProps({
@@ -229,6 +232,38 @@
eventManager.removeEvents() eventManager.removeEvents()
myEvent.emit('openDepthCanvas', data) myEvent.emit('openDepthCanvas', data)
} }
const isSubord = computed(() => props.node.id == stateManager.activeNodeID.value)
const tier = computed(() => Number(props.node?.data?.tier || 0))
//只有3d模型才有三级菜单,目前三级菜单内容少直接禁用按钮
const isAdd3d = computed(() => (tier.value === 2 && props.node?.data?.superiorNodeType === NODE_DATATYPE.TO_3D_MODEL) || props.node?.data?.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL)
const isReturned = computed(() => {
return (
props.node.data.type == NODE_DATATYPE.RESULT_IMAGE &&
props.node.data.data.imageProcessTasks[0].status == 'RETURNED'
)
})
const isAdd = computed(
() =>
NODE_DATATYPE.RESULT_IMAGE === props.node.data.type &&
!(tier.value === NODE_DATATIER.TO_3VIEW) &&
isReturned.value &&
isAdd3d.value &&
isSubord.value
)
const onAdd = () => {
const tier_ = tier.value + 1
const subordNodes = stateManager.getSubordNodes(props.node.id)
stateManager.nodeManager.createCardsSelect({
data: {
tier: tier_,
superiorID: props.node.id,
createIndexPosition: subordNodes.length + 1,
isActive: subordNodes.length == 0
}
})
stateManager.setActiveNodeID('')
}
document.addEventListener('mousedown', hideMenu) document.addEventListener('mousedown', hideMenu)
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener('mousedown', hideMenu) document.removeEventListener('mousedown', hideMenu)
@@ -250,6 +285,25 @@
gap: 8px; gap: 8px;
} }
} }
.add {
position: absolute;
width: 32px;
height: 32px;
border: 2px solid #fff;
top: var(--top);
right: -16px;
transform: translateY(-50%);
background-color: #ed8936;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 25px;
box-shadow: 0 8px 20px 0 #71809633;
cursor: pointer;
z-index: 20;
}
.result-image { .result-image {
width: 244px; width: 244px;
border-radius: 16px; border-radius: 16px;

View File

@@ -170,7 +170,7 @@
/** 点击节点 */ /** 点击节点 */
const clickNode = (event) => { const clickNode = (event) => {
let node = event.node let node = event.node
stateManager.showNodeConnections(node.id) // stateManager.showNodeConnections(node.id)
} }
/** 删除节点 */ /** 删除节点 */
const deleteNode = (id) => { const deleteNode = (id) => {

View File

@@ -94,7 +94,7 @@ export class EventManager {
handleDelete(event: any, activeNodeID: string) { handleDelete(event: any, activeNodeID: string) {
event.preventDefault() event.preventDefault()
if (!activeNodeID) return console.warn('没有选中节点') if (!activeNodeID) return console.warn('没有选中节点')
this.stateManager.deleteNode(activeNodeID, { isElMessageBox: true }) this.stateManager.getSubordinateAllNodes(activeNodeID, { isElMessageBox: true })
} }
/** 处理键盘事件 */ /** 处理键盘事件 */
_handleKeyDown: any _handleKeyDown: any

View File

@@ -10,7 +10,6 @@ interface NodeData {
superiorNodeType?: string// 上级节点类型 superiorNodeType?: string// 上级节点类型
disableDelete?: boolean// 是否禁用删除 disableDelete?: boolean// 是否禁用删除
disableCopy?: boolean// 是否禁用复制 disableCopy?: boolean// 是否禁用复制
originalImage?: string// 要进行生成的图片
createIndexPosition?: number// 创建索引位置 createIndexPosition?: number// 创建索引位置
isActive?: boolean// 是否激活 isActive?: boolean// 是否激活
} }
@@ -35,7 +34,7 @@ export class NodeManager {
/** 删除节点 */ /** 删除节点 */
deleteNode(id: string) { deleteNode(id: string) {
this.stateManager.deleteNode(id) this.stateManager.getSubordinateAllNodes(id, { isElMessageBox: true })
} }
/** 添加节点 */ /** 添加节点 */
addNode(node: any) { addNode(node: any) {
@@ -45,27 +44,35 @@ export class NodeManager {
/** 创建节点 */ /** 创建节点 */
createNode(options: NodeOptions) { createNode(options: NodeOptions) {
const superiorID = options?.data?.superiorID const superiorID = options?.data?.superiorID
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
//获取上级节点所生成的最后一个node设置位置为最后一个节点的xy 加上 节点间距 //获取上级节点所生成的最后一个node设置位置为最后一个节点的xy 加上 节点间距
const superiorGenerateNodes = this.stateManager.getSubordNodes(superiorID) const superiorGenerateNodes = this.stateManager.getSubordNodes(superiorID)
const currentNode = superiorGenerateNodes.find((node) => {
return node.data.createIndexPosition === options?.data?.createIndexPosition
})
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => { const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
}, superiorGenerateNodes[0]) }, superiorGenerateNodes[0])
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
console.log(snode)
const id = options.id || createId() const id = options.id || createId()
const positionX = options.positionX || 0 const positionX = options.positionX || 0
const positionY = options.positionY || 0 const positionY = options.positionY || 0
const position = options.position || const position = options.position ||
( (
endGenerateNode? currentNode ?
currentNode.position :
endGenerateNode ?
{ {
x: endGenerateNode.position.x + positionX, x: endGenerateNode.position.x + positionX,
y: endGenerateNode.position.y + positionY + this.ranksep + 200 y: endGenerateNode.position.y + positionY + this.ranksep + 200
} : } : snode ?
!snode ?
{ x: positionX, y: positionY } :
{ {
x: snode.position.x + snode.dimensions.width + this.nodesep + positionX, x: snode.position.x + snode.dimensions.width + this.nodesep + positionX,
y: snode.position.y + positionY y: snode.position.y + positionY
} :
{
x: positionX,
y: positionY
} }
) )
const data = options?.data || {} const data = options?.data || {}
@@ -103,6 +110,7 @@ export class NodeManager {
data: { data: {
tier: NODE_DATATIER.CARDS_SELECT, tier: NODE_DATATIER.CARDS_SELECT,
type: NODE_DATATYPE.CARDS_SELECT, type: NODE_DATATYPE.CARDS_SELECT,
createIndexPosition: options?.data?.createIndexPosition || 1,
...(options?.data || {}), ...(options?.data || {}),
}, },
} }
@@ -113,7 +121,7 @@ export class NodeManager {
const options_ = { const options_ = {
...(options ? options : {}), ...(options ? options : {}),
component: NODE_COMPONENT.CARD, component: NODE_COMPONENT.CARD,
data: { data: {
...(options?.data || {}), ...(options?.data || {}),
} }
} }

View File

@@ -101,7 +101,7 @@ export class StateManager {
source: source, source: source,
target: target, target: target,
selectable: false, selectable: false,
visible: (node.data.type !== NODE_DATATYPE.RESULT_IMAGE || node.data.isActive), visible: (node.data.isActive),
type: 'default' type: 'default'
}) })
} }
@@ -111,7 +111,7 @@ export class StateManager {
} }
/** 设置激活节点 */ /** 设置激活节点 */
setActiveNodeID(id: string) { this.activeNodeID.value = id } setActiveNodeID(id: string) { this.activeNodeID.value = id;this.showNodeConnections(id) }
/** 添加节点 */ /** 添加节点 */
addNode(node: NodesItem) { addNode(node: NodesItem) {
this.nodes.value.push(node); this.nodes.value.push(node);
@@ -119,37 +119,69 @@ export class StateManager {
this.exportFlow() this.exportFlow()
} }
/** 删除节点 */ /** 删除节点 */
async deleteNode(id: string, { isElMessageBox } = { isElMessageBox: false }) { async deleteNode(id: string) {
const node = this.getNodeById(id)
if (!node) return console.warn(`没有找到指定id:${id}`)
if (node.data.disableDelete) return console.warn('该节点禁用删除')
let deletePromise: any = true
if (isElMessageBox) {
deletePromise = await new Promise<void>((resolve, reject) => {
ElMessageBox.confirm(
t('flowCanvas.deleteCardConfirm'),
'',
{
confirmButtonText: t('flowCanvas.confirm'),
cancelButtonText: t('flowCanvas.cancel'),
}
).then(() => {
resolve(true)
}).catch(() => {
resolve(false)
})
})
}
if (!deletePromise) return console.log('删除操作被取消')
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()
this.exportFlow()
} }
/** 获取节点 */ /** 获取节点 */
getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) } getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) }
/** 获取下级节点 */ /** 获取下级节点 */
getSubordNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.data.superiorID === id) } getSubordNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.data.superiorID === id) }
getLastNode() { console.log(this.nodes.value); return this.nodes.value[this.nodes.value.length - 1] } getLastNode() { console.log(this.nodes.value); return this.nodes.value[this.nodes.value.length - 1] }
/** 获取上级生成节点的图片 */
getSuperiorNodeImage(superiorID: string) {
const superiorNode = this.getNodeById(superiorID)
if(!superiorNode){
ElMessage.error(t('flowCanvas.cannotFindSuperiorImage'))
return null
}
const superiorNodeUrl = superiorNode.data.data.imageProcessTasks.filter((item)=>{
return item.taskId == superiorNode.data.data.selectTaskId
})[0]?.url
return superiorNodeUrl
}
/** 获取下级所有子级节点 */
async getSubordinateAllNodes(id: string,{ isElMessageBox } = { isElMessageBox: false }) {
const node = this.getNodeById(id)
if (!node) return console.warn(`没有找到指定id:${id}`)
if (node.data.disableDelete) return ElMessage.error(t('flowCanvas.initialNodeProhibited'))
const result = [node]
const findChildren = (parentId: string) => {
const children = this.nodes.value.filter(item => item.data.superiorID === parentId)
children.forEach(child => {
if(child.data.type !== NODE_DATATYPE.RESULT_IMAGE){
result.push(child)
}
findChildren(child.id)
})
}
let deletePromise: any = true
if (isElMessageBox && result.length > 1) {
deletePromise = await new Promise<void>((resolve, reject) => {
ElMessageBox.confirm(
t('flowCanvas.deleteSubordinateCard'),
'',
{
confirmButtonText: t('flowCanvas.confirm'),
cancelButtonText: t('flowCanvas.cancel'),
}
).then(() => {resolve(true)
}).catch(() => {
resolve(false)
})
})
}
if(!deletePromise) return console.log('删除操作被取消')
this.deleteNode(id)
result.forEach(item => {
this.deleteNode(item.id)
})
this.recordState()
this.exportFlow()
}
/** 设置工具 */ /** 设置工具 */
setTool(tool: string) { this.tool.value = tool } setTool(tool: string) { this.tool.value = tool }
/** 设置光标 */ /** 设置光标 */
@@ -222,9 +254,9 @@ export class StateManager {
/** 显示指定子节点和父节点连接线,隐藏父节点和其他子节点链接线, */ /** 显示指定子节点和父节点连接线,隐藏父节点和其他子节点链接线, */
showNodeConnections(id: string) { showNodeConnections(id: string) {
if(!id) return
const node = this.getNodeById(id) const node = this.getNodeById(id)
if(node.data.component != NODE_DATATYPE.RESULT_IMAGE && node.data.superiorID) return if(!node?.data?.superiorID) return
let edges_ = JSON.parse(JSON.stringify(this.edges.value))
this.nodes.value.forEach((nodeItem) => { this.nodes.value.forEach((nodeItem) => {
if(node.data.superiorID === nodeItem.data.superiorID && nodeItem.id == id) { if(node.data.superiorID === nodeItem.data.superiorID && nodeItem.id == id) {
nodeItem.data.isActive = true nodeItem.data.isActive = true

View File

@@ -15,7 +15,8 @@ import { computed } from "vue";
const props = defineProps({ const props = defineProps({
name: { name: {
type: String, type: String,
required: true, default: "",
// required: true,
}, },
color: { color: {
type: String, type: String,
@@ -30,7 +31,7 @@ const props = defineProps({
default: 'rem', default: 'rem',
} }
}); });
const iconName = computed(() => `#icon-${props.name}`); const iconName = computed(() => `#icon-${props?.name}`);
const svgClass = computed(() => { const svgClass = computed(() => {
if (props.name) return `svg-icon icon-${props.name}`; if (props.name) return `svg-icon icon-${props.name}`;
return "svg-icon"; return "svg-icon";

View File

@@ -173,21 +173,17 @@ export default {
deleteHint:'Once deleted, you wont be able to view this conversation again.', deleteHint:'Once deleted, you wont be able to view this conversation again.',
restoreChat:'Restore chat?', restoreChat:'Restore chat?',
restoreHint:'Once deleted, you wont be able to view this conversation again.', restoreHint:'Once deleted, you wont be able to view this conversation again.',
cancel: 'cancel', Cancel: 'Cancel',
Confirm: 'Confirm', Confirm: 'Confirm',
export: 'Export', export: 'Export',
}, },
//generateSketch
generateSketch: {
restore: 'Restore',
delete: 'Delete',
edit: 'Edit'
},
flowCanvas: { flowCanvas: {
deleteCardConfirm: 'Are you sure you want to delete this function card?',
confirm: 'Confirm', confirm: 'Confirm',
cancel: 'Cancel', cancel: 'Cancel',
confirmLeave: 'Are you sure you want to leave? You may have unsaved changes.', confirmLeave: 'Are you sure you want to leave? You may have unsaved changes.',
cannotFindSuperiorImage: 'Cannot find the superior image',
deleteSubordinateCard: 'After deletion, all the function cards will also be deleted.',
initialNodeProhibited: 'Initial node is prohibited from being deleted.',
}, },
assistant: { assistant: {
inputPlaceholder: 'Ask anything', inputPlaceholder: 'Ask anything',

View File

@@ -168,9 +168,9 @@ export default {
deleteHint: '删除后将无法恢复该对话。', deleteHint: '删除后将无法恢复该对话。',
restoreChat: '恢复对话?', restoreChat: '恢复对话?',
restoreHint: '恢复后将显示该对话。', restoreHint: '恢复后将显示该对话。',
cancel: '取消', Cancel: '取消',
Confirm: '确认', Confirm: '确认',
export: '导出' export: '导出',
}, },
//generateSketch //generateSketch
generateSketch: { generateSketch: {
@@ -179,10 +179,11 @@ export default {
edit: '编辑' edit: '编辑'
}, },
flowCanvas: { flowCanvas: {
deleteCardConfirm: '确定要删除该功能卡片吗?',
confirm: '确认', confirm: '确认',
cancel: '取消', cancel: '取消',
confirmLeave: '您可能有未保存的更改,确定要离开吗?', confirmLeave: '您可能有未保存的更改,确定要离开吗?',
cannotFindSuperiorImage: '找不到上级图片',
initialNodeProhibited: 'Initial node is prohibited from being deleted.',
}, },
assistant: { assistant: {
inputPlaceholder: '请输入' inputPlaceholder: '请输入'

View File

@@ -58,6 +58,9 @@ const setVersionsList = (res)=>{
} }
traverseArray(res,'',(item,i,father)=>{ traverseArray(res,'',(item,i,father)=>{
item.versionId = father?`${father.versionId}-${i+1}`:'1' item.versionId = father?`${father.versionId}-${i+1}`:'1'
if(item.id == projectStore.state.nodeId){
selectItem.value = {...item}
}
}) })
versionsList.value = res versionsList.value = res
} }
@@ -110,10 +113,6 @@ const versionDelete = (versionDetail)=>{
treeKey.value++ treeKey.value++
} }
watch(()=>projectStore.state.nodeId,(newVal,oldVal)=>{
if(!newVal || newVal === selectItem?.value?.id)return
selectItem.value = {id:newVal}
})
let data = reactive({}) let data = reactive({})
// onMounted(() => {setVersionsList('')}) // onMounted(() => {setVersionsList('')})
@@ -126,7 +125,6 @@ const {} = toRefs(data)
<el-drawer <el-drawer
v-model="versionTreeData.drawer" v-model="versionTreeData.drawer"
:close-on-press-escape="false" :close-on-press-escape="false"
:close-on-click-modal="false"
:size="treeState ? '73.5rem' : '73.5rem'" :size="treeState ? '73.5rem' : '73.5rem'"
body-class="versionTreeBody" body-class="versionTreeBody"
:with-header="false" :with-header="false"

View File

@@ -7,6 +7,8 @@ import InputNode from './InputNode.vue'//主
import SecondaryNode from './secondaryNode.vue'//分支 import SecondaryNode from './secondaryNode.vue'//分支
import { useLayout } from '@/utils/treeDiagram' import { useLayout } from '@/utils/treeDiagram'
import dialogVue from "../../components/dialog.vue"; import dialogVue from "../../components/dialog.vue";
import { ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
const props = defineProps({ const props = defineProps({
selectItem: { selectItem: {
type: Object, type: Object,
@@ -22,7 +24,7 @@ const emit = defineEmits([
'versionRestore', 'versionRestore',
'versionDelete', 'versionDelete',
]) ])
const {t:$t} = useI18n()
const dialogDeleteRef = ref() const dialogDeleteRef = ref()
const dialogRestoreRef = ref() const dialogRestoreRef = ref()
@@ -89,9 +91,8 @@ watch(()=>props.treeList.length, (newVal, oldVal) => {
watch(()=>props.selectItem.versionId, (newVal, oldVal) => { watch(()=>props.selectItem.versionId, (newVal, oldVal) => {
}) })
const versionRestore = ()=>{ const versionRestore = async ()=>{
emit('versionRestore') dialogDeleteRef.value?.open()
// dialogRestoreRef.value?.open()
} }
const versionDelete = ()=>{ const versionDelete = ()=>{
@@ -139,24 +140,24 @@ defineExpose({push})
</div> --> </div> -->
</div> </div>
</div> </div>
<!-- <dialogVue <dialogVue
:textData="{ :textData="{
title: $t('VersionTree.deleteChat'), title: $t('VersionTree.deleteChat'),
text: $t('VersionTree.deleteHint'), text: $t('VersionTree.deleteHint'),
submitText: $t('VersionTree.delete'), submitText: $t('VersionTree.delete'),
cancelText: $t('VersionTree.cancel'), cancelText: $t('VersionTree.Cancel'),
}" }"
:styleData="{ :styleData="{
width: '40.6rem' width: '40.6rem'
}" }"
:callBack="()=>emit('versionDelete')" :callBack="()=>emit('versionRestore')"
ref="dialogDeleteRef" /> ref="dialogDeleteRef" />
<dialogVue <!-- <dialogVue
:textData="{ :textData="{
title: $t('VersionTree.restoreChat'), title: $t('VersionTree.restoreChat'),
text: $t('VersionTree.restoreHint'), text: $t('VersionTree.restoreHint'),
submitText: $t('VersionTree.confirm'), submitText: $t('VersionTree.confirm'),
cancelText: $t('VersionTree.cancel'), cancelText: $t('VersionTree.Cancel'),
}" }"
:styleData="{ :styleData="{
width: '40.6rem' width: '40.6rem'
@@ -225,10 +226,9 @@ defineExpose({push})
margin-bottom: 2rem; margin-bottom: 2rem;
background-color: #ffffff; background-color: #ffffff;
cursor: pointer; cursor: pointer;
pointer-events: none; display: none;
&.active{ &.active{
background-color: #f5f5f5; display: flex;
pointer-events: auto;
} }
&:hover{ &:hover{
background-color: #f5f5f5; background-color: #f5f5f5;

View File

@@ -44,7 +44,7 @@
<span class="label" v-show="!item.edit">{{ item.name }}</span> <span class="label" v-show="!item.edit">{{ item.name }}</span>
<el-popover <el-popover
placement="right" placement="right"
trigger="click" trigger="contextmenu"
width="10rem" width="10rem"
popper-style=" popper-style="
padding: .6rem 0.7rem; padding: .6rem 0.7rem;
@@ -54,7 +54,9 @@
v-model:visible="item.visible" v-model:visible="item.visible"
> >
<template #reference> <template #reference>
<span @click.stop class="icon"><svg-icon name="more" size="16" /></span> <span @click.stop="item.visible = !item.visible" class="icon">
<svg-icon name="more" size="16" />
</span>
</template> </template>
<div class="history-item-menu"> <div class="history-item-menu">
<div class="rename" @click="onRenameHistoryItem(item)"> <div class="rename" @click="onRenameHistoryItem(item)">