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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<template>
<div class="layer-item" @click="onClickLayer">
<div class="layer-item">
<div class="item" @click="onClickLayer">
<div class="drag"><svg-icon name="dc-drag" size="18" /></div>
<div class="thumb">
<img v-if="layer.thumbnail" :src="layer.thumbnail" />
@@ -26,9 +27,18 @@
><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> -->
<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>
<slot></slot>
</div>
</template>
<script setup lang="ts">
@@ -42,6 +52,10 @@
layer: {
type: Object,
default: () => ({})
},
isGroup: {
type: Boolean,
default: false
}
})
const nameInputRef = ref(null)
@@ -72,10 +86,32 @@
const info = props.layer.info
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>
<style lang="less" scoped>
.layer-item {
&:last-child > .item {
border-bottom: none;
}
&:not([draging='true']) > .item:hover {
background-color: #f5f5f5;
}
&.active > .item {
background-color: #ededed !important;
}
&.drag {
opacity: 0;
}
&.ghost > .item,
&.chosen > .item {
box-shadow: inset 0 0 5px #aaa;
background-color: #ededed !important;
}
> .item {
width: 100%;
height: 9.5rem;
display: flex;
@@ -85,23 +121,7 @@
background-color: #fafafa;
gap: 1rem;
border-bottom: 0.2rem solid #ededed;
&:last-child {
border-bottom: none;
}
&:not([draging='true']):hover {
background-color: #f5f5f5;
}
&.active {
background-color: #ededed !important;
}
&.drag {
opacity: 0;
}
&.ghost,
&.chosen {
box-shadow: inset 0 0 5px #aaa;
background-color: #ededed !important;
}
> .drag {
padding: 0.3rem;
cursor: move;
@@ -149,6 +169,16 @@
align-items: center;
justify-content: center;
}
> .show-group {
> .c-svg {
transition: transform 0.2s;
transform: rotate(-90deg);
}
&.active > .c-svg {
transform: rotate(0deg);
}
}
}
}
}
</style>

View File

@@ -12,28 +12,16 @@
<div class="content">
<VueDraggable
:model-value="list"
@start="handleDragStart"
@end="handleDragEnd"
@start="(e) => handleDragStart(e)"
@end="(e) => handleDragEnd(e)"
@add="(e) => handleAdd(e)"
class="sortable-layers"
:data-container-type="'root'"
:data-parent-id="null"
:animation="250"
:disabled="false"
handle=".drag"
ghost-class="ghost"
chosen-class="chosen"
drag-class="drag"
v-bind="config"
:group="{
name: 'groupName',
name: 'group',
pull: 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
v-for="layer in list"
@@ -41,8 +29,35 @@
:layer="layer"
:draging="draging"
: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>
</div>
</div>
</template>
@@ -53,14 +68,65 @@
import layerItem from './layer-item.vue'
const draging = ref(false)
const layerManager = inject('layerManager') as any
const canvasManager = inject('canvasManager') as any
const list = computed(() => layerManager.layers.value)
const handleDragStart = () => {
draging.value = true
const config = ref({
'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
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 = () => {
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>

View File

@@ -1,5 +1,6 @@
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 {
@@ -7,6 +8,7 @@ export class AISelectboxToolManager {
canvasManager: any
stateManager: any
layerManager: any
toolManager: any
isDragging: boolean = false
startX: number = 0
@@ -16,8 +18,17 @@ export class AISelectboxToolManager {
this.canvasManager = options.canvasManager
this.stateManager = options.stateManager
this.layerManager = options.layerManager
this.toolManager = options.toolManager
}
/** 处理切换工具 */
handleToolChange(oldTool: string, newTool: string) {
if (newTool === OperationType.SELECTBOX) {
// 切换到智能框选工具
} else {
// 切换到普通框选工具
}
}
mouseDownEvent(e) {
this.isDragging = true
this.startX = e.absolutePointer.x
@@ -63,7 +74,7 @@ export class AISelectboxToolManager {
this.canvasManager.canvas.remove(this.demoObject)
this.canvasManager.canvas.renderAll()
this.createSelectbox()
// this.createSelectbox()
}
@@ -81,15 +92,11 @@ export class AISelectboxToolManager {
stroke: "rgba(255, 77, 71, 1)",
strokeWidth: 1.5,
strokeDashArray: [4, 4],
fill: "rgba(255, 186, 186, 0.5)",
fill: "transparent",
strokeUniform: true, // 保持描边宽度不随缩放改变
// strokeLineCap: "round",// 折线端点样式
// strokeLineJoin: "bevel", // 折线连接样式
// selectable: false,
// evented: false,
excludeFromExport: true,
hoverCursor: "default",
moveCursor: "default",
selectable: false,
evented: false,
absolutePositioned: true,
};
async createSelectbox() {
const url = "http://118.31.39.42:3000/falls/1a48ed3a-1faa-4fcd-bf07-765dba1702c5.png"
@@ -112,200 +119,31 @@ export class AISelectboxToolManager {
}).join(" L ");
const path = new fabric.Path(`M ${str} z`);
path.set({
left: left + minX * scaleX,
top: top + minY * scaleY,
left: left + minX,
top: top + minY,
scaleX: scaleX,
scaleY: scaleY,
...this.selectionStyle,
});
const rect1 = new fabric.Rect({
left: 0,
top: 0,
width: 100,
height: 100,
fill: '#f00',
info: {
id: createId("rect"),
name: '矩形图层',
const group = await this.layerManager.createGroupLayer({
clipPath: path,
}, false, false)
const rect = await this.layerManager.createRectLayer({
width: path.width,
height: path.height,
left: left + minX,
top: top + minY,
fill: "rgba(255, 186, 186, 0.5)",
info: { parentId: group.info.id },
}, false, true)
await this.canvasManager.updateSubLayerClipPath()
await this.layerManager.updateLayerThumbnailsById(rect.info.id, "", false)
await this.layerManager.updateLayerThumbnailsById(group.info.id, rect.thumbnail)
this.stateManager.recordState()
this.toolManager.setTool(OperationType.SELECT)
}
})
const rect2 = new fabric.Rect({
left: 200,
top: 200,
width: 100,
height: 100,
fill: '#ff0',
info: {
id: createId("rect"),
name: '矩形图层',
}
})
this.layerManager.createGroupLayer({
child: [rect1, rect2],
})
// this.canvasManager.canvas.add(path)
// this.canvasManager.canvas.renderAll()
}
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 { CanvasEventManager } from "./events/CanvasEventManager";
import { OperationType } from '../tools/layerHelper'
import { cloneObjects } from '../tools/canvasMethod'
import { createId } from '../../tools/tools'
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 = function () {
const args = [...arguments]
@@ -129,25 +130,28 @@ export class CanvasManager {
/** 测试-开始 */
// this.stateManager.setIsRecord(false)
// const rect = await this.layerManager.createRectLayer({ left: 200 })
// await this.layerManager.createStarLayer({ left: 400 })
// await this.layerManager.createArrowLayer({ left: 600 })
// const groupObject = await this.layerManager.createGroupLayer()
// const parentId = groupObject.info.id
// 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.stateManager.setIsRecord(true)
/** 测试-结束 */
this.resetZoom(false, true)// 画布居中
this.stateManager.toolManager.setTool(OperationType.SELECT)
this.layerManager.updateLayers()
this.stateManager.recordState()
// this.stateManager.toolManager.setTool(OperationType.RECTANGLE)
}
/** 画布添加对象 */
async add(obj: any, isRecord = true) {
this.canvas.add(obj)
const id = obj?.info?.id || ""
if (id) {
this.layerManager.updateLayers()
await this.layerManager.updateLayers(!!obj.info.parentId)
this.renderAll()
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() {
// 创建画布事件管理器
@@ -177,8 +210,18 @@ export class CanvasManager {
}
/** 设置激活对象 */
setActiveObjectById(id: string) {
this.discardActiveObject()
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()
}
resetZoom(animated = true, adaptive = true) {
@@ -222,11 +265,11 @@ export class CanvasManager {
renderAll() {
this.canvas.renderAll()
}
deleteObjectById(id: string) {
deleteObjectById(id: string, isUpdate = true) {
const object = this.getObjectById(id)
if (object) {
this.canvas.remove(object)
this.layerManager.updateLayers()
if (isUpdate) this.layerManager.updateLayers()
this.renderAll()
}
}
@@ -235,13 +278,6 @@ export class CanvasManager {
this.canvas.discardActiveObject()
this.renderAll()
}
// 拖拽排序
dragSort(id, newIndex) {
this.canvas.moveTo(this.getObjectById(id), newIndex)
this.layerManager.updateLayers()
this.renderAll()
this.stateManager.recordState()
}
/** 画笔事件 */
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 activeLayer = this.getObjectById(activeID)
if (activeLayer) {
if (activeLayer && activeLayer.fill?.repeat !== "repeat") {
this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
} else {
const emptyLayer = this.layerManager.createEmptyLayer(false);
const emptyLayer = await this.layerManager.createEmptyLayer(false);
this.layerManager.setActiveID(emptyLayer.info.id, false)
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
}

View File

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

View File

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

View File

@@ -131,7 +131,7 @@ export class ShapeToolManager {
upRectangle(object) {
if (object.width === 0) object.width = 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) {
this.layerManager.createLineLayer(object, true)
this.layerManager.createLineLayer(object)
}
/** 绘制椭圆 */
@@ -170,7 +170,7 @@ export class ShapeToolManager {
upEllipse(object) {
if (object.rx === 0) object.rx = 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) {
if (object.width === 0) object.width = 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) {
if (object.width === 0) object.width = 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,
}, true)
} else {
this.layerManager.createArrowLayer(object, true)
this.layerManager.createArrowLayer(object)
}
}

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ export class CanvasEventManager {
}
this.shapeToolManager = new ShapeToolManager(managers)
this.aiSelectboxToolManager = new AISelectboxToolManager(managers)
this.stateManager.setManager({ aiSelectboxToolManager: this.aiSelectboxToolManager })
// 初始化所有事件
this.initEvents();
@@ -59,9 +60,8 @@ export class CanvasEventManager {
// 共享事件
this.setupSelectionEvents();
this.setupObjectEvents();
// this.setupDoubleClickEvents();
this.setupDoubleClickEvents();
// this.setupHandlePathCreated();
}
setupZoomEvents() {
@@ -730,8 +730,10 @@ export class CanvasEventManager {
});
this.canvas.on("object:modified", async (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 (target.type === "group") await this.canvasManager.updateSubLayerClipPath()
this.stateManager.recordState();
});
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) {
const selected = opt.selected[0];
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、平板和移动设备

View File

@@ -93,3 +93,157 @@ export function angleBetweenPointsDegrees(x1, y1, x2, y2) {
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,
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)
})
// 导出图片

View File

@@ -68,3 +68,11 @@ export const BlendMode = {
DESTINATION_IN: "destination-in", // 目标内
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)">
<slot></slot>
</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>
</template>
<script lang="ts" setup>
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'
const handles = ref({
[NODE_TYPE.INPUT]: [{ id: 'Right', type: 'source', position: Position.Right }],
@@ -48,39 +45,6 @@
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 arr = [NODE_DATATYPE.RESULT_IMAGE]
return arr.includes(props.node?.data?.type)
@@ -106,25 +70,6 @@
> .item {
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 {
--top: 50%;
}

View File

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

View File

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

View File

@@ -42,12 +42,15 @@
import ColorPalette from './color-palette.vue'
import To3View from './to-3view.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 ToVideo from './to-video.vue'
// import AddPrint from './add-print.vue'
// import ToCAD from './to-cad.vue'
const { t } = useI18n()
const attrs = useAttrs()
const componentRef = ref(null)
const components = [
@@ -143,14 +146,13 @@
const onGenerateClick = async () => {
const data = componentRef.value?.getApiData?.() || {}
const subordNode = stateManager.getSubordNodeById(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)
if(!attrs.node?.data?.originalImage)console.log('originalImage 找不到原始图片')
const apiData = {
sketchId: props.sketchId,
imageUrl: attrs.node?.data?.originalImage,
imageUrl: superiorNodeUrl,
...data,
}
const taskList = await currentComponent.value.api(apiData).then((rv)=>{
@@ -178,8 +180,9 @@
})
}
//删除功能卡片
const onDeleteClick = ()=>{
stateManager.deleteNode(attrs.node.id,{isElMessageBox:true})
const onDeleteClick = async ()=>{
console.log(stateManager.nodes)
stateManager.getSubordinateAllNodes(attrs.node.id,{ isElMessageBox: true })
}
const setDate = () => {
for (const key in props.data) {

View File

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

View File

@@ -13,7 +13,7 @@
const attrs = useAttrs()
const stateManager = inject('stateManager') as any
const data = reactive({
url: attrs.node.data.originalImage,
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
})
const getApiData = ()=>{
let glbUrl = null

View File

@@ -53,6 +53,9 @@
</div>
</div>
</div>
<div class="add" @mousedown.stop v-if="isAdd" @click="onAdd">
<svg-icon name="add" size="14" size-unit="px" />
</div>
</div>
</template>
@@ -61,7 +64,7 @@
import { downloadImage } from '../../../tools/tools'
import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch, computed, onMounted } from '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 openThreeModelPreview = inject('openThreeModelPreview') as (url: string) => void
const props = defineProps({
@@ -229,6 +232,38 @@
eventManager.removeEvents()
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)
onBeforeUnmount(() => {
document.removeEventListener('mousedown', hideMenu)
@@ -250,6 +285,25 @@
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 {
width: 244px;
border-radius: 16px;

View File

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

View File

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

View File

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

View File

@@ -101,7 +101,7 @@ export class StateManager {
source: source,
target: target,
selectable: false,
visible: (node.data.type !== NODE_DATATYPE.RESULT_IMAGE || node.data.isActive),
visible: (node.data.isActive),
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) {
this.nodes.value.push(node);
@@ -119,37 +119,69 @@ export class StateManager {
this.exportFlow()
}
/** 删除节点 */
async deleteNode(id: string, { isElMessageBox } = { isElMessageBox: false }) {
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('删除操作被取消')
async deleteNode(id: string) {
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) }
/** 获取下级节点 */
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] }
/** 获取上级生成节点的图片 */
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 }
/** 设置光标 */
@@ -222,9 +254,9 @@ export class StateManager {
/** 显示指定子节点和父节点连接线,隐藏父节点和其他子节点链接线, */
showNodeConnections(id: string) {
if(!id) return
const node = this.getNodeById(id)
if(node.data.component != NODE_DATATYPE.RESULT_IMAGE && node.data.superiorID) return
let edges_ = JSON.parse(JSON.stringify(this.edges.value))
if(!node?.data?.superiorID) return
this.nodes.value.forEach((nodeItem) => {
if(node.data.superiorID === nodeItem.data.superiorID && nodeItem.id == id) {
nodeItem.data.isActive = true

View File

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

View File

@@ -173,21 +173,17 @@ export default {
deleteHint:'Once deleted, you wont be able to view this conversation again.',
restoreChat:'Restore chat?',
restoreHint:'Once deleted, you wont be able to view this conversation again.',
cancel: 'cancel',
Cancel: 'Cancel',
Confirm: 'Confirm',
export: 'Export',
},
//generateSketch
generateSketch: {
restore: 'Restore',
delete: 'Delete',
edit: 'Edit'
},
flowCanvas: {
deleteCardConfirm: 'Are you sure you want to delete this function card?',
confirm: 'Confirm',
cancel: 'Cancel',
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: {
inputPlaceholder: 'Ask anything',

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@
<span class="label" v-show="!item.edit">{{ item.name }}</span>
<el-popover
placement="right"
trigger="click"
trigger="contextmenu"
width="10rem"
popper-style="
padding: .6rem 0.7rem;
@@ -54,7 +54,9 @@
v-model:visible="item.visible"
>
<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>
<div class="history-item-menu">
<div class="rename" @click="onRenameHistoryItem(item)">