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

This commit is contained in:
X1627315083@163.com
2026-03-18 10:59:15 +08:00
24 changed files with 809 additions and 83 deletions

View File

@@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.085 5.92551H0.54125C0.387639 5.92551 0.259028 5.87405 0.155417 5.77113C0.0518057 5.66835 0 5.54071 0 5.38821C0 5.23571 0.0518057 5.10662 0.155417 5.00092C0.259028 4.89509 0.387639 4.84217 0.54125 4.84217H9.085L5.18958 0.946756C5.0825 0.839673 5.02625 0.713491 5.02083 0.568214C5.01556 0.422936 5.07056 0.289255 5.18583 0.167172C5.30125 0.0531442 5.43028 -0.00254935 5.57292 8.95351e-05C5.71556 0.00272842 5.84319 0.0605064 5.95583 0.173423L10.6935 4.91717C10.7633 4.98703 10.8141 5.06044 10.846 5.13738C10.8781 5.21433 10.8942 5.29759 10.8942 5.38717C10.8942 5.47662 10.8781 5.55988 10.846 5.63696C10.8141 5.71391 10.766 5.78446 10.7019 5.84863L5.96 10.5905C5.84458 10.7059 5.71903 10.7615 5.58333 10.7572C5.44764 10.7529 5.32208 10.6937 5.20667 10.5797C5.09139 10.4576 5.03375 10.3254 5.03375 10.1832C5.03375 10.041 5.09139 9.91446 5.20667 9.80363L9.085 5.92551Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 995 B

View File

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.725098" y="0.724609" width="13.0481" height="13.0481" rx="2.17468" stroke="black" stroke-width="1.44978"/>
<path d="M16.6733 5.79687V12.3209C16.6733 14.723 14.726 16.6703 12.3239 16.6703H4.3501" stroke="black" stroke-width="1.44978" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@@ -0,0 +1,3 @@
<svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.84125 5.40854L8.06875 0.172916C8.18375 0.0576386 8.31264 0 8.45542 0C8.59833 0 8.7275 0.0575006 8.84292 0.172501C8.9582 0.287501 9.01583 0.416458 9.01583 0.559375C9.01583 0.70243 8.9582 0.831667 8.84292 0.947084L3.30604 6.47583C3.16924 6.61264 3.01083 6.68104 2.83083 6.68104C2.65083 6.68104 2.49243 6.61264 2.35563 6.47583L0.172917 4.30125C0.0576393 4.18625 0 4.05736 0 3.91458C0 3.77167 0.0575 3.6425 0.1725 3.52708C0.2875 3.41181 0.416458 3.35417 0.559375 3.35417C0.702431 3.35417 0.831667 3.41181 0.947083 3.52708L2.84125 5.40854Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 664 B

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="M7.59042 15.1667C6.54722 15.1667 5.56472 14.9693 4.64292 14.5746C3.72111 14.1799 2.91431 13.6367 2.2225 12.9452C1.53069 12.2537 0.987292 11.4475 0.592292 10.5267C0.19743 9.60597 0 8.62215 0 7.57521C0 6.52826 0.197361 5.54736 0.592083 4.6325C0.986806 3.71764 1.52993 2.91431 2.22146 2.2225C2.91299 1.53069 3.71917 0.987292 4.64 0.592292C5.56069 0.19743 6.54451 0 7.59146 0C8.6384 0 9.61931 0.197361 10.5342 0.592083C11.449 0.986805 12.2524 1.52993 12.9442 2.22146C13.636 2.91299 14.1794 3.7175 14.5744 4.635C14.9692 5.55264 15.1667 6.53306 15.1667 7.57625C15.1667 8.61944 14.9693 9.60194 14.5746 10.5238C14.1799 11.4456 13.6367 12.2524 12.9452 12.9442C12.2537 13.636 11.4492 14.1794 10.5317 14.5744C9.61403 14.9692 8.63361 15.1667 7.59042 15.1667ZM7.58333 14.0833C9.38889 14.0833 10.9236 13.4514 12.1875 12.1875C13.4514 10.9236 14.0833 9.38889 14.0833 7.58333C14.0833 5.77778 13.4514 4.24306 12.1875 2.97917C10.9236 1.71528 9.38889 1.08333 7.58333 1.08333C5.77778 1.08333 4.24306 1.71528 2.97917 2.97917C1.71528 4.24306 1.08333 5.77778 1.08333 7.58333C1.08333 9.38889 1.71528 10.9236 2.97917 12.1875C4.24306 13.4514 5.77778 14.0833 7.58333 14.0833Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.197083 9.55862C0.0656943 9.42723 0 9.28244 0 9.12424C0 8.96619 0.0656943 8.82251 0.197083 8.6932L8.70521 0.193203C8.8366 0.0670915 8.97944 0.0027174 9.13375 7.85068e-05C9.28819 -0.00256038 9.43062 0.0613275 9.56104 0.191744C9.6934 0.324105 9.75958 0.469384 9.75958 0.627578C9.75958 0.785634 9.69389 0.929315 9.5625 1.05862L1.0625 9.55862C0.933194 9.69001 0.789514 9.7557 0.631458 9.7557C0.473264 9.7557 0.328472 9.69001 0.197083 9.55862Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="15" viewBox="0 0 16 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.33333 3.41354L6.54479 0.519166C6.67799 0.340555 6.83611 0.209376 7.01917 0.125626C7.20208 0.0418756 7.39556 0 7.59958 0C7.80347 0 7.99438 0.0434724 8.17229 0.130417C8.35021 0.217222 8.50701 0.346805 8.64271 0.519166L10.8542 3.41354L14.2579 4.53521C14.5454 4.64104 14.7674 4.80819 14.9238 5.03667C15.0803 5.26514 15.1585 5.5175 15.1585 5.79375C15.1585 5.92528 15.14 6.05632 15.1029 6.18687C15.0658 6.31757 15.0048 6.44278 14.9198 6.5625L12.7179 9.57208L12.8013 12.835C12.8013 13.2182 12.6705 13.5428 12.409 13.809C12.1476 14.0752 11.8348 14.2083 11.4706 14.2083C11.4613 14.2083 11.344 14.1901 11.1185 14.1538L7.58333 13.149L4.04688 14.1546C3.97826 14.1818 3.91743 14.1976 3.86437 14.2019C3.81132 14.2062 3.75722 14.2083 3.70208 14.2083C3.33694 14.2083 3.01938 14.0752 2.74938 13.809C2.47951 13.5428 2.35153 13.2182 2.36542 12.835L2.44875 9.57208L0.23875 6.5625C0.152917 6.43986 0.0916667 6.31799 0.055 6.19688C0.0183333 6.07563 0 5.94729 0 5.81187C0 5.52576 0.0824306 5.26479 0.247292 5.02896C0.412014 4.79326 0.642083 4.62604 0.9375 4.52729L4.33333 3.41354ZM4.96 4.34458L1.27562 5.57208C1.19549 5.59875 1.14076 5.65354 1.11146 5.73646C1.08201 5.81924 1.09403 5.89535 1.1475 5.96479L3.53792 9.26375L3.44875 12.8606C3.44333 12.9515 3.47535 13.0235 3.54479 13.0769C3.61424 13.1303 3.69174 13.1438 3.77729 13.1171L7.58333 12.0192L11.4102 13.0963C11.4958 13.1229 11.5733 13.1095 11.6427 13.056C11.7122 13.0027 11.7442 12.9306 11.7388 12.8398L11.6267 9.24208L14.0192 5.94396C14.0726 5.87451 14.0847 5.7984 14.0552 5.71562C14.0259 5.63271 13.9712 5.57792 13.891 5.55125L10.1858 4.34458L7.78375 1.23729C7.73556 1.16771 7.66875 1.13292 7.58333 1.13292C7.49792 1.13292 7.43111 1.16771 7.38292 1.23729L4.96 4.34458Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.706857 12C0.414255 12 0.206211 11.8612 0.0827259 11.5837C-0.0406133 11.3062 -0.0260342 11.0369 0.126463 10.7758L6.41961 0.342906C6.56321 0.114302 6.75667 0 7 0C7.24333 0 7.43679 0.114302 7.58039 0.342906L13.8735 10.7758C14.026 11.0369 14.0406 11.3062 13.9173 11.5837C13.7938 11.8612 13.5857 12 13.2931 12H0.706857ZM1.53284 10.7327H12.4672L7 1.61777L1.53284 10.7327Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@@ -2,14 +2,31 @@
<div class="header-tools">
<template v-for="(v, i) in tools" :key="i">
<span class="line" v-if="v.type === 'line'"></span>
<div v-else class="item">
<span
v-else
class="icon"
@click="onClickTool(v)"
:class="{ active: v.name === tool, disabled: v.disabled }"
:class="{
active: v.name === tool,
disabled: v.disabled
}"
>
<svg-icon :name="v.icon" :size="v.iconSize" />
</span>
<span class="more" v-if="v.child" @click="onClickMore(v)">
<svg-icon name="dc-down_arrow2" size="7" />
</span>
<div v-if="v.child" class="child" v-show="v.showChild">
<div v-for="(v_, i_) in v.child" :key="i_" @click="onClickTool(v_, v)">
<span v-show="tool === v_.name" class="dui">
<svg-icon name="dc-dui" size="9" />
</span>
<span class="icon"><svg-icon :name="v_.icon" :size="v_.iconSize" /></span>
<span class="label">{{ v_.label }}</span>
<span class="tip">{{ v_.tip }}</span>
</div>
</div>
</div>
</template>
<button class="export" @click="emit('export')">
<span class="icon"><svg-icon name="export" size="12" /></span>
@@ -42,48 +59,108 @@
const toolManager = inject('toolManager') as any
const objectManager = inject('objectManager') as any
const canvasManager = inject('canvasManager') as any
const layerManager = inject('layerManager') as any
const tool = computed(() => toolManager.currentTool.value)
const historyIndex = computed(() => stateManager.historyIndex.value)
const historyList = computed(() => stateManager.historyList.value)
const isUndo = computed(() => !historyList.value[historyIndex.value - 1])
const isRedo = computed(() => !historyList.value[historyIndex.value + 1])
const activeLayerId = computed(() => layerManager.activeID.value)
const tools = ref([
{ name: OperationType.SELECT, icon: 'dc-select', iconSize: 16, disabled: ref(false) },
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
{ name: OperationType.DRAW, icon: 'dc-brush', iconSize: 18, disabled: ref(false) },
{ name: OperationType.ERASER, icon: 'dc-eraser', iconSize: 18, disabled: ref(false) },
{ name: OperationType.SELECT, icon: 'dc-select', iconSize: 16 },
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18 },
{ name: OperationType.DRAW, icon: 'dc-brush', iconSize: 18 },
{ name: OperationType.ERASER, icon: 'dc-eraser', iconSize: 18 },
{ icon: 'dc-image', iconSize: 17, on: () => onImageClick() },
{ name: OperationType.SELECTBOX, icon: 'dc-selectbox', iconSize: 16 },
{
name: OperationType.IMAGE,
icon: 'dc-image',
iconSize: 17,
disabled: ref(false),
on: () => onImageClick()
name: OperationType.RECTANGLE,
icon: 'dc-rectangle',
iconSize: 16,
showChild: false,
child: [
{
name: OperationType.RECTANGLE,
label: 'Rectangle',
icon: 'dc-rectangle',
iconSize: 13
},
{ name: OperationType.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
{ name: OperationType.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
{
name: OperationType.LINE,
label: 'Line',
icon: 'dc-line',
iconSize: 10
},
{
name: OperationType.ARROW,
label: 'Arrow',
icon: 'dc-arrow',
iconSize: 11
},
{
name: OperationType.ELLIPSE,
label: 'Ellipse',
icon: 'dc-ellipse',
iconSize: 15
},
{
name: OperationType.TRIANGLE,
label: 'Polygon',
icon: 'dc-triangle',
iconSize: 14
},
{
name: OperationType.STAR,
label: 'Star',
icon: 'dc-star',
iconSize: 15
}
]
},
{ type: 'line' },
{
name: OperationType.UNDO,
icon: 'dc-undo',
iconSize: 18,
disabled: isUndo,
on: () => stateManager.undoState()
},
{
name: OperationType.REDO,
icon: 'dc-redo',
iconSize: 18,
disabled: isRedo,
on: () => stateManager.redoState()
},
{
name: 'copy',
icon: 'dc-copy',
iconSize: 16,
disabled: computed(() => !activeLayerId.value),
on: () => onCopyActiveLayer()
},
{
name: 'delete',
icon: 'dc-delete',
iconSize: 18,
disabled: computed(() => !activeLayerId.value),
on: () => onDeleteActiveLayer()
}
])
const onClickTool = (tool: any) => {
if (tool.disabled?.value) return
if (tool.on) {
tool.on()
} else {
toolManager.setTool(tool.name)
const onClickTool = (v: any, parent?: any) => {
if (v.disabled?.value) return
if (parent) {
parent.name = v.name
parent.icon = v.icon
parent.iconSize = v.iconSize * 1.23
}
tools.value.forEach((v) => v.hasOwnProperty('showChild') && (v.showChild = false))
v.on ? v.on() : toolManager.setTool(v.name)
}
const onClickMore = (v: any) => {
tools.value.forEach((item) => {
if (item.hasOwnProperty('showChild') && item.name !== v.name) item.showChild = false
})
v.showChild = !v.showChild
}
const onImageClick = async () => {
const layer = await importLocalImage(false)
@@ -91,6 +168,14 @@
objectManager.setBlendMode(id, BlendMode.MULTIPLY)
objectManager.setFillRepeat(id)
}
const onCopyActiveLayer = () => {
if (!activeLayerId.value) return
layerManager.copyLayerById(activeLayerId.value)
}
const onDeleteActiveLayer = () => {
if (!activeLayerId.value) return
layerManager.deleteLayerById(activeLayerId.value)
}
const onWorkbench = async () => {
exportCanvasToImage(canvasManager.canvas).then((url) => {
emit('workbench', { url })
@@ -123,15 +208,19 @@
border-radius: 0.2rem;
margin: 0 0.6rem;
}
> .icon {
> .item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
> span {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
--svg-icon-color: #000;
border-radius: 0.4rem;
height: 3rem;
&:not(.disabled).active,
&:not(.disabled):hover {
background-color: #ebebeb;
@@ -141,6 +230,61 @@
cursor: not-allowed;
}
}
> .icon {
width: 3rem;
}
> .more {
width: 1.1rem;
}
> .child {
cursor: pointer;
position: absolute;
min-width: 21rem;
top: calc(100% + 2rem);
left: 0;
padding: 0.6rem 0;
border: 0.1remx solid #d9d9d9;
background-color: #fff;
border-radius: 1.2rem;
box-shadow: 0 0.4rem 0.4rem 0 rgba(0, 0, 0, 0.25);
> div {
height: 3.2rem;
display: flex;
align-items: center;
// justify-content: center;
gap: 0.8rem;
position: relative;
padding: 0 1.6rem;
> .dui {
position: absolute;
left: 1.5rem;
transform: translateX(-50%);
}
> .icon {
width: 2rem;
height: 2rem;
margin-left: 0.8rem;
}
> .label {
flex: 1;
font-size: 1.4rem;
color: #000;
}
> .tip {
font-size: 1.3rem;
color: rgba(13, 13, 13, 0.65);
}
&:not(.disabled).active,
&:not(.disabled):hover {
background-color: #ebebeb;
}
&.disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
}
> button {
cursor: pointer;
border: none;

View File

@@ -33,6 +33,19 @@
/>
</div>
</div>
<div>
<div class="label">Opacity</div>
<div class="value">
<depth-slider
v-model="opacity"
:min="0"
:max="100"
:tipFormatter="(v) => v + '%'"
@input="inputOpacity"
@change="changeOpacity"
/>
</div>
</div>
<div>
<div class="label">Gap X</div>
<div class="value">
@@ -101,6 +114,7 @@
const gapX = ref(0)
const gapY = ref(0)
const offset = ref({ x: 0, y: 0 })
const opacity = ref(100)
const updateData = async () => {
await nextTick()
@@ -113,6 +127,7 @@
x: Math.round((fill.offsetX / props.object.width) * 100),
y: Math.round((fill.offsetY / props.object.height) * 100)
}
opacity.value = Math.round(props.object.opacity * 100)
}
updateData()
@@ -138,6 +153,15 @@
objectManager.updateFillRepeatGap(id.value, options, isRecord)
}
const inputOpacity = () => setOpacity(false)
const changeOpacity = () => setOpacity(true)
const setOpacity = (isRecord: boolean) => {
const options = {
opacity: opacity.value / 100
}
objectManager.updateOpacity(id.value, options, isRecord)
}
stateManager.event.add('canvas:undo', updateData)
stateManager.event.add('canvas:redo', updateData)
onBeforeUnmount(() => {

View File

@@ -22,7 +22,7 @@
><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><svg-icon name="dc-down_arrow" size="11" /></span> -->
</div>
</div>
</template>

View File

@@ -63,7 +63,7 @@
layerManager.dragSort(data.info.id, newIndex)
}
const addLayer = () => {
layerManager.createEmptyLayer()
layerManager.createEmptyLayer(true, true)
}
</script>

View File

@@ -3,6 +3,7 @@ import { fabric } from 'fabric-with-all'
import { createId } from '../../tools/tools'
import { exportObjectsToImage, exportObjectToThumbnail } from '../tools/exportMethod'
import { OperationType } from '../tools/layerHelper'
import { getArrowPath, cloneObjects, getStarArr } from '../tools/canvasMethod'
export class LayerManager {
stateManager: any
@@ -53,6 +54,7 @@ export class LayerManager {
this.canvasManager.renderAll()
}
}
/** 删除指定图层 */
deleteLayerById(id, isActive = true) {
this.canvasManager.deleteObjectById(id)
if (id === this.activeID.value && isActive) {
@@ -60,6 +62,24 @@ export class LayerManager {
}
if (isActive) this.stateManager.recordState()
}
/** 复制指定图层 */
copyLayerById(id) {
const object = this.canvasManager.getObjectById(id)
if (!object) return console.warn('复制图层失败对象不存在ID:', id)
cloneObjects([object]).then(objects => {
const newObject = objects[0]
const info = JSON.parse(JSON.stringify(newObject.info))
info.id = createId("image")
// info.name = info.name
newObject.set({
top: newObject.top + 15,
left: newObject.left + 15,
info: info,
})
this.canvasManager.add(newObject)
this.setActiveID(newObject.info.id)
})
}
// 拖拽排序
dragSort(id, newIndex) {
const index = Math.abs(this.layers.value.length - newIndex - 1)
@@ -90,7 +110,7 @@ export class LayerManager {
}
}
/** 创建空图层 */
createEmptyLayer(isRecord = true) {
createEmptyLayer(isRecord = true, isActive = false) {
const emptyObject = new fabric.Rect({
width: 0,
height: 0,
@@ -102,6 +122,7 @@ export class LayerManager {
})
this.setLayerPosition(emptyObject)
this.canvasManager.add(emptyObject, isRecord)
if (isActive) this.setActiveID(emptyObject.info.id, false)
return emptyObject
}
/** 创建文本图层 */
@@ -138,23 +159,109 @@ export class LayerManager {
if (isActive) this.setActiveID(rectObject.info.id)
return rectObject
}
/** 创建圆形图层 */
async createCircleLayer(options?: any, isActive = false) {
const circleObject = new fabric.Circle({
/** 创建直线图层 */
async createLineLayer(options?: any, isActive = false) {
const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0]
delete options.x1
delete options.y1
delete options.x2
delete options.y2
const lineObject = new fabric.Line(line, {
stroke: 'black', // 线条颜色
strokeWidth: 2, // 线条粗细
...(options || {}),
info: {
id: createId("line"),
name: '直线图层',
...(options?.info || {}),
}
})
this.setLayerPosition(lineObject, options)
await this.canvasManager.add(lineObject)
if (isActive) this.setActiveID(lineObject.info.id)
return lineObject
}
/** 创建椭圆图层 */
async createEllipseLayer(options?: any, isActive = false) {
const ellipseObject = new fabric.Ellipse({
radius: 50,
fill: '#000',
...(options || {}),
info: {
id: createId("circle"),
name: '圆图层',
id: createId("ellipse"),
name: '圆图层',
...(options?.info || {}),
}
})
this.setLayerPosition(circleObject, options)
await this.canvasManager.add(circleObject)
if (isActive) this.setActiveID(circleObject.info.id)
return circleObject
this.setLayerPosition(ellipseObject, options)
await this.canvasManager.add(ellipseObject)
if (isActive) this.setActiveID(ellipseObject.info.id)
return ellipseObject
}
/** 创建三角形图层 */
async createTriangleLayer(options?: any, isActive = false) {
const triangleObject = new fabric.Triangle({
width: 100,
height: 100,
fill: '#000',
...(options || {}),
info: {
id: createId("triangle"),
name: '三角形图层',
...(options?.info || {}),
}
})
this.setLayerPosition(triangleObject, options)
await this.canvasManager.add(triangleObject)
if (isActive) this.setActiveID(triangleObject.info.id)
return triangleObject
}
/** 创建五角星图层 */
async createStarLayer(options?: any, isActive = false) {
const width = options?.width || 100
const height = options?.height || 100
delete options.points
const starObject = new fabric.Polygon(getStarArr(width, height), {
fill: '#000',
...(options || {}),
info: {
id: createId("star"),
name: '五角星图层',
...(options?.info || {}),
}
})
this.setLayerPosition(starObject, options)
await this.canvasManager.add(starObject)
if (isActive) this.setActiveID(starObject.info.id)
return starObject
}
/** 创建箭头图层 */
async createArrowLayer(options?: any, isActive = false) {
const width = options?.width || 100
const height = options?.height || 10
delete options.width
delete options.height
const arrowObject = new fabric.Path(getArrowPath(width, height), {
stroke: '#000', // 只设置边框颜色
strokeWidth: 3, // 边框宽度
fill: 'transparent', // 不填充
strokeLineCap: 'round',
strokeLineJoin: 'round',
...(options || {}),
info: {
id: createId("star"),
name: '箭头图层',
...(options?.info || {}),
}
});
this.setLayerPosition(arrowObject, options)
this.canvasManager.add(arrowObject)
if (isActive) this.setActiveID(arrowObject.info.id)
return arrowObject
}
/** 创建图片图层 */
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true) {
const canvasWidth = this.canvasManager.canvasWidth

View File

@@ -207,5 +207,24 @@ export class ObjectManager {
}
}
/** 修改透明度
* @param id 目标对象ID
* @param options 透明度参数
* @param options.opacity 透明度
* @param isRecord 是否记录
*/
async updateOpacity(id: string, options: any, isRecord: boolean) {
const object = this.getFillRepeatObject(id)
if (!object) return null
const opacity = options.opacity
object.set("opacity", opacity);
this.canvasManager.renderAll()
if (isRecord) {
this.stateManager.recordState()
this.layerManager.updateLayerThumbnailsById(id)
}
}
dispose() { }
}

View File

@@ -1,3 +1,4 @@
import { OperationType, OperationTypes } from "../tools/layerHelper";
import { fabric } from 'fabric-with-all'
/** 矩形工具管理器 */
export class RectToolManager {
@@ -10,6 +11,9 @@ export class RectToolManager {
startX: number = 0
startY: number = 0
demoObject: any
tools = [
OperationType.RECTANGLE
]
constructor(options) {
this.canvasManager = options.canvasManager
this.stateManager = options.stateManager

View File

@@ -0,0 +1,255 @@
import { OperationType, OperationTypes } from "../tools/layerHelper";
import { getStarArr, getArrowPath, distance, angleBetweenPointsDegrees } from "../tools/canvasMethod";
import { fabric } from 'fabric-with-all'
/** 形状管理器 */
export class ShapeToolManager {
// 管理器
canvasManager: any
stateManager: any
layerManager: any
toolManager: any
isDragging: boolean = false
startX: number = 0
startY: number = 0
demoObject: any
tools = [
OperationType.RECTANGLE,
OperationType.LINE,
OperationType.ARROW,
OperationType.ELLIPSE,
OperationType.TRIANGLE,
OperationType.STAR,
]
constructor(options) {
this.canvasManager = options.canvasManager
this.stateManager = options.stateManager
this.layerManager = options.layerManager
this.toolManager = options.toolManager
}
mouseDownEvent(e) {
this.isDragging = false
this.demoObject = null
this.startX = e.absolutePointer.x
this.startY = e.absolutePointer.y
const currentTool = this.toolManager.currentTool.value
if (currentTool === OperationType.RECTANGLE) {
this.demoObject = this.downRectangle()
} else if (currentTool === OperationType.LINE) {
this.demoObject = this.downLine()
} else if (currentTool === OperationType.ELLIPSE) {
this.demoObject = this.downEllipse()
} else if (currentTool === OperationType.TRIANGLE) {
this.demoObject = this.downTriangle()
} else if (currentTool === OperationType.STAR) {
this.demoObject = this.downStar()
} else if (currentTool === OperationType.ARROW) {
this.demoObject = this.downArrow()
}
if (!this.demoObject) return;
this.demoObject.set({
evented: false,
})
this.demoObject.set
this.isDragging = true
this.canvasManager.canvas.add(this.demoObject)
this.canvasManager.canvas.renderAll()
}
mouseMoveEvent(e) {
if (!this.isDragging) return;
var width = e.absolutePointer.x - this.startX
var height = e.absolutePointer.y - this.startY
var left = this.startX
var top = this.startY
if (width < 0) {
left += width
width = -width
}
if (height < 0) {
top += height
height = -height
}
const currentTool = this.toolManager.currentTool.value
if (currentTool === OperationType.RECTANGLE) {
this.moveRectangle({ width, height, left, top })
} else if (currentTool === OperationType.LINE) {
this.moveLine(e.absolutePointer)
} else if (currentTool === OperationType.ELLIPSE) {
this.moveEllipse({ width, height, left, top })
} else if (currentTool === OperationType.TRIANGLE) {
this.moveTriangle({ width, height, left, top })
} else if (currentTool === OperationType.STAR) {
this.moveStar({ width, height, left, top })
} else if (currentTool === OperationType.ARROW) {
this.moveArrow(e.absolutePointer)
}
this.demoObject.set({
evented: false,
})
this.canvasManager.canvas.renderAll()
}
mouseUpEvent(e) {
if (!this.isDragging) return;
this.isDragging = false;
const object = this.demoObject.toJSON("evented")
const currentTool = this.toolManager.currentTool.value
if (currentTool === OperationType.RECTANGLE) {
this.upRectangle(object)
} else if (currentTool === OperationType.LINE) {
this.upLine(object)
} else if (currentTool === OperationType.ELLIPSE) {
this.upEllipse(object)
} else if (currentTool === OperationType.TRIANGLE) {
this.upTriangle(object)
} else if (currentTool === OperationType.STAR) {
this.upStar(object)
} else if (currentTool === OperationType.ARROW) {
this.upArrow(object)
}
this.canvasManager.canvas.remove(this.demoObject)
this.demoObject = null
this.canvasManager.canvas.renderAll()
}
/** 绘制矩形 */
downRectangle() {
const rect = new fabric.Rect({
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
})
return rect
}
moveRectangle({ width, height, left, top }) {
this.demoObject.set({ width, height, left, top })
}
upRectangle(object) {
if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100
this.layerManager.createRectLayer(object, true)
}
/** 绘制直线 */
downLine() {
const line = new fabric.Line([this.startX, this.startY, this.startX, this.startY], {
stroke: 'black', // 线条颜色
strokeWidth: 2 // 线条粗细
})
return line
}
moveLine({ x, y }) {
this.demoObject.set({
x1: this.startX,
y1: this.startY,
x2: x,
y2: y,
})
}
upLine(object) {
this.layerManager.createLineLayer(object, true)
}
/** 绘制椭圆 */
downEllipse() {
const circle = new fabric.Ellipse({
left: this.startX,
top: this.startY,
fill: '#000',
})
return circle
}
moveEllipse({ width, height, left, top }) {
this.demoObject.set({ rx: width / 2, ry: height / 2, left, top })
}
upEllipse(object) {
if (object.rx === 0) object.rx = 50
if (object.ry === 0) object.ry = 50
this.layerManager.createEllipseLayer(object, true)
}
/** 绘制三角形 */
downTriangle() {
const triangle = new fabric.Triangle({
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
})
return triangle
}
moveTriangle({ width, height, left, top }) {
this.demoObject.set({ width, height, left, top })
}
upTriangle(object) {
if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100
this.layerManager.createTriangleLayer(object, true)
}
/** 绘制五角星 */
downStar() {
const star = new fabric.Polygon(getStarArr(0, 0), {
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
strokeLineJoin: 'round', // 圆角连接
strokeLineCap: 'round', // 圆角端点
});
return star
}
moveStar({ width, height, left, top }) {
this.demoObject.set({ left, top, width, height, points: getStarArr(width, height) })
}
upStar(object) {
if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100
this.layerManager.createStarLayer(object, true)
}
/** 绘制箭头 */
downArrow() {
return new fabric.Path();
}
moveArrow({ x, y }) {
const width = distance(this.startX, this.startY, x, y)
const angle = angleBetweenPointsDegrees(this.startX, this.startY, x, y)
this.canvasManager.canvas.remove(this.demoObject)
const arrow = new fabric.Path(getArrowPath(width, 10), {
left: this.startX,
top: this.startY,
stroke: '#000', // 只设置边框颜色
strokeWidth: 3, // 边框宽度
fill: 'transparent', // 不填充
strokeLineCap: 'round',
strokeLineJoin: 'round',
originY: 'center',
angle: angle,
});
this.canvasManager.canvas.add(arrow)
this.demoObject = arrow
}
upArrow(object) {
if (object.originY !== "center") {
this.layerManager.createArrowLayer({
left: this.startX,
top: this.startY,
}, true)
} else {
this.layerManager.createArrowLayer(object, true)
}
}
dispose() { }
}

View File

@@ -54,6 +54,32 @@ export class ToolManager {
name: OperationType.RECTANGLE,
cursor: "crosshair",
},
/** 直线工具 */
{
name: OperationType.LINE,
cursor: "crosshair",
},
/** 箭头工具 */
{
name: OperationType.ARROW,
cursor: "crosshair",
},
/** 椭圆工具 */
{
name: OperationType.ELLIPSE,
cursor: "crosshair",
},
/** 三角形工具 */
{
name: OperationType.TRIANGLE,
cursor: "crosshair",
},
/** 五角星工具 */
{
name: OperationType.STAR,
cursor: "crosshair",
},
]
}
onMounted() {

View File

@@ -1,6 +1,6 @@
import { isBoolean } from "lodash-es";
import { OperationType, OperationTypes } from "../../tools/layerHelper";
import { RectToolManager } from "../RectToolManager"
import { ShapeToolManager } from "../ShapeToolManager"
import { AISelectboxToolManager } from "../AISelectboxToolManager"
@@ -32,7 +32,7 @@ export class CanvasEventManager {
toolManager: this.toolManager,
layerManager: this.layerManager,
}
this.rectToolManager = new RectToolManager(managers)
this.shapeToolManager = new ShapeToolManager(managers)
this.aiSelectboxToolManager = new AISelectboxToolManager(managers)
// 初始化所有事件
@@ -209,9 +209,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseDownEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseDownEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseDownEvent(opt);
} else if (opt.e.altKey || opt.e.which === 2 || currentTool === OperationType.PAN) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX;
@@ -237,9 +237,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseMoveEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseMoveEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseMoveEvent(opt);
} else if (this.canvas.isDragging) {
const vpt = this.canvas.viewportTransform;
vpt[4] += opt.e.clientX - this.canvas.lastPosX;
@@ -321,9 +321,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseDownEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseDownEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 平滑停止任何正在进行的惯性动画
@@ -386,9 +386,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseMoveEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseMoveEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseMoveEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 检查是否是触摸事件
@@ -496,9 +496,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseUpEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 重置触摸状态
@@ -668,9 +668,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseUpEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseUpEvent(opt);
} else if (this.canvas.isDragging) {
// if (this.lastMousePositions.length > 1 && opt && opt.e) {
// this.animationManager.applyInertiaEffect(
@@ -1074,7 +1074,7 @@ export class CanvasEventManager {
}
dispose() {
this.rectToolManager?.dispose()
this.shapeToolManager?.dispose()
this.aiSelectboxToolManager?.dispose()
// 移除所有事件监听
this.canvas.off();

View File

@@ -9,13 +9,14 @@ export class KeyEventManager {
/** 处理键盘事件 */
_handleKeyDown: any
handleKeyDown(event: any) {
const activeID = this.stateManager.layerManager.activeID.value
const ctrl = event.ctrlKey ? 'ctrl-' : "";
const shift = event.shiftKey ? 'shift-' : "";
const key = event.key;
const reg = new RegExp(`^${ctrl}${shift}${key}$`, 'i')
const list = [
// { key: "ctrl-c", handler: () => this.handleCopy(event) },
// { key: "delete", handler: () => this.handleDelete(event) },
{ key: "ctrl-c", handler: () => this.stateManager.layerManager.copyLayerById(activeID) },
{ key: "delete", handler: () => this.stateManager.layerManager.deleteLayerById(activeID) },
{ key: "ctrl-z", handler: () => this.stateManager.undoState() },
{ key: "ctrl-shift-z", handler: () => this.stateManager.redoState() },
]

View File

@@ -40,3 +40,56 @@ export async function getObjectsBoundingBox(objects = []) {
height: box2.y - box1.y,
}
}
/** 获取五角星数组 */
export function getStarArr(width = 0, height = 0) {
const arr = [
{ x: 0, y: -0.5 }, // 顶点0 (上)
{ x: 0.15, y: -0.15 }, // 顶点1 (内)
{ x: 0.50, y: -0.15 }, // 顶点2 (右上外)
{ x: 0.20, y: 0.10 }, // 顶点3 (内)
{ x: 0.30, y: 0.50 }, // 顶点4 (右下外)
{ x: 0.0, y: 0.25 }, // 顶点5 (内)
{ x: -0.30, y: 0.50 }, // 顶点6 (左下外)
{ x: -0.20, y: 0.10 }, // 顶点7 (内)
{ x: -0.50, y: -0.15 }, // 顶点8 (左上外)
{ x: -0.15, y: -0.15 } // 顶点9 (内)
]
return arr.map(item => ({
x: item.x * width,
y: item.y * height,
}))
}
/** 获取箭头路径 */
export function getArrowPath(width = 0, height = 0) {
const arr = [
["M", 0, height / 2],
["L", width, height / 2],
["M", width - 8, 0],
["L", width, height / 2],
["L", width - 8, height],
]
var path = ""
arr.forEach(item => {
path += item.join(" ") + " "
})
return path
}
/** 计算两点之间的距离 */
export function distance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
/** 计算两点之间的角度(角度) */
export function angleBetweenPointsDegrees(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
// 计算弧度并转换为角度
const rad = Math.atan2(dy, dx);
const deg = rad * 180 / Math.PI;
return deg;
}

View File

@@ -29,12 +29,16 @@ export const OperationType = {
PAN: "pan", // 拖拽模式
DRAW: "draw", // 绘画模式
ERASER: "eraser", // 橡皮擦模式
IMAGE: "image",// 图片工具模式
SELECTBOX: "selectbox",// 选择框工具模式
RECTANGLE: "rectangle",// 矩形工具模式
LINE: "line",// 直线工具模式
ARROW: "arrow",// 箭头工具模式
ELLIPSE: "ellipse",// 椭圆工具模式
TRIANGLE: "triangle",// 三角形工具模式
STAR: "star",// 五角星工具模式
TEXT: "text",// 文字工具模式
UNDO: "undo",// 撤销工具模式
REDO: "redo",// 重做工具模式
};
// 所有操作模式类型列表

View File

@@ -17,9 +17,12 @@ import 'element-plus/dist/index.css'
import ignoredWarning from './ignoredWarning'
import vEllipsis from './utils/ellispsis'
const app = createApp(App)
ignoredWarning(app)
app.directive('ellipsis', vEllipsis)
app.use(router)
.use(ElementPlus)
.use(store)

52
src/utils/ellispsis.ts Normal file
View File

@@ -0,0 +1,52 @@
import type { Directive } from 'vue'
/**
* 多行文本省略指令(悬浮显示完整内容)
* @directive v-ellipsis
* @param {number} [value=3] - 超过value行数时显示省略号,不传参数默认为3
*/
const applyStyles = (el: HTMLElement, binding: any) => {
const lines = typeof binding.value === 'number' && binding.value > 0 ? binding.value : 3
el.style.display = '-webkit-box'
el.style.webkitBoxOrient = 'vertical'
el.style.overflow = 'hidden'
el.style.webkitLineClamp = lines.toString()
el.style.maxHeight = `${lines}lh`
}
const checkTruncated = (el: HTMLElement) => {
const isTruncated = el.scrollHeight > el.clientHeight + 1
if (isTruncated) {
el.title = el.textContent?.trim() || ''
} else {
el.removeAttribute('title')
}
}
const vEllipsis: Directive<HTMLElement> = {
mounted(el, binding) {
applyStyles(el, binding)
checkTruncated(el)
const ro = new ResizeObserver(() => checkTruncated(el))
ro.observe(el)
;(el as any)._ellipsisObserver = ro
},
updated(el, binding) {
applyStyles(el, binding)
checkTruncated(el)
},
unmounted(el) {
const ro = (el as any)._ellipsisObserver
if (ro) {
ro.disconnect()
delete (el as any)._ellipsisObserver
}
}
}
export default vEllipsis

View File

@@ -53,12 +53,12 @@
</template>
<template v-else-if="type === 'url'">
<div class="url-list flex">
<div class="url-item" v-for="item in urlList" :key="item">
<div class="url-title" @click="handleClickUrl(item)">
<div class="url-item flex flex-col" v-for="item in urlList" :key="item">
<div class="url-title" v-ellipsis="3" @click="handleClickUrl(item)">
{{ item }}
<img src="@/assets/images/link-outer.png" class="link-outer" />
</div>
<div class="url-link">{{ item }}</div>
<div class="url-link" v-ellipsis="3">{{ item }}</div>
</div>
</div>
</template>
@@ -328,16 +328,27 @@
.url-item {
width: 24rem;
height: 28.7rem;
line-height: 2rem;
word-break: break-all;
background: url('@/assets/images/web-card.png') no-repeat;
background-size: 100% 100%;
padding: 5rem 1.5rem;
row-gap: 0.6rem;
// .url-title,.url-link{
// // 两行省略
// display: -webkit-box;
// -webkit-line-clamp: 2;
// line-clamp: 2;
// -webkit-box-orient: vertical;
// overflow: hidden;
// text-overflow: ellipsis;
// }
.url-title {
cursor: pointer;
font-family: 'Medium';
font-size: 1.6rem;
color: #232323;
padding-bottom: 0.6rem;
max-height: 4rem;
.link-outer {
width: 1.2rem;
height: 1.2rem;

View File

@@ -88,6 +88,7 @@
}
const handleGetProjectInfoAndHistory = () => {
handleOpenSketch()
getProjectInfo({ id: route.params.id }).then((res) => {
if (res) agentRef.value.setChatInfo(res)
let data = res?.project || res