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

This commit is contained in:
2026-03-19 16:49:56 +08:00
24 changed files with 374 additions and 245 deletions

View File

@@ -53,7 +53,7 @@
zoom: { default: 1, type: Number },
step: { default: 0.1, type: Number }
})
const emit = defineEmits(['export', 'export-local', 'import-local', 'export-close', 'workbench'])
const emit = defineEmits(['export', 'workbench'])
const importLocalImage = inject('importLocalImage') as (isRecord?: boolean) => void
const stateManager = inject('stateManager') as any
const toolManager = inject('toolManager') as any
@@ -178,7 +178,8 @@
}
const onWorkbench = async () => {
exportCanvasToImage(canvasManager.canvas).then((url) => {
emit('workbench', { url })
const json = canvasManager.getCanvasJSON()
emit('workbench', { url, json })
})
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="fill-repeat">
<div class="fill-repeat h">
<div>
<div class="title">Image</div>
<div class="content">
@@ -99,6 +99,8 @@
<script setup lang="ts">
import { ref, inject, computed, nextTick, onBeforeUnmount } from 'vue'
import DepthOffsetTool from '../tools/depth-offset-tool.vue'
import DepthSlider from '../tools/depth-slider.vue'
import DepthInput from '../tools/depth-input.vue'
import { getTransformScaleAngle } from '../../manager/ObjectManager'
const objectManager = inject('objectManager') as any
const stateManager = inject('stateManager') as any
@@ -159,7 +161,7 @@
const options = {
opacity: opacity.value / 100
}
objectManager.updateOpacity(id.value, options, isRecord)
objectManager.updateProperty(id.value, options, isRecord)
}
stateManager.event.add('canvas:undo', updateData)

View File

@@ -10,6 +10,7 @@
<div class="content" v-if="isShow" v-show="show">
<!-- <basic-info :object="activeObject" /> -->
<fill-repeat :object="activeObject" v-if="isRepeat" />
<!-- <shape-setting :object="activeObject" v-if="isShape && !isRepeat" /> -->
</div>
</div>
</template>
@@ -17,6 +18,7 @@
<script setup lang="ts">
import { ref, inject, computed, watch, onMounted } from 'vue'
import FillRepeat from './fill-repeat.vue'
import ShapeSetting from './shape-setting.vue'
const props = defineProps({})
const layerManager = inject('layerManager') as any
const canvasManager = inject('canvasManager') as any
@@ -25,6 +27,8 @@
const layers = computed(() => layerManager.layers.value)
const activeObject = ref(null)
// const shapes = ['rect', 'line', 'path', 'triangle', 'polygon', 'ellipse']
// const isShape = computed(() => shapes.includes(activeObject.value?.type))
const isRepeat = computed(() => activeObject.value?.fill?.repeat === 'repeat')
const isShow = computed(() => isRepeat.value)
@@ -85,7 +89,12 @@
overflow-y: auto;
&:deep(> div) {
> div {
margin-bottom: 1.6rem;
margin-bottom: var(--details-item-margin-bottom, 1.6rem);
&:last-child {
margin-bottom: 0;
}
}
&.h > div {
> .title {
font-size: 1.4rem;
color: #000;
@@ -97,6 +106,20 @@
padding: 0 1.4rem;
}
}
&.v > div {
display: flex;
align-items: center;
justify-content: center;
> .label {
min-width: 6rem;
margin-right: 0.8rem;
font-size: 1.2rem;
text-align: right;
}
> .value {
flex: 1;
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
<template>
<div class="shape-setting v">
<!-- <div>
<div class="label">填充颜色</div>
<div class="value">
<el-color-picker
v-model="data.fill"
show-alpha
:predefine="['transparent', '#000', '#f00', '#0f0', '#00f']"
@change="onChange"
/>
</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { ref, inject, computed, nextTick, onBeforeUnmount, reactive } from 'vue'
import { ElColorPicker } from 'element-plus'
import { getTransformScaleAngle } from '../../manager/ObjectManager'
import DepthOffsetTool from '../tools/depth-offset-tool.vue'
import DepthSlider from '../tools/depth-slider.vue'
import DepthInput from '../tools/depth-input.vue'
const objectManager = inject('objectManager') as any
const stateManager = inject('stateManager') as any
const props = defineProps({
object: {
type: Object,
default: () => ({})
}
})
const id = computed(() => props.object.info.id)
const data = reactive({
fill: '',
stroke: '',
strokeWidth: 0
})
const updateData = async () => {
await nextTick()
data.fill = props.object.fill
data.stroke = props.object.stroke
data.strokeWidth = props.object.strokeWidth
}
updateData()
const onInpot = () => setPriority(false)
const onChange = () => setPriority(true)
const setPriority = (isRecord: boolean) => {
const options = { ...data }
objectManager.updateProperty(id.value, options, isRecord)
}
stateManager.event.add('canvas:undo', updateData)
stateManager.event.add('canvas:redo', updateData)
onBeforeUnmount(() => {
stateManager.event.remove('canvas:undo', updateData)
stateManager.event.remove('canvas:redo', updateData)
})
</script>
<style lang="less" scoped>
.shape-setting {
--details-item-margin-bottom: 1rem;
> div {
}
}
</style>

View File

@@ -6,12 +6,7 @@
<template v-if="isReady">
<layer-panel />
<details-panel />
<depth-header-tools
@export="exportCanvas"
@export-local="exportCanvasToLocalStorage"
@import-local="importCanvasFromLocalStorage"
@workbench="(v) => emit('workbench', v)"
/>
<depth-header-tools @export="exportCanvas" @workbench="(v) => emit('workbench', v)" />
<brush-control-panel :currentTool="toolManager.currentTool.value" />
<zoom
:zoom="canvasManager.currentZoom.value / 100"
@@ -44,6 +39,9 @@
import { KeyEventManager } from './manager/events/KeyEventManager'
import { ObjectManager } from './manager/ObjectManager'
import { useGlobalStore } from '@/stores'
const globalStore = useGlobalStore()
const emit = defineEmits(['workbench', 'close'])
const props = defineProps({
config: {
@@ -88,15 +86,21 @@
const observer = ref(null)
onMounted(async () => {
globalStore.setLoading(true)
keyEventManager.registerEvents()
const url = props.config.url || ''
const json = props.config.json || ''
await canvasManager.initCanvas({
canvasRef,
canvasViewWidth: canvasContainerRef.value.clientWidth,
canvasViewHeight: canvasContainerRef.value.clientHeight,
canvasWidth: props.config.width || 750,
canvasHeight: props.config.height || 600,
url: props.config.url || ''
url: json ? '' : url
})
if (json) await canvasManager.loadJSON(json)
globalStore.setLoading(false)
stateManager.onMounted()
canvasManager.onMounted()
layerManager.onMounted()
@@ -164,24 +168,13 @@
a.download = 'canvas.png'
a.click()
})
}
// 导出到本地存储
const exportCanvasToLocalStorage = () => {
const json = canvasManager.getCanvasJSON()
localStorage.setItem('canvasJSON', json)
}
// 从本地存储导入
const importCanvasFromLocalStorage = () => {
const json = localStorage.getItem('canvasJSON')
if (!json) return
canvasManager.loadJSON(json, (success) => {
if (success) {
console.log('导入成功')
} else {
console.log('导入失败')
}
stateManager.clearState(true)
})
// console.log(canvasManager.getCanvasJSON())
// const object = canvasManager.getCanvasDisUrlJSON()
// console.log(object)
// const canvas = canvasManager.processCanvasDisUrlJSON(object)
// console.log(canvas)
}
</script>
<style lang="less">

View File

@@ -7,23 +7,36 @@
</template>
<script setup lang="ts">
import { base64Tofile } from '../tools/tools'
import { uploadImage } from '@/api/upload'
import FullscreenDialog from '../components/fullscreen-dialog.vue'
import depthCanvas from './depth-canvas.vue'
import { ref } from 'vue'
const dialogVisible = ref(false)
const config = ref({
id: '',
url: ''
url: '',
json: ''
})
const open = (options) => {
config.value = options
console.log(config.value)
config.value.json = sessionStorage.getItem('canvasJson_' + config.value.id)
dialogVisible.value = true
}
// 工作区
const onWorkbench = (options) => {
const onWorkbench = async (options) => {
const json = options.json
sessionStorage.setItem('canvasJson_' + config.value.id, json)
const file = base64Tofile(options.url, 'canvas.png')
const formData = new FormData()
formData.append('file', file)
const url = await uploadImage(formData)
config.value.onWorkbench?.({ url })
dialogVisible.value = false
config.value.onWorkbench?.(options)
}
// 关闭
const onClose = () => {

View File

@@ -289,10 +289,7 @@ export class AnimationManager {
* @param {Boolean} adaptive 是否自适应缩放
*/
async resetZoom(animated = true, adaptive = false) {
const canvasViewWidth = this.canvasManager.canvasViewWidth;
const canvasViewHeight = this.canvasManager.canvasViewHeight;
const canvasWidth = this.canvasManager.canvasWidth;
const canvasHeight = this.canvasManager.canvasHeight;
const { canvasViewWidth, canvasViewHeight, canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize();
const scaleX = canvasViewWidth / canvasWidth * 0.8
const scaleY = canvasViewHeight / canvasHeight * 0.8
const scale = Math.min(scaleX, scaleY, 1)

View File

@@ -1,5 +1,5 @@
import { fabric } from 'fabric-with-all'
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { createCanvas } from '../tools/canvasFactory'
import { AnimationManager } from './AnimationManager'
import { detectDeviceType } from '../tools/index'
@@ -44,10 +44,6 @@ export class CanvasManager {
eventManager: any
deviceInfo: any
canvas: any
canvasViewWidth: number
canvasViewHeight: number
canvasWidth: number
canvasHeight: number
currentZoom: any
constructor(options) {
this.stateManager = options.stateManager;
@@ -55,23 +51,31 @@ export class CanvasManager {
this.currentZoom = ref(100)
}
onMounted() { }
getCanvasSize() {
return {
canvasViewWidth: this.canvas.width,
canvasViewHeight: this.canvas.height,
canvasWidth: this.canvas.clipPath.width,
canvasHeight: this.canvas.clipPath.height,
}
}
setCanvasViewSize(options) {
this.canvasViewWidth = options.canvasViewWidth || 1920
this.canvasViewHeight = options.canvasViewHeight || 1080
this.canvas.setWidth(this.canvasViewWidth)
this.canvas.setHeight(this.canvasViewHeight)
var canvasViewWidth = options.canvasViewWidth || 1920
var canvasViewHeight = options.canvasViewHeight || 1080
this.canvas.setWidth(canvasViewWidth)
this.canvas.setHeight(canvasViewHeight)
}
/** 初始化画布 */
async initCanvas(options: CanvasInitOptions) {
this.layerManager = this.stateManager.layerManager
this.canvasWidth = options.canvasWidth || 750
this.canvasHeight = options.canvasHeight || 600
var canvasWidth = options.canvasWidth || 750
var canvasHeight = options.canvasHeight || 600
var image = null;
if (options.url) {
await new Promise((resolve) => {
fabric.Image.fromURL(options.url, async (img) => {
this.canvasWidth = img.width
this.canvasHeight = img.height
canvasWidth = img.width
canvasHeight = img.height
img.set({
left: 0,
top: 0,
@@ -104,8 +108,8 @@ export class CanvasManager {
this.canvas.clipPath = new fabric.Rect({
left: 0,
top: 0,
width: this.canvasWidth,
height: this.canvasHeight
width: canvasWidth,
height: canvasHeight
})
// 动画管理器
@@ -122,19 +126,10 @@ export class CanvasManager {
/** 测试-开始 */
// this.stateManager.setIsRecord(false)
// // 创建矩形
// const rect = await this.layerManager.createRectLayer({
// left: 400,
// top: 100,
// })
// //创建圆形
// const circle = await this.layerManager.createCircleLayer({
// left: 200,
// top: 200,
// })
// // 文字
// const text = await this.layerManager.createTextLayer('Hello World');
// this.layerManager.setActiveID(text.info.id)
// const rect = await this.layerManager.createRectLayer({ left: 200 })
// await this.layerManager.createStarLayer({ left: 400 })
// await this.layerManager.createArrowLayer({ left: 600 })
// this.layerManager.setActiveID(rect.info.id)
// this.stateManager.setIsRecord(true)
/** 测试-结束 */
@@ -275,20 +270,59 @@ export class CanvasManager {
return JSON.stringify(json)
}
/** 加载画布JSON */
loadJSON(json: string, callback?: (success: boolean) => void) {
let jsonObj = null;
try {
jsonObj = JSON.parse(json)
} catch (error) {
console.error('JSON解析错误:', error)
}
if (!jsonObj) return callback?.(false);
this.canvas.loadFromJSON(jsonObj, () => {
this.layerManager.updateLayers()
this.renderAll()
callback?.(true)
loadJSON(json: string, rerecord = true) {
return new Promise((resolve) => {
let jsonObj = null;
try {
jsonObj = JSON.parse(json)
} catch (error) {
console.error('JSON解析错误:', error)
}
if (!jsonObj) return resolve(false)
this.canvas.loadFromJSON(jsonObj, () => {
if (rerecord) this.stateManager.clearState()
this.resetZoom(false)
this.layerManager.updateLayers()
this.renderAll()
if (rerecord) this.stateManager.recordState()
resolve(true)
})
})
}
/** 导出画布为处理图片的JSON */
getCanvasDisUrlJSON() {
const canvas = this.canvas.toJSON()
const images = [];
var i = 0;
const create = (url) => {
const logo = `xxxxxxxx_${i++}_xxxxxxxx`;
images.push({ index: logo, url })
return logo
}
canvas.objects.forEach((object: any) => {
if (object.thumbnail) {
object.thumbnail = create(object.thumbnail)
}
if (object.src) {
object.src = create(object.src)
}
if (object.fill?.source) {
object.fill.source = create(object.fill.source)
}
if (object.info?.fill?.source) {
object.info.fill.source = create(object.info.fill.source)
}
})
return { canvas: JSON.stringify(canvas), images }
}
/** 处理JSON为正常画布 */
processCanvasDisUrlJSON(obj: { canvas: string, images: Object[] }) {
var json = obj.canvas;
const images = obj.images || []
images.forEach((v: any) => json = json.replace(new RegExp(v.index), v.url))
return JSON.parse(json)
}
dispose() {
this.animationManager?.dispose()
this.eventManager?.dispose()

View File

@@ -121,8 +121,8 @@ export class LayerManager {
/** 设置图层位置-不设置默认居中 */
setLayerPosition(object, options?: any) {
const width = this.canvasManager.canvasWidth
const height = this.canvasManager.canvasHeight
const width = this.canvasManager.canvas.clipPath.width
const height = this.canvasManager.canvas.clipPath.height
if (options && options.top !== undefined) {
object.set({ top: options.top })
@@ -173,6 +173,7 @@ export class LayerManager {
width: 100,
height: 100,
fill: '#000',
strokeWidth: 0,
...(options || {}),
info: {
id: createId("rect"),
@@ -212,6 +213,7 @@ export class LayerManager {
const ellipseObject = new fabric.Ellipse({
radius: 50,
fill: '#000',
strokeWidth: 0,
...(options || {}),
info: {
id: createId("ellipse"),
@@ -230,6 +232,7 @@ export class LayerManager {
width: 100,
height: 100,
fill: '#000',
strokeWidth: 0,
...(options || {}),
info: {
id: createId("triangle"),
@@ -249,6 +252,7 @@ export class LayerManager {
delete options.points
const starObject = new fabric.Polygon(getStarArr(width, height), {
fill: '#000',
strokeWidth: 0,
...(options || {}),
info: {
id: createId("star"),
@@ -290,8 +294,7 @@ export class LayerManager {
/** 创建图片图层 */
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true) {
const canvasWidth = this.canvasManager.canvasWidth
const canvasHeight = this.canvasManager.canvasHeight
const { canvasWidth, canvasHeight } = this.canvasManager.getCanvasSize();
const imageObject = await new Promise((resolve) => {
const url = typeof imgOrUrl === 'string' ? imgOrUrl : imgOrUrl.src
@@ -340,10 +343,11 @@ export class LayerManager {
const index = this.canvasManager.getObjects().indexOf(targetLayer);
this.deleteLayerById(targetLayer.info.id, false)
this.setActiveID(mergedImage.info.id, false)
await this.canvasManager.add(mergedImage);
await this.canvasManager.add(mergedImage, false);
this.canvasManager.canvas.moveTo(mergedImage, index);
this.canvasManager.renderAll()
this.updateLayers()
this.stateManager.recordState()
return true;
}
/** 设置激活对象可擦除 */

View File

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

View File

@@ -1,67 +0,0 @@
import { OperationType, OperationTypes } from "../tools/layerHelper";
import { fabric } from 'fabric-with-all'
/** 矩形工具管理器 */
export class RectToolManager {
// 管理器
canvasManager: any
stateManager: any
layerManager: any
isDragging: boolean = false
startX: number = 0
startY: number = 0
demoObject: any
tools = [
OperationType.RECTANGLE
]
constructor(options) {
this.canvasManager = options.canvasManager
this.stateManager = options.stateManager
this.layerManager = options.layerManager
}
mouseDownEvent(e) {
this.isDragging = true
this.startX = e.absolutePointer.x
this.startY = e.absolutePointer.y
const rect = new fabric.Rect({
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
evented: false,
})
this.demoObject = rect
this.canvasManager.canvas.add(rect)
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
}
this.demoObject.set({ width, height, left, top })
this.canvasManager.canvas.renderAll()
}
mouseUpEvent(e) {
if (!this.isDragging) return;
this.isDragging = false;
const object = this.demoObject.toJSON("evented")
if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100
this.layerManager.createRectLayer(object, true)
this.canvasManager.canvas.remove(this.demoObject)
this.canvasManager.canvas.renderAll()
}
dispose() { }
}

View File

@@ -120,6 +120,7 @@ export class ShapeToolManager {
width: 0,
height: 0,
fill: '#000',
strokeWidth: 0,
})
return rect
}
@@ -159,6 +160,7 @@ export class ShapeToolManager {
left: this.startX,
top: this.startY,
fill: '#000',
strokeWidth: 0,
})
return circle
}
@@ -180,6 +182,7 @@ export class ShapeToolManager {
width: 0,
height: 0,
fill: '#000',
strokeWidth: 0,
})
return triangle
}
@@ -203,6 +206,7 @@ export class ShapeToolManager {
fill: '#000',
strokeLineJoin: 'round', // 圆角连接
strokeLineCap: 'round', // 圆角端点
strokeWidth: 0,
});
return star

View File

@@ -92,7 +92,7 @@ export class StateManager {
if (!state) return
this.running.value = true
this.historyIndex.value = index
this.canvasManager.loadJSON(state.canvas, () => {
this.canvasManager.loadJSON(state.canvas, false).then(() => {
this.event.emit('canvas:undo', state)
this.running.value = false
})
@@ -105,7 +105,7 @@ export class StateManager {
if (!state) return
this.running.value = true
this.historyIndex.value = index
this.canvasManager.loadJSON(state.canvas, () => {
this.canvasManager.loadJSON(state.canvas, false).then(() => {
this.event.emit('canvas:redo', state)
this.running.value = false
})

View File

@@ -11,10 +11,14 @@
<svg-icon :name="v.icon" :size="v.iconSize" />
</span>
</template>
<button class="export" @click="emit('export')">
<span class="icon"><svg-icon name="export" size="11" /></span>
<span class="text">Export</span>
</button>
<div>
<button class="export" @click="emit('export')">
<span class="icon"><svg-icon name="export" size="11" /></span>
<span class="text">Export</span>
</button>
<div v-loading="true" class="mask" v-if="downloadData.status == 'loading'"></div>
</div>
<!-- <button class="import" @click="emit('import')">
<span class="text">Import</span>
</button> -->
@@ -26,7 +30,8 @@
import { TOOLS } from '../manager/ToolManager'
const props = defineProps({
zoom: { default: 1, type: Number },
step: { default: 0.1, type: Number }
step: { default: 0.1, type: Number },
downloadData: { default: {}, type: Object }
})
const emit = defineEmits(['export', 'import'])
const stateManager = inject('stateManager') as any
@@ -109,8 +114,21 @@
cursor: not-allowed;
}
}
> button {
width: 10rem;
> div{
position: relative;
> .mask{
position: absolute !important;
top: 0;
left: 0;
height: 100%;
width: 100%;
zoom: .6;
pointer-events: none;
}
}
button {
// width: 10rem;
padding: 0 2.4rem;
height: 3rem;
border-radius: 0.4rem;
border: none;
@@ -118,12 +136,15 @@
color: #fff;
font-size: 1.1rem;
display: flex;
gap: 0.8rem;
align-items: center;
justify-content: center;
gap: 0.8rem;
position: relative;
cursor: pointer;
&:active {
opacity: 0.8;
}
}
}
</style>

View File

@@ -52,6 +52,8 @@
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) || tier.value != 2)
const isReturned = computed(() => {
return (
props.node.data.type == NODE_DATATYPE.RESULT_IMAGE &&
@@ -63,7 +65,8 @@
!isSubord.value &&
NODE_DATATYPE.RESULT_IMAGE === props.node.data.type &&
!(tier.value === NODE_DATATIER.TO_3VIEW) &&
isReturned.value
isReturned.value &&
isAdd3d.value
)
const onAdd = () => {
const tier_ = tier.value + 1

View File

@@ -155,12 +155,6 @@
}
})
})
stateManager.recordState()
// } else {
// subordNode.data.data.url =
// 'https://s3-alpha-sig.figma.com/img/8ce2/f1a4/12b93da90e5f17109e7430f14837fd14?Expires=1773619200&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=kmLsTFtXJqfvuxj6husWlDkRDMOIRDjzUUjb7zh79GkBKihUHc0f59k5OAImHTPdaiEREUCCpn~8sQ-si5lenuauJpApCmAU~NsxjfQhuh9m5O~GiHenr2fKu0DIJ75-oCE3859fyxoSFXQgZ9PRmeb98kikMR6uRX9nI5TPUHgKO8ZgkhDBTW~iyaDT~1ybnoK7elPa6T2VzfO-bpIyY-MZ71VRq3RxwmZRxduqHEb3Dh-jjrHyh2SoQsHmUjSJOf-uYilfvpGUResZAjAq8ZVLEjvhzKC2bmCNZIp3RmhYO8ctU7pd5t91J6Xaa6jBLtGfMxbqIm652EC79K0RoA__'
// setTimeout(() => stateManager.recordState())
// }
}
//删除功能卡片
const onDeleteClick = ()=>{

View File

@@ -28,23 +28,23 @@
const shortcutList = ref([
{
label: 'Change the...',
value: 'Change the...'
value: 'Change the style to a realistic design. '
},
{
label: 'Bright Colors...',
value: 'Bright Colors...'
value: 'Bright colors with modern patterns, change the style to a realistic furniture design. '
},
{
label: 'Make the...',
value: 'Make the...'
value: 'Make the structure more refined and balanced, change the style to a realistic furniture style. '
},
{
label: 'Imagine...',
value: 'Imagine...'
value: 'Imagine this furniture with detailed fabric textures, change the style to a realistic design. '
},
{
label: 'Wood Materials with...',
value: 'Wood Materials with...'
value: 'Wood materials with natural oak texture and soft fabric, change the style to a realistic furniture design.'
}
])
const modeList = ref([

View File

@@ -23,6 +23,7 @@
</button>
</div>
<img
v-loadimg="false"
class="image"
v-if="item.status == 'RETURNED'"
:src="item?.url"
@@ -58,10 +59,11 @@
<script setup lang="ts">
import myEvent from '@/utils/myEvent'
import { downloadImage } from '../../../tools/tools'
import { downloadImage, base64Tofile } 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 { uploadImage } from '@/api/upload'
const openImagePreview = inject('openImagePreview') as (url: string) => void
const openThreeModelPreview = inject('openThreeModelPreview') as (url: string) => void
const props = defineProps({
@@ -161,6 +163,7 @@
item.scale.x = -item.scale.x
})
stateManager.recordState()
stateManager.exportFlow(stateManager.saveCanvasTimeInterval)
}
},
{
@@ -174,6 +177,7 @@
item.scale.y = -item.scale.y
})
stateManager.recordState()
stateManager.exportFlow(stateManager.saveCanvasTimeInterval)
}
}
])
@@ -201,11 +205,19 @@
eventManager.registerEvents()
}
const depthCanvasWorkbench = (options)=>{
data.imageProcessTasks.forEach((item) => {
if(item.taskId == options.taskId){
item.url = options.url
}
console.log(options)
// 1. 提取 MIME 类型和 Base64 数据
const file = base64Tofile(options.url,'image.png')
const formData = new FormData()
formData.append('file', file)
uploadImage(formData).then((res) => {
data.imageProcessTasks.forEach((item) => {
if(item.taskId == options.taskId){
item.url = res
}
})
})
}
const onEdit = (item: any) => {

View File

@@ -47,7 +47,7 @@
</template>
</VueFlow>
</div>
<header-tools @export="exportFlow" @import="importFlow" />
<header-tools @export="exportFlow" @import="importFlow" :downloadData="downloadData" />
<zoom
:zoom="stateManager.zoom.value"
:step="0.1"
@@ -107,9 +107,8 @@
const vueFlow = ref<any>()
const nodeTypes = ref([NODE_TYPE.INPUT, NODE_TYPE.SECONDARY, NODE_TYPE.OUTPUT, NODE_TYPE.ALONE])
// 状态管理器
const stateManager = new StateManager({ vueFlow })
const stateManager = new StateManager({ vueFlow,sketchId:props.config.imgId })
provide('stateManager', stateManager)
// 事件管理器
@@ -191,29 +190,34 @@
}
// 导出流程
const getFlowJson = () => {
if(!stateManager.isSave.value)return ''
return JSON.stringify(stateManager.nodes.value)
}
const exportFlow = () => {
const downloadData = ref<any>({
amount: 0,
progress: 0,
status: 'success',//success
})
const exportFlow = async () => {
// console.log(vueFlow.value)
// console.log(vueFlow.value.toImage)
let arr = stateManager.nodes.value.filter((v) => v.data.type === NODE_COMPONENT.RESULT_IMAGE)
let imgList = []
arr.forEach((v) => {
arr.forEach((v,i) => {
v.data.data.imageProcessTasks.forEach((item,index) => {
let url = item.url
let name = url?.split(".").pop().split("?").shift();
imgList.push({url:url,name:`${v.data.type}${index == 0?'':index}.${name}`})
imgList.push({url:url,name:`${v.data.type}${i}-${index == 0?'':index}.${name}`})
})
})
downImgListToZip(imgList)
console.log(imgList)
downloadData.value.amount = imgList.length
downloadData.value.status = 'loading'
await downImgListToZip(imgList,(progress)=>{
downloadData.value.progress = progress
if(progress == downloadData.value.amount){
downloadData.value.status = 'success'
}
})
return
// flowManager.exportFlow()
const str = getFlowJson()
stateManager.isSave.value = false
emit('exportFlow', str)
// localStorage.setItem('flow_json', str)
}
// 导入流程
const importFlow = async (json) => {

View File

@@ -1,13 +1,13 @@
<template>
<fullscreen-dialog v-model="dialogVisible" @close="close" hide-destroy>
<flow-canvas ref="flowCanvasRef" :config="config" @exportFlow="exportFlow" />
<flow-canvas ref="flowCanvasRef" :config="config" />
</fullscreen-dialog>
</template>
<script setup lang="ts">
import FullscreenDialog from '../components/fullscreen-dialog.vue'
import flowCanvas from './flow-canvas.vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref } from 'vue'
import { getSketchFlowCanvas, putSketchFlowCanvas } from '@/api/flow-canvas'
import { useI18n } from 'vue-i18n'
@@ -31,43 +31,12 @@
config.value.json = json
dialogVisible.value = true
}
const exportFlow = async (str) => {
if(!config.value.imgId || !str)return
await new Promise((resolve) => {
putSketchFlowCanvas({
id: config.value.imgId,
canvasData: str },true).then(() => {
resolve(true)
}).catch(() => {
resolve(true)
})
})
}
const close = async () => {
const str = flowCanvasRef.value?.getFlowJson()
await exportFlow(str)
dialogVisible.value = false
}
const handleBeforeUnload = (event) => {
const str = flowCanvasRef.value?.getFlowJson()
if (str) {
event.preventDefault()
event.returnValue = $t('flowCanvas.confirmLeave')
return $t('flowCanvas.confirmLeave')
}
}
onMounted(() => {
// 添加事件监听
window.addEventListener('beforeunload', handleBeforeUnload)
})
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
})
defineExpose({
open,
close,
exportFlow
})
</script>
<style lang="less" scoped>

View File

@@ -38,6 +38,7 @@ export class EventManager {
}
})
this.stateManager.recordState()
this.stateManager.exportFlow(this.stateManager.saveCanvasTimeInterval)
}
/** 处理点击 */
handleClick(event: any) {

View File

@@ -2,6 +2,8 @@ import { ref, computed } from "vue";
import { NODE_TYPE, NODE_DATATYPE } from '../tools/index.d'
import { ElMessageBox } from 'element-plus'
import i18n from '@/lang'
import { putSketchFlowCanvas } from '@/api/flow-canvas'
const t = i18n.global.t
export interface NodesItem {
@@ -37,8 +39,12 @@ export class StateManager {
toolManager: any
generateManager: any
// 是否有数据没保存
isSave: any
// 保存画布数据定时器
saveCanvasTime: any
// 保存画布数据定时器时间间隔
saveCanvasTimeInterval: any
// 打开画布线稿id
sketchId: any
// 设置管理器
setManager(options) {
options.eventManager && (this.eventManager = options.eventManager)
@@ -57,7 +63,10 @@ export class StateManager {
this.mxHistory = ref(50)
this.historyList = ref([])
this.historyIndex = ref(0)
this.isSave = ref(false)
this.sketchId = options.sketchId
this.saveCanvasTimeInterval = 6000
this.saveCanvasTime = null
this.activeNodeID = ref("")
this.nodes = ref<NodesItem[]>([]);
@@ -100,8 +109,6 @@ export class StateManager {
})
return arr
})
window.nodes = this.nodes
window.aaa = this
}
/** 设置激活节点 */
@@ -110,6 +117,7 @@ export class StateManager {
addNode(node: NodesItem) {
this.nodes.value.push(node);
this.recordState()
this.exportFlow()
}
/** 删除节点 */
async deleteNode(id: string, { isElMessageBox } = { isElMessageBox: false }) {
@@ -136,6 +144,7 @@ export class StateManager {
if (!deletePromise) return console.log('删除操作被取消')
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) }
@@ -176,8 +185,22 @@ export class StateManager {
const size = this.historyList.value.length - this.mxHistory.value
if (size > 0) this.historyList.value.splice(0, size)
this.historyIndex.value = this.historyList.value.length - 1
this.isSave.value = true
}
/** 画布数据存储 */
async exportFlow (time:number = 0){
if(!this.sketchId)return
clearTimeout(this.saveCanvasTime)
await new Promise((resolve) => {
this.saveCanvasTime = setTimeout(()=>{
putSketchFlowCanvas({
id: this.sketchId,
canvasData: JSON.stringify(this.nodes.value) }).then(() => {
resolve(true)
}).catch(() => {
resolve(true)
})
},time)
})
}
/** 撤回状态 */
undoState() {
@@ -186,6 +209,7 @@ export class StateManager {
if (!state) return
this.historyIndex.value = index
this.nodes.value = JSON.parse(state.nodes)
this.exportFlow(this.saveCanvasTimeInterval)
}
/** 重做状态 */
redoState() {
@@ -194,6 +218,7 @@ export class StateManager {
if (!state) return
this.historyIndex.value = index
this.nodes.value = JSON.parse(state.nodes)
this.exportFlow(this.saveCanvasTimeInterval)
}
/** 显示指定子节点和父节点连接线,隐藏父节点和其他子节点链接线, */
@@ -211,7 +236,7 @@ export class StateManager {
}
dispose() {
this.isSave.value = false
clearTimeout(this.saveCanvasTime)
this.historyList.value = []
this.historyIndex.value = 0
}

View File

@@ -19,10 +19,11 @@ export const downloadImage = (url: string, name: string) => {
}
/** 批量下载图片 */
export const downImgListToZip = async (imagesParams) => {
export const downImgListToZip = async (imagesParams,callback) => {
const zip = new JSZip()
const promises = []
// 遍历下载每个图片
let progress = 0
imagesParams.forEach((img, index) => {
const promise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
@@ -32,18 +33,17 @@ export const downImgListToZip = async (imagesParams) => {
if (xhr.status === 200) {
const fileName = img.name
zip.file(fileName, xhr.response)
progress++
callback(progress)
resolve('')
} else {
reject(new Error(`下载失败: ${img.url}`))
}
}
xhr.onerror = () => reject(new Error(`网络错误: ${img.url}`))
xhr.send()
})
promises.push(promise)
console.log(promises,zip)
})
// 等待所有图片下载完成
@@ -56,7 +56,25 @@ export const downImgListToZip = async (imagesParams) => {
link.href = URL.createObjectURL(content)
link.download = 'DesignFiles'
link.click()
URL.revokeObjectURL(link.href)
// URL.revokeObjectURL(link.href)
})
.catch((error) => console.error('下载失败:', error))
}
/** base64转二进制 */
export const base64Tofile = (base64: string,name: string) => {
const arr = base64.split(',')
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
// 2. 转换为 Uint8Array
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
// 3. 创建 Blob
const blob = new Blob([u8arr], { type: mime })
// 4. 创建 File 对象(可选)
const file = new File([blob], name, { type: mime })
return file
}

View File

@@ -30,6 +30,19 @@ app.use(router)
.use(i18n)
.mount('#app')
const vLoadimg = (el, binding) => {
const src = binding.value
if (el.src === src) return
const img = new Image()
img.src = src
img.onload = () => {
el.src = src
}
img.onerror = () => {
console.log('图片加载失败:', src)
}
}
// 注册
app.directive('loadimg', vLoadimg)
flexible();