合并画布代码
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Command } from "./Command";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 创建背景图层命令
|
||||
@@ -195,10 +195,13 @@ export class BackgroundSizeCommand extends Command {
|
||||
this.newWidth = options.newWidth;
|
||||
this.newHeight = options.newHeight;
|
||||
this.historyManager = options.historyManager;
|
||||
this.isRedGreenMode = options.isRedGreenMode;
|
||||
|
||||
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
|
||||
|
||||
// 记录原尺寸
|
||||
this.oldWidth = this.canvas.width;
|
||||
this.oldHeight = this.canvas.height;
|
||||
this.oldWidth = this.bgLayer.fabricObject.width;
|
||||
this.oldHeight = this.bgLayer.fabricObject.height;
|
||||
|
||||
// 查找背景图层
|
||||
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
|
||||
|
||||
@@ -609,12 +609,15 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
...options,
|
||||
name: `上传纹理: ${options.name || options.file?.name || '未知'}`,
|
||||
name: `上传纹理: ${options.name || options.file?.name || "未知"}`,
|
||||
description: `上传自定义纹理文件`,
|
||||
});
|
||||
|
||||
this.file = options.file;
|
||||
this.name = options.name || options.file?.name?.replace(/\.[^/.]+$/, "") || "自定义纹理";
|
||||
this.name =
|
||||
options.name ||
|
||||
options.file?.name?.replace(/\.[^/.]+$/, "") ||
|
||||
"自定义纹理";
|
||||
this.category = options.category || "自定义材质";
|
||||
this.texturePresetManager = options.texturePresetManager;
|
||||
this.brushManager = options.brushManager;
|
||||
@@ -623,13 +626,13 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
|
||||
async execute() {
|
||||
if (!this.file || !this.texturePresetManager) {
|
||||
throw new Error('缺少必要的文件或纹理预设管理器');
|
||||
throw new Error("缺少必要的文件或纹理预设管理器");
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建文件 data URL
|
||||
const dataUrl = await this._fileToDataUrl(this.file);
|
||||
|
||||
|
||||
// 添加到纹理预设管理器
|
||||
this.uploadedTextureId = this.texturePresetManager.addCustomTexture({
|
||||
name: this.name,
|
||||
@@ -654,10 +657,10 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
return {
|
||||
textureId: this.uploadedTextureId,
|
||||
dataUrl: dataUrl,
|
||||
name: this.name
|
||||
name: this.name,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('纹理上传失败:', error);
|
||||
console.error("纹理上传失败:", error);
|
||||
throw new Error(`纹理上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -672,7 +675,7 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
this.texturePresetManager.removeCustomTexture(this.uploadedTextureId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('撤销纹理上传失败:', error);
|
||||
console.error("撤销纹理上传失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -680,7 +683,7 @@ export class TextureUploadCommand extends BaseBrushCommand {
|
||||
/**
|
||||
* 将文件转换为 data URL
|
||||
* @private
|
||||
* @param {File} file
|
||||
* @param {File} file
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
_fileToDataUrl(file) {
|
||||
|
||||
100
src/component/Canvas/CanvasEditor/commands/EraserCommand.js
Normal file
100
src/component/Canvas/CanvasEditor/commands/EraserCommand.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { isArray } from "lodash-es";
|
||||
import { optimizeCanvasRendering } from "../utils/helper";
|
||||
import { restoreObjectLayerAssociations } from "../utils/layerUtils";
|
||||
import { Command } from "./Command";
|
||||
|
||||
/**
|
||||
* 橡皮擦操作命令
|
||||
* 支持橡皮擦操作的撤销和重做
|
||||
*/
|
||||
export class EraserCommand extends Command {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} options 命令选项
|
||||
* @param {Object} options.canvas Fabric画布实例
|
||||
* @param {Object} options.layerManager 图层管理器
|
||||
* @param {Object} options.beforeSnapshot 擦除前的状态快照
|
||||
* @param {Object} options.afterSnapshot 擦除后的状态快照
|
||||
* @param {Array} options.affectedObjects 擦除影响的对象列表(可选)
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "橡皮擦操作",
|
||||
description: `擦除`,
|
||||
...options,
|
||||
});
|
||||
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.beforeSnapshot = options.beforeSnapshot;
|
||||
this.afterSnapshot = options.afterSnapshot;
|
||||
this.affectedObjects = options.affectedObjects || [];
|
||||
this.fristLoad = true; // 是否是第一次加载
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行橡皮擦操作
|
||||
*/
|
||||
async execute() {
|
||||
if (!this.beforeSnapshot || !this.afterSnapshot) {
|
||||
console.warn("缺少状态快照,无法执行橡皮擦命令");
|
||||
return false;
|
||||
}
|
||||
if (!this.fristLoad)
|
||||
await this._restoreCanvasState(this.afterSnapshot); // 应用重做的状态
|
||||
else await this.layerManager?.updateLayersObjectsInteractivity?.(false);
|
||||
this.fristLoad = false; // 标记为非第一次加载
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销橡皮擦操作
|
||||
*/
|
||||
async undo() {
|
||||
if (!this.beforeSnapshot) {
|
||||
console.warn("缺少擦除前的状态快照,无法撤销");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 恢复到擦除前的状态
|
||||
await this._restoreCanvasState(this.beforeSnapshot);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复画布状态
|
||||
* @param {Object} snapshot 状态快照
|
||||
* @private
|
||||
*/
|
||||
async _restoreCanvasState(snapshot) {
|
||||
// 对比 eraser erasable 两个属性,如果当前对象的eraser属性和erasable属性不一致,则需要更新对象的eraser属性
|
||||
if (!snapshot || !snapshot.objects) return;
|
||||
// 优化渲染 - 统一批处理 支持异步回调 防止闪屏
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
return new Promise((resolve) => {
|
||||
this.canvas.loadFromJSON(snapshot, async () => {
|
||||
// 恢复图层关联
|
||||
this._restoreObjectLayerAssociations();
|
||||
// 确保所有对象的交互性正确设置
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.(false);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象与图层的关联关系
|
||||
* @private
|
||||
*/
|
||||
_restoreObjectLayerAssociations() {
|
||||
if (!this.layerManager) return;
|
||||
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
restoreObjectLayerAssociations(
|
||||
this.layerManager?.layers?.value,
|
||||
canvasObjects
|
||||
);
|
||||
}
|
||||
}
|
||||
1087
src/component/Canvas/CanvasEditor/commands/GroupCommands.js
Normal file
1087
src/component/Canvas/CanvasEditor/commands/GroupCommands.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js";
|
||||
import { ToolCommand } from "./ToolCommands.js";
|
||||
import { ClearSelectionCommand } from "./SelectionCommands.js";
|
||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 套索抠图命令
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { Command, FunctionCommand } from "./Command";
|
||||
import { getLiquifyReferenceManager } from "../managers/LiquifyReferenceManager";
|
||||
|
||||
/**
|
||||
* 液化命令基类
|
||||
@@ -192,81 +193,6 @@ export class LiquifyCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图层栅格化命令
|
||||
* 用于将复杂图层栅格化为单一图像,以便进行液化操作
|
||||
*/
|
||||
export class RasterizeForLiquifyCommand extends Command {
|
||||
/**
|
||||
* 创建栅格化命令
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Object} options.canvas Fabric.js画布实例
|
||||
* @param {Object} options.layerManager 图层管理器实例
|
||||
* @param {String} options.layerId 需要栅格化的图层ID
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: options.name || "栅格化图层",
|
||||
description: options.description || "将图层栅格化为单一图像以便液化操作",
|
||||
});
|
||||
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.layerId = options.layerId;
|
||||
this.originalLayer = null;
|
||||
this.rasterizedImageObj = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行栅格化操作
|
||||
* @returns {Promise<Object>} 栅格化后的图像对象
|
||||
*/
|
||||
async execute() {
|
||||
if (!this.canvas || !this.layerManager || !this.layerId) {
|
||||
throw new Error("栅格化命令缺少必要参数");
|
||||
}
|
||||
|
||||
// 保存原始图层信息
|
||||
this.originalLayer = this.layerManager.getLayerById(this.layerId);
|
||||
if (!this.originalLayer) {
|
||||
throw new Error(`图层ID不存在: ${this.layerId}`);
|
||||
}
|
||||
|
||||
// 栅格化图层
|
||||
const rasterizedImage = await this.layerManager.rasterizeLayer(
|
||||
this.layerId
|
||||
);
|
||||
if (!rasterizedImage) {
|
||||
throw new Error("栅格化图层失败");
|
||||
}
|
||||
|
||||
this.rasterizedImageObj = rasterizedImage;
|
||||
return rasterizedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销栅格化操作
|
||||
* 注意:完整撤销栅格化是复杂的,这里提供近似还原
|
||||
* @returns {Promise<boolean>} 撤销结果
|
||||
*/
|
||||
async undo() {
|
||||
if (!this.canvas || !this.layerManager || !this.originalLayer) {
|
||||
throw new Error("无法撤销:缺少必要的状态信息");
|
||||
}
|
||||
|
||||
// 恢复图层为原始状态是复杂的,这里可能需要与LayerManager协作
|
||||
// 这个实现可能需要根据实际的LayerManager功能来调整
|
||||
const restored = await this.layerManager.restoreLayerFromBackup(
|
||||
this.layerId
|
||||
);
|
||||
if (!restored) {
|
||||
console.warn("无法完全还原栅格化前的图层状态");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 液化工具初始化命令
|
||||
* 用于初始化液化工具的状态,不执行实际操作
|
||||
@@ -550,6 +476,405 @@ export class LiquifyResetCommand extends LiquifyCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 液化状态命令 - 最优化版本
|
||||
* 使用引用管理器避免对象引用丢失,支持高性能的状态管理
|
||||
*/
|
||||
export class LiquifyStateCommand extends Command {
|
||||
/**
|
||||
* 创建液化状态命令
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Object} options.canvas Fabric.js画布实例
|
||||
* @param {Object} options.layerManager 图层管理器实例
|
||||
* @param {Object} options.targetObject 目标对象(保持引用)
|
||||
* @param {String} options.targetLayerId 目标图层ID
|
||||
* @param {ImageData} options.initialImageData 初始图像数据
|
||||
* @param {ImageData} options.finalImageData 最终图像数据
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: options.name || "液化操作",
|
||||
description: options.description || "液化变形操作的状态记录",
|
||||
});
|
||||
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.targetObject = options.targetObject;
|
||||
this.targetLayerId = options.targetLayerId;
|
||||
this.targetObjectId = options.targetObjectId;
|
||||
|
||||
// 获取引用管理器实例
|
||||
this.refManager = getLiquifyReferenceManager();
|
||||
|
||||
// 注册对象到引用管理器
|
||||
this.objectRefId = this.refManager.registerObject(
|
||||
this.targetObject,
|
||||
this.targetObjectId || `liquify_${Date.now()}`
|
||||
);
|
||||
|
||||
// 保存状态快照ID
|
||||
this.initialSnapshotId = null;
|
||||
this.finalSnapshotId = null;
|
||||
|
||||
// 设置图像数据
|
||||
this.initialImageData = options.initialImageData;
|
||||
this.finalImageData = options.finalImageData;
|
||||
|
||||
this.currentState = "initial";
|
||||
|
||||
// 创建初始快照
|
||||
if (this.initialImageData) {
|
||||
this._createInitialSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令 - 应用最终状态
|
||||
*/
|
||||
async execute() {
|
||||
if (!this.finalImageData) {
|
||||
throw new Error("缺少最终状态数据");
|
||||
}
|
||||
|
||||
// 确保有最终状态快照
|
||||
if (!this.finalSnapshotId) {
|
||||
await this._createFinalSnapshot();
|
||||
}
|
||||
|
||||
// 通过引用管理器更新图像数据
|
||||
await this.refManager.updateObjectImageData(
|
||||
this.objectRefId,
|
||||
this.finalImageData
|
||||
);
|
||||
|
||||
this.currentState = "final";
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log("✅ 液化命令执行完成,应用最终状态");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销命令 - 恢复初始状态
|
||||
*/
|
||||
async undo() {
|
||||
if (!this.initialImageData || !this.initialSnapshotId) {
|
||||
throw new Error("缺少初始状态数据或快照");
|
||||
}
|
||||
|
||||
// 通过引用管理器恢复到初始快照
|
||||
await this.refManager.restoreFromSnapshot(
|
||||
this.objectRefId,
|
||||
this.initialSnapshotId
|
||||
);
|
||||
|
||||
this.currentState = "initial";
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log("🔄 液化命令撤销完成,恢复初始状态");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做命令 - 重新应用最终状态
|
||||
*/
|
||||
async redo() {
|
||||
return this.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最终状态的图像数据
|
||||
* @param {ImageData} finalImageData 最终状态的图像数据
|
||||
*/
|
||||
setFinalImageData(finalImageData) {
|
||||
this.finalImageData = finalImageData;
|
||||
this.finalSnapshotId = null; // 重置快照ID,下次执行时重新创建
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态标记
|
||||
* @returns {String} 'initial' 或 'final'
|
||||
*/
|
||||
getCurrentState() {
|
||||
return this.currentState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查命令是否有效
|
||||
* @returns {Boolean} 是否有效
|
||||
*/
|
||||
isValid() {
|
||||
const targetObject = this.refManager.getObjectRef(this.objectRefId);
|
||||
return !!(targetObject && this.initialImageData && this.finalImageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标对象的当前引用
|
||||
* @returns {Object|null} Fabric对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.refManager.getObjectRef(this.objectRefId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
if (this.objectRefId) {
|
||||
// 清理快照
|
||||
if (this.initialSnapshotId) {
|
||||
this.refManager.stateSnapshots.delete(this.initialSnapshotId);
|
||||
}
|
||||
if (this.finalSnapshotId) {
|
||||
this.refManager.stateSnapshots.delete(this.finalSnapshotId);
|
||||
}
|
||||
|
||||
// 注意:不要清理对象引用,因为可能有其他命令在使用
|
||||
console.log(`🗑️ 液化状态命令资源已清理: ${this.objectRefId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
getMemoryStats() {
|
||||
const refManagerStats = this.refManager.getMemoryStats();
|
||||
const commandMemory = this._calculateCommandMemory();
|
||||
|
||||
return {
|
||||
...refManagerStats,
|
||||
commandMemory,
|
||||
totalCommandMemory: commandMemory,
|
||||
};
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
/**
|
||||
* 创建初始状态快照
|
||||
* @private
|
||||
*/
|
||||
async _createInitialSnapshot() {
|
||||
if (!this.initialImageData) return;
|
||||
|
||||
this.initialSnapshotId = `${this.objectRefId}_initial_${Date.now()}`;
|
||||
|
||||
// 手动创建初始快照,包含图像数据
|
||||
const fabricObject = this.refManager.getObjectRef(this.objectRefId);
|
||||
if (fabricObject) {
|
||||
const snapshot = {
|
||||
timestamp: Date.now(),
|
||||
properties: this.refManager._captureObjectState(fabricObject),
|
||||
imageData: this.initialImageData,
|
||||
eventListeners:
|
||||
this.refManager.eventListeners.get(this.objectRefId) || {},
|
||||
};
|
||||
|
||||
this.refManager.stateSnapshots.set(this.initialSnapshotId, snapshot);
|
||||
console.log(`📸 初始状态快照已创建: ${this.initialSnapshotId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建最终状态快照
|
||||
* @private
|
||||
*/
|
||||
async _createFinalSnapshot() {
|
||||
if (!this.finalImageData) return;
|
||||
|
||||
this.finalSnapshotId = `${this.objectRefId}_final_${Date.now()}`;
|
||||
|
||||
// 手动创建最终快照,包含图像数据
|
||||
const fabricObject = this.refManager.getObjectRef(this.objectRefId);
|
||||
if (fabricObject) {
|
||||
const snapshot = {
|
||||
timestamp: Date.now(),
|
||||
properties: this.refManager._captureObjectState(fabricObject),
|
||||
imageData: this.finalImageData,
|
||||
eventListeners:
|
||||
this.refManager.eventListeners.get(this.objectRefId) || {},
|
||||
};
|
||||
|
||||
this.refManager.stateSnapshots.set(this.finalSnapshotId, snapshot);
|
||||
console.log(`📸 最终状态快照已创建: ${this.finalSnapshotId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算命令本身的内存使用量
|
||||
* @returns {Number} 内存使用量(字节)
|
||||
* @private
|
||||
*/
|
||||
_calculateCommandMemory() {
|
||||
let bytes = 0;
|
||||
|
||||
// 计算ImageData内存使用
|
||||
if (this.initialImageData) {
|
||||
bytes += this.initialImageData.width * this.initialImageData.height * 4;
|
||||
}
|
||||
if (this.finalImageData) {
|
||||
bytes += this.finalImageData.width * this.finalImageData.height * 4;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量液化状态命令 - 用于处理多个对象的液化操作
|
||||
*/
|
||||
export class BatchLiquifyStateCommand extends Command {
|
||||
/**
|
||||
* 创建批量液化状态命令
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Array} options.commands 液化状态命令列表
|
||||
* @param {Object} options.canvas Fabric.js画布实例
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
name: options.name || "批量液化操作",
|
||||
description:
|
||||
options.description || `批量液化${options.commands?.length || 0}个对象`,
|
||||
});
|
||||
|
||||
this.commands = options.commands || [];
|
||||
this.canvas = options.canvas;
|
||||
this.refManager = getLiquifyReferenceManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加液化状态命令
|
||||
* @param {LiquifyStateCommand} command 液化状态命令
|
||||
*/
|
||||
addCommand(command) {
|
||||
this.commands.push(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量操作
|
||||
*/
|
||||
async execute() {
|
||||
const results = [];
|
||||
const updates = [];
|
||||
|
||||
// 准备批量更新数据
|
||||
for (const command of this.commands) {
|
||||
if (command.finalImageData && command.objectRefId) {
|
||||
updates.push({
|
||||
refId: command.objectRefId,
|
||||
imageData: command.finalImageData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 执行批量更新
|
||||
if (updates.length > 0) {
|
||||
const updateResults = await this.refManager.batchUpdate(updates);
|
||||
results.push(...updateResults);
|
||||
}
|
||||
|
||||
// 更新命令状态
|
||||
this.commands.forEach((command) => {
|
||||
command.currentState = "final";
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销批量操作
|
||||
*/
|
||||
async undo() {
|
||||
// 逆序撤销
|
||||
for (let i = this.commands.length - 1; i >= 0; i--) {
|
||||
await this.commands[i].undo();
|
||||
}
|
||||
|
||||
this.canvas.renderAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做批量操作
|
||||
*/
|
||||
async redo() {
|
||||
return this.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查批量命令是否有效
|
||||
* @returns {Boolean} 是否有效
|
||||
*/
|
||||
isValid() {
|
||||
return (
|
||||
this.commands.length > 0 && this.commands.every((cmd) => cmd.isValid())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有子命令的资源
|
||||
*/
|
||||
dispose() {
|
||||
this.commands.forEach((command) => {
|
||||
if (command.dispose) {
|
||||
command.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化fabric对象为JSON
|
||||
* @param {Object} fabricObject fabric对象
|
||||
* @returns {Object} 序列化后的对象状态
|
||||
*/
|
||||
export function serializeFabricObject(fabricObject) {
|
||||
if (!fabricObject) {
|
||||
throw new Error("无法序列化:对象为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用fabric.js的toObject方法序列化
|
||||
const serializedObject = fabricObject.toObject([
|
||||
"id",
|
||||
"objectId",
|
||||
"uid",
|
||||
"layerId",
|
||||
"name",
|
||||
"type",
|
||||
]);
|
||||
|
||||
// 记录额外的元数据
|
||||
const metadata = {
|
||||
timestamp: Date.now(),
|
||||
objectType: fabricObject.type,
|
||||
objectId: fabricObject.id || fabricObject.objectId || fabricObject.uid,
|
||||
layerId: fabricObject.layerId,
|
||||
bounds: fabricObject.getBoundingRect(),
|
||||
};
|
||||
|
||||
return {
|
||||
serializedObject,
|
||||
metadata,
|
||||
version: "1.0",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("序列化fabric对象失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:创建液化状态命令
|
||||
* @param {Object} options 配置选项
|
||||
* @returns {LiquifyStateCommand} 状态命令实例
|
||||
*/
|
||||
export function createLiquifyStateCommand(options) {
|
||||
return new LiquifyStateCommand(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:创建液化重置命令
|
||||
* @param {Object} options 配置选项
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,11 @@
|
||||
import { OperationType } from "../utils/layerHelper.js";
|
||||
import {
|
||||
findObjectById,
|
||||
generateId,
|
||||
optimizeCanvasRendering,
|
||||
} from "../utils/helper.js";
|
||||
import { LayerType, OperationType } from "../utils/layerHelper.js";
|
||||
import { Command, CompositeCommand } from "./Command.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 批量初始化红绿图模式命令
|
||||
@@ -29,100 +34,134 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
this.originalNormalObjects = null;
|
||||
this.originalNormalOpacities = new Map();
|
||||
this.originalToolState = null;
|
||||
this.originalActiveLayerId = null;
|
||||
|
||||
// 存储加载的图片对象
|
||||
this.clothingImage = null;
|
||||
this.redGreenImage = null;
|
||||
|
||||
// 存储新创建的图层ID
|
||||
this.newEmptyLayerId = null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
try {
|
||||
// 禁用画布渲染以避免闪烁
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 1. 设置画布背景为白色
|
||||
this.originalCanvasBackground = this.canvas.backgroundColor;
|
||||
this.canvas.setBackgroundColor("#ffffff", () => {});
|
||||
|
||||
// 1. 设置画布背景为白色
|
||||
this.originalCanvasBackground = this.canvas.backgroundColor;
|
||||
this.canvas.setBackgroundColor('#ffffff', () => {});
|
||||
// 2. 查找图层结构
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
// 2. 查找图层结构
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
|
||||
throw new Error("缺少必要的图层结构");
|
||||
}
|
||||
|
||||
const normalLayer = normalLayers[0]; // 使用第一个普通图层
|
||||
|
||||
// 3. 保存原始状态
|
||||
this.originalBackgroundObject = backgroundLayer.fabricObject ? {
|
||||
...backgroundLayer.fabricObject.toObject(),
|
||||
ref: backgroundLayer.fabricObject
|
||||
} : null;
|
||||
|
||||
this.originalFixedObjects = fixedLayer.fabricObject
|
||||
? [fixedLayer.fabricObject]
|
||||
: [];
|
||||
|
||||
this.originalNormalObjects = normalLayer.fabricObjects
|
||||
? [...normalLayer.fabricObjects]
|
||||
: [];
|
||||
|
||||
// 保存普通图层透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
this.originalNormalOpacities.set(layer.id, layer.opacity || 1);
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
this.originalNormalOpacities.set(
|
||||
`${layer.id}_${obj.id || "unknown"}`,
|
||||
obj.opacity || 1
|
||||
);
|
||||
});
|
||||
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
|
||||
throw new Error("缺少必要的图层结构");
|
||||
}
|
||||
});
|
||||
|
||||
// 保存工具状态
|
||||
if (this.toolManager) {
|
||||
this.originalToolState = {
|
||||
currentTool: this.toolManager.getCurrentTool(),
|
||||
isRedGreenMode: this.toolManager.isRedGreenMode,
|
||||
};
|
||||
}
|
||||
const normalLayer = normalLayers[0]; // 使用第一个普通图层
|
||||
|
||||
// 4. 确保背景图层大小正确
|
||||
await this._setupBackgroundLayer(backgroundLayer);
|
||||
// 3. 保存原始状态
|
||||
this.originalBackgroundObject = backgroundLayer.fabricObject
|
||||
? {
|
||||
...backgroundLayer.fabricObject.toObject(),
|
||||
ref: backgroundLayer.fabricObject,
|
||||
}
|
||||
: null;
|
||||
|
||||
// 5. 并行加载两个图片
|
||||
const [clothingImg, redGreenImg] = await Promise.all([
|
||||
this._loadImage(this.clothingImageUrl),
|
||||
this._loadImage(this.redGreenImageUrl)
|
||||
]);
|
||||
this.originalFixedObjects = fixedLayer.fabricObject
|
||||
? [fixedLayer.fabricObject]
|
||||
: [];
|
||||
|
||||
// 6. 设置衣服底图到固定图层
|
||||
await this._setupClothingImage(clothingImg, fixedLayer);
|
||||
this.originalNormalObjects = normalLayer.fabricObjects
|
||||
? [...normalLayer.fabricObjects]
|
||||
: [];
|
||||
|
||||
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
|
||||
await this._setupRedGreenImage(redGreenImg, normalLayer, this.clothingImage);
|
||||
// 保存当前活动图层ID
|
||||
this.originalActiveLayerId = this.layerManager.getActiveLayerId();
|
||||
|
||||
// 8. 设置普通图层透明度
|
||||
this._setupNormalLayerOpacity(normalLayers);
|
||||
// 保存普通图层透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
this.originalNormalOpacities.set(layer.id, layer.opacity || 1);
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
this.originalNormalOpacities.set(
|
||||
`${layer.id}_${obj.id || "unknown"}`,
|
||||
obj.opacity || 1
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 9. 配置工具管理器
|
||||
this._setupToolManager();
|
||||
// 保存工具状态
|
||||
if (this.toolManager) {
|
||||
this.originalToolState = {
|
||||
currentTool: this.toolManager.getCurrentTool(),
|
||||
isRedGreenMode: this.toolManager.isRedGreenMode,
|
||||
};
|
||||
}
|
||||
|
||||
// 10. 重新启用渲染并执行一次性渲染
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
// 5. 并行加载两个图片
|
||||
const [clothingImg, redGreenImg] = await Promise.all([
|
||||
this._loadImage(this.clothingImageUrl),
|
||||
this._loadImage(this.redGreenImageUrl),
|
||||
]);
|
||||
|
||||
console.log("批量红绿图模式初始化完成", {
|
||||
衣服底图: this.clothingImageUrl,
|
||||
红绿图: this.redGreenImageUrl,
|
||||
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
|
||||
画布背景: "白色",
|
||||
// 6. 设置衣服底图到固定图层
|
||||
await this._setupClothingImage(clothingImg, fixedLayer);
|
||||
|
||||
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
|
||||
await this._setupRedGreenImage(
|
||||
redGreenImg,
|
||||
normalLayer,
|
||||
this.clothingImage
|
||||
);
|
||||
|
||||
// 4. 确保背景图层大小和衣服地图大小一致
|
||||
await this._setupBackgroundLayer(backgroundLayer, this.clothingImage);
|
||||
|
||||
// 8. 设置普通图层透明度
|
||||
this._setupNormalLayerOpacity(normalLayers); // 这里不需要在这里设置透明度 由图层统一处理
|
||||
|
||||
// 9. 创建新的空白图层并设置为活动图层
|
||||
this.newEmptyLayerId = await this._createAndActivateEmptyLayer();
|
||||
|
||||
// 设置普通图层的裁剪对象为衣服底图
|
||||
if (this.redGreenImage) {
|
||||
// const clipPathImg = this.redGreenImage;
|
||||
// clipPathImg.set({
|
||||
// absolutePositioned: true,
|
||||
// });
|
||||
this.redGreenImage.set({
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
const activeLayer = this.layerManager.getActiveLayer();
|
||||
activeLayer.clippingMask = this.redGreenImage.toObject(["id"]);
|
||||
activeLayer.opacity = this.normalLayerOpacity;
|
||||
// activeLayer?.fabricObjects.forEach((obj) => {
|
||||
// obj.set({
|
||||
// clipPath: clipPathImg,
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
// 10. 配置工具管理器
|
||||
this._setupToolManager();
|
||||
|
||||
console.log("批量红绿图模式初始化完成", {
|
||||
衣服底图: this.clothingImageUrl,
|
||||
红绿图: this.redGreenImageUrl,
|
||||
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
|
||||
画布背景: "白色",
|
||||
新建空图层ID: this.newEmptyLayerId,
|
||||
});
|
||||
|
||||
await this.layerManager.updateLayersObjectsInteractivity(false);
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -134,86 +173,125 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的空白图层并设置为活动图层
|
||||
* @returns {Promise<string>} 新创建的图层ID
|
||||
* @private
|
||||
*/
|
||||
async _createAndActivateEmptyLayer() {
|
||||
// 创建新的空白图层
|
||||
const newLayerName = "绘制图层";
|
||||
const newLayerId = this.layerManager.createLayer(
|
||||
newLayerName,
|
||||
LayerType.GROUP,
|
||||
{
|
||||
undoable: false,
|
||||
}
|
||||
);
|
||||
|
||||
// 设置为活动图层
|
||||
if (newLayerId) {
|
||||
this.layerManager.setActiveLayer(newLayerId);
|
||||
}
|
||||
|
||||
return newLayerId;
|
||||
}
|
||||
|
||||
async undo() {
|
||||
try {
|
||||
// 禁用渲染
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
|
||||
// 1. 恢复画布背景
|
||||
if (this.originalCanvasBackground !== null) {
|
||||
this.canvas.setBackgroundColor(this.originalCanvasBackground, () => {});
|
||||
}
|
||||
|
||||
// 2. 恢复图层对象
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
// 移除当前添加的对象
|
||||
if (this.clothingImage) {
|
||||
this.canvas.remove(this.clothingImage);
|
||||
}
|
||||
if (this.redGreenImage) {
|
||||
this.canvas.remove(this.redGreenImage);
|
||||
}
|
||||
|
||||
// 恢复背景图层
|
||||
if (backgroundLayer && this.originalBackgroundObject) {
|
||||
if (this.originalBackgroundObject.ref) {
|
||||
backgroundLayer.fabricObject = this.originalBackgroundObject.ref;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复固定图层
|
||||
if (fixedLayer) {
|
||||
fixedLayer.fabricObject = this.originalFixedObjects.length > 0
|
||||
? this.originalFixedObjects[0]
|
||||
: null;
|
||||
|
||||
if (fixedLayer.fabricObject) {
|
||||
this.canvas.add(fixedLayer.fabricObject);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复普通图层
|
||||
if (normalLayers.length > 0) {
|
||||
const normalLayer = normalLayers[0];
|
||||
normalLayer.fabricObjects = [...this.originalNormalObjects];
|
||||
this.originalNormalObjects.forEach((obj) => {
|
||||
this.canvas.add(obj);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 恢复透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
if (this.originalNormalOpacities.has(layer.id)) {
|
||||
layer.opacity = this.originalNormalOpacities.get(layer.id);
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 1. 恢复画布背景
|
||||
if (this.originalCanvasBackground !== null) {
|
||||
this.canvas.setBackgroundColor(
|
||||
this.originalCanvasBackground,
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
const key = `${layer.id}_${obj.id || "unknown"}`;
|
||||
if (this.originalNormalOpacities.has(key)) {
|
||||
obj.opacity = this.originalNormalOpacities.get(key);
|
||||
}
|
||||
// 2. 恢复图层对象
|
||||
const layers = this.layerManager.layers?.value || [];
|
||||
const backgroundLayer = layers.find((layer) => layer.isBackground);
|
||||
const fixedLayer = layers.find((layer) => layer.isFixed);
|
||||
const normalLayers = layers.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
// 移除当前添加的对象
|
||||
if (this.clothingImage) {
|
||||
this.canvas.remove(this.clothingImage);
|
||||
}
|
||||
if (this.redGreenImage) {
|
||||
this.canvas.remove(this.redGreenImage);
|
||||
}
|
||||
|
||||
// 移除新创建的空白图层
|
||||
if (this.newEmptyLayerId) {
|
||||
const emptyLayerIndex = layers.findIndex(
|
||||
(layer) => layer.id === this.newEmptyLayerId
|
||||
);
|
||||
if (emptyLayerIndex !== -1) {
|
||||
layers.splice(emptyLayerIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复背景图层
|
||||
if (backgroundLayer && this.originalBackgroundObject) {
|
||||
if (this.originalBackgroundObject.ref) {
|
||||
backgroundLayer.fabricObject = this.originalBackgroundObject.ref;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复固定图层
|
||||
if (fixedLayer) {
|
||||
fixedLayer.fabricObject =
|
||||
this.originalFixedObjects.length > 0
|
||||
? this.originalFixedObjects[0]
|
||||
: null;
|
||||
|
||||
if (fixedLayer.fabricObject) {
|
||||
this.canvas.add(fixedLayer.fabricObject);
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复普通图层
|
||||
if (normalLayers.length > 0) {
|
||||
const normalLayer = normalLayers[0];
|
||||
normalLayer.fabricObjects = [...this.originalNormalObjects];
|
||||
this.originalNormalObjects.forEach((obj) => {
|
||||
this.canvas.add(obj);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 恢复工具状态
|
||||
if (this.toolManager && this.originalToolState) {
|
||||
this.toolManager.isRedGreenMode = this.originalToolState.isRedGreenMode;
|
||||
if (this.originalToolState.currentTool) {
|
||||
this.toolManager.setTool(this.originalToolState.currentTool);
|
||||
// 3. 恢复透明度
|
||||
normalLayers.forEach((layer) => {
|
||||
if (this.originalNormalOpacities.has(layer.id)) {
|
||||
layer.opacity = this.originalNormalOpacities.get(layer.id);
|
||||
}
|
||||
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
const key = `${layer.id}_${obj.id || "unknown"}`;
|
||||
if (this.originalNormalOpacities.has(key)) {
|
||||
obj.opacity = this.originalNormalOpacities.get(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复活动图层
|
||||
if (this.originalActiveLayerId) {
|
||||
this.layerManager.setActiveLayer(this.originalActiveLayerId);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 重新启用渲染
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
// 4. 恢复工具状态
|
||||
if (this.toolManager && this.originalToolState) {
|
||||
this.toolManager.isRedGreenMode =
|
||||
this.originalToolState.isRedGreenMode;
|
||||
if (this.originalToolState.currentTool) {
|
||||
this.toolManager.setTool(this.originalToolState.currentTool);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -226,35 +304,42 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
/**
|
||||
* 设置背景图层
|
||||
*/
|
||||
async _setupBackgroundLayer(backgroundLayer) {
|
||||
async _setupBackgroundLayer(backgroundLayer, clothingImage) {
|
||||
let backgroundObject = backgroundLayer.fabricObject;
|
||||
const { object } = findObjectById(this.canvas, backgroundObject.id);
|
||||
|
||||
if (!backgroundObject) {
|
||||
if (!object) {
|
||||
// 创建白色背景矩形
|
||||
backgroundObject = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
fill: "#ffffff",
|
||||
object = new fabric.Rect({
|
||||
left: this.canvas.width / 2,
|
||||
top: this.canvas.height / 2,
|
||||
width: clothingImage.width,
|
||||
height: clothingImage.height,
|
||||
scaleX: clothingImage.scaleX,
|
||||
scaleY: clothingImage.scaleY,
|
||||
fill: "transparent", // 确保背景是透明的
|
||||
selectable: false,
|
||||
evented: false,
|
||||
isBackground: true,
|
||||
layerId: backgroundLayer.id,
|
||||
layerName: backgroundLayer.name,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
this.canvas.add(backgroundObject);
|
||||
this.canvas.sendToBack(backgroundObject);
|
||||
backgroundLayer.fabricObject = backgroundObject;
|
||||
this.canvas.add(object);
|
||||
this.canvas.sendToBack(object);
|
||||
backgroundLayer.fabricObject = object;
|
||||
} else {
|
||||
// 更新现有背景对象大小
|
||||
backgroundObject.set({
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
left: 0,
|
||||
top: 0,
|
||||
fill: "#ffffff", // 确保背景是白色
|
||||
object.set({
|
||||
width: clothingImage.width,
|
||||
height: clothingImage.height,
|
||||
scaleX: clothingImage.scaleX,
|
||||
scaleY: clothingImage.scaleY,
|
||||
left: this.canvas.width / 2,
|
||||
top: this.canvas.height / 2,
|
||||
fill: "transparent", // 确保背景是透明的
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -299,6 +384,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
evented: false,
|
||||
layerId: fixedLayer.id,
|
||||
layerName: fixedLayer.name,
|
||||
id: generateId("clothingImage"),
|
||||
});
|
||||
|
||||
// 清除固定图层原有内容
|
||||
@@ -332,6 +418,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
evented: false,
|
||||
layerId: normalLayer.id,
|
||||
layerName: normalLayer.name,
|
||||
id: generateId("redGreenImage"),
|
||||
});
|
||||
|
||||
// 清除普通图层原有内容
|
||||
@@ -341,6 +428,8 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
});
|
||||
}
|
||||
|
||||
// 给img设置裁剪,裁剪图为衣服底图
|
||||
|
||||
// 添加到画布和普通图层
|
||||
this.canvas.add(img);
|
||||
normalLayer.fabricObjects = [img];
|
||||
@@ -354,13 +443,6 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
||||
normalLayers.forEach((layer) => {
|
||||
// 设置图层透明度
|
||||
layer.opacity = this.normalLayerOpacity;
|
||||
|
||||
// 更新图层中所有对象的透明度
|
||||
if (layer.fabricObjects) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
obj.opacity = this.normalLayerOpacity;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Command, CompositeCommand } from "./Command.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { createLayer, LayerType } from "../utils/layerHelper.js";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<transition name="brush-control-canel-fade">
|
||||
<div v-if="isVisible" class="brush-control-panel">
|
||||
<!-- 笔刷大小控制 -->
|
||||
<VerticalSlider
|
||||
@@ -55,80 +55,89 @@
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
|
||||
<!-- 颜色选择器 - 仅在特定工具下显示 -->
|
||||
<div v-if="showColorPicker" class="color-picker-container">
|
||||
<label for="color-picker" class="current-color-label">
|
||||
<div
|
||||
class="current-color"
|
||||
:style="{ backgroundColor: brushColor }"
|
||||
></div>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
id="color-picker"
|
||||
class="system-color-picker"
|
||||
v-model="customColor"
|
||||
@input="setBrushColor(customColor)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 透明度控制 - 仅在特定工具下显示 -->
|
||||
<VerticalSlider
|
||||
v-if="showOpacitySlider"
|
||||
v-model="brushOpacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:presets="opacityPresets"
|
||||
:memorized-values="memorizedOpacities"
|
||||
:is-percentage="true"
|
||||
custom-class="opacity-slider"
|
||||
:active-threshold="0.01"
|
||||
:step="0.01"
|
||||
v-model:showTooltip="showOpacityTooltip"
|
||||
@slide-start="handleOpacitySlideStart"
|
||||
@slide-end="handleOpacitySlideEnd"
|
||||
@click="showOpacityTooltip = true"
|
||||
>
|
||||
<template #tooltip-content>
|
||||
<div class="tooltip-header">
|
||||
<div class="tooltip-title">Opacity</div>
|
||||
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
|
||||
<SvgIcon name="CClose" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="opacity-preview">
|
||||
<div class="opacity-checker"></div>
|
||||
<!-- 1.这里加上过渡动画 颜色选择器 - 仅在特定工具下显示 -->
|
||||
<transition name="color-picker-fade" mode="out-in">
|
||||
<div
|
||||
v-if="showColorPicker"
|
||||
class="color-picker-container"
|
||||
key="color-picker"
|
||||
>
|
||||
<label for="color-picker" class="current-color-label">
|
||||
<div
|
||||
class="opacity-color"
|
||||
:style="{
|
||||
backgroundColor: brushColor,
|
||||
opacity: brushOpacity,
|
||||
}"
|
||||
class="current-color"
|
||||
:style="{ backgroundColor: brushColor }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="tooltip-text">
|
||||
{{ Math.round(brushOpacity * 100) }}%
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
id="color-picker"
|
||||
class="system-color-picker"
|
||||
v-model="customColor"
|
||||
@input="setBrushColor(customColor)"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- 2.这里加上过渡动画 透明度控制 - 仅在特定工具下显示 -->
|
||||
<transition name="opacity-slider-fade" mode="out-in">
|
||||
<VerticalSlider
|
||||
v-if="showOpacitySlider"
|
||||
key="opacity-slider"
|
||||
v-model="brushOpacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:presets="opacityPresets"
|
||||
:memorized-values="memorizedOpacities"
|
||||
:is-percentage="true"
|
||||
custom-class="opacity-slider"
|
||||
:active-threshold="0.01"
|
||||
:step="0.01"
|
||||
v-model:showTooltip="showOpacityTooltip"
|
||||
@slide-start="handleOpacitySlideStart"
|
||||
@slide-end="handleOpacitySlideEnd"
|
||||
@click="showOpacityTooltip = true"
|
||||
>
|
||||
<template #tooltip-content>
|
||||
<div class="tooltip-header">
|
||||
<div class="tooltip-title">Opacity</div>
|
||||
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
|
||||
<SvgIcon name="CClose" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tooltip-controls">
|
||||
<button
|
||||
class="control-btn add"
|
||||
v-if="!memorizedOpacities.includes(brushOpacity)"
|
||||
@click="memorizeOpacity"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="control-btn remove"
|
||||
@click="removeMemorizedOpacity"
|
||||
v-if="canRemoveOpacity"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<div class="opacity-preview">
|
||||
<div class="opacity-checker"></div>
|
||||
<div
|
||||
class="opacity-color"
|
||||
:style="{
|
||||
backgroundColor: brushColor,
|
||||
opacity: brushOpacity,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
<div class="tooltip-content">
|
||||
<div class="tooltip-text">
|
||||
{{ Math.round(brushOpacity * 100) }}%
|
||||
</div>
|
||||
<div class="tooltip-controls">
|
||||
<button
|
||||
class="control-btn add"
|
||||
v-if="!memorizedOpacities.includes(brushOpacity)"
|
||||
@click="memorizeOpacity"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="control-btn remove"
|
||||
@click="removeMemorizedOpacity"
|
||||
v-if="canRemoveOpacity"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
</transition>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -139,7 +148,6 @@ import { BrushStore } from "../store/BrushStore";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { inject } from "vue";
|
||||
import VerticalSlider from "./VerticalSlider.vue";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: {
|
||||
@@ -462,6 +470,13 @@ watch(
|
||||
color: #333;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
// 添加高度过渡动画
|
||||
transition: height 0.3s ease-out, min-height 0.3s ease-out;
|
||||
// overflow: hidden;
|
||||
|
||||
transform: translate3d(0, -50%, 0); // 确保使用3D变换以提高性能
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 笔刷大小预览相关样式
|
||||
@@ -660,27 +675,49 @@ watch(
|
||||
}
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
.brush-control-canel-fade-enter-active,
|
||||
.brush-control-canel-fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
.brush-control-canel-fade-enter-from,
|
||||
.brush-control-canel-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px) translateY(-50%);
|
||||
}
|
||||
|
||||
// 颜色选择器过渡动画
|
||||
.color-picker-fade-enter-active,
|
||||
.color-picker-fade-leave-active {
|
||||
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
|
||||
}
|
||||
.color-picker-fade-enter-from,
|
||||
.color-picker-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.95);
|
||||
}
|
||||
|
||||
// 透明度滑块过渡动画
|
||||
.opacity-slider-fade-enter-active,
|
||||
.opacity-slider-fade-leave-active {
|
||||
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
|
||||
}
|
||||
.opacity-slider-fade-enter-from,
|
||||
.opacity-slider-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.95);
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-height: 600px) {
|
||||
.brush-control-panel {
|
||||
transform: translateY(-50%);
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.brush-control-panel {
|
||||
left: 10px;
|
||||
// padding: 12px;
|
||||
padding: 12px 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,338 +1,350 @@
|
||||
<template>
|
||||
<div class="brush-panel">
|
||||
<div class="brush-panel-content">
|
||||
<!-- 笔刷类型选择 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷类型</span>
|
||||
</div>
|
||||
<div class="brush-type-grid">
|
||||
<div
|
||||
v-for="brush in brushStore.state.availableBrushes"
|
||||
:key="brush.id"
|
||||
@click="setBrushTypeWithCommand(brush.id)"
|
||||
:class="[
|
||||
'brush-type-item',
|
||||
{ active: brushStore.state.type === brush.id },
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="getBrushPreviewStyle(brush)"
|
||||
></div>
|
||||
<span class="brush-name">{{ brush.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动态渲染笔刷可配置属性 -->
|
||||
<template
|
||||
v-for="(properties, category) in propertiesByCategory"
|
||||
:key="category"
|
||||
>
|
||||
<div class="brush-panel" @click.stop="">
|
||||
<div class="brush-panel-wrapper">
|
||||
<div class="brush-panel-content">
|
||||
<!-- 笔刷类型选择 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ category }}</span>
|
||||
<div class="section-actions" v-if="category === '材质'">
|
||||
<button class="action-btn" @click="showLibrary = !showLibrary">
|
||||
<i class="icon-library">📚</i> 材质库
|
||||
</button>
|
||||
<span>笔刷类型</span>
|
||||
</div>
|
||||
<div class="brush-type-grid">
|
||||
<div
|
||||
v-for="brush in brushStore.state.availableBrushes"
|
||||
:key="brush.id"
|
||||
@click="setBrushTypeWithCommand(brush.id)"
|
||||
:class="[
|
||||
'brush-type-item',
|
||||
{ active: brushStore.state.type === brush.id },
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="getBrushPreviewStyle(brush)"
|
||||
></div>
|
||||
<span class="brush-name">{{ brush.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 针对每个属性,根据其类型渲染合适的控件 -->
|
||||
<div class="property-list">
|
||||
<div
|
||||
v-for="prop in properties"
|
||||
:key="prop.id"
|
||||
class="property-item"
|
||||
>
|
||||
<!-- 滑块控件 -->
|
||||
<template v-if="prop.type === 'slider'">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<span class="slider-value">
|
||||
{{ formatPropertyValue(prop) }}
|
||||
</span>
|
||||
<!-- 动态渲染笔刷可配置属性 -->
|
||||
<template
|
||||
v-for="(properties, category) in propertiesByCategory"
|
||||
:key="category"
|
||||
>
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ category }}</span>
|
||||
<div class="section-actions" v-if="category === '材质'">
|
||||
<button class="action-btn" @click="showLibrary = !showLibrary">
|
||||
<i class="icon-library">📚</i> 材质库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 针对每个属性,根据其类型渲染合适的控件 -->
|
||||
<div class="property-list">
|
||||
<div
|
||||
v-for="prop in properties"
|
||||
:key="prop.id"
|
||||
class="property-item"
|
||||
>
|
||||
<!-- 滑块控件 -->
|
||||
<template v-if="prop.type === 'slider'">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<span class="slider-value">
|
||||
{{ formatPropertyValue(prop) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) =>
|
||||
handlePropertyChange(
|
||||
prop.id,
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="prop.min || 0"
|
||||
:max="prop.max || 100"
|
||||
:step="prop.step || 1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<!-- 预设值按钮,如果定义了预设 -->
|
||||
<div v-if="prop.presets" class="property-presets">
|
||||
<button
|
||||
v-for="preset in prop.presets"
|
||||
:key="preset"
|
||||
@click="handlePropertyChange(prop.id, preset)"
|
||||
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
</template>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<template v-else-if="prop.type === 'color'">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: prop.value }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
<!-- 如果是主颜色,显示最近使用的颜色 -->
|
||||
<div v-if="prop.id === 'color'" class="recent-colors">
|
||||
<div
|
||||
v-for="(color, index) in brushStore.state
|
||||
.recentColors"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="handlePropertyChange(prop.id, color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<template v-else-if="prop.type === 'checkbox'">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.checked)
|
||||
"
|
||||
:id="`toggle-${prop.id}`"
|
||||
/>
|
||||
<label :for="`toggle-${prop.id}`"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<template v-else-if="prop.type === 'select'">
|
||||
<div class="select-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<select
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) =>
|
||||
handlePropertyChange(
|
||||
prop.id,
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
:min="prop.min || 0"
|
||||
:max="prop.max || 100"
|
||||
:step="prop.step || 1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<!-- 预设值按钮,如果定义了预设 -->
|
||||
<div v-if="prop.presets" class="property-presets">
|
||||
<button
|
||||
v-for="preset in prop.presets"
|
||||
:key="preset"
|
||||
@click="handlePropertyChange(prop.id, preset)"
|
||||
:class="{ active: Math.abs(prop.value - preset) < 0.1 }"
|
||||
class="property-select"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
<option
|
||||
v-for="option in prop.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<template v-else-if="prop.type === 'color'">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
<!-- 文件选择器(用于材质) -->
|
||||
<template v-else-if="prop.type === 'file'">
|
||||
<div class="file-property">
|
||||
<div class="file-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: prop.value }"
|
||||
></div>
|
||||
class="file-preview"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
|
||||
<div v-else class="no-file">点击上传材质图片</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button
|
||||
class="select-file-btn"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
上传图片
|
||||
</button>
|
||||
<button
|
||||
class="clear-file-btn"
|
||||
@click="handlePropertyChange(prop.id, '')"
|
||||
v-if="prop.value"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
</template>
|
||||
|
||||
<!-- 材质网格选择器 -->
|
||||
<template v-else-if="prop.type === 'texture-grid'">
|
||||
<div class="texture-grid-property">
|
||||
<div class="texture-grid-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="texture-grid">
|
||||
<div
|
||||
v-for="texture in prop.options"
|
||||
:key="texture.value"
|
||||
class="texture-item"
|
||||
:class="{ active: prop.value === texture.value }"
|
||||
@click="handleTextureSelect(texture.value)"
|
||||
>
|
||||
<img
|
||||
:src="texture.preview || texture.value"
|
||||
:alt="texture.label"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-label">{{ texture.label }}</span>
|
||||
</div>
|
||||
<!-- 自定义纹理上传按钮 -->
|
||||
<div
|
||||
class="texture-item upload-item"
|
||||
@click="triggerTextureUpload"
|
||||
>
|
||||
<div class="upload-icon">
|
||||
<span>+</span>
|
||||
</div>
|
||||
<span class="texture-label">上传纹理</span>
|
||||
</div>
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleTextureUpload"
|
||||
style="display: none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 其他类型的属性 -->
|
||||
<template v-else>
|
||||
<div class="generic-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<input
|
||||
type="color"
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
<!-- 如果是主颜色,显示最近使用的颜色 -->
|
||||
<div v-if="prop.id === 'color'" class="recent-colors">
|
||||
<div
|
||||
v-for="(color, index) in brushStore.state.recentColors"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="handlePropertyChange(prop.id, color)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<template v-else-if="prop.type === 'checkbox'">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.checked)
|
||||
"
|
||||
:id="`toggle-${prop.id}`"
|
||||
/>
|
||||
<label :for="`toggle-${prop.id}`"></label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<template v-else-if="prop.type === 'select'">
|
||||
<div class="select-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<select
|
||||
:value="prop.value"
|
||||
@change="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="property-select"
|
||||
>
|
||||
<option
|
||||
v-for="option in prop.options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 文件选择器(用于材质) -->
|
||||
<template v-else-if="prop.type === 'file'">
|
||||
<div class="file-property">
|
||||
<div class="file-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="file-preview" @click="handleFileSelect(prop.id)">
|
||||
<img v-if="prop.value" :src="prop.value" alt="材质预览" />
|
||||
<div v-else class="no-file">点击上传材质图片</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button
|
||||
class="select-file-btn"
|
||||
@click="handleFileSelect(prop.id)"
|
||||
>
|
||||
上传图片
|
||||
</button>
|
||||
<button
|
||||
class="clear-file-btn"
|
||||
@click="handlePropertyChange(prop.id, '')"
|
||||
v-if="prop.value"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 材质网格选择器 -->
|
||||
<template v-else-if="prop.type === 'texture-grid'">
|
||||
<div class="texture-grid-property">
|
||||
<div class="texture-grid-header">
|
||||
<span>{{ prop.name }}</span>
|
||||
</div>
|
||||
<div class="texture-grid">
|
||||
<div
|
||||
v-for="texture in prop.options"
|
||||
:key="texture.value"
|
||||
class="texture-item"
|
||||
:class="{ active: prop.value === texture.value }"
|
||||
@click="handleTextureSelect(texture.value)"
|
||||
>
|
||||
<img
|
||||
:src="texture.preview || texture.value"
|
||||
:alt="texture.label"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-label">{{ texture.label }}</span>
|
||||
</div>
|
||||
<!-- 自定义纹理上传按钮 -->
|
||||
<div class="texture-item upload-item" @click="triggerTextureUpload">
|
||||
<div class="upload-icon">
|
||||
<span>+</span>
|
||||
</div>
|
||||
<span class="texture-label">上传纹理</span>
|
||||
</div>
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleTextureUpload"
|
||||
style="display: none;"
|
||||
class="property-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 其他类型的属性 -->
|
||||
<template v-else>
|
||||
<div class="generic-property">
|
||||
<span>{{ prop.name }}</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="
|
||||
(e) => handlePropertyChange(prop.id, e.target.value)
|
||||
"
|
||||
class="property-input"
|
||||
/>
|
||||
<!-- 属性描述提示 -->
|
||||
<div v-if="prop.description" class="property-description">
|
||||
{{ prop.description }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 属性描述提示 -->
|
||||
<div v-if="prop.description" class="property-description">
|
||||
{{ prop.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 材质库弹窗 -->
|
||||
<div
|
||||
v-if="showLibrary"
|
||||
class="texture-library-overlay"
|
||||
@click.self="showLibrary = false"
|
||||
>
|
||||
<div class="texture-library-modal">
|
||||
<div class="modal-header">
|
||||
<h3>材质库</h3>
|
||||
<button class="close-btn" @click="showLibrary = false">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<div class="texture-categories">
|
||||
<button
|
||||
v-for="(category, index) in textureCategories"
|
||||
:key="index"
|
||||
:class="[
|
||||
'category-btn',
|
||||
{ active: selectedCategory === category },
|
||||
]"
|
||||
@click="selectedCategory = category"
|
||||
>
|
||||
{{ category }}
|
||||
<!-- 材质库弹窗 -->
|
||||
<div
|
||||
v-if="showLibrary"
|
||||
class="texture-library-overlay"
|
||||
@click.self="showLibrary = false"
|
||||
>
|
||||
<div class="texture-library-modal">
|
||||
<div class="modal-header">
|
||||
<h3>材质库</h3>
|
||||
<button class="close-btn" @click="showLibrary = false">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="texture-list">
|
||||
<div
|
||||
v-for="(texture, index) in filteredTextures"
|
||||
:key="index"
|
||||
class="library-texture-item"
|
||||
@click="selectLibraryTexture(texture.path)"
|
||||
>
|
||||
<img
|
||||
:src="texture.thumbnail"
|
||||
:alt="texture.name"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-name">{{ texture.name }}</span>
|
||||
<div class="modal-content">
|
||||
<div class="texture-categories">
|
||||
<button
|
||||
v-for="(category, index) in textureCategories"
|
||||
:key="index"
|
||||
:class="[
|
||||
'category-btn',
|
||||
{ active: selectedCategory === category },
|
||||
]"
|
||||
@click="selectedCategory = category"
|
||||
>
|
||||
{{ category }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="texture-list">
|
||||
<div
|
||||
v-for="(texture, index) in filteredTextures"
|
||||
:key="index"
|
||||
class="library-texture-item"
|
||||
@click="selectLibraryTexture(texture.path)"
|
||||
>
|
||||
<img
|
||||
:src="texture.thumbnail"
|
||||
:alt="texture.name"
|
||||
class="texture-thumbnail"
|
||||
/>
|
||||
<span class="texture-name">{{ texture.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="upload-btn"
|
||||
@click="handleFileSelect('texturePath')"
|
||||
>
|
||||
上传新材质
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="upload-btn" @click="handleFileSelect('texturePath')">
|
||||
上传新材质
|
||||
</div>
|
||||
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷预设</span>
|
||||
<button
|
||||
class="save-preset-btn"
|
||||
@click="saveCurrentAsPreset"
|
||||
title="保存当前设置为预设"
|
||||
>
|
||||
<i class="save-icon">+</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>笔刷预设</span>
|
||||
<button
|
||||
class="save-preset-btn"
|
||||
@click="saveCurrentAsPreset"
|
||||
title="保存当前设置为预设"
|
||||
>
|
||||
<i class="save-icon">+</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="presets-container">
|
||||
<div
|
||||
v-for="(preset, index) in brushStore.state.presets"
|
||||
:key="index"
|
||||
class="preset-item"
|
||||
@click="applyPresetWithCommand(index)"
|
||||
>
|
||||
<div class="presets-container">
|
||||
<div
|
||||
class="preset-color"
|
||||
:style="{
|
||||
backgroundColor: preset.color,
|
||||
width: preset.size + 'px',
|
||||
height: preset.size + 'px',
|
||||
opacity: preset.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="preset-name">{{ preset.name }}</span>
|
||||
v-for="(preset, index) in brushStore.state.presets"
|
||||
:key="index"
|
||||
class="preset-item"
|
||||
@click="applyPresetWithCommand(index)"
|
||||
>
|
||||
<div
|
||||
class="preset-color"
|
||||
:style="{
|
||||
backgroundColor: preset.color,
|
||||
width: preset.size + 'px',
|
||||
height: preset.size + 'px',
|
||||
opacity: preset.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="preset-name">{{ preset.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,10 +460,7 @@ function handleTextureSelect(textureId) {
|
||||
|
||||
// 触发纹理上传文件选择
|
||||
function triggerTextureUpload() {
|
||||
const fileInput = textureFileInput.value;
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
handleFileSelect("texturePath");
|
||||
}
|
||||
|
||||
// 处理纹理文件上传
|
||||
@@ -461,15 +470,15 @@ async function handleTextureUpload(event) {
|
||||
|
||||
try {
|
||||
// 验证文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('请选择图片文件');
|
||||
if (!file.type.startsWith("image/")) {
|
||||
alert("请选择图片文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证文件大小 (限制为 5MB)
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
alert('文件大小不能超过 5MB');
|
||||
alert("文件大小不能超过 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -478,8 +487,8 @@ async function handleTextureUpload(event) {
|
||||
const brushManager = toolManager?.brushManager;
|
||||
|
||||
if (!texturePresetManager || !brushManager) {
|
||||
console.error('缺少必要的管理器实例');
|
||||
alert('系统错误:无法上传纹理');
|
||||
console.error("缺少必要的管理器实例");
|
||||
alert("系统错误:无法上传纹理");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,12 +502,11 @@ async function handleTextureUpload(event) {
|
||||
});
|
||||
|
||||
await commandManager.execute(command);
|
||||
|
||||
|
||||
// 清空文件输入,允许重复上传同一文件
|
||||
event.target.value = '';
|
||||
|
||||
event.target.value = "";
|
||||
} catch (error) {
|
||||
console.error('纹理上传失败:', error);
|
||||
console.error("纹理上传失败:", error);
|
||||
alert(`上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -673,29 +681,44 @@ onMounted(() => {
|
||||
const brushStore = BrushStore;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="less">
|
||||
.brush-panel {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 62px;
|
||||
padding: 0;
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
max-height: 85vh;
|
||||
width: 30%;
|
||||
/* overflow-y: auto; */
|
||||
min-width: 280px;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
position: relative;
|
||||
animation: panelFadeIn 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
|
||||
backdrop-filter: blur(2px); /* 添加模糊效果 */
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
z-index: 1000; /* 确保面板在最上层 */
|
||||
|
||||
.brush-panel-wrapper {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
max-height: 85vh; /* 限制最大高度 */
|
||||
/*优化ios上的滚动效果*/
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.brush-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加指向整个面板的倒三角 */
|
||||
.brush-panel::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 20px;
|
||||
top: -9px;
|
||||
right: 58px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
@@ -716,12 +739,6 @@ const brushStore = BrushStore;
|
||||
}
|
||||
}
|
||||
|
||||
.brush-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brush-section {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
@@ -746,9 +763,10 @@ const brushStore = BrushStore;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05); /* 更柔和的边框 */
|
||||
}
|
||||
|
||||
.section-header::after {
|
||||
/* .section-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
@@ -757,18 +775,18 @@ const brushStore = BrushStore;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid #999; /* 更柔和的颜色 */
|
||||
border-top: 6px solid #999;
|
||||
transform: translateY(-50%);
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
} */
|
||||
|
||||
.section-header:hover {
|
||||
/* .section-header:hover {
|
||||
background-color: rgba(248, 249, 250, 1);
|
||||
}
|
||||
|
||||
.section-header:hover::after {
|
||||
border-top-color: #666;
|
||||
}
|
||||
} */
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
@@ -1114,6 +1132,12 @@ const brushStore = BrushStore;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.property-select {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-select:focus {
|
||||
border-color: #4285f4;
|
||||
outline: none;
|
||||
|
||||
@@ -17,6 +17,7 @@ provide("brushStore", BrushStore);
|
||||
|
||||
const toolManager = inject("toolManager");
|
||||
const layerManager = inject("layerManager");
|
||||
const isShowLayerPanel = inject("isShowLayerPanel", ref(false));
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: String,
|
||||
@@ -24,6 +25,7 @@ const props = defineProps({
|
||||
canvasHeight: Number,
|
||||
canvasColor: String,
|
||||
brushSize: Number,
|
||||
enabledRedGreenMode: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -40,9 +42,9 @@ const showBrushPanel = ref(false);
|
||||
const brushPanelRef = ref(null);
|
||||
|
||||
// 计算属性
|
||||
const shouldShowBrushSettings = computed(() => {
|
||||
return props.activeTool === OperationType.DRAW;
|
||||
});
|
||||
// const shouldShowBrushSettings = computed(() => {
|
||||
// return props.activeTool === OperationType.DRAW;
|
||||
// });
|
||||
|
||||
function updateCanvasSize() {
|
||||
if (!layerManager) {
|
||||
@@ -86,6 +88,11 @@ function updateCanvasColor() {
|
||||
|
||||
// 切换笔刷面板显示状态
|
||||
function toggleBrushPanel() {
|
||||
// 如果笔刷没有激活 则激活笔刷工具
|
||||
if (toolManager?.activeTool !== OperationType.DRAW) {
|
||||
toolManager.setToolWithCommand(OperationType.DRAW);
|
||||
}
|
||||
|
||||
showBrushPanel.value = !showBrushPanel.value;
|
||||
}
|
||||
|
||||
@@ -176,16 +183,37 @@ function syncBrushStoreToManager() {
|
||||
|
||||
// 点击外部时关闭笔刷面板
|
||||
function handleClickOutside(event) {
|
||||
if (
|
||||
showBrushPanel.value &&
|
||||
brushPanelRef.value &&
|
||||
!brushPanelRef.value.contains(event.target) &&
|
||||
!event.target.closest(".brush-selector")
|
||||
) {
|
||||
showBrushPanel.value = false;
|
||||
// if (isShowLayerPanel.value) {
|
||||
// // 如果点击的是图层面板或其内部元素,则不关闭
|
||||
// if (event.target.closest(".layers-panel")) {
|
||||
// return;
|
||||
// }
|
||||
// // 关闭图层面板
|
||||
// isShowLayerPanel.value = false;
|
||||
// }
|
||||
|
||||
if (showBrushPanel.value) {
|
||||
// 检查是否点击了笔刷选择器按钮
|
||||
if (event.target.closest(".brush-selector")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否点击了笔刷面板或其内部元素
|
||||
if (event.target.closest(".brush-panel")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果都不是,则关闭面板
|
||||
if (showBrushPanel.value) {
|
||||
showBrushPanel.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showLayerPanel() {
|
||||
isShowLayerPanel.value = !isShowLayerPanel.value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取工具管理器和笔刷管理器
|
||||
const brushManager = toolManager?.brushManager;
|
||||
@@ -229,94 +257,51 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="canvas-header">
|
||||
<span class="canvas-title">Canvas</span>
|
||||
|
||||
<!-- 默认设置 -->
|
||||
<div
|
||||
v-if="
|
||||
<div class="canvas-header-wrapper">
|
||||
<span class="canvas-title">Canvas</span>
|
||||
<!-- 默认设置 -->
|
||||
<!-- v-if="
|
||||
!activeTool ||
|
||||
activeTool === OperationType.SELECT ||
|
||||
activeTool === OperationType.PAN
|
||||
"
|
||||
class="canvas-settings"
|
||||
>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Width</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasWidth"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasWidth', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Height</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasHeight"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasHeight', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color</span>
|
||||
<div class="color-picker-wrapper">
|
||||
" -->
|
||||
<div class="canvas-settings" v-if="!props.enabledRedGreenMode">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Width</span>
|
||||
<input
|
||||
type="color"
|
||||
:value="canvasColor"
|
||||
class="color-picker"
|
||||
@input="$emit('update:canvasColor', $event.target.value)"
|
||||
@change="updateCanvasColor"
|
||||
type="text"
|
||||
:value="canvasWidth"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasWidth', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Height</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasHeight"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasHeight', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
:value="canvasColor"
|
||||
class="color-picker"
|
||||
@input="$emit('update:canvasColor', $event.target.value)"
|
||||
@change="updateCanvasColor"
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绘图工具设置 -->
|
||||
<div v-if="shouldShowBrushSettings" class="canvas-settings">
|
||||
<!-- 简化的笔刷控制UI -->
|
||||
<!-- <div class="setting-group">
|
||||
<span class="setting-label">大小:</span>
|
||||
<input
|
||||
type="range"
|
||||
:value="BrushStore.state.size"
|
||||
min="0.5"
|
||||
max="100"
|
||||
step="0.5"
|
||||
class="size-slider"
|
||||
@input="handleBrushSizeChange"
|
||||
/>
|
||||
<span class="size-value">{{ BrushStore.state.size }}px</span>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">笔刷:</span>
|
||||
<div class="brush-selector" @click="toggleBrushPanel">
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="{
|
||||
backgroundColor: BrushStore.state.color,
|
||||
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
|
||||
opacity: BrushStore.state.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="brush-dropdown">▼</span>
|
||||
</div>
|
||||
|
||||
<!-- 笔刷面板 -->
|
||||
<div
|
||||
v-if="showBrushPanel"
|
||||
class="brush-panel-container"
|
||||
ref="brushPanelRef"
|
||||
>
|
||||
<BrushPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">颜色:</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input
|
||||
@@ -327,64 +312,71 @@ onMounted(() => {
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本工具设置 -->
|
||||
<div v-if="activeTool === OperationType.TEXT" class="canvas-settings">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Font:</span>
|
||||
<select class="font-select">
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Size:</span>
|
||||
<input
|
||||
type="number"
|
||||
class="setting-input"
|
||||
value="16"
|
||||
min="8"
|
||||
max="72"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color:</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color" class="color-picker" value="#000000" />
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传工具设置 -->
|
||||
<div v-if="activeTool === OperationType.UPLOAD" class="canvas-settings">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Upload Type:</span>
|
||||
<select class="setting-select">
|
||||
<option value="image">Image</option>
|
||||
<option value="vector">Vector Graphics</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 导出设置 -->
|
||||
<div class="setting-group export-group">
|
||||
<!-- <div class="setting-group export-group">
|
||||
<span class="export-model-select">exportModel.select:</span>
|
||||
<span class="export-model-dropdown">▼</span>
|
||||
</div> -->
|
||||
|
||||
<!-- 绘图工具设置 -->
|
||||
<div class="canvas-settings gap-20" v-if="!props.enabledRedGreenMode">
|
||||
<div
|
||||
class="btn"
|
||||
:class="{ active: showBrushPanel }"
|
||||
@click="toggleBrushPanel"
|
||||
>
|
||||
<!-- <span class="setting-label">笔刷:</span>/ -->
|
||||
<div class="brush-selector">
|
||||
<SvgIcon name="CBrushTop" size="22"></SvgIcon>
|
||||
<!-- <div
|
||||
class="brush-preview"
|
||||
:style="{
|
||||
backgroundColor: BrushStore.state.color,
|
||||
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
|
||||
opacity: BrushStore.state.opacity,
|
||||
}"
|
||||
></div> -->
|
||||
<!-- <span class="brush-dropdown">▼</span> -->
|
||||
</div>
|
||||
<!-- 笔刷面板 -->
|
||||
<div
|
||||
v-if="showBrushPanel"
|
||||
class="brush-panel-container"
|
||||
ref="brushPanelRef"
|
||||
>
|
||||
<Teleport to="body">
|
||||
<BrushPanel />
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="btn"
|
||||
:class="{ active: isShowLayerPanel }"
|
||||
@click="showLayerPanel"
|
||||
>
|
||||
<SvgIcon name="CLayout" size="26"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="less">
|
||||
.canvas-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
user-select: none;
|
||||
height: 52px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
.canvas-header-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-title {
|
||||
@@ -393,19 +385,44 @@ onMounted(() => {
|
||||
margin-right: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.canvas-title::before {
|
||||
content: "⟳";
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
// &:before {
|
||||
// // /* content: "⟳";
|
||||
// // margin-right: 5px;
|
||||
// // font-size: 14px; */
|
||||
// }
|
||||
}
|
||||
|
||||
.canvas-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
color: #213547;
|
||||
.btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: #f0f0f0;
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
// color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gap-20 {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
@@ -476,15 +493,15 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.brush-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
width: 80px;
|
||||
justify-content: space-between;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// border: 1px solid #ddd;
|
||||
// border-radius: 4px;
|
||||
// padding: 5px;
|
||||
// cursor: pointer;
|
||||
// background-color: white;
|
||||
// width: 80px;
|
||||
// justify-content: space-between;
|
||||
}
|
||||
|
||||
.brush-preview {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,369 @@
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "select"]);
|
||||
|
||||
const menuRef = ref(null);
|
||||
const adjustedPosition = ref({ x: 0, y: 0 });
|
||||
const hoveredItem = ref(null);
|
||||
const submenuPositions = ref(new Map());
|
||||
const hideTimer = ref(null); // 添加隐藏定时器
|
||||
|
||||
// 计算菜单位置,处理边界问题
|
||||
const calculatePosition = () => {
|
||||
if (!menuRef.value || !props.visible) return;
|
||||
|
||||
const menu = menuRef.value;
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
let x = props.position.x;
|
||||
let y = props.position.y;
|
||||
|
||||
// 右边界检测
|
||||
if (x + menuRect.width > windowWidth - 10) {
|
||||
x = x - menuRect.width;
|
||||
}
|
||||
|
||||
// 底边界检测
|
||||
if (y + menuRect.height > windowHeight - 10) {
|
||||
y = windowHeight - menuRect.height - 10;
|
||||
}
|
||||
|
||||
// 左边界检测
|
||||
if (x < 10) {
|
||||
x = 10;
|
||||
}
|
||||
|
||||
// 顶边界检测
|
||||
if (y < 10) {
|
||||
y = 10;
|
||||
}
|
||||
|
||||
adjustedPosition.value = { x, y };
|
||||
};
|
||||
|
||||
// 计算子菜单位置
|
||||
const calculateSubmenuPosition = (itemElement, itemIndex) => {
|
||||
if (!itemElement || !menuRef.value) return { x: 0, y: 0, direction: "right" };
|
||||
|
||||
const itemRect = itemElement.getBoundingClientRect();
|
||||
const menuRect = menuRef.value.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// 预估子菜单宽度(可以根据实际情况调整)
|
||||
const submenuWidth = 200;
|
||||
const submenuHeight = 300; // 预估高度
|
||||
|
||||
let x = itemRect.right + 4;
|
||||
// 直接使用菜单项相对于主菜单容器的偏移量
|
||||
let y = itemElement.offsetTop;
|
||||
let direction = "right";
|
||||
|
||||
// 右边界检测,如果右侧空间不足,显示在左侧
|
||||
if (x + submenuWidth > windowWidth - 10) {
|
||||
x = itemRect.left - submenuWidth - 4;
|
||||
direction = "left";
|
||||
}
|
||||
|
||||
// 底边界检测 - 基于子菜单的绝对位置检查
|
||||
const absoluteSubmenuBottom = itemRect.top + submenuHeight;
|
||||
if (absoluteSubmenuBottom > windowHeight - 10) {
|
||||
// 计算可用的最大Y位置(相对于主菜单)
|
||||
const maxAbsoluteY = windowHeight - submenuHeight - 10;
|
||||
const maxRelativeY = maxAbsoluteY - menuRect.top;
|
||||
y = Math.max(0, maxRelativeY);
|
||||
}
|
||||
|
||||
// 左边界检测
|
||||
if (x < 10) {
|
||||
x = 10;
|
||||
direction = "right";
|
||||
}
|
||||
|
||||
// 确保 y 不为负数
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
}
|
||||
|
||||
y = 0;
|
||||
|
||||
const position = { x, y, direction };
|
||||
submenuPositions.value.set(itemIndex, position);
|
||||
return position;
|
||||
};
|
||||
|
||||
// 清除隐藏定时器
|
||||
const clearHideTimer = () => {
|
||||
if (hideTimer.value) {
|
||||
clearTimeout(hideTimer.value);
|
||||
hideTimer.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 显示子菜单
|
||||
const showSubmenu = (item, index, element) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
clearHideTimer();
|
||||
hoveredItem.value = index;
|
||||
nextTick(() => {
|
||||
calculateSubmenuPosition(element, index);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏子菜单(延迟)
|
||||
const hideSubmenu = (index) => {
|
||||
clearHideTimer();
|
||||
hideTimer.value = setTimeout(() => {
|
||||
if (hoveredItem.value === index) {
|
||||
hoveredItem.value = null;
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// 处理鼠标进入菜单项
|
||||
const handleItemMouseEnter = (item, index, event) => {
|
||||
const element = event.target.closest(".context-menu-item");
|
||||
showSubmenu(item, index, element);
|
||||
};
|
||||
|
||||
// 处理鼠标在菜单项内移动
|
||||
const handleItemMouseMove = (item, index, event) => {
|
||||
// 如果当前菜单项有子菜单但子菜单未显示,则显示子菜单
|
||||
if (
|
||||
item.children &&
|
||||
item.children.length > 0 &&
|
||||
hoveredItem.value !== index
|
||||
) {
|
||||
const element = event.target.closest(".context-menu-item");
|
||||
showSubmenu(item, index, element);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标离开菜单项
|
||||
const handleItemMouseLeave = (item, index) => {
|
||||
// 只有当有子菜单时才延迟隐藏
|
||||
if (item.children && item.children.length > 0) {
|
||||
hideSubmenu(index);
|
||||
} else {
|
||||
hoveredItem.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标进入子菜单
|
||||
const handleSubmenuMouseEnter = (index) => {
|
||||
clearHideTimer();
|
||||
hoveredItem.value = index;
|
||||
};
|
||||
|
||||
// 处理鼠标离开子菜单
|
||||
const handleSubmenuMouseLeave = (index) => {
|
||||
hideSubmenu(index);
|
||||
};
|
||||
|
||||
// 监听可见性和位置变化
|
||||
watch([() => props.visible, () => props.position], () => {
|
||||
if (props.visible) {
|
||||
nextTick(() => {
|
||||
calculatePosition();
|
||||
});
|
||||
} else {
|
||||
hoveredItem.value = null;
|
||||
submenuPositions.value.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// 处理菜单项点击
|
||||
const handleItemClick = (item, index) => {
|
||||
if (item.disabled || item.type === "divider") return;
|
||||
|
||||
// 如果有子菜单,不关闭菜单
|
||||
if (item.children && item.children.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("select", item, index);
|
||||
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// 处理子菜单项点击
|
||||
const handleSubItemClick = (subItem, parentIndex, subIndex) => {
|
||||
if (subItem.disabled || subItem.type === "divider") return;
|
||||
|
||||
emit("select", subItem, `${parentIndex}-${subIndex}`);
|
||||
|
||||
if (subItem.action) {
|
||||
subItem.action();
|
||||
}
|
||||
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// 处理外部点击关闭
|
||||
const handleOutsideClick = (event) => {
|
||||
if (menuRef.value && !menuRef.value.contains(event.target)) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
// 处理 ESC 键关闭
|
||||
const handleEscKey = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleOutsideClick, true);
|
||||
document.addEventListener("contextmenu", handleOutsideClick, true);
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", handleOutsideClick, true);
|
||||
document.removeEventListener("contextmenu", handleOutsideClick, true);
|
||||
document.removeEventListener("keydown", handleEscKey);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<transition name="context-menu">
|
||||
<div
|
||||
v-if="visible"
|
||||
ref="menuRef"
|
||||
class="context-menu"
|
||||
:style="{
|
||||
top: `${adjustedPosition.y}px`,
|
||||
left: `${adjustedPosition.x}px`,
|
||||
}"
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<!-- 分隔线 -->
|
||||
<div
|
||||
v-if="item.type === 'divider'"
|
||||
class="context-menu-divider"
|
||||
></div>
|
||||
|
||||
<!-- 菜单项 -->
|
||||
<div
|
||||
v-else
|
||||
class="context-menu-item"
|
||||
:class="{
|
||||
disabled: item.disabled,
|
||||
danger: item.danger,
|
||||
'has-children': item.children && item.children.length > 0,
|
||||
hovered: hoveredItem === index,
|
||||
}"
|
||||
@click="handleItemClick(item, index)"
|
||||
@mouseenter="handleItemMouseEnter(item, index, $event)"
|
||||
@mousemove="handleItemMouseMove(item, index, $event)"
|
||||
@mouseleave="handleItemMouseLeave(item, index)"
|
||||
>
|
||||
<span class="context-menu-icon" v-if="item.icon">
|
||||
<SvgIcon
|
||||
:name="item.icon"
|
||||
size="14"
|
||||
:style="{
|
||||
transform: item.inverIcon ? `rotate(90deg)` : 'none',
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
<span class="context-menu-label">{{ item.label }}</span>
|
||||
<span class="context-menu-shortcut" v-if="item.shortcut">
|
||||
{{ item.shortcut }}
|
||||
</span>
|
||||
<span
|
||||
class="context-menu-arrow"
|
||||
v-if="item.children && item.children.length > 0"
|
||||
>
|
||||
<SvgIcon name="CRight" size="12" />
|
||||
</span>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<transition name="context-submenu">
|
||||
<div
|
||||
v-if="
|
||||
item.children &&
|
||||
item.children.length > 0 &&
|
||||
hoveredItem === index
|
||||
"
|
||||
class="context-submenu"
|
||||
:class="{
|
||||
'submenu-left':
|
||||
submenuPositions.get(index)?.direction === 'left',
|
||||
}"
|
||||
@mouseenter="handleSubmenuMouseEnter(index)"
|
||||
@mouseleave="handleSubmenuMouseLeave"
|
||||
>
|
||||
<template
|
||||
v-for="(subItem, subIndex) in item.children"
|
||||
:key="subIndex"
|
||||
>
|
||||
<!-- 子菜单分隔线 -->
|
||||
<div
|
||||
v-if="subItem.type === 'divider'"
|
||||
class="context-menu-divider"
|
||||
></div>
|
||||
|
||||
<!-- 子菜单项 -->
|
||||
<div
|
||||
v-else
|
||||
class="context-menu-item"
|
||||
:class="{
|
||||
disabled: subItem.disabled,
|
||||
danger: subItem.danger,
|
||||
}"
|
||||
@click="handleSubItemClick(subItem, index, subIndex)"
|
||||
>
|
||||
<span class="context-menu-icon" v-if="subItem.icon">
|
||||
<SvgIcon
|
||||
:name="subItem.icon"
|
||||
size="14"
|
||||
:style="{
|
||||
transform: subItem.inverIcon
|
||||
? `rotate(90deg)`
|
||||
: 'none',
|
||||
}"
|
||||
/>
|
||||
</span>
|
||||
<span class="context-menu-label">{{ subItem.label }}</span>
|
||||
<span class="context-menu-shortcut" v-if="subItem.shortcut">
|
||||
{{ subItem.shortcut }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import "./contextMenu.less";
|
||||
</style>
|
||||
@@ -0,0 +1,514 @@
|
||||
<script setup>
|
||||
import { ref, nextTick, computed, inject } from "vue";
|
||||
import { Checkbox } from "ant-design-vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
||||
import { isGroupLayer } from "../../utils/layerHelper";
|
||||
|
||||
// 设置组件名称,用于递归渲染
|
||||
defineOptions({
|
||||
name: "LayerItem",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
layer: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isChild: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isMultiSelectMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
editingName: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
canDelete: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
thumbnailUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
isHidenDragHandle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
expandedGroupIds: {
|
||||
type: Set,
|
||||
default: () => new Set(),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"click",
|
||||
"double-click",
|
||||
"context-menu",
|
||||
"checkbox-change",
|
||||
"toggle-visibility",
|
||||
"toggle-lock",
|
||||
"delete",
|
||||
"edit-confirm",
|
||||
"edit-cancel",
|
||||
"edit-keydown",
|
||||
"touch-start",
|
||||
"touch-move",
|
||||
"touch-end",
|
||||
"child-layers-sort",
|
||||
"update-child-layers",
|
||||
"toggle-group-expanded",
|
||||
// 新增子图层专用事件
|
||||
"toggle-child-visibility",
|
||||
"toggle-child-lock",
|
||||
"delete-child",
|
||||
"rename-child",
|
||||
// v-model相关事件
|
||||
"update:editingName",
|
||||
]);
|
||||
|
||||
const layerManager = inject("layerManager", null);
|
||||
|
||||
// 计算属性
|
||||
const isGroupLayerType = computed(() => {
|
||||
return isGroupLayer(props.layer);
|
||||
});
|
||||
|
||||
// 计算属性:检查组图层是否展开
|
||||
const isGroupExpanded = computed(() => {
|
||||
return props.expandedGroupIds.has(props.layer.id);
|
||||
});
|
||||
|
||||
// 获取子图层
|
||||
const childLayers = computed(() => {
|
||||
if (!isGroupLayerType.value) return [];
|
||||
|
||||
// 优先使用 layer.children 属性
|
||||
if (props.layer.children && Array.isArray(props.layer.children)) {
|
||||
return props.layer.children;
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
// 切换组图层展开/收起状态
|
||||
const toggleGroupExpanded = () => {
|
||||
emit("toggle-group-expanded", props.layer.id);
|
||||
};
|
||||
|
||||
// 获取图层类型图标
|
||||
function getLayerTypeIcon(layer) {
|
||||
if (!layer) return "🖼️";
|
||||
|
||||
if (isGroupLayer(layer)) {
|
||||
return "📁";
|
||||
}
|
||||
|
||||
if (layer.fabricObject) {
|
||||
switch (layer.fabricObject.type) {
|
||||
case "image":
|
||||
return "🖼️";
|
||||
case "text":
|
||||
return "📝";
|
||||
case "rect":
|
||||
return "▢";
|
||||
case "circle":
|
||||
return "⬤";
|
||||
case "path":
|
||||
return "✎";
|
||||
default:
|
||||
return "⬤";
|
||||
}
|
||||
}
|
||||
|
||||
return "🖼️";
|
||||
}
|
||||
|
||||
function getLayerTypeText(layerType) {
|
||||
const typeMap = {
|
||||
EMPTY: "空图层",
|
||||
TEXT: "文本",
|
||||
IMAGE: "图片",
|
||||
SHAPE: "形状",
|
||||
GROUP: "组合",
|
||||
BACKGROUND: "背景",
|
||||
FIXED: "固定",
|
||||
};
|
||||
|
||||
return typeMap[layerType] || "未知";
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
function handleClick(event) {
|
||||
emit("click", props.layer, event);
|
||||
}
|
||||
|
||||
function handleDoubleClick(event) {
|
||||
emit("double-click", props.layer, event);
|
||||
}
|
||||
|
||||
function handleContextMenu(event) {
|
||||
emit("context-menu", event, props.layer);
|
||||
}
|
||||
|
||||
function handleCheckboxChange(event) {
|
||||
emit("checkbox-change", props.layer.id, event);
|
||||
}
|
||||
|
||||
function handleToggleVisibility() {
|
||||
if (props.isChild) {
|
||||
// 子图层需要传递父图层ID - 从父级组件获取
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
emit("toggle-child-visibility", props.layer.id, parentId);
|
||||
} else {
|
||||
// 一级图层
|
||||
emit("toggle-visibility", props.layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleLock() {
|
||||
if (props.isChild) {
|
||||
// 子图层需要传递父图层ID - 从父级组件获取
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
emit("toggle-child-lock", props.layer.id, parentId);
|
||||
} else {
|
||||
// 一级图层
|
||||
emit("toggle-lock", props.layer);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!props.canDelete) {
|
||||
console.warn("当前图层无法删除:", props.layer.id);
|
||||
return;
|
||||
}
|
||||
if (props.isChild) {
|
||||
// 子图层删除:需要传递父图层ID
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
if (parentId) {
|
||||
emit("delete-child", props.layer.id, parentId);
|
||||
} else {
|
||||
console.warn("无法找到子图层的父图层ID:", props.layer.id);
|
||||
}
|
||||
} else {
|
||||
// 一级图层删除
|
||||
emit("delete", props.layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditConfirm() {
|
||||
if (props.isChild) {
|
||||
// 子图层重命名:需要传递父图层ID
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
if (props.editingName && props.editingName.trim() && parentId) {
|
||||
emit("rename-child", props.layer.id, parentId, props.editingName.trim());
|
||||
} else if (!parentId) {
|
||||
console.warn("无法找到子图层的父图层ID:", props.layer.id);
|
||||
}
|
||||
// 发送编辑取消事件,清理编辑状态
|
||||
emit("edit-cancel");
|
||||
} else {
|
||||
// 一级图层重命名
|
||||
emit("edit-confirm");
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditCancel() {
|
||||
emit("edit-cancel");
|
||||
}
|
||||
|
||||
function handleEditKeydown(event) {
|
||||
emit("edit-keydown", event); // 修复事件名称,从 "edit-keyboard" 改为 "edit-keydown"
|
||||
}
|
||||
|
||||
function handleTouchStart(event) {
|
||||
emit("touch-start", event, props.layer);
|
||||
}
|
||||
|
||||
function handleTouchMove(event) {
|
||||
emit("touch-move", event);
|
||||
}
|
||||
|
||||
function handleTouchEnd(event) {
|
||||
emit("touch-end", event);
|
||||
}
|
||||
|
||||
function handleUpdateChildLayers(newChildren) {
|
||||
// 更新当前组图层的children数组
|
||||
console.log(
|
||||
"更新子图层顺序:",
|
||||
"父图层ID:",
|
||||
props.layer.id,
|
||||
"新顺序:",
|
||||
newChildren
|
||||
);
|
||||
emit("update-child-layers", props.layer.id, newChildren);
|
||||
}
|
||||
|
||||
// 子图层递归事件处理
|
||||
function handleChildClick(childLayer, event) {
|
||||
emit("click", childLayer, event);
|
||||
}
|
||||
|
||||
function handleChildDoubleClick(childLayer, event) {
|
||||
emit("double-click", childLayer, event);
|
||||
}
|
||||
|
||||
function handleChildContextMenu(event, childLayer) {
|
||||
emit("context-menu", event, childLayer);
|
||||
}
|
||||
|
||||
function handleChildToggleVisibility(childLayerId) {
|
||||
emit("toggle-visibility", childLayerId);
|
||||
}
|
||||
|
||||
function handleChildToggleLock(childLayer) {
|
||||
emit("toggle-lock", childLayer);
|
||||
}
|
||||
|
||||
// 动画钩子函数
|
||||
function onEnter(el) {
|
||||
// 设置初始状态
|
||||
el.style.height = "0";
|
||||
el.style.opacity = "0";
|
||||
el.style.paddingTop = "0";
|
||||
el.style.paddingBottom = "0";
|
||||
el.style.marginTop = "0";
|
||||
el.style.marginBottom = "0";
|
||||
el.style.overflow = "hidden";
|
||||
|
||||
// 强制重排
|
||||
el.offsetHeight;
|
||||
|
||||
// 获取最终高度
|
||||
el.style.height = "auto";
|
||||
const finalHeight = el.scrollHeight;
|
||||
el.style.height = "0";
|
||||
|
||||
// 执行动画
|
||||
requestAnimationFrame(() => {
|
||||
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
el.style.height = finalHeight + "px";
|
||||
el.style.opacity = "1";
|
||||
el.style.paddingTop = "";
|
||||
el.style.paddingBottom = "";
|
||||
el.style.marginTop = "";
|
||||
el.style.marginBottom = "";
|
||||
});
|
||||
}
|
||||
|
||||
function onLeave(el) {
|
||||
// 设置当前高度
|
||||
el.style.height = el.scrollHeight + "px";
|
||||
el.style.overflow = "hidden";
|
||||
|
||||
// 强制重排
|
||||
el.offsetHeight;
|
||||
|
||||
// 执行收起动画
|
||||
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
el.style.height = "0";
|
||||
el.style.opacity = "0";
|
||||
el.style.paddingTop = "0";
|
||||
el.style.paddingBottom = "0";
|
||||
el.style.marginTop = "0";
|
||||
el.style.marginBottom = "0";
|
||||
}
|
||||
|
||||
// 查找父图层ID的辅助方法 - 增强版本
|
||||
function findParentLayerId() {
|
||||
// 首先检查 layer 对象是否已经有 parentId 属性
|
||||
if (props.layer.parentId) {
|
||||
return props.layer.parentId;
|
||||
}
|
||||
|
||||
// 如果没有,尝试从 layerManager 中查找
|
||||
if (layerManager && layerManager.layers) {
|
||||
for (const layer of layerManager.layers.value) {
|
||||
if (
|
||||
layer.children &&
|
||||
Array.isArray(layer.children) &&
|
||||
layer.children.some((child) => child.id === props.layer.id)
|
||||
) {
|
||||
return layer.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn("无法找到图层的父图层:", props.layer.id);
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- 主图层项 -->
|
||||
<div
|
||||
:class="[
|
||||
'layer-item',
|
||||
{
|
||||
'child-layer': isChild,
|
||||
active: isActive,
|
||||
selected: isSelected,
|
||||
'group-layer': isGroupLayerType,
|
||||
editing: isEditing,
|
||||
'multi-select-mode': isMultiSelectMode,
|
||||
invisible: !layer.visible,
|
||||
locked: layer.locked,
|
||||
'fixed-layer': layer.isBackground || layer.isFixed,
|
||||
},
|
||||
]"
|
||||
@click="handleClick"
|
||||
@dblclick="handleDoubleClick"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
>
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="layer-drag-handle" :title="$t('拖拽排序')">
|
||||
<SvgIcon
|
||||
v-if="!isHidenDragHandle"
|
||||
:name="isChild ? 'CSort' : 'CSort'"
|
||||
:size="16"
|
||||
></SvgIcon>
|
||||
</div>
|
||||
|
||||
<!-- 图层头部 -->
|
||||
<div class="layer-header">
|
||||
<!-- 多选复选框 -->
|
||||
<div
|
||||
v-if="isMultiSelectMode && !isChild"
|
||||
class="layer-checkbox"
|
||||
@click.stop
|
||||
>
|
||||
<Checkbox :checked="isSelected" @change="handleCheckboxChange" />
|
||||
</div>
|
||||
|
||||
<!-- 图层预览图标 -->
|
||||
<div class="layer-review">
|
||||
<img
|
||||
v-if="thumbnailUrl"
|
||||
:src="thumbnailUrl"
|
||||
class="layer-thumbnail"
|
||||
:alt="$t('图层预览')"
|
||||
/>
|
||||
<span v-else class="layer-type-icon">{{
|
||||
getLayerTypeIcon(layer)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 图层名称 -->
|
||||
<div class="layer-name-container" :title="layer.name">
|
||||
<div class="layer-name-wrapper">
|
||||
<span v-if="!isEditing" class="layer-name text-ellipsis">
|
||||
{{ layer.name }}
|
||||
</span>
|
||||
<input
|
||||
v-else
|
||||
:value="editingName"
|
||||
:data-layer-id="layer.id"
|
||||
:data-child-layer-id="isChild ? layer.id : undefined"
|
||||
class="layer-name-input"
|
||||
@blur="handleEditConfirm"
|
||||
@keydown="handleEditKeydown"
|
||||
@click.stop
|
||||
@input="$emit('update:editingName', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图层操作按钮 -->
|
||||
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
|
||||
<!-- 可见性切换 -->
|
||||
<div
|
||||
class="visibility-btn"
|
||||
@click.stop="handleToggleVisibility"
|
||||
:title="$t('显示/隐藏图层')"
|
||||
>
|
||||
<SvgIcon v-if="layer.visible" name="CEye" :size="16"></SvgIcon>
|
||||
<SvgIcon v-else name="CUnEye" :size="16"></SvgIcon>
|
||||
</div>
|
||||
|
||||
<!-- 锁定状态 -->
|
||||
<span
|
||||
v-if="layer.locked"
|
||||
class="status-icon locked"
|
||||
:class="{ disabled: layer.isBackground || layer.isFixed }"
|
||||
:title="$t('锁定')"
|
||||
@click.stop="handleToggleLock"
|
||||
>
|
||||
<SvgIcon name="CLock" :size="18"></SvgIcon>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="status-icon"
|
||||
:title="$t('未锁定')"
|
||||
@click.stop="handleToggleLock"
|
||||
>
|
||||
<SvgIcon name="CUnLock" :size="18"></SvgIcon>
|
||||
</span>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<div
|
||||
class="delete-btn"
|
||||
:class="{ disabled: !canDelete }"
|
||||
:title="$t('删除图层')"
|
||||
@click.stop="handleDelete"
|
||||
>
|
||||
<SvgIcon name="CDelete" size="14"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组图层展开/收起图标 -->
|
||||
<div
|
||||
v-if="isGroupLayerType && !isChild"
|
||||
class="group-expand-icon"
|
||||
@click.stop="toggleGroupExpanded"
|
||||
@dblclick.stop=""
|
||||
:title="isGroupExpanded ? $t('收起组') : $t('展开组')"
|
||||
>
|
||||
<SvgIcon
|
||||
name="CRight"
|
||||
:size="12"
|
||||
:style="{
|
||||
transform: isGroupExpanded ? 'rotate(45deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图层状态指示器 -->
|
||||
<!-- <div v-if="!isChild" class="layer-status">
|
||||
<span
|
||||
v-if="isGroupLayerType"
|
||||
class="status-icon group"
|
||||
:title="$t('组图层')"
|
||||
>
|
||||
📁
|
||||
</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import "./layersPanel.less";
|
||||
</style>
|
||||
@@ -0,0 +1,328 @@
|
||||
<script setup>
|
||||
import { computed, inject } from "vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import LayerItem from "./LayerItem.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "LayersList",
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
layers: Array,
|
||||
activeLayerId: String,
|
||||
sortableRootLayers: Array,
|
||||
selectedLayerIds: Array,
|
||||
isMultiSelectMode: Boolean,
|
||||
editingLayerId: String,
|
||||
editingLayerName: String,
|
||||
thumbnailManager: Object,
|
||||
groupName: String,
|
||||
expandedGroupIds: Set, // 新增:展开状态集合
|
||||
isChild: Boolean,
|
||||
parentLayerId: String, // 新增:父图层ID
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"layer-click",
|
||||
"layer-double-click",
|
||||
"context-menu",
|
||||
"checkbox-change",
|
||||
"toggle-visibility",
|
||||
"toggle-lock",
|
||||
"delete",
|
||||
"edit-confirm",
|
||||
"edit-cancel",
|
||||
"edit-keydown",
|
||||
"touch-start",
|
||||
"touch-move",
|
||||
"touch-end",
|
||||
"update:editing-name",
|
||||
"root-layers-sort",
|
||||
"child-layers-sort",
|
||||
"select-child-layer",
|
||||
"start-child-layer-edit",
|
||||
"child-context-menu",
|
||||
"finish-child-layer-edit",
|
||||
"cancel-child-layer-edit",
|
||||
"child-layer-edit-keydown",
|
||||
"toggle-group-expanded",
|
||||
// 新增子图层专用事件
|
||||
"toggle-child-visibility",
|
||||
"toggle-child-lock",
|
||||
"delete-child",
|
||||
"rename-child",
|
||||
]);
|
||||
|
||||
// 检查图层是否被选中
|
||||
const isLayerSelected = (layerId) => {
|
||||
return props.selectedLayerIds.includes(layerId);
|
||||
};
|
||||
|
||||
// 获取图层缩略图URL
|
||||
function getLayerThumbnail(layerId) {
|
||||
if (props.thumbnailManager) {
|
||||
return props.thumbnailManager.getLayerThumbnail(layerId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 事件转发方法
|
||||
const forwardEvent = (eventName, ...args) => {
|
||||
emit(eventName, ...args);
|
||||
};
|
||||
|
||||
// 处理根级图层拖拽排序
|
||||
const handleRootLayersSort = (event) => {
|
||||
if (props.isChild) {
|
||||
// 子图层事件处理
|
||||
// 确保排序只影响当前组图层的children,而不是全局layers
|
||||
emit(
|
||||
"child-layers-sort",
|
||||
event,
|
||||
props.sortableRootLayers,
|
||||
props.parentLayerId
|
||||
);
|
||||
} else {
|
||||
emit("root-layers-sort", event);
|
||||
}
|
||||
};
|
||||
|
||||
const canDeleteComputed = computed(() => {
|
||||
// 如果是子图层,检查父图层是否可以删除
|
||||
if (props.isChild) {
|
||||
const parentLayer = props.layers.find(
|
||||
(layer) => layer.id === props.parentLayerId
|
||||
);
|
||||
return parentLayer?.children?.length > 1;
|
||||
}
|
||||
// 否则直接返回根图层的可删除状态
|
||||
return props.layers.length > 3;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layers-list">
|
||||
<!-- 可排序的根级图层 -->
|
||||
<VueDraggable
|
||||
:model-value="sortableRootLayers"
|
||||
@end="handleRootLayersSort"
|
||||
class="sortable-layers"
|
||||
:animation="200"
|
||||
:disabled="false"
|
||||
handle=".layer-drag-handle"
|
||||
ghost-class="ghost"
|
||||
chosen-class="chosen"
|
||||
drag-class="drag"
|
||||
:group="groupName"
|
||||
>
|
||||
<!-- 遍历可排序的根级图层 -->
|
||||
<template v-for="(layer, index) in sortableRootLayers" :key="layer.id">
|
||||
<div class="layer-group">
|
||||
<!-- 使用 LayerItem 子组件 -->
|
||||
<LayerItem
|
||||
:layer="layer"
|
||||
:is-child="isChild"
|
||||
:is-active="layer.id === activeLayerId"
|
||||
:is-selected="isLayerSelected(layer.id)"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:is-editing="editingLayerId === layer.id"
|
||||
:editing-name="editingLayerName"
|
||||
:can-delete="
|
||||
canDeleteComputed &&
|
||||
!layer.isBackground &&
|
||||
!layer.isFixed &&
|
||||
!layer.locked
|
||||
"
|
||||
:thumbnail-url="getLayerThumbnail(layer.id)"
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
@click="(...args) => forwardEvent('layer-click', ...args)"
|
||||
@double-click="
|
||||
(...args) => forwardEvent('layer-double-click', ...args)
|
||||
"
|
||||
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
|
||||
@checkbox-change="
|
||||
(...args) => forwardEvent('checkbox-change', ...args)
|
||||
"
|
||||
@toggle-visibility="
|
||||
(...args) => forwardEvent('toggle-visibility', ...args)
|
||||
"
|
||||
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
|
||||
@delete="(...args) => forwardEvent('delete', ...args)"
|
||||
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
|
||||
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
|
||||
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
|
||||
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
|
||||
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
|
||||
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
|
||||
@update:editing-name="
|
||||
(...args) => forwardEvent('update:editing-name', ...args)
|
||||
"
|
||||
@toggle-group-expanded="
|
||||
(...args) => forwardEvent('toggle-group-expanded', ...args)
|
||||
"
|
||||
@toggle-child-visibility="
|
||||
(...args) => forwardEvent('toggle-child-visibility', ...args)
|
||||
"
|
||||
@toggle-child-lock="
|
||||
(...args) => forwardEvent('toggle-child-lock', ...args)
|
||||
"
|
||||
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
|
||||
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
|
||||
/>
|
||||
|
||||
<!-- 子图层列表 (递归渲染) -->
|
||||
<div
|
||||
v-if="
|
||||
layer?.children?.length > 0 &&
|
||||
!layer.isBackground &&
|
||||
!layer.isFixed &&
|
||||
expandedGroupIds?.has(layer.id)
|
||||
"
|
||||
class="child-layers"
|
||||
>
|
||||
<LayersList
|
||||
:layers="layers"
|
||||
:sortableRootLayers="layer.children"
|
||||
:active-layer-id="activeLayerId"
|
||||
:selected-layer-ids="selectedLayerIds"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:editing-layer-id="editingLayerId"
|
||||
:editing-layer-name="editingLayerName"
|
||||
:thumbnail-manager="thumbnailManager"
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
:isChild="true"
|
||||
:parentLayerId="layer.id"
|
||||
group-name="layers-child"
|
||||
@layer-click="(...args) => forwardEvent('layer-click', ...args)"
|
||||
@layer-double-click="
|
||||
(...args) => forwardEvent('layer-double-click', ...args)
|
||||
"
|
||||
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
|
||||
@checkbox-change="
|
||||
(...args) => forwardEvent('checkbox-change', ...args)
|
||||
"
|
||||
@toggle-visibility="
|
||||
(...args) => forwardEvent('toggle-visibility', ...args)
|
||||
"
|
||||
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
|
||||
@delete="(...args) => forwardEvent('delete', ...args)"
|
||||
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
|
||||
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
|
||||
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
|
||||
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
|
||||
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
|
||||
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
|
||||
@update:editing-name="
|
||||
(...args) => forwardEvent('update:editing-name', ...args)
|
||||
"
|
||||
@root-layers-sort="
|
||||
(...args) => forwardEvent('root-layers-sort', ...args)
|
||||
"
|
||||
@child-layers-sort="
|
||||
(...args) => forwardEvent('child-layers-sort', ...args)
|
||||
"
|
||||
@select-child-layer="
|
||||
(...args) => forwardEvent('select-child-layer', ...args)
|
||||
"
|
||||
@start-child-layer-edit="
|
||||
(...args) => forwardEvent('start-child-layer-edit', ...args)
|
||||
"
|
||||
@child-context-menu="
|
||||
(...args) => forwardEvent('child-context-menu', ...args)
|
||||
"
|
||||
@toggle-child-visibility="
|
||||
(...args) => forwardEvent('toggle-child-visibility', ...args)
|
||||
"
|
||||
@toggle-child-lock="
|
||||
(...args) => forwardEvent('toggle-child-lock', ...args)
|
||||
"
|
||||
@finish-child-layer-edit="
|
||||
(...args) => forwardEvent('finish-child-layer-edit', ...args)
|
||||
"
|
||||
@cancel-child-layer-edit="
|
||||
(...args) => forwardEvent('cancel-child-layer-edit', ...args)
|
||||
"
|
||||
@child-layer-edit-keydown="
|
||||
(...args) => forwardEvent('child-layer-edit-keydown', ...args)
|
||||
"
|
||||
@toggle-group-expanded="
|
||||
(...args) => forwardEvent('toggle-group-expanded', ...args)
|
||||
"
|
||||
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
|
||||
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 从父组件的样式文件中继承相关样式
|
||||
.layers-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.sortable-layers {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
// .layer-group {
|
||||
// // margin-bottom: 1px;
|
||||
// }
|
||||
|
||||
.child-layers {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
background-color: #e0e0e0;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-layers {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
// 拖拽状态样式
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.drag {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.layers-list {
|
||||
.child-layers {
|
||||
padding-left: 25px;
|
||||
&::after {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,232 @@
|
||||
// 右键菜单样式
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 1000;
|
||||
max-width: 280px;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
// overflow: hidden;
|
||||
top: 60px; // 默认位置,可根据实际需要调整
|
||||
// 动画相关
|
||||
&.context-menu-enter-active,
|
||||
&.context-menu-leave-active {
|
||||
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.context-menu-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
&.context-menu-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
white-space: nowrap;
|
||||
min-height: 32px;
|
||||
position: relative;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: not-allowed;
|
||||
|
||||
.context-menu-icon {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: #fff2f0;
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
.context-menu-icon {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-children {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.hovered {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.context-menu-label {
|
||||
flex: 1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.context-menu-shortcut {
|
||||
margin-left: 16px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.context-menu-arrow {
|
||||
margin-left: 8px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
margin: 4px 0;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
// 子菜单样式
|
||||
.context-submenu {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 1001;
|
||||
min-width: 160px;
|
||||
max-width: 280px;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
// overflow: hidden;
|
||||
|
||||
&.submenu-left {
|
||||
left: auto;
|
||||
right: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 子菜单动画
|
||||
.context-submenu-enter-active,
|
||||
.context-submenu-leave-active {
|
||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.context-submenu-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-8px);
|
||||
}
|
||||
|
||||
.context-submenu-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-4px);
|
||||
}
|
||||
|
||||
// 响应式优化
|
||||
@media (max-width: 768px) {
|
||||
.context-menu {
|
||||
min-width: 140px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
min-height: 36px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.context-menu-shortcut {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.context-submenu {
|
||||
min-width: 140px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题支持
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.context-menu {
|
||||
background-color: #1f1f1f;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.32),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&.hovered {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-icon {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.context-menu-shortcut {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.context-menu-arrow {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.context-submenu {
|
||||
background-color: #1f1f1f;
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.32),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
// 文本省略样式
|
||||
.text-ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 主容器样式
|
||||
.layers-panel-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
z-index: 6;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
max-height: 85vh;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
// 头部样式
|
||||
.layers-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-actions-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
.normal-actions,
|
||||
.multi-select-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮样式
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: none;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #40a9ff;
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background-color: #f5f5f5;
|
||||
color: #bfbfbf;
|
||||
border-color: #e6e6e6;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #bfbfbf;
|
||||
border-color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊按钮样式
|
||||
.group-btn {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #91d5ff;
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
background-color: #bae7ff;
|
||||
border-color: #69c0ff;
|
||||
}
|
||||
|
||||
&.disabled{
|
||||
background-color: #f0f5ff;
|
||||
border-color: #d9ecff;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.ungroup-btn {
|
||||
background-color: #fff2e8;
|
||||
border-color: #ffbb96;
|
||||
color: #fa8c16;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffd8bf;
|
||||
border-color: #ff9c6e;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
&.disabled{
|
||||
background-color: #f0f5ff;
|
||||
border-color: #d9ecff;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-selected-btn {
|
||||
background-color: #fff2f0;
|
||||
border-color: #ffccc7;
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffebe6;
|
||||
border-color: #ff7875;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-selection-btn {
|
||||
background-color: #f6f6f6;
|
||||
border-color: #d9d9d9;
|
||||
color: #595959;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6e6e6;
|
||||
border-color: #bfbfbf;
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
// 多选信息提示
|
||||
.multi-select-info {
|
||||
padding: 10px 6px;
|
||||
// background-color: #e6f7ff;
|
||||
background-color: rgba(238, 238, 238,0.4);
|
||||
border-bottom: 1px solid #91d5ff;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
// 图层列表
|
||||
.layers-list {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// 图层项样式
|
||||
.layer-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid #f5f2f2;
|
||||
padding-left: 30px;
|
||||
padding-right: 10px;
|
||||
|
||||
&.group-layer {
|
||||
background-color: rgba(240, 248, 255, 0.3);
|
||||
border-color: #e6f7ff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #91d5ff;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #bae7ff;
|
||||
border-color: #91d5ff;
|
||||
// box-shadow: 0 0 0 1px #1890ff;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
background-color: #fff7e6;
|
||||
border-color: #ffd666;
|
||||
}
|
||||
|
||||
// &.multi-select-mode {
|
||||
// // padding-left: 30px;
|
||||
// }
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图层头部
|
||||
.layer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
// 图层预览
|
||||
.layer-review {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
|
||||
10px 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.layer-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.layer-type-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 可见性按钮
|
||||
.visibility-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.hidden {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 图层名称
|
||||
.layer-name-container {
|
||||
flex: 1;
|
||||
margin: 0 6px;
|
||||
overflow: hidden;
|
||||
// max-width: 204px;
|
||||
.layer-name-wrapper{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layer-name-input {
|
||||
width: 100%;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid #1890ff;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// 图层状态
|
||||
.layer-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 12px;
|
||||
|
||||
&.locked {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&.group {
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 图层操作
|
||||
.layer-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.disabled{
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
// pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽手柄
|
||||
.layer-drag-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
cursor: move;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
margin-right: 4px;
|
||||
background: #eee;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 复选框
|
||||
.layer-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 0;
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// input[type="checkbox"] {
|
||||
// width: 16px;
|
||||
// height: 16px;
|
||||
// cursor: pointer;
|
||||
// accent-color: #1890ff;
|
||||
// }
|
||||
}
|
||||
|
||||
// 子图层样式
|
||||
.child-layers {
|
||||
}
|
||||
|
||||
.child-layer {
|
||||
padding: 8px 20px 8px 32px;
|
||||
background-color: rgba(240, 240, 240, 0.3);
|
||||
border-left: 2px solid #e0e0e0;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(224, 224, 224, 0.5);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(230, 247, 255, 0.5);
|
||||
border-left: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
&.editing {
|
||||
background-color: rgba(255, 247, 230, 0.5);
|
||||
border-left: 2px solid #ffd666;
|
||||
}
|
||||
|
||||
.layer-actions {
|
||||
position: static;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layer-indent {
|
||||
width: 20px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.layer-info {
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
|
||||
.layer-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.layer-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.layer-type {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.child-drag-handle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: move;
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
margin-right: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 固定图层样式
|
||||
.fixed-layers {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
// background-color: #fafafa;
|
||||
|
||||
background-color: rgba(238, 238, 238,0.4);
|
||||
.layer-drag-handle{
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-layer {
|
||||
background-color: #fafafa;
|
||||
// border-left: 3px solid #1890ff;
|
||||
|
||||
// &:hover {
|
||||
// background-color: #e6f7ff;
|
||||
// }
|
||||
}
|
||||
|
||||
.fixed-layer-indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1890ff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.background-indicator,
|
||||
.fixed-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.background-icon,
|
||||
.fixed-icon {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 拖拽样式
|
||||
.sortable-layers {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background-color: #f0f0f0;
|
||||
border: 2px dashed #1890ff;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
|
||||
.drag {
|
||||
opacity: 0.8;
|
||||
transform: rotate(5deg);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
// 子图层拖拽样式
|
||||
.child-layers {
|
||||
.ghost {
|
||||
opacity: 0.4;
|
||||
background-color: #fff7e6;
|
||||
border: 2px dashed #faad14;
|
||||
}
|
||||
|
||||
.chosen {
|
||||
background-color: #fff7e6;
|
||||
border: 1px solid #faad14;
|
||||
}
|
||||
|
||||
.drag {
|
||||
opacity: 0.7;
|
||||
transform: rotate(3deg);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.layers-panel-inner {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
padding: 12px;
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.layer-drag-handle,
|
||||
.visibility-btn {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.layer-review {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.multi-select-info {
|
||||
// padding: 12px;
|
||||
|
||||
small {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
// .layer-name-container {
|
||||
// // max-width: 182px;
|
||||
// }
|
||||
}
|
||||
|
||||
// iPad 优化
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.layer-item {
|
||||
padding: 10px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.layer-drag-handle:hover,
|
||||
.visibility-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸设备优化
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.layer-item {
|
||||
padding-left: 30px;
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border-color: #d9d9d9;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组图层展开/收起图标样式
|
||||
.group-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 4px;
|
||||
|
||||
// &:hover {
|
||||
// background-color: rgba(0, 0, 0, 0.1);
|
||||
// }
|
||||
|
||||
// 展开/收起图标的过渡动画
|
||||
.svg-icon {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 组图层样式
|
||||
.group-layer {
|
||||
.layer-type-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层缩进和连接线
|
||||
.child-layers {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-indent {
|
||||
width: 16px;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层展开/收起动画样式
|
||||
.child-layers-expand-enter-active,
|
||||
.child-layers-expand-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.child-layers-expand-enter-from {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.child-layers-expand-leave-to {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// 展开图标旋转动画优化
|
||||
.group-expand-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
margin-right: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
// 展开/收起图标的过渡动画
|
||||
.svg-icon {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层展开时的额外样式
|
||||
.child-layers {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, rgba(240, 248, 255, 0.1) 0%, rgba(240, 248, 255, 0.05) 100%);
|
||||
// border-radius: 4px;
|
||||
// margin-top: 2px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: linear-gradient(to bottom, #e0e0e0 0%, rgba(224, 224, 224, 0.3) 100%);
|
||||
}
|
||||
|
||||
// 子图层项动画
|
||||
.layer-item {
|
||||
animation: slideInRight 0.2s ease-out;
|
||||
animation-fill-mode: both;
|
||||
|
||||
&:nth-child(1) { animation-delay: 0.05s; }
|
||||
&:nth-child(2) { animation-delay: 0.1s; }
|
||||
&:nth-child(3) { animation-delay: 0.15s; }
|
||||
&:nth-child(4) { animation-delay: 0.2s; }
|
||||
&:nth-child(5) { animation-delay: 0.25s; }
|
||||
}
|
||||
}
|
||||
|
||||
// 子图层项进入动画
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端动画优化
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.child-layers-expand-enter-active,
|
||||
.child-layers-expand-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.group-expand-icon {
|
||||
&:hover {
|
||||
transform: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.child-layers .layer-item {
|
||||
animation-duration: 0.15s;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: String,
|
||||
@@ -156,7 +155,7 @@ const redGreenToolsList = ref([
|
||||
{
|
||||
id: OperationType.RED_BRUSH,
|
||||
title: "Red Brush (R)",
|
||||
action: () => selectTool(OperationType.RED_BRUSH),
|
||||
action: () => selectTool(OperationType.RED_BRUSH, true),
|
||||
icon: { name: "CBrush", size: "24" },
|
||||
class: "red-brush-btn",
|
||||
style: { color: "#FF0000" },
|
||||
@@ -164,7 +163,7 @@ const redGreenToolsList = ref([
|
||||
{
|
||||
id: OperationType.GREEN_BRUSH,
|
||||
title: "Green Brush (G)",
|
||||
action: () => selectTool(OperationType.GREEN_BRUSH),
|
||||
action: () => selectTool(OperationType.GREEN_BRUSH, true),
|
||||
icon: { name: "CBrush", size: "24" },
|
||||
class: "green-brush-btn",
|
||||
style: { color: "#00AA00" },
|
||||
@@ -172,7 +171,7 @@ const redGreenToolsList = ref([
|
||||
{
|
||||
id: OperationType.ERASER,
|
||||
title: "Eraser (E)",
|
||||
action: () => selectTool(OperationType.ERASER),
|
||||
action: () => selectTool(OperationType.ERASER, true),
|
||||
icon: { name: "CEraser", size: "22" },
|
||||
class: "eraser-btn",
|
||||
},
|
||||
@@ -197,8 +196,8 @@ const toolsList = computed(() => {
|
||||
return props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
|
||||
});
|
||||
|
||||
function selectTool(tool) {
|
||||
emit("tool-selected", tool);
|
||||
function selectTool(tool, isRedGreenMode = false) {
|
||||
emit("tool-selected", tool, isRedGreenMode);
|
||||
}
|
||||
|
||||
function triggerImageUpload() {
|
||||
@@ -261,15 +260,15 @@ function handleKeyDown(event) {
|
||||
|
||||
switch (key) {
|
||||
case "R":
|
||||
selectTool(OperationType.RED_BRUSH);
|
||||
selectTool(OperationType.RED_BRUSH, true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "G":
|
||||
selectTool(OperationType.GREEN_BRUSH);
|
||||
selectTool(OperationType.GREEN_BRUSH, true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "E":
|
||||
selectTool(OperationType.ERASER);
|
||||
selectTool(OperationType.ERASER, true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
@@ -323,6 +322,8 @@ onUnmounted(() => {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
background-color: #ffffff;
|
||||
user-select: none;
|
||||
min-width: 58px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
defineAsyncComponent,
|
||||
shallowRef,
|
||||
provide,
|
||||
defineExpose,
|
||||
} from "vue";
|
||||
import { CanvasManager } from "./managers/CanvasManager";
|
||||
import { LayerManager } from "./managers/LayerManager";
|
||||
@@ -21,14 +22,14 @@ import { RedGreenModeManager } from "./managers/RedGreenModeManager";
|
||||
// 导入封装的组件
|
||||
import ToolsSidebar from "./components/ToolsSidebar.vue";
|
||||
import HeaderMenu from "./components/HeaderMenu.vue";
|
||||
import LayersPanel from "./components/LayersPanel.vue";
|
||||
import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
|
||||
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
||||
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
||||
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
||||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||||
import { OperationType } from "./utils/layerHelper.js";
|
||||
import { ToolManager } from "./managers/ToolManager.js";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { uploadImageAndCreateLayer } from "./utils/imageHelper.js";
|
||||
// import MinimapPanel from "./components/MinimapPanel.vue";
|
||||
const KeyboardShortcutHelp = defineAsyncComponent(() =>
|
||||
@@ -70,13 +71,17 @@ const currentZoom = ref(100);
|
||||
const canvasWidth = ref(CanvasConfig.width);
|
||||
const canvasHeight = ref(CanvasConfig.height);
|
||||
const canvasColor = ref(CanvasConfig.backgroundColor);
|
||||
const layerWidth = ref(CanvasConfig.layerWidth); // 假设侧边栏宽度为 250px
|
||||
const layerWidth = ref(CanvasConfig.layerWidth);
|
||||
const brushSize = ref(CanvasConfig.brushSize); // 画笔大小
|
||||
const canvasManagerLoaded = ref(false); // 画布是否加载完成
|
||||
|
||||
// 红绿图模式状态
|
||||
const isRedGreenMode = ref(false);
|
||||
|
||||
const isShowLayerPanel = ref(true); // 是否显示图层面板
|
||||
|
||||
provide("isShowLayerPanel", isShowLayerPanel); // 提供红绿图模式状态给子组件
|
||||
|
||||
// 小地图设置
|
||||
// const minimapEnabled = ref(true);
|
||||
// const minimapManager = ref(null);
|
||||
@@ -110,7 +115,9 @@ function toggleShortcutHelp() {
|
||||
function handleToolSelect(tool) {
|
||||
activeTool.value = tool;
|
||||
// toolManager.setActiveTool(tool); // 更新工具管理器中的当前工具 普通模式,不可撤回操作
|
||||
toolManager.setToolWithCommand(tool); // 命令模式 可撤回操作
|
||||
toolManager.setToolWithCommand(tool, {
|
||||
undoable: props.enabledRedGreenMode ? false : true, // 普通模式下工具选择不可撤销
|
||||
}); // 命令模式 可撤回操作
|
||||
}
|
||||
|
||||
function toggleMinimap(enabled) {
|
||||
@@ -127,9 +134,10 @@ onMounted(async () => {
|
||||
canvasHeight.value = canvasContainerRef.value.clientWidth;
|
||||
canvasWidth.value = canvasContainerRef.value.clientHeight;
|
||||
}
|
||||
|
||||
// 创建管理器实例
|
||||
canvasManager = new CanvasManager(canvasRef.value, {
|
||||
width: canvasContainerRef.value.clientWidth - layerWidth.value, // 初始化的时候需要减去侧边栏宽度
|
||||
width: canvasContainerRef.value.clientWidth,
|
||||
height: canvasContainerRef.value.clientHeight,
|
||||
// backgroundColor: canvasColor.value,
|
||||
currentZoom,
|
||||
@@ -139,7 +147,6 @@ onMounted(async () => {
|
||||
canvasColor,
|
||||
enabledRedGreenMode: props.enabledRedGreenMode,
|
||||
});
|
||||
console.log(canvasManager,canvasManager.thumbnailManager)
|
||||
canvasManager.canvas.activeLayerId = activeLayerId;
|
||||
canvasManager.canvas.activeElementId = activeElementId;
|
||||
|
||||
@@ -155,6 +162,7 @@ onMounted(async () => {
|
||||
canvasWidth: canvasWidth.value,
|
||||
canvasHeight: canvasHeight.value,
|
||||
backgroundColor: canvasColor.value,
|
||||
isRedGreenMode: props.enabledRedGreenMode,
|
||||
layers,
|
||||
activeLayerId,
|
||||
canvasManager, // 添加对 canvasManager 的引用
|
||||
@@ -202,6 +210,7 @@ onMounted(async () => {
|
||||
layerManager.setToolManager(toolManager); // 将工具管理器传递给图层管理器
|
||||
canvasManager.setToolManager(toolManager); // 将工具管理器传递给画布管理器
|
||||
canvasManager.setLayerManager(layerManager);
|
||||
canvasManager.setCommandManager(commandManager); // 将命令管理器传递给画布管理器
|
||||
|
||||
// 初始化快捷键管理器
|
||||
keyboardManager = new KeyboardManager({
|
||||
@@ -369,13 +378,18 @@ function updateCanvasSize() {
|
||||
const containerWidth = canvasContainerRef.value.clientWidth;
|
||||
const containerHeight = canvasContainerRef.value.clientHeight;
|
||||
|
||||
// 如果启用了红绿图模式,使用 layerManager 的缩放方法
|
||||
if (props.enabledRedGreenMode && layerManager) {
|
||||
layerManager.resizeCanvasWithScale(containerWidth, containerHeight);
|
||||
} else {
|
||||
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||||
canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||||
}
|
||||
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||||
canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||||
|
||||
// // 如果启用了红绿图模式,使用 layerManager 的缩放方法
|
||||
// if (props.enabledRedGreenMode && layerManager) {
|
||||
// layerManager.resizeCanvasWithScale(containerWidth, containerHeight, {
|
||||
// undoable: false, // 可撤销操作
|
||||
// });
|
||||
// } else {
|
||||
// // 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||||
// canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,15 +402,17 @@ function addLayer() {
|
||||
}
|
||||
|
||||
function setActiveLayer(layerId) {
|
||||
if (activeElementId.value && canvasManager && canvasManager.canvas) {
|
||||
if (layerId !== activeLayerId.value) {
|
||||
layerManager.setActiveLayer(layerId, {
|
||||
undoable: true, // 可撤销
|
||||
});
|
||||
|
||||
const activeObject = canvasManager.canvas.getActiveObject();
|
||||
if (activeObject) {
|
||||
canvasManager.canvas.discardActiveObject();
|
||||
canvasManager.canvas.renderAll();
|
||||
}
|
||||
activeElementId.value = null;
|
||||
}
|
||||
layerManager.setActiveLayer(layerId);
|
||||
}
|
||||
|
||||
function toggleLayerVisibility(layerId) {
|
||||
@@ -570,9 +586,9 @@ defineExpose({
|
||||
getCanvasManager: () => canvasManager, // 获取画布管理器实例
|
||||
canvasManagerLoaded,
|
||||
// 加载新数据到画布
|
||||
loadJSON: (json) => {
|
||||
loadJSON: (json, calllBack) => {
|
||||
try {
|
||||
if (json) canvasManager?.loadJSON?.(json);
|
||||
if (json) canvasManager?.loadJSON?.(json, calllBack);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("加载画布JSON失败:", error);
|
||||
@@ -611,42 +627,133 @@ defineExpose({
|
||||
expPicType,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 移动图层位置
|
||||
* @param {string} layerId 图层ID
|
||||
* @param {string} direction 移动方向,'up'或'down'
|
||||
* @returns {boolean} 是否移动成功
|
||||
*/
|
||||
moveLayer(layerId, direction) {
|
||||
if (!layerManager) return false;
|
||||
const result = layerManager.moveLayer(layerId, direction);
|
||||
|
||||
// 使用高级排序重建画布顺序
|
||||
if (result) {
|
||||
layerManager.forceRebuildCanvasOrder();
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 拖拽排序图层
|
||||
* @param {number} oldIndex 原索引
|
||||
* @param {number} newIndex 新索引
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
reorderLayers(oldIndex, newIndex, layerId) {
|
||||
if (!layerManager) return false;
|
||||
|
||||
// 优先使用高级排序功能
|
||||
if (layerManager.layerSort) {
|
||||
return layerManager.advancedReorderLayers(oldIndex, newIndex, layerId);
|
||||
} else {
|
||||
// 降级到基础排序
|
||||
return layerManager.reorderLayers(oldIndex, newIndex, layerId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 智能排序图层
|
||||
* 根据对象类型和位置自动调整图层顺序
|
||||
* @param {Array<string>} targetLayerIds 要排序的图层ID数组,null表示排序所有普通图层
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
smartSortLayers(targetLayerIds = null) {
|
||||
if (!layerManager) return false;
|
||||
return layerManager.smartSortLayers(targetLayerIds);
|
||||
},
|
||||
|
||||
/**
|
||||
* 优化图层结构
|
||||
* 清理空图层、重新排序等
|
||||
* @returns {Object} 优化结果统计
|
||||
*/
|
||||
optimizeLayerStructure() {
|
||||
if (!layerManager)
|
||||
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
|
||||
return layerManager.optimizeLayerStructure();
|
||||
},
|
||||
|
||||
/**
|
||||
* 强制重建画布对象顺序
|
||||
* 当图层顺序发生变化后调用此方法确保画布对象顺序正确
|
||||
*/
|
||||
forceRebuildCanvasOrder() {
|
||||
if (!layerManager) return;
|
||||
layerManager.forceRebuildCanvasOrder();
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证画布对象顺序是否正确
|
||||
* @returns {boolean} 顺序是否正确
|
||||
*/
|
||||
validateObjectOrder() {
|
||||
if (!layerManager) return true;
|
||||
return layerManager.validateObjectOrder();
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量重新排序多个图层
|
||||
* @param {Array} reorderOperations 排序操作数组 [{layerId, oldIndex, newIndex}]
|
||||
* @returns {boolean} 是否全部操作成功
|
||||
*/
|
||||
batchReorderLayers(reorderOperations) {
|
||||
if (!layerManager) return false;
|
||||
return layerManager.batchReorderLayers(reorderOperations);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 头部菜单组件 -->
|
||||
<HeaderMenu
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:canvasWidth="canvasWidth"
|
||||
:canvasHeight="canvasHeight"
|
||||
:canvasColor="canvasColor"
|
||||
:brushSize="brushSize"
|
||||
@update:canvasWidth="canvasWidth = $event"
|
||||
@update:canvasHeight="canvasHeight = $event"
|
||||
@update:canvasColor="canvasColor = $event"
|
||||
@update:brushSize="brushSize = $event"
|
||||
@canvas-size-change="updateCanvasSize"
|
||||
@canvas-color-change="updateCanvasColor"
|
||||
/>
|
||||
<div class="header-menu">
|
||||
<HeaderMenu
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:canvasWidth="canvasWidth"
|
||||
:canvasHeight="canvasHeight"
|
||||
:canvasColor="canvasColor"
|
||||
:brushSize="brushSize"
|
||||
:enabledRedGreenMode="enabledRedGreenMode"
|
||||
@update:canvasWidth="canvasWidth = $event"
|
||||
@update:canvasHeight="canvasHeight = $event"
|
||||
@update:canvasColor="canvasColor = $event"
|
||||
@update:brushSize="brushSize = $event"
|
||||
@canvas-size-change="updateCanvasSize"
|
||||
@canvas-color-change="updateCanvasColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- :minimapEnabled="minimapEnabled" -->
|
||||
<!-- 工具栏组件 -->
|
||||
<ToolsSidebar
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:isRedGreenMode="isRedGreenMode"
|
||||
@tool-selected="handleToolSelect"
|
||||
@red-green-tool-selected="handleRedGreenToolSelect"
|
||||
@toggle-red-green-mode="toggleRedGreenMode"
|
||||
@trigger-image-upload="triggerImageUpload"
|
||||
@add-text="handleAddText"
|
||||
@zoom-in="zoomIn"
|
||||
@zoom-out="zoomOut"
|
||||
/>
|
||||
<div style="min-width: 58px">
|
||||
<ToolsSidebar
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:isRedGreenMode="isRedGreenMode"
|
||||
@tool-selected="handleToolSelect"
|
||||
@red-green-tool-selected="handleRedGreenToolSelect"
|
||||
@toggle-red-green-mode="toggleRedGreenMode"
|
||||
@trigger-image-upload="triggerImageUpload"
|
||||
@add-text="handleAddText"
|
||||
@zoom-in="zoomIn"
|
||||
@zoom-out="zoomOut"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="canvas-container"
|
||||
@@ -665,14 +772,14 @@ defineExpose({
|
||||
|
||||
<!-- 文本编辑面板 -->
|
||||
<TextEditorPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager?.canvas"
|
||||
:commandManager="commandManager"
|
||||
/>
|
||||
|
||||
<!-- 液化编辑面板 -->
|
||||
<LiquifyPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager?.canvas"
|
||||
:commandManager="commandManager"
|
||||
:liquifyManager="liquifyManager"
|
||||
@@ -682,7 +789,7 @@ defineExpose({
|
||||
|
||||
<!-- 选区面板 -->
|
||||
<SelectionPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager?.canvas"
|
||||
:commandManager="commandManager"
|
||||
:selectionManager="selectionManager"
|
||||
@@ -705,22 +812,29 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
<!-- 图层面板组件 -->
|
||||
<LayersPanel
|
||||
class="layers-panel"
|
||||
:style="{ width: layerWidth + 'px' }"
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:activeLayerId="activeLayerId"
|
||||
:activeElementId="activeElementId"
|
||||
:thumbnailManager="canvasManager?.thumbnailManager"
|
||||
@add-layer="addLayer"
|
||||
@set-active-layer="setActiveLayer"
|
||||
@toggle-layer-visibility="toggleLayerVisibility"
|
||||
@move-layer-up="moveLayerUp"
|
||||
@move-layer-down="moveLayerDown"
|
||||
@remove-layer="removeLayer"
|
||||
@layers-reorder="handleLayersReorder"
|
||||
@child-layers-reorder="handleChildLayersReorder"
|
||||
/>
|
||||
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="layers-panel"
|
||||
v-if="isShowLayerPanel && !enabledRedGreenMode"
|
||||
>
|
||||
<LayersPanel
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeLayerId="activeLayerId"
|
||||
:activeElementId="activeElementId"
|
||||
:thumbnailManager="canvasManager.thumbnailManager"
|
||||
@add-layer="addLayer"
|
||||
@set-active-layer="setActiveLayer"
|
||||
@toggle-layer-visibility="toggleLayerVisibility"
|
||||
@move-layer-up="moveLayerUp"
|
||||
@move-layer-down="moveLayerDown"
|
||||
@remove-layer="removeLayer"
|
||||
@layers-reorder="handleLayersReorder"
|
||||
@child-layers-reorder="handleChildLayersReorder"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="footer-actions">
|
||||
@@ -769,6 +883,15 @@ defineExpose({
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 77;
|
||||
& > .header-menu {
|
||||
height: 52px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@@ -972,10 +1095,33 @@ button:hover {
|
||||
}
|
||||
|
||||
.layers-panel {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 10px;
|
||||
transition: width 0.3s ease;
|
||||
background: #fff;
|
||||
width: 250px;
|
||||
flex: none;
|
||||
width: 350px;
|
||||
max-height: 85vh;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
|
||||
backdrop-filter: blur(2px); /* 添加模糊效果 */
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||||
z-index: 1000; /* 确保面板在最上层 */
|
||||
border: 1px solid #e0e0e0;
|
||||
/* 添加指向整个面板的倒三角 */
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
right: 6px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-bottom: 10px solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
|
||||
filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.05));
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
/* 添加触控设备的样式调整 */
|
||||
@media (pointer: coarse) {
|
||||
@@ -1016,4 +1162,15 @@ button:hover {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import initAligningGuidelines, {
|
||||
initCenteringGuidelines,
|
||||
} from "../utils/helperLine";
|
||||
@@ -14,10 +14,15 @@ import { createCanvas } from "../utils/canvasFactory";
|
||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
import { RedGreenModeManager } from "./RedGreenModeManager";
|
||||
import { EraserStateManager } from "./EraserStateManager";
|
||||
import { deepClone, optimizeCanvasRendering } from "../utils/helper";
|
||||
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
||||
import { isFunction } from "lodash-es";
|
||||
import {
|
||||
ChangeFixedImageCommand,
|
||||
AddImageToLayerCommand,
|
||||
} from "../commands/ObjectLayerCommands";
|
||||
restoreObjectLayerAssociations,
|
||||
simplifyLayers,
|
||||
validateLayerAssociations,
|
||||
} from "../utils/layerUtils";
|
||||
|
||||
export class CanvasManager {
|
||||
constructor(canvasElement, options) {
|
||||
@@ -34,6 +39,7 @@ export class CanvasManager {
|
||||
this.canvasHeight = options.canvasHeight || this.height; // 画布高度
|
||||
this.canvasColor = options.canvasColor || "#ffffff"; // 画布背景颜色
|
||||
this.enabledRedGreenMode = options.enabledRedGreenMode || false; // 是否启用红绿图模式
|
||||
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
||||
// 初始化画布
|
||||
this.initializeCanvas();
|
||||
}
|
||||
@@ -65,14 +71,6 @@ export class CanvasManager {
|
||||
layers: this.layers,
|
||||
});
|
||||
|
||||
// 初始化红绿图模式管理器
|
||||
this.redGreenModeManager = new RedGreenModeManager({
|
||||
canvas: this.canvas,
|
||||
layerManager: null, // 稍后设置
|
||||
toolManager: null, // 稍后设置
|
||||
commandManager: null, // 稍后设置
|
||||
});
|
||||
|
||||
// 设置画布辅助线
|
||||
initAligningGuidelines(this.canvas);
|
||||
|
||||
@@ -90,7 +88,7 @@ export class CanvasManager {
|
||||
*/
|
||||
_initCanvasEvents() {
|
||||
// 添加笔刷图像转换处理回调
|
||||
this.canvas.onBrushImageConverted = (fabricImage) => {
|
||||
this.canvas.onBrushImageConverted = async (fabricImage) => {
|
||||
// 如果图层管理器存在,将图像合并到当前活动图层
|
||||
if (this.layerManager) {
|
||||
// 获取当前活动图层
|
||||
@@ -107,21 +105,49 @@ export class CanvasManager {
|
||||
});
|
||||
|
||||
// 执行高保真合并操作
|
||||
this.eventManager?.mergeLayerObjectsForPerformance?.({
|
||||
await this.eventManager?.mergeLayerObjectsForPerformance?.({
|
||||
fabricImage,
|
||||
activeLayer,
|
||||
});
|
||||
|
||||
// 返回false表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
||||
return false;
|
||||
// 返回true表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
||||
return true;
|
||||
} else {
|
||||
console.warn("没有活动图层,使用默认行为添加图像");
|
||||
}
|
||||
}
|
||||
|
||||
// 返回true表示使用默认行为(直接添加到画布)
|
||||
return true;
|
||||
// 返回false表示使用默认行为(直接添加到画布)
|
||||
return false;
|
||||
};
|
||||
|
||||
this.eraserStateManager = new EraserStateManager(
|
||||
this.canvas,
|
||||
this.layerManager
|
||||
);
|
||||
|
||||
// 监听擦除开始事件
|
||||
this.canvas.on("erasing:start", () => {
|
||||
console.log("开始擦除");
|
||||
this.eraserStateManager.startErasing();
|
||||
});
|
||||
|
||||
// 监听擦除结束事件
|
||||
this.canvas.on("erasing:end", async (e) => {
|
||||
console.log("擦除完成", e.targets);
|
||||
// 可以在这里保存状态到命令管理器
|
||||
const affectedObjects = e.targets || [];
|
||||
const command = this.eraserStateManager.endErasing(affectedObjects);
|
||||
if (command && this.commandManager) {
|
||||
await this.commandManager?.executeCommand?.(command);
|
||||
} else {
|
||||
await command?.execute?.(); // 如果没有命令管理器,直接执行命令
|
||||
}
|
||||
|
||||
// 更新交互性
|
||||
command &&
|
||||
(await this.layerManager?.updateLayersObjectsInteractivity?.());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +194,10 @@ export class CanvasManager {
|
||||
if (this.redGreenModeManager) {
|
||||
this.redGreenModeManager.layerManager = this.layerManager;
|
||||
}
|
||||
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager.setLayerManager(this.layerManager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,7 +350,8 @@ export class CanvasManager {
|
||||
|
||||
/**
|
||||
* 居中所有画布元素
|
||||
* 计算所有对象的边界框,然后将它们整体居中显示
|
||||
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
||||
* 这样可以保持对象间的相对位置关系不变
|
||||
*/
|
||||
centerAllObjects() {
|
||||
if (!this.canvas) return;
|
||||
@@ -333,45 +364,56 @@ export class CanvasManager {
|
||||
(obj) => obj.visible !== false && !obj.excludeFromExport
|
||||
);
|
||||
|
||||
// 如果只有背景层或没有可见对象,只居中背景层
|
||||
if (
|
||||
visibleObjects.length === 0 ||
|
||||
(visibleObjects.length === 1 && visibleObjects[0].isBackground)
|
||||
) {
|
||||
// 尝试居中背景层
|
||||
this.centerBackgroundLayer(this.width, this.height);
|
||||
return;
|
||||
}
|
||||
// 如果没有可见对象,直接返回
|
||||
if (visibleObjects.length === 0) return;
|
||||
|
||||
// 单独处理背景层
|
||||
// 获取背景对象
|
||||
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
||||
const contentObjects = backgroundObject
|
||||
? visibleObjects.filter((obj) => obj !== backgroundObject)
|
||||
: visibleObjects;
|
||||
|
||||
// 如果只有背景层,居中背景层
|
||||
if (contentObjects.length === 0 && backgroundObject) {
|
||||
this.centerBackgroundLayer(this.width, this.height);
|
||||
// 如果只有背景层或没有背景层,使用原有逻辑
|
||||
if (!backgroundObject) {
|
||||
console.warn("未找到背景层,使用默认居中逻辑");
|
||||
// 如果只有一个对象且可能是背景,直接居中
|
||||
if (visibleObjects.length === 1) {
|
||||
const obj = visibleObjects[0];
|
||||
obj.set({
|
||||
left: this.width / 2,
|
||||
top: this.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
obj.setCoords();
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算内容对象的边界
|
||||
const bounds = this._calculateObjectsBounds(contentObjects);
|
||||
|
||||
// 计算所有对象的中心点
|
||||
const objectsCenterX = bounds.left + bounds.width / 2;
|
||||
const objectsCenterY = bounds.top + bounds.height / 2;
|
||||
// 记录背景层居中前的位置
|
||||
const backgroundOldLeft = backgroundObject.left;
|
||||
const backgroundOldTop = backgroundObject.top;
|
||||
|
||||
// 计算画布中心点
|
||||
const canvasCenterX = this.width / 2;
|
||||
const canvasCenterY = this.height / 2;
|
||||
|
||||
// 计算需要移动的距离
|
||||
const deltaX = canvasCenterX - objectsCenterX;
|
||||
const deltaY = canvasCenterY - objectsCenterY;
|
||||
// 设置背景层居中
|
||||
backgroundObject.set({
|
||||
left: canvasCenterX,
|
||||
top: canvasCenterY,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
// 移动所有对象,包括背景层
|
||||
visibleObjects.forEach((obj) => {
|
||||
// 计算背景层的偏移量
|
||||
const deltaX = backgroundObject.left - backgroundOldLeft;
|
||||
const deltaY = backgroundObject.top - backgroundOldTop;
|
||||
|
||||
// 将相同的偏移量应用到所有其他对象上
|
||||
const otherObjects = visibleObjects.filter(
|
||||
(obj) => obj !== backgroundObject
|
||||
);
|
||||
|
||||
otherObjects.forEach((obj) => {
|
||||
obj.set({
|
||||
left: obj.left + deltaX,
|
||||
top: obj.top + deltaY,
|
||||
@@ -456,6 +498,7 @@ export class CanvasManager {
|
||||
// 如果需要裁剪背景层以外的内容,则更新蒙层位置
|
||||
// 创建或更新蒙层
|
||||
CanvasConfig.isCropBackground &&
|
||||
!this.enabledRedGreenMode &&
|
||||
this.createOrUpdateMask(backgroundLayerObject);
|
||||
return true;
|
||||
}
|
||||
@@ -480,12 +523,13 @@ export class CanvasManager {
|
||||
|
||||
// 创建蒙层 - 使用透明矩形作为裁剪区域
|
||||
this.maskLayer = new fabric.Rect({
|
||||
id: "canvasMaskLayer",
|
||||
width: bgWidth,
|
||||
height: bgHeight,
|
||||
left: left,
|
||||
top: top,
|
||||
fill: "transparent",
|
||||
stroke: "#cccccc",
|
||||
stroke: "transparent",
|
||||
strokeWidth: 1,
|
||||
strokeDashArray: [5, 5],
|
||||
selectable: false,
|
||||
@@ -504,20 +548,12 @@ export class CanvasManager {
|
||||
this.canvas.clipPath = new fabric.Rect({
|
||||
width: bgWidth,
|
||||
height: bgHeight,
|
||||
left: 0,
|
||||
top: 0,
|
||||
left: left,
|
||||
top: top,
|
||||
originX: backgroundLayerObject.originX || "left",
|
||||
originY: backgroundLayerObject.originY || "top",
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
// 设置蒙层位置
|
||||
this.canvas.clipPath.set({
|
||||
left: left,
|
||||
top: top,
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
getBackgroundLayer() {
|
||||
if (!this.canvas) return null;
|
||||
@@ -655,6 +691,39 @@ export class CanvasManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 更改固定图层的图片
|
||||
* @param {String} imageUrl 新的图片URL
|
||||
* @param {Object} options 选项
|
||||
* @param {String} options.targetLayerType 目标图层类型(background/fixed)
|
||||
* @param {Boolean} options.undoable 是否可撤销,默认不可撤销
|
||||
* @return {Object} 执行结果
|
||||
* */
|
||||
async changeFixedImage(imageUrl, options = {}) {
|
||||
if (!this.layerManager) {
|
||||
console.error("图层管理器未设置,无法更改固定图层图片");
|
||||
return;
|
||||
}
|
||||
const command = new ChangeFixedImageCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
imageUrl: imageUrl,
|
||||
targetLayerType: options.targetLayerType || "fixed", // background/fixed
|
||||
});
|
||||
|
||||
command.undoable =
|
||||
options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
|
||||
|
||||
return (
|
||||
(await command?.execute?.()) || {
|
||||
success: false,
|
||||
layerId: null,
|
||||
imageUrl: null,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出图片
|
||||
* @param {Object} options 导出选项
|
||||
@@ -663,6 +732,7 @@ export class CanvasManager {
|
||||
* @param {String} options.layerId 导出具体图层ID
|
||||
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @returns {String} 导出的图片数据URL
|
||||
*/
|
||||
exportImage(options = {}) {
|
||||
@@ -672,7 +742,39 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
try {
|
||||
return this.exportManager.exportImage(options);
|
||||
// 自动设置红绿图模式相关参数
|
||||
const enhancedOptions = {
|
||||
...options,
|
||||
// 如果没有明确指定,则根据当前模式自动设置
|
||||
restoreOpacityInRedGreen:
|
||||
options.restoreOpacityInRedGreen !== undefined
|
||||
? options.restoreOpacityInRedGreen
|
||||
: true, // 默认在红绿图模式下恢复透明度
|
||||
};
|
||||
|
||||
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
|
||||
if (
|
||||
this.enabledRedGreenMode &&
|
||||
!options.layerId &&
|
||||
(!options.layerIdArray || options.layerIdArray.length === 0)
|
||||
) {
|
||||
console.log("检测到红绿图模式,自动包含所有普通图层进行导出");
|
||||
|
||||
// 获取所有非背景、非固定的普通图层ID
|
||||
const normalLayerIds =
|
||||
this.layers?.value
|
||||
?.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed && layer.visible
|
||||
)
|
||||
?.map((layer) => layer.id) || [];
|
||||
|
||||
if (normalLayerIds.length > 0) {
|
||||
enhancedOptions.layerIdArray = normalLayerIds;
|
||||
console.log("红绿图模式导出图层:", normalLayerIds);
|
||||
}
|
||||
}
|
||||
|
||||
return this.exportManager.exportImage(enhancedOptions);
|
||||
} catch (error) {
|
||||
console.error("CanvasManager导出图片失败:", error);
|
||||
throw error;
|
||||
@@ -709,129 +811,129 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
// 简化图层数据,在loadJSON时要根据id恢复引用
|
||||
let tempLayers = this.layers ? this.layers.value : [];
|
||||
// // 简化图层数据,在loadJSON时要根据id恢复引用
|
||||
// let tempLayers = this.layers ? this.layers.value : [];
|
||||
// // 创建对象ID映射表,用于快速查找
|
||||
// tempLayers = tempLayers.map((layer) => {
|
||||
// const newLayer = { ...layer };
|
||||
|
||||
// 为所有fabric对象生成ID(如果没有的话)
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (!obj.id) {
|
||||
obj.id = `obj_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
||||
}
|
||||
});
|
||||
// // 处理fabricObjects数组
|
||||
// if (Array.isArray(layer.fabricObjects)) {
|
||||
// newLayer.fabricObjects = layer.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// 创建对象ID映射表,用于快速查找
|
||||
const objectIdMap = new Map();
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.id) {
|
||||
objectIdMap.set(obj, obj.id);
|
||||
}
|
||||
});
|
||||
// // 确保对象有ID
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
tempLayers = tempLayers.map((layer) => {
|
||||
const newLayer = { ...layer };
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object", // 保存类型信息用于调试
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newLayer.fabricObjects = [];
|
||||
// }
|
||||
|
||||
// 处理fabricObjects数组
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
newLayer.fabricObjects = layer.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
// if (layer.clippingMask) {
|
||||
// layer.clippingMask = {
|
||||
// id: layer.clippingMask.id,
|
||||
// };
|
||||
// }
|
||||
|
||||
// 确保对象有ID
|
||||
if (!item.id) {
|
||||
item.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
// // 处理单个fabricObject
|
||||
// if (layer.fabricObject) {
|
||||
// if (!layer.fabricObject.id) {
|
||||
// layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
// newLayer.fabricObject = {
|
||||
// id: layer.fabricObject.id,
|
||||
// type: layer.fabricObject.type || "object",
|
||||
// };
|
||||
// } else {
|
||||
// newLayer.fabricObject = null;
|
||||
// }
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type || "object", // 保存类型信息用于调试
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
} else {
|
||||
newLayer.fabricObjects = [];
|
||||
}
|
||||
// // 处理子图层
|
||||
// if (Array.isArray(layer.children)) {
|
||||
// newLayer.children = layer.children.map((cItem) => {
|
||||
// const newChild = { ...cItem };
|
||||
|
||||
// 处理单个fabricObject
|
||||
if (layer.fabricObject) {
|
||||
if (!layer.fabricObject.id) {
|
||||
layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
newLayer.fabricObject = {
|
||||
id: layer.fabricObject.id,
|
||||
type: layer.fabricObject.type || "object",
|
||||
};
|
||||
} else {
|
||||
newLayer.fabricObject = null;
|
||||
}
|
||||
// // 处理子图层的fabricObjects
|
||||
// if (Array.isArray(cItem.fabricObjects)) {
|
||||
// newChild.fabricObjects = cItem.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// 处理子图层
|
||||
if (Array.isArray(layer.children)) {
|
||||
newLayer.children = layer.children.map((cItem) => {
|
||||
const newChild = { ...cItem };
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
// 处理子图层的fabricObjects
|
||||
if (Array.isArray(cItem.fabricObjects)) {
|
||||
newChild.fabricObjects = cItem.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object",
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newChild.fabricObjects = [];
|
||||
// }
|
||||
|
||||
if (!item.id) {
|
||||
item.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
// // 处理子图层的fabricObject
|
||||
// if (cItem.fabricObject) {
|
||||
// if (!cItem.fabricObject.id) {
|
||||
// cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
// newChild.fabricObject = {
|
||||
// id: cItem.fabricObject.id,
|
||||
// type: cItem.fabricObject.type || "object",
|
||||
// };
|
||||
// } else {
|
||||
// newChild.fabricObject = null;
|
||||
// }
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type || "object",
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
} else {
|
||||
newChild.fabricObjects = [];
|
||||
}
|
||||
|
||||
// 处理子图层的fabricObject
|
||||
if (cItem.fabricObject) {
|
||||
if (!cItem.fabricObject.id) {
|
||||
cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
newChild.fabricObject = {
|
||||
id: cItem.fabricObject.id,
|
||||
type: cItem.fabricObject.type || "object",
|
||||
};
|
||||
} else {
|
||||
newChild.fabricObject = null;
|
||||
}
|
||||
|
||||
return newChild;
|
||||
});
|
||||
} else {
|
||||
newLayer.children = [];
|
||||
}
|
||||
|
||||
return newLayer;
|
||||
});
|
||||
// return newChild;
|
||||
// });
|
||||
// } else {
|
||||
// newLayer.children = [];
|
||||
// }
|
||||
|
||||
// return newLayer;
|
||||
// });
|
||||
try {
|
||||
console.log(
|
||||
"获取画布JSON数据...",
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
);
|
||||
return JSON.stringify({
|
||||
canvas: this.canvas.toJSON([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"isBackground",
|
||||
"isLocked",
|
||||
"isVisible",
|
||||
"isFixed",
|
||||
"parentId",
|
||||
"excludeFromExport",
|
||||
"eraser",
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]),
|
||||
layers: tempLayers,
|
||||
layers: JSON.stringify(
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
), // 简化图层数据
|
||||
version: "1.0", // 添加版本信息
|
||||
timestamp: new Date().toISOString(), // 添加时间戳
|
||||
canvasWidth: this.canvasWidth.value,
|
||||
@@ -845,7 +947,7 @@ export class CanvasManager {
|
||||
}
|
||||
}
|
||||
|
||||
loadJSON(json) {
|
||||
loadJSON(json, calllBack) {
|
||||
console.log("加载画布JSON数据:", json);
|
||||
|
||||
// 确保传入的json是字符串格式
|
||||
@@ -859,7 +961,7 @@ export class CanvasManager {
|
||||
const parsedJson = JSON.parse(json);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempLayers = parsedJson?.layers || [];
|
||||
const tempLayers = JSON.parse(parsedJson?.layers) || [];
|
||||
const canvasData = parsedJson?.canvas;
|
||||
|
||||
if (!tempLayers) {
|
||||
@@ -872,9 +974,11 @@ export class CanvasManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
||||
this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
||||
this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
||||
this.layers.value = tempLayers;
|
||||
|
||||
// this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
||||
// this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
||||
// this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
||||
|
||||
console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode);
|
||||
|
||||
@@ -885,107 +989,57 @@ export class CanvasManager {
|
||||
this.canvas.clear();
|
||||
|
||||
// 加载画布数据
|
||||
this.canvas.loadFromJSON(canvasData, () => {
|
||||
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
||||
try {
|
||||
// 重置画布数据
|
||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||
this.canvas.loadFromJSON(canvasData, async () => {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
||||
try {
|
||||
// 重置画布数据
|
||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||
|
||||
// 创建对象ID映射表,用于快速查找
|
||||
const objectIdMap = new Map();
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
// 重新构建对象关系
|
||||
restoreObjectLayerAssociations(
|
||||
this.layers.value,
|
||||
this.canvas.getObjects()
|
||||
);
|
||||
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.id) {
|
||||
objectIdMap.set(obj.id, obj);
|
||||
}
|
||||
});
|
||||
// 验证图层关联关系 - 稳定后可以注释
|
||||
const isValidate = validateLayerAssociations(
|
||||
this.layers.value,
|
||||
this.canvas.getObjects()
|
||||
);
|
||||
|
||||
// 辅助函数:根据ID查找对象
|
||||
const findObjectById = (id) => {
|
||||
if (!id) return null;
|
||||
return objectIdMap.get(id) || null;
|
||||
};
|
||||
console.log("图层关联验证结果:", isValidate);
|
||||
|
||||
// 恢复图层数据
|
||||
this.layers.value = tempLayers.map((layer) => {
|
||||
const restoredLayer = { ...layer };
|
||||
this.canvas.activeLayerId.value =
|
||||
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
|
||||
|
||||
// 恢复fabricObjects数组
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
restoredLayer.fabricObjects = layer.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item || !item.id) return null;
|
||||
return findObjectById(item.id);
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
} else {
|
||||
restoredLayer.fabricObjects = [];
|
||||
}
|
||||
// // 如果检测到红绿图模式内容,进行缩放调整
|
||||
// if (this.enabledRedGreenMode) {
|
||||
// this._rescaleRedGreenModeContent();
|
||||
// }
|
||||
|
||||
// 恢复单个fabricObject
|
||||
if (layer.fabricObject && layer.fabricObject.id) {
|
||||
restoredLayer.fabricObject = findObjectById(
|
||||
layer.fabricObject.id
|
||||
);
|
||||
} else {
|
||||
restoredLayer.fabricObject = null;
|
||||
}
|
||||
// 重载代码后支持回调中操作一些内容
|
||||
await calllBack?.();
|
||||
|
||||
// 恢复子图层
|
||||
if (Array.isArray(layer.children)) {
|
||||
restoredLayer.children = layer.children.map((cItem) => {
|
||||
const restoredChild = { ...cItem };
|
||||
// 确保所有对象的交互性正确设置
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.(
|
||||
false
|
||||
);
|
||||
console.log(this.layerManager.layers.value);
|
||||
debugger;
|
||||
|
||||
// 恢复子图层的fabricObjects
|
||||
if (Array.isArray(cItem.fabricObjects)) {
|
||||
restoredChild.fabricObjects = cItem.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item || !item.id) return null;
|
||||
return findObjectById(item.id);
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
} else {
|
||||
restoredChild.fabricObjects = [];
|
||||
}
|
||||
// 更新所有缩略图
|
||||
setTimeout(() => {
|
||||
this.updateAllThumbnails();
|
||||
}, 100);
|
||||
|
||||
// 恢复子图层的fabricObject
|
||||
if (cItem.fabricObject && cItem.fabricObject.id) {
|
||||
restoredChild.fabricObject = findObjectById(
|
||||
cItem.fabricObject.id
|
||||
);
|
||||
} else {
|
||||
restoredChild.fabricObject = null;
|
||||
}
|
||||
|
||||
return restoredChild;
|
||||
});
|
||||
} else {
|
||||
restoredLayer.children = [];
|
||||
}
|
||||
|
||||
return restoredLayer;
|
||||
});
|
||||
|
||||
this.canvas.activeLayerId.value =
|
||||
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
|
||||
|
||||
// 如果检测到红绿图模式内容,进行缩放调整
|
||||
if (this.enabledRedGreenMode) {
|
||||
this._rescaleRedGreenModeContent();
|
||||
console.log("画布JSON数据加载完成");
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("恢复图层数据失败:", error);
|
||||
reject(new Error("恢复图层数据失败: " + error.message));
|
||||
}
|
||||
|
||||
// 更新所有缩略图
|
||||
setTimeout(() => {
|
||||
this.updateAllThumbnails();
|
||||
}, 100);
|
||||
|
||||
console.log("画布JSON数据加载完成");
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("恢复图层数据失败:", error);
|
||||
reject(new Error("恢复图层数据失败: " + error.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { EraserCommand } from "../commands/EraserCommand";
|
||||
|
||||
/**
|
||||
* 橡皮擦状态管理器
|
||||
* 用于管理橡皮擦操作的状态快照
|
||||
*/
|
||||
export class EraserStateManager {
|
||||
constructor(canvas, layerManager) {
|
||||
this.canvas = canvas;
|
||||
this.layerManager = layerManager;
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
|
||||
setLayerManager(layerManager) {
|
||||
this.layerManager = layerManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始橡皮擦操作 - 捕获初始状态
|
||||
*/
|
||||
startErasing() {
|
||||
console.log("橡皮擦操作开始 - 捕获状态快照");
|
||||
this.currentSnapshot = this._captureCanvasSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束橡皮擦操作 - 创建命令
|
||||
* @param {Array} affectedObjects 受影响的对象
|
||||
* @returns {EraserCommand|null} 创建的橡皮擦命令
|
||||
*/
|
||||
endErasing(affectedObjects = []) {
|
||||
if (!this.currentSnapshot) {
|
||||
console.warn("没有初始状态快照,无法创建橡皮擦命令");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!affectedObjects || affectedObjects.length === 0) {
|
||||
console.log("没有对象被擦除,不创建命令");
|
||||
this.currentSnapshot = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`橡皮擦操作结束 - 影响了 ${affectedObjects.length} 个对象`);
|
||||
|
||||
// 捕获擦除后的状态
|
||||
const afterSnapshot = this._captureCanvasSnapshot();
|
||||
|
||||
// 创建橡皮擦命令
|
||||
const command = new EraserCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
affectedObjects: affectedObjects,
|
||||
beforeSnapshot: this.currentSnapshot,
|
||||
afterSnapshot: afterSnapshot,
|
||||
});
|
||||
|
||||
// 重置状态
|
||||
this.currentSnapshot = null;
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获画布状态快照
|
||||
* @returns {Object} 画布状态快照
|
||||
* @private
|
||||
*/
|
||||
_captureCanvasSnapshot() {
|
||||
try {
|
||||
return this.canvas.toJSON([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"isBackground",
|
||||
"isLocked",
|
||||
"isVisible",
|
||||
"isFixed",
|
||||
"parentId",
|
||||
"eraser",
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("捕获画布状态快照失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前擦除操作
|
||||
*/
|
||||
cancelErasing() {
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* 液化对象引用管理器
|
||||
* 专门处理液化操作中的对象引用管理,避免引用丢失问题
|
||||
*/
|
||||
export class LiquifyReferenceManager {
|
||||
constructor() {
|
||||
// 对象引用池
|
||||
this.objectRefs = new Map();
|
||||
// 状态快照池
|
||||
this.stateSnapshots = new Map();
|
||||
// ImageData缓存池
|
||||
this.imageDataCache = new Map();
|
||||
// 事件监听器备份
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册对象到引用管理器
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {String} objectId 对象唯一ID
|
||||
* @returns {String} 引用ID
|
||||
*/
|
||||
registerObject(fabricObject, objectId) {
|
||||
const refId = objectId || this._generateRefId();
|
||||
|
||||
// 保存对象引用
|
||||
this.objectRefs.set(refId, fabricObject);
|
||||
|
||||
// 备份事件监听器
|
||||
this._backupEventListeners(refId, fabricObject);
|
||||
|
||||
// 创建初始状态快照
|
||||
this._createStateSnapshot(refId, fabricObject);
|
||||
|
||||
console.log(`📝 对象已注册到引用管理器: ${refId}`);
|
||||
return refId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象引用
|
||||
* @param {String} refId 引用ID
|
||||
* @returns {Object|null} Fabric对象
|
||||
*/
|
||||
getObjectRef(refId) {
|
||||
return this.objectRefs.get(refId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对象的图像数据,保持引用不变
|
||||
* @param {String} refId 引用ID
|
||||
* @param {ImageData} newImageData 新的图像数据
|
||||
* @returns {Promise<Boolean>} 更新结果
|
||||
*/
|
||||
async updateObjectImageData(refId, newImageData) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
if (!fabricObject || !newImageData) {
|
||||
throw new Error(`无法更新对象图像数据: 对象不存在或数据无效 (${refId})`);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = newImageData.width;
|
||||
tempCanvas.height = newImageData.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
tempCtx.putImageData(newImageData, 0, 0);
|
||||
|
||||
// 创建新的图像元素
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 保存当前状态
|
||||
const currentState = this._captureObjectState(fabricObject);
|
||||
|
||||
// 更新图像源
|
||||
if (fabricObject.setElement) {
|
||||
fabricObject.setElement(img);
|
||||
} else if (fabricObject._element) {
|
||||
fabricObject._element = img;
|
||||
fabricObject._originalElement = img;
|
||||
}
|
||||
|
||||
// 恢复非图像属性
|
||||
this._restoreObjectState(fabricObject, currentState);
|
||||
|
||||
// 标记需要重新渲染
|
||||
fabricObject.dirty = true;
|
||||
fabricObject.setCoords();
|
||||
|
||||
// 更新缓存
|
||||
this._updateImageDataCache(refId, newImageData);
|
||||
|
||||
console.log(`✅ 对象图像数据更新成功: ${refId}`);
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
console.error("更新对象状态失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error("加载图像数据失败"));
|
||||
};
|
||||
|
||||
img.src = tempCanvas.toDataURL();
|
||||
} catch (error) {
|
||||
console.error("创建临时canvas失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对象状态快照
|
||||
* @param {String} refId 引用ID
|
||||
* @param {String} snapshotId 快照ID
|
||||
* @returns {String} 快照ID
|
||||
*/
|
||||
createSnapshot(refId, snapshotId = null) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
if (!fabricObject) {
|
||||
throw new Error(`无法创建快照: 对象不存在 (${refId})`);
|
||||
}
|
||||
|
||||
const snapId = snapshotId || `${refId}_${Date.now()}`;
|
||||
const snapshot = this._createStateSnapshot(snapId, fabricObject);
|
||||
|
||||
console.log(`📸 已创建对象快照: ${snapId}`);
|
||||
return snapId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象到指定快照状态
|
||||
* @param {String} refId 引用ID
|
||||
* @param {String} snapshotId 快照ID
|
||||
* @returns {Promise<Boolean>} 恢复结果
|
||||
*/
|
||||
async restoreFromSnapshot(refId, snapshotId) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
const snapshot = this.stateSnapshots.get(snapshotId);
|
||||
|
||||
if (!fabricObject || !snapshot) {
|
||||
throw new Error(
|
||||
`无法恢复快照: 对象或快照不存在 (${refId}, ${snapshotId})`
|
||||
);
|
||||
}
|
||||
|
||||
// 恢复图像数据
|
||||
if (snapshot.imageData) {
|
||||
await this.updateObjectImageData(refId, snapshot.imageData);
|
||||
}
|
||||
|
||||
// 恢复对象属性
|
||||
if (snapshot.properties) {
|
||||
this._restoreObjectState(fabricObject, snapshot.properties);
|
||||
}
|
||||
|
||||
// 恢复事件监听器
|
||||
if (snapshot.eventListeners) {
|
||||
this._restoreEventListeners(refId, fabricObject, snapshot.eventListeners);
|
||||
}
|
||||
|
||||
console.log(`🔄 对象快照恢复成功: ${refId} -> ${snapshotId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新多个对象
|
||||
* @param {Array} updates 更新列表 [{refId, imageData}, ...]
|
||||
* @returns {Promise<Array>} 更新结果
|
||||
*/
|
||||
async batchUpdate(updates) {
|
||||
const results = [];
|
||||
|
||||
for (const update of updates) {
|
||||
try {
|
||||
const result = await this.updateObjectImageData(
|
||||
update.refId,
|
||||
update.imageData
|
||||
);
|
||||
results.push({ refId: update.refId, success: true, result });
|
||||
} catch (error) {
|
||||
console.error(`批量更新失败 ${update.refId}:`, error);
|
||||
results.push({
|
||||
refId: update.refId,
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理不再使用的引用
|
||||
* @param {String} refId 引用ID
|
||||
*/
|
||||
cleanup(refId) {
|
||||
this.objectRefs.delete(refId);
|
||||
this.stateSnapshots.delete(refId);
|
||||
this.imageDataCache.delete(refId);
|
||||
this.eventListeners.delete(refId);
|
||||
|
||||
console.log(`🗑️ 已清理对象引用: ${refId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有引用
|
||||
*/
|
||||
cleanupAll() {
|
||||
this.objectRefs.clear();
|
||||
this.stateSnapshots.clear();
|
||||
this.imageDataCache.clear();
|
||||
this.eventListeners.clear();
|
||||
|
||||
console.log("🗑️ 已清理所有对象引用");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
getMemoryStats() {
|
||||
return {
|
||||
objectRefs: this.objectRefs.size,
|
||||
stateSnapshots: this.stateSnapshots.size,
|
||||
imageDataCache: this.imageDataCache.size,
|
||||
eventListeners: this.eventListeners.size,
|
||||
totalMemoryUsage: this._calculateMemoryUsage(),
|
||||
};
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
/**
|
||||
* 生成引用ID
|
||||
* @returns {String} 引用ID
|
||||
* @private
|
||||
*/
|
||||
_generateRefId() {
|
||||
return `ref_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份事件监听器
|
||||
* @param {String} refId 引用ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @private
|
||||
*/
|
||||
_backupEventListeners(refId, fabricObject) {
|
||||
const listeners = {};
|
||||
|
||||
// 备份常见的事件监听器
|
||||
const eventTypes = [
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"mousemove",
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
];
|
||||
|
||||
eventTypes.forEach((eventType) => {
|
||||
if (
|
||||
fabricObject.__eventListeners &&
|
||||
fabricObject.__eventListeners[eventType]
|
||||
) {
|
||||
listeners[eventType] = [...fabricObject.__eventListeners[eventType]];
|
||||
}
|
||||
});
|
||||
|
||||
this.eventListeners.set(refId, listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复事件监听器
|
||||
* @param {String} refId 引用ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {Object} listeners 监听器备份
|
||||
* @private
|
||||
*/
|
||||
_restoreEventListeners(refId, fabricObject, listeners) {
|
||||
Object.keys(listeners).forEach((eventType) => {
|
||||
if (listeners[eventType] && listeners[eventType].length > 0) {
|
||||
listeners[eventType].forEach((listener) => {
|
||||
fabricObject.on(eventType, listener);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建状态快照
|
||||
* @param {String} snapId 快照ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {Object} 快照数据
|
||||
* @private
|
||||
*/
|
||||
_createStateSnapshot(snapId, fabricObject) {
|
||||
const snapshot = {
|
||||
timestamp: Date.now(),
|
||||
properties: this._captureObjectState(fabricObject),
|
||||
imageData: this._captureImageData(fabricObject),
|
||||
eventListeners: this.eventListeners.get(snapId.split("_")[0]) || {},
|
||||
};
|
||||
|
||||
this.stateSnapshots.set(snapId, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获对象状态
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {Object} 对象状态
|
||||
* @private
|
||||
*/
|
||||
_captureObjectState(fabricObject) {
|
||||
return {
|
||||
left: fabricObject.left,
|
||||
top: fabricObject.top,
|
||||
scaleX: fabricObject.scaleX,
|
||||
scaleY: fabricObject.scaleY,
|
||||
angle: fabricObject.angle,
|
||||
flipX: fabricObject.flipX,
|
||||
flipY: fabricObject.flipY,
|
||||
opacity: fabricObject.opacity,
|
||||
visible: fabricObject.visible,
|
||||
selectable: fabricObject.selectable,
|
||||
evented: fabricObject.evented,
|
||||
id: fabricObject.id,
|
||||
layerId: fabricObject.layerId,
|
||||
customData: fabricObject.customData ? { ...fabricObject.customData } : {},
|
||||
filters: fabricObject.filters ? [...fabricObject.filters] : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象状态
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {Object} state 状态数据
|
||||
* @private
|
||||
*/
|
||||
_restoreObjectState(fabricObject, state) {
|
||||
if (!state) return;
|
||||
|
||||
fabricObject.set({
|
||||
left: state.left,
|
||||
top: state.top,
|
||||
scaleX: state.scaleX,
|
||||
scaleY: state.scaleY,
|
||||
angle: state.angle,
|
||||
flipX: state.flipX,
|
||||
flipY: state.flipY,
|
||||
opacity: state.opacity,
|
||||
visible: state.visible,
|
||||
selectable: state.selectable,
|
||||
evented: state.evented,
|
||||
});
|
||||
|
||||
// 恢复自定义属性
|
||||
if (state.customData) {
|
||||
fabricObject.customData = { ...state.customData };
|
||||
}
|
||||
|
||||
// 恢复滤镜
|
||||
if (state.filters && state.filters.length > 0) {
|
||||
fabricObject.filters = [...state.filters];
|
||||
fabricObject.applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获图像数据
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {ImageData|null} 图像数据
|
||||
* @private
|
||||
*/
|
||||
_captureImageData(fabricObject) {
|
||||
try {
|
||||
if (
|
||||
fabricObject._element &&
|
||||
fabricObject._element.width &&
|
||||
fabricObject._element.height
|
||||
) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = fabricObject._element.width;
|
||||
canvas.height = fabricObject._element.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(fabricObject._element, 0, 0);
|
||||
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("无法捕获图像数据:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图像数据缓存
|
||||
* @param {String} refId 引用ID
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
_updateImageDataCache(refId, imageData) {
|
||||
this.imageDataCache.set(refId, {
|
||||
data: imageData,
|
||||
timestamp: Date.now(),
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算内存使用量(近似值)
|
||||
* @returns {Number} 内存使用量(字节)
|
||||
* @private
|
||||
*/
|
||||
_calculateMemoryUsage() {
|
||||
let totalBytes = 0;
|
||||
|
||||
// 计算ImageData缓存大小
|
||||
this.imageDataCache.forEach((cache) => {
|
||||
totalBytes += cache.width * cache.height * 4; // RGBA = 4 bytes per pixel
|
||||
});
|
||||
|
||||
return totalBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单例引用管理器
|
||||
*/
|
||||
let liquifyReferenceManagerInstance = null;
|
||||
|
||||
export function getLiquifyReferenceManager() {
|
||||
if (!liquifyReferenceManagerInstance) {
|
||||
liquifyReferenceManagerInstance = new LiquifyReferenceManager();
|
||||
}
|
||||
return liquifyReferenceManagerInstance;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
||||
import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js";
|
||||
|
||||
@@ -35,7 +35,6 @@ export class RedGreenModeManager {
|
||||
* @param {String} options.redGreenImageUrl 红绿图URL
|
||||
* @param {Number} options.normalLayerOpacity 普通图层透明度 (0-1)
|
||||
* @param {Function} options.onImageGenerated 图片生成回调
|
||||
* @param {Boolean} options.useBatchMode 是否使用批量模式 (默认true,减少闪烁)
|
||||
* @returns {Promise<boolean>} 是否初始化成功
|
||||
*/
|
||||
async initialize(options = {}) {
|
||||
@@ -70,13 +69,8 @@ export class RedGreenModeManager {
|
||||
throw new Error("缺少必需的图片URL参数");
|
||||
}
|
||||
|
||||
// 使用批量模式或传统模式
|
||||
const useBatchMode = options.useBatchMode !== false; // 默认为true
|
||||
|
||||
let initCommand;
|
||||
|
||||
// 使用新的批量初始化命令,减少页面闪烁
|
||||
initCommand = new BatchInitializeRedGreenModeCommand({
|
||||
const initCommand = new BatchInitializeRedGreenModeCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
toolManager: this.toolManager,
|
||||
@@ -94,10 +88,11 @@ export class RedGreenModeManager {
|
||||
await initCommand.execute();
|
||||
}
|
||||
|
||||
this.registerRedGreenMouseUpEvent();
|
||||
// 标记为已初始化
|
||||
this.isInitialized = true;
|
||||
|
||||
this.registerRedGreenMouseUpEvent();
|
||||
|
||||
// 启用图层管理器的红绿图模式
|
||||
if (
|
||||
this.layerManager &&
|
||||
@@ -106,6 +101,9 @@ export class RedGreenModeManager {
|
||||
this.layerManager.enableRedGreenMode();
|
||||
}
|
||||
|
||||
// 更新交互性
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
|
||||
// 重置工具管理器状态
|
||||
// 默认红色笔刷
|
||||
if (this.toolManager) {
|
||||
@@ -116,7 +114,6 @@ export class RedGreenModeManager {
|
||||
衣服底图: this.clothingImageUrl,
|
||||
红绿图: this.redGreenImageUrl,
|
||||
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
|
||||
批量模式: useBatchMode ? "已启用" : "已禁用",
|
||||
画布背景: "白色",
|
||||
});
|
||||
|
||||
@@ -131,17 +128,17 @@ export class RedGreenModeManager {
|
||||
// 注册鼠标抬起事件
|
||||
registerRedGreenMouseUpEvent() {
|
||||
this.canvas.on("mouse:up", (event) => {
|
||||
if (!this.isInitialized) {
|
||||
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 可以在这里添加更多逻辑,比如生成图片或更新状态
|
||||
if (this.onImageGenerated) {
|
||||
const imageData = this.canvasManager.exportImage();
|
||||
console.log("生成红绿图图片数据:", imageData);
|
||||
this.onImageGenerated(imageData);
|
||||
}
|
||||
requestAnimationFrame(async () => {
|
||||
if (!this.isInitialized) {
|
||||
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
||||
return;
|
||||
}
|
||||
if (this.onImageGenerated) {
|
||||
const imageData = await this.canvasManager.exportImage();
|
||||
this.onImageGenerated(imageData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { BrushManager } from "./brushes/brushManager";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import {
|
||||
InitLiquifyToolCommand,
|
||||
RasterizeForLiquifyCommand,
|
||||
} from "../commands/LiquifyCommands";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { InitLiquifyToolCommand } from "../commands/LiquifyCommands";
|
||||
import { RasterizeLayerCommand } from "../commands/GroupCommands";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { h } from "vue";
|
||||
|
||||
/**
|
||||
* 工具管理器
|
||||
@@ -378,6 +378,8 @@ export class ToolManager {
|
||||
previousTool: this.activeTool.value,
|
||||
});
|
||||
|
||||
command.undoable = options.undoable !== undefined ? options.undoable : true;
|
||||
|
||||
// 执行命令
|
||||
this.commandManager.execute(command, { ...options });
|
||||
}
|
||||
@@ -690,18 +692,9 @@ export class ToolManager {
|
||||
}
|
||||
} else if (checkResult.needsRasterization) {
|
||||
// 需要栅格化 (多个对象或组)
|
||||
// 询问用户是否要栅格化
|
||||
if (
|
||||
confirm(
|
||||
checkResult.isGroup
|
||||
? "组对象需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
: "当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
)
|
||||
) {
|
||||
// 用户确认栅格化,执行栅格化操作
|
||||
this._rasterizeLayerForLiquify(activeLayerId);
|
||||
return; // 栅格化后会重新调用液化功能,这里直接返回
|
||||
}
|
||||
// 使用Modal询问用户是否要栅格化
|
||||
this._showRasterizeConfirmModal(checkResult.isGroup, activeLayerId);
|
||||
return; // 等待用户确认,不继续执行
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,51 +709,42 @@ export class ToolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并准备液化操作
|
||||
* 显示栅格化确认Modal对话框
|
||||
* @param {Boolean} isGroup 是否为组对象
|
||||
* @param {String} layerId 图层ID
|
||||
* @private
|
||||
*/
|
||||
_checkAndPrepareForLiquify(layerId) {
|
||||
// 确保存在液化管理器
|
||||
const liquifyManager = this.canvasManager?.liquifyManager;
|
||||
if (!liquifyManager) {
|
||||
console.error("液化管理器未初始化");
|
||||
return;
|
||||
}
|
||||
_showRasterizeConfirmModal(isGroup, layerId) {
|
||||
const title = "栅格化图层";
|
||||
const content = "需要先栅格化才能进行液化操作,是否立即栅格化?";
|
||||
|
||||
// 检查图层是否适合液化
|
||||
const checkResult = liquifyManager.checkLayerForLiquify(layerId);
|
||||
|
||||
if (checkResult.isEmpty) {
|
||||
// 空图层
|
||||
alert("当前图层为空,无法进行液化操作");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkResult.isGroup) {
|
||||
// 询问是否栅格化组
|
||||
if (confirm("组对象需要栅格化才能进行液化操作,是否立即栅格化?")) {
|
||||
Modal.confirm({
|
||||
title,
|
||||
content,
|
||||
okText: "确定栅格化",
|
||||
cancelText: "取消",
|
||||
centered: true,
|
||||
icon: h("span", { style: "color: #faad14;" }, "⚠️"),
|
||||
onOk: () => {
|
||||
// 用户确认栅格化,执行栅格化操作
|
||||
this._rasterizeLayerForLiquify(layerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkResult.needsRasterization) {
|
||||
// 询问是否栅格化图层
|
||||
if (
|
||||
confirm(
|
||||
"当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
)
|
||||
) {
|
||||
this._rasterizeLayerForLiquify(layerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果图层可以直接液化(单个图像对象)
|
||||
if (checkResult.valid) {
|
||||
this._startLiquify(layerId);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log("用户取消了栅格化操作");
|
||||
// 用户取消,触发液化面板显示事件但不能液化
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("showLiquifyPanel", {
|
||||
detail: {
|
||||
activeLayerId: layerId,
|
||||
layerStatus: { needsRasterization: true, isGroup },
|
||||
canLiquify: false,
|
||||
targetObject: null,
|
||||
originalImageData: null,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -772,25 +756,52 @@ export class ToolManager {
|
||||
if (!this.commandManager || !this.layerManager) return;
|
||||
|
||||
try {
|
||||
// 导入液化相关命令
|
||||
// 显示加载Modal
|
||||
const loadingModal = Modal.info({
|
||||
title: "正在栅格化",
|
||||
content: "正在栅格化图层,请稍候...",
|
||||
okButtonProps: { style: { display: "none" } },
|
||||
centered: true,
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
});
|
||||
|
||||
// 创建栅格化命令
|
||||
const rasterizeCommand = new RasterizeForLiquifyCommand({
|
||||
const rasterizeCommand = new RasterizeLayerCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
layerId: layerId,
|
||||
layers: this.layerManager.layers,
|
||||
activeLayerId: this.layerManager.activeLayerId,
|
||||
});
|
||||
|
||||
// 执行命令
|
||||
const result = await this.commandManager.execute(rasterizeCommand);
|
||||
|
||||
// 关闭加载Modal
|
||||
loadingModal.destroy();
|
||||
|
||||
if (result) {
|
||||
// 栅格化成功,启动液化
|
||||
message.success("图层已成功栅格化,可以进行液化操作");
|
||||
this._startLiquify(layerId);
|
||||
} else {
|
||||
// 栅格化失败
|
||||
Modal.error({
|
||||
title: "栅格化失败",
|
||||
content: "栅格化失败,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("栅格化图层失败:", error);
|
||||
alert("栅格化失败,无法进行液化操作");
|
||||
Modal.error({
|
||||
title: "栅格化错误",
|
||||
content: `栅格化失败:${error.message}`,
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +814,12 @@ export class ToolManager {
|
||||
// 获取图层信息
|
||||
const layer = this.layerManager.getLayerById(layerId);
|
||||
if (!layer) {
|
||||
console.error("图层不存在");
|
||||
Modal.error({
|
||||
title: "图层错误",
|
||||
content: "图层不存在",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -812,14 +828,24 @@ export class ToolManager {
|
||||
if (layer.isBackground) {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (!layer.fabricObject) {
|
||||
console.error("背景图层为空");
|
||||
Modal.warning({
|
||||
title: "背景图层为空",
|
||||
content: "背景图层为空,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
targetObject = layer.fabricObject;
|
||||
} else {
|
||||
// 普通图层使用 fabricObjects (复数)
|
||||
if (!layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.error("图层为空");
|
||||
Modal.warning({
|
||||
title: "图层为空",
|
||||
content: "图层为空,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
targetObject = layer.fabricObjects[0];
|
||||
@@ -828,11 +854,26 @@ export class ToolManager {
|
||||
// 确保liquifyManager可用
|
||||
const liquifyManager = this.canvasManager?.liquifyManager;
|
||||
if (!liquifyManager) {
|
||||
console.error("液化管理器未初始化");
|
||||
Modal.error({
|
||||
title: "液化管理器错误",
|
||||
content: "液化管理器未初始化",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示准备中的Modal
|
||||
const preparingModal = Modal.info({
|
||||
title: "准备液化环境",
|
||||
content: "正在准备液化环境,请稍候...",
|
||||
okButtonProps: { style: { display: "none" } },
|
||||
centered: true,
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
});
|
||||
|
||||
// 准备液化环境
|
||||
liquifyManager.initialize({
|
||||
canvas: this.canvas,
|
||||
@@ -855,6 +896,9 @@ export class ToolManager {
|
||||
// 执行初始化命令
|
||||
await this.commandManager.execute(initCommand);
|
||||
|
||||
// 关闭准备Modal
|
||||
preparingModal.destroy();
|
||||
|
||||
// 触发液化面板显示事件
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("showLiquifyPanel", {
|
||||
@@ -867,7 +911,12 @@ export class ToolManager {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("启动液化工具失败:", error);
|
||||
alert("启动液化工具失败:" + error.message);
|
||||
Modal.error({
|
||||
title: "液化工具启动失败",
|
||||
content: `启动液化工具失败:${error.message}`,
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -577,6 +577,49 @@ export class TexturePresetManager {
|
||||
cacheSize: this.textureCache.size,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证纹理文件
|
||||
* @param {File} file 要验证的文件
|
||||
* @returns {Boolean} 是否为有效的纹理文件
|
||||
*/
|
||||
validateTextureFile(file) {
|
||||
if (!file) {
|
||||
console.warn("文件不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith("image/")) {
|
||||
console.warn("文件类型无效,必须是图片文件");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小(限制为 10MB)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
console.warn("文件大小超过限制(10MB)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查支持的图片格式
|
||||
const supportedTypes = [
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
];
|
||||
|
||||
if (!supportedTypes.includes(file.type)) {
|
||||
console.warn("不支持的图片格式");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import "./fabric.brushes.js";
|
||||
import { BrushStore } from "../../store/BrushStore";
|
||||
import { brushRegistry } from "./BrushRegistry";
|
||||
|
||||
@@ -16,7 +17,7 @@ import { MarkerBrush } from "./types/MarkerBrush";
|
||||
import { CustomPenBrush } from "./types/CustomPenBrush";
|
||||
import { RibbonBrush } from "./types/RibbonBrush";
|
||||
import { ShadedBrush } from "./types/ShadedBrush";
|
||||
import { SketchyBrush } from "./types/SketchyBrush";
|
||||
// import { SketchyBrush } from "./types/SketchyBrush";
|
||||
import { SpraypaintBrush } from "./types/SpraypaintBrush";
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,11 @@ export class BrushManager {
|
||||
|
||||
// 初始化笔刷注册
|
||||
this._registerDefaultBrushes();
|
||||
|
||||
// 初始化橡皮擦状态管理器
|
||||
this.eraserStateManager = null;
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = []; // 当前擦除会话中被影响的对象
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,64 +57,112 @@ export class BrushManager {
|
||||
_registerDefaultBrushes() {
|
||||
// 注册铅笔笔刷
|
||||
brushRegistry.register("pencil", PencilBrush, {
|
||||
name: "铅笔",
|
||||
name: "Pencil",
|
||||
description: "基础铅笔工具,适合精细线条绘制",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册材质笔刷
|
||||
brushRegistry.register("texture", TextureBrush);
|
||||
brushRegistry.register("texture", TextureBrush, {
|
||||
name: "Texture",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册集成的笔刷类型
|
||||
brushRegistry.register("crayon", CrayonBrush);
|
||||
brushRegistry.register("fur", FurBrush);
|
||||
brushRegistry.register("ink", InkBrush);
|
||||
brushRegistry.register("longfur", LongfurBrush);
|
||||
brushRegistry.register("writing", WritingBrush);
|
||||
brushRegistry.register("marker", MarkerBrush);
|
||||
brushRegistry.register("pen", CustomPenBrush);
|
||||
brushRegistry.register("ribbon", RibbonBrush);
|
||||
brushRegistry.register("shaded", ShadedBrush);
|
||||
brushRegistry.register("sketchy", SketchyBrush);
|
||||
brushRegistry.register("spraypaint", SpraypaintBrush);
|
||||
brushRegistry.register("crayon", CrayonBrush, {
|
||||
name: "Crayon",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("fur", FurBrush, {
|
||||
name: "Texture",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("ink", InkBrush, {
|
||||
name: "Ink",
|
||||
description: "墨水笔刷,适合书写和绘图",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("", LongfurBrush, {
|
||||
name: "Longfur",
|
||||
description: "长毛发笔刷,适合绘制动物毛皮、草或头发",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("writing", WritingBrush, {
|
||||
name: "Writing",
|
||||
description: "书法笔刷,模拟中国传统书法毛笔效果,具有笔锋和墨色变化",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("marker", MarkerBrush, {
|
||||
name: "Marker",
|
||||
description: "马克笔笔刷,适合粗线条和填充",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("pen", CustomPenBrush, {
|
||||
name: "Pen",
|
||||
description: "自定义钢笔笔刷,适合书写和绘图",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("ribbon", RibbonBrush, {
|
||||
name: "Ribbon",
|
||||
description: "丝带笔刷,适合创建流动的丝带效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("shaded", ShadedBrush, {
|
||||
name: "Shaded",
|
||||
description: "阴影笔刷,适合创建渐变和阴影效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
// brushRegistry.register("sketchy", SketchyBrush);
|
||||
brushRegistry.register("spraypaint", SpraypaintBrush, {
|
||||
name: "Spraypaint",
|
||||
description: "喷漆笔刷,模拟喷漆效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册喷枪笔刷
|
||||
brushRegistry.register(
|
||||
"spray",
|
||||
class SprayBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "spray",
|
||||
name: "喷枪",
|
||||
description: "模拟喷枪效果,创建散点效果",
|
||||
category: "基础笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册喷枪笔刷
|
||||
// brushRegistry.register(
|
||||
// "spray",
|
||||
// class SprayBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "spray",
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// category: "基础笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.SprayBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
return this.brush;
|
||||
}
|
||||
// create() {
|
||||
// this.brush = new fabric.SprayBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
if (options.density !== undefined) {
|
||||
brush.density = options.density;
|
||||
}
|
||||
// if (options.density !== undefined) {
|
||||
// brush.density = options.density;
|
||||
// }
|
||||
|
||||
if (options.randomOpacity !== undefined) {
|
||||
brush.randomOpacity = options.randomOpacity;
|
||||
}
|
||||
// if (options.randomOpacity !== undefined) {
|
||||
// brush.randomOpacity = options.randomOpacity;
|
||||
// }
|
||||
|
||||
if (options.dotWidth !== undefined) {
|
||||
brush.dotWidth = options.dotWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
// if (options.dotWidth !== undefined) {
|
||||
// brush.dotWidth = options.dotWidth;
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// }
|
||||
// );
|
||||
// 注册橡皮擦笔刷
|
||||
brushRegistry.register(
|
||||
"eraser",
|
||||
@@ -187,87 +241,87 @@ export class BrushManager {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册水彩笔刷
|
||||
brushRegistry.register(
|
||||
"watercolor",
|
||||
class WatercolorBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "watercolor",
|
||||
name: "水彩",
|
||||
description: "模拟水彩效果,带有流动感和透明感",
|
||||
category: "特效笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册水彩笔刷
|
||||
// brushRegistry.register(
|
||||
// "watercolor",
|
||||
// class WatercolorBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "watercolor",
|
||||
// name: "水彩",
|
||||
// description: "模拟水彩效果,带有流动感和透明感",
|
||||
// category: "特效笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
// 创建一个自定义的PencilBrush来模拟水彩效果
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
// create() {
|
||||
// // 创建一个自定义的PencilBrush来模拟水彩效果
|
||||
// this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
|
||||
// 水彩效果特有的属性
|
||||
this.brush.globalCompositeOperation = "multiply";
|
||||
this.brush.shadow = new fabric.Shadow({
|
||||
color: this.options.color || "#000",
|
||||
blur: 5,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
});
|
||||
// // 水彩效果特有的属性
|
||||
// this.brush.globalCompositeOperation = "multiply";
|
||||
// this.brush.shadow = new fabric.Shadow({
|
||||
// color: this.options.color || "#000",
|
||||
// blur: 5,
|
||||
// offsetX: 0,
|
||||
// offsetY: 0,
|
||||
// });
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
// 水彩笔刷特有的配置
|
||||
brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
|
||||
}
|
||||
}
|
||||
);
|
||||
// // 水彩笔刷特有的配置
|
||||
// brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// 注册粉笔笔刷
|
||||
brushRegistry.register(
|
||||
"chalk",
|
||||
class ChalkBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "chalk",
|
||||
name: "粉笔",
|
||||
description: "模拟粉笔效果,有颗粒感和不连续性",
|
||||
category: "特效笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册粉笔笔刷
|
||||
// brushRegistry.register(
|
||||
// "chalk",
|
||||
// class ChalkBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "chalk",
|
||||
// name: "粉笔",
|
||||
// description: "模拟粉笔效果,有颗粒感和不连续性",
|
||||
// category: "特效笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
// create() {
|
||||
// this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
|
||||
// 自定义绘画方法来模拟粉笔效果
|
||||
const originalOnMouseMove = this.brush.onMouseMove;
|
||||
this.brush.onMouseMove = function (pointer, options) {
|
||||
// 随机调整坐标位置,增加粉笔质感
|
||||
const jitter = 2;
|
||||
pointer.x += (Math.random() - 0.5) * jitter;
|
||||
pointer.y += (Math.random() - 0.5) * jitter;
|
||||
// // 自定义绘画方法来模拟粉笔效果
|
||||
// const originalOnMouseMove = this.brush.onMouseMove;
|
||||
// this.brush.onMouseMove = function (pointer, options) {
|
||||
// // 随机调整坐标位置,增加粉笔质感
|
||||
// const jitter = 2;
|
||||
// pointer.x += (Math.random() - 0.5) * jitter;
|
||||
// pointer.y += (Math.random() - 0.5) * jitter;
|
||||
|
||||
// 调用原始方法
|
||||
originalOnMouseMove.call(this, pointer, options);
|
||||
};
|
||||
// // 调用原始方法
|
||||
// originalOnMouseMove.call(this, pointer, options);
|
||||
// };
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
// 粉笔特有的设置
|
||||
brush.strokeDashArray = [5, 5]; // 虚线效果
|
||||
}
|
||||
}
|
||||
);
|
||||
// // 粉笔特有的设置
|
||||
// brush.strokeDashArray = [5, 5]; // 虚线效果
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,6 +625,148 @@ export class BrushManager {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置依赖管理器
|
||||
*/
|
||||
setManagers({ layerManager, commandManager }) {
|
||||
this.layerManager = layerManager;
|
||||
this.commandManager = commandManager;
|
||||
|
||||
// 初始化橡皮擦状态管理器
|
||||
if (this.canvas && this.layerManager) {
|
||||
this.eraserStateManager = new EraserStateManager(
|
||||
this.canvas,
|
||||
this.layerManager
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建橡皮擦
|
||||
* @param {Object} options 橡皮擦选项
|
||||
*/
|
||||
createEraser(options = {}) {
|
||||
if (!this.canvas) {
|
||||
console.error("画布未初始化");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接使用 fabric-with-erasing 库提供的 EraserBrush
|
||||
this.brush = new fabric.EraserBrush(this.canvas);
|
||||
|
||||
// 应用配置
|
||||
this.configure(this.brush, {
|
||||
width: this.brushSize.value,
|
||||
color: this.brushColor.value,
|
||||
opacity: this.brushOpacity.value,
|
||||
inverted: options.inverted || false,
|
||||
...options,
|
||||
});
|
||||
|
||||
// 设置画布为绘图模式
|
||||
this.canvas.isDrawingMode = true;
|
||||
this.canvas.freeDrawingBrush = this.brush;
|
||||
|
||||
// 绑定橡皮擦事件处理器
|
||||
// this._bindEraserEvents();
|
||||
|
||||
console.log("橡皮擦创建成功");
|
||||
return this.brush;
|
||||
} catch (error) {
|
||||
console.error("创建橡皮擦失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定橡皮擦事件处理器
|
||||
* @private
|
||||
*/
|
||||
_bindEraserEvents() {
|
||||
if (!this.canvas || !this.eraserStateManager) return;
|
||||
|
||||
// 监听擦除开始事件
|
||||
this.canvas.on("erasing:start", this._handleErasingStart.bind(this));
|
||||
|
||||
// 监听擦除结束事件
|
||||
this.canvas.on("erasing:end", this._handleErasingEnd.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑橡皮擦事件处理器
|
||||
* @private
|
||||
*/
|
||||
_unbindEraserEvents() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
this.canvas.off("erasing:start", this._handleErasingStart.bind(this));
|
||||
this.canvas.off("erasing:end", this._handleErasingEnd.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理擦除开始事件
|
||||
* @param {Object} e 事件对象
|
||||
* @private
|
||||
*/
|
||||
_handleErasingStart(e) {
|
||||
console.log("橡皮擦开始擦除");
|
||||
|
||||
if (!this.eraserStateManager) return;
|
||||
|
||||
// 标记擦除状态
|
||||
this.isErasingActive = true;
|
||||
this.currentErasedObjects = [];
|
||||
|
||||
// 捕获擦除前的状态
|
||||
this.eraserStateManager.startErasing();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理擦除结束事件
|
||||
* @param {Object} e 事件对象
|
||||
* @private
|
||||
*/
|
||||
_handleErasingEnd(e) {
|
||||
console.log("橡皮擦擦除结束", e);
|
||||
|
||||
if (!this.eraserStateManager || !this.isErasingActive) return;
|
||||
|
||||
// 获取被擦除的对象
|
||||
const affectedObjects = e.targets || [];
|
||||
|
||||
// 创建橡皮擦命令
|
||||
const eraserCommand = this.eraserStateManager.endErasing(affectedObjects);
|
||||
|
||||
// 如果有有效的命令且有命令管理器,执行命令
|
||||
if (eraserCommand && this.commandManager) {
|
||||
// 注意:不需要调用 execute(),因为擦除操作已经完成
|
||||
// 只需要将命令添加到历史记录中以支持撤销
|
||||
this.commandManager.addToHistory(eraserCommand);
|
||||
console.log("橡皮擦操作已添加到命令历史");
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前擦除操作
|
||||
*/
|
||||
cancelErasing() {
|
||||
if (!this.isErasingActive) return;
|
||||
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager.cancelErasing();
|
||||
}
|
||||
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = [];
|
||||
|
||||
console.log("当前擦除操作已取消");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前笔刷类型
|
||||
* @returns {String} 当前笔刷类型ID
|
||||
@@ -646,7 +842,6 @@ export class BrushManager {
|
||||
|
||||
return this.canvas.freeDrawingBrush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建橡皮擦
|
||||
* @returns {Object} 橡皮擦笔刷
|
||||
@@ -703,9 +898,20 @@ export class BrushManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁资源
|
||||
* 销毁笔刷管理器
|
||||
*/
|
||||
dispose() {
|
||||
// 解绑事件
|
||||
// this._unbindEraserEvents();
|
||||
|
||||
// 取消进行中的擦除操作
|
||||
this.cancelErasing();
|
||||
|
||||
// 清理状态管理器
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager = null;
|
||||
}
|
||||
|
||||
// 销毁当前笔刷
|
||||
if (this.activeBrush) {
|
||||
this.activeBrush.destroy();
|
||||
|
||||
1748
src/component/Canvas/CanvasEditor/managers/brushes/fabric.brushes.js
Normal file
1748
src/component/Canvas/CanvasEditor/managers/brushes/fabric.brushes.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import texturePresetManager from "../TexturePresetManager";
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,72 @@ export class TextureBrush extends BaseBrush {
|
||||
// 创建fabric原生纹理笔刷
|
||||
this.brush = new fabric.PatternBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
|
||||
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(
|
||||
this.brush
|
||||
);
|
||||
const self = this; // 保存外部this引用
|
||||
|
||||
this.brush._finalizeAndAddPath = function () {
|
||||
console.log("TextureBrush: _finalizeAndAddPath called");
|
||||
const ctx = this.canvas.contextTop;
|
||||
ctx.closePath();
|
||||
|
||||
// 应用点简化(如果PatternBrush支持)
|
||||
if (this.decimate && this._points) {
|
||||
this._points = this.decimatePoints(this._points, this.decimate);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"TextureBrush: points count =",
|
||||
this._points ? this._points.length : 0
|
||||
);
|
||||
|
||||
// 检查是否有有效的路径数据
|
||||
if (!this._points || this._points.length < 2) {
|
||||
// 如果点数不足,直接请求重新渲染
|
||||
console.log("TextureBrush: Not enough points, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const pathData = this.convertPointsToSVGPath(this._points);
|
||||
|
||||
const isEmpty = self._isEmptySVGPath(pathData);
|
||||
console.log("TextureBrush: isEmpty =", isEmpty);
|
||||
|
||||
if (isEmpty) {
|
||||
// 如果路径为空,直接请求重新渲染
|
||||
console.log("TextureBrush: Path is empty, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先触发事件,模拟原生行为
|
||||
const path = this.createPath(pathData);
|
||||
this.canvas.fire("before:path:created", { path: path });
|
||||
|
||||
console.log("TextureBrush: Calling convertToImg");
|
||||
|
||||
// 调用 convertToImg 方法将绘制内容转换为图片
|
||||
if (typeof this.convertToImg === "function") {
|
||||
this.convertToImg();
|
||||
console.log("TextureBrush: convertToImg called successfully");
|
||||
} else {
|
||||
console.warn(
|
||||
"convertToImg method not found, falling back to original behavior"
|
||||
);
|
||||
// 如果没有convertToImg方法,回退到原始行为
|
||||
this.canvas.add(path);
|
||||
this.canvas.fire("path:created", { path: path });
|
||||
this.canvas.clearContext(this.canvas.contextTop);
|
||||
}
|
||||
|
||||
// 重置阴影
|
||||
this._resetShadow();
|
||||
};
|
||||
|
||||
// 配置笔刷基本属性
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
// 如果有选中的材质,则设置纹理
|
||||
@@ -221,6 +286,11 @@ export class TextureBrush extends BaseBrush {
|
||||
canvasTexture.width = width;
|
||||
canvasTexture.height = height;
|
||||
|
||||
// 应用透明度设置
|
||||
if (this.textureOpacity < 1) {
|
||||
ctx.globalAlpha = this.textureOpacity;
|
||||
}
|
||||
|
||||
// 绘制前应用旋转
|
||||
if (this.textureAngle !== 0) {
|
||||
ctx.save();
|
||||
@@ -233,26 +303,16 @@ export class TextureBrush extends BaseBrush {
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
}
|
||||
|
||||
// 应用透明度
|
||||
if (this.textureOpacity < 1) {
|
||||
ctx.globalAlpha = this.textureOpacity;
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
// 缓存处理后的纹理图像
|
||||
this._processedTextureCanvas = canvasTexture;
|
||||
|
||||
// 创建Pattern对象
|
||||
const pattern = new fabric.Pattern({
|
||||
source: canvasTexture,
|
||||
repeat: this.textureRepeat,
|
||||
});
|
||||
// 使用fabric.js 5.x正确的API方式设置PatternBrush
|
||||
// 直接设置source属性,这是PatternBrush的标准用法
|
||||
this.brush.source = canvasTexture;
|
||||
|
||||
// 设置笔刷源纹理
|
||||
if (typeof this.brush.setSource === "function") {
|
||||
this.brush.setSource(pattern);
|
||||
} else if (typeof this.brush.source === "object") {
|
||||
this.brush.source = pattern;
|
||||
} else if (typeof this.brush.pattern === "object") {
|
||||
this.brush.pattern = pattern;
|
||||
// 通知画布重绘
|
||||
if (this.canvas && this.canvas.requestRenderAll) {
|
||||
this.canvas.requestRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,4 +912,37 @@ export class TextureBrush extends BaseBrush {
|
||||
getTextureStats() {
|
||||
return texturePresetManager.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 SVG 路径是否为空
|
||||
* @private
|
||||
* @param {Array} pathData SVG 路径数据
|
||||
* @returns {Boolean} 是否为空路径
|
||||
*/
|
||||
_isEmptySVGPath(pathData) {
|
||||
if (!pathData || pathData.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查路径是否只包含移动命令或者是一个点
|
||||
let hasDrawing = false;
|
||||
let moveCount = 0;
|
||||
|
||||
for (let i = 0; i < pathData.length; i++) {
|
||||
const command = pathData[i];
|
||||
if (command[0] === "M") {
|
||||
moveCount++;
|
||||
} else if (
|
||||
command[0] === "L" ||
|
||||
command[0] === "Q" ||
|
||||
command[0] === "C"
|
||||
) {
|
||||
hasDrawing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果只有移动命令且超过1个,或者没有绘制命令,则认为是空路径
|
||||
return !hasDrawing || (moveCount > 0 && pathData.length <= moveCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ export class CanvasEventManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并图层中的对象为图像以提高性能
|
||||
* 合并图层中的对象为组以提高性能
|
||||
* @param {Object} options 合并选项
|
||||
* @param {fabric.Image} options.fabricImage 新的图像对象
|
||||
* @param {Object} options.activeLayer 当前活动图层
|
||||
@@ -594,7 +594,6 @@ export class CanvasEventManager {
|
||||
console.warn("合并对象失败:没有活动图层");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否需要合并
|
||||
const hasExistingObjects =
|
||||
Array.isArray(activeLayer.fabricObjects) &&
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
|
||||
import { LiquifyCPUManager } from "./LiquifyCPUManager";
|
||||
import { LayerType } from "../../utils/layerHelper";
|
||||
|
||||
export class EnhancedLiquifyManager {
|
||||
/**
|
||||
@@ -313,16 +314,40 @@ export class EnhancedLiquifyManager {
|
||||
if (param in this.params) {
|
||||
this.params[param] = value;
|
||||
|
||||
// 同步更新当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
// 同步更新当前渲染器 - 关键修复:确保参数正确传递
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.setParam === "function"
|
||||
) {
|
||||
console.log(`EnhancedLiquifyManager 设置参数: ${param}=${value}`);
|
||||
this.activeRenderer.setParam(param, value);
|
||||
} else {
|
||||
console.warn(
|
||||
`EnhancedLiquifyManager: 无法设置参数 ${param},渲染器未就绪`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(`EnhancedLiquifyManager: 无效参数 ${param}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("EnhancedLiquifyManager 批量设置参数:", params);
|
||||
|
||||
if (params && typeof params === "object") {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.setParam(key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -348,6 +373,36 @@ export class EnhancedLiquifyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.startDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.startDeformation(x, y);
|
||||
}
|
||||
console.log(
|
||||
`开始液化操作,渲染模式=${this.renderMode}, 初始点: (${x}, ${y})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.endDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.endDeformation();
|
||||
}
|
||||
console.log(`结束液化操作,渲染模式=${this.renderMode}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用液化变形
|
||||
* @param {Object} target 目标对象
|
||||
@@ -468,7 +523,9 @@ export class EnhancedLiquifyManager {
|
||||
// 注意:这里不自动切换,因为可能会导致中途渲染结果不一致
|
||||
}
|
||||
}
|
||||
|
||||
setRealtimeUpdater(realtimeUpdater) {
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
}
|
||||
/**
|
||||
* 重置液化操作
|
||||
* @returns {ImageData} 重置后的图像数据
|
||||
@@ -519,7 +576,7 @@ export class EnhancedLiquifyManager {
|
||||
|
||||
// 检查图层是否为空
|
||||
let objectsToCheck = [];
|
||||
if (layer.isBackground || layer.type === "background") {
|
||||
if (layer.isBackground || layer.type === "background" || layer.isFixed) {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (layer.fabricObject) {
|
||||
objectsToCheck = [layer.fabricObject];
|
||||
@@ -548,7 +605,10 @@ export class EnhancedLiquifyManager {
|
||||
objectsToCheck[0].type === "rasterized-layer");
|
||||
|
||||
// 检查是否为组
|
||||
const isGroup = objectsToCheck.some((obj) => obj.type === "group");
|
||||
const isGroup =
|
||||
objectsToCheck.some((obj) => obj.type === "group") ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
layer.children?.length > 0;
|
||||
|
||||
// 如果不是单一图像,需要栅格化
|
||||
const needsRasterization = !isImage || isGroup;
|
||||
@@ -572,26 +632,34 @@ export class EnhancedLiquifyManager {
|
||||
async _getImageData(fabricObject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
// 创建临时canvas - 关键修复:使用原始图像尺寸,不考虑fabric对象的缩放
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = fabricObject.width * fabricObject.scaleX;
|
||||
tempCanvas.height = fabricObject.height * fabricObject.scaleY;
|
||||
// 使用图像的原始尺寸,而不是缩放后的尺寸
|
||||
tempCanvas.width = fabricObject.width;
|
||||
tempCanvas.height = fabricObject.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 如果对象有图像元素
|
||||
if (fabricObject._element) {
|
||||
// 绘制原始尺寸的图像
|
||||
tempCtx.drawImage(
|
||||
fabricObject._element,
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
} else if (fabricObject.getSrc) {
|
||||
// 通过URL创建图像
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
tempCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
@@ -615,6 +683,13 @@ export class EnhancedLiquifyManager {
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
`获取图像数据: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +
|
||||
`对象缩放=(${fabricObject.scaleX}, ${fabricObject.scaleY}), ` +
|
||||
`图像数据尺寸=${imageData.width}x${imageData.height}`
|
||||
);
|
||||
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* 混合液化管理器 - 根据模式智能选择算法
|
||||
*/
|
||||
export class HybridLiquifyManager {
|
||||
constructor(options = {}) {
|
||||
this.config = {
|
||||
gridSize: options.gridSize || 16,
|
||||
maxStrength: options.maxStrength || 200,
|
||||
smoothingIterations: options.smoothingIterations || 1,
|
||||
relaxFactor: options.relaxFactor || 0.1,
|
||||
};
|
||||
|
||||
this.params = {
|
||||
size: 80,
|
||||
pressure: 0.8,
|
||||
distortion: 0,
|
||||
power: 0.8,
|
||||
};
|
||||
|
||||
this.modes = {
|
||||
PUSH: "push",
|
||||
CLOCKWISE: "clockwise",
|
||||
COUNTERCLOCKWISE: "counterclockwise",
|
||||
PINCH: "pinch",
|
||||
EXPAND: "expand",
|
||||
CRYSTAL: "crystal",
|
||||
EDGE: "edge",
|
||||
RECONSTRUCT: "reconstruct",
|
||||
};
|
||||
|
||||
// 定义哪些模式使用像素算法
|
||||
this.pixelModes = new Set([
|
||||
this.modes.CLOCKWISE,
|
||||
this.modes.COUNTERCLOCKWISE,
|
||||
this.modes.CRYSTAL,
|
||||
this.modes.EDGE,
|
||||
]);
|
||||
|
||||
// 定义哪些模式使用网格算法
|
||||
this.meshModes = new Set([
|
||||
this.modes.PUSH,
|
||||
this.modes.PINCH,
|
||||
this.modes.EXPAND,
|
||||
this.modes.RECONSTRUCT,
|
||||
]);
|
||||
|
||||
this.currentMode = this.modes.PUSH;
|
||||
this.originalImageData = null;
|
||||
this.currentImageData = null;
|
||||
this.mesh = null;
|
||||
this.initialized = false;
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
|
||||
// 持续按压相关状态
|
||||
this.pressStartTime = 0;
|
||||
this.pressDuration = 0;
|
||||
this.accumulatedRotation = 0;
|
||||
this.accumulatedScale = 0;
|
||||
this.isHolding = false;
|
||||
this.continuousTimer = null;
|
||||
|
||||
// 鼠标状态
|
||||
this.initialMouseX = 0;
|
||||
this.initialMouseY = 0;
|
||||
this.currentMouseX = 0;
|
||||
this.currentMouseY = 0;
|
||||
this.isDragging = false;
|
||||
this.dragDistance = 0;
|
||||
}
|
||||
|
||||
// ...existing initialization methods...
|
||||
|
||||
/**
|
||||
* 应用液化变形 - 智能选择算法
|
||||
*/
|
||||
applyDeformation(x, y) {
|
||||
if (!this.initialized || !this.originalImageData) {
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
// 更新鼠标位置
|
||||
this.currentMouseX = x;
|
||||
this.currentMouseY = y;
|
||||
|
||||
const { size, pressure, power } = this.params;
|
||||
const radius = size;
|
||||
const strength = pressure * power;
|
||||
|
||||
// 根据模式选择算法
|
||||
if (this.pixelModes.has(this.currentMode)) {
|
||||
return this._applyPixelDeformation(x, y, radius, strength);
|
||||
} else if (this.meshModes.has(this.currentMode)) {
|
||||
return this._applyMeshDeformation(x, y, radius, strength);
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级液化算法 - 适用于旋转、水晶、边缘模式
|
||||
*/
|
||||
_applyPixelDeformation(centerX, centerY, radius, strength) {
|
||||
const data = this.currentImageData.data;
|
||||
const width = this.currentImageData.width;
|
||||
const height = this.currentImageData.height;
|
||||
const tempData = new Uint8ClampedArray(data);
|
||||
|
||||
const processRadius = Math.min(radius, Math.min(width, height) / 2);
|
||||
|
||||
// 计算影响区域边界
|
||||
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + processRadius));
|
||||
const minY = Math.max(0, Math.floor(centerY - processRadius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + processRadius));
|
||||
|
||||
switch (this.currentMode) {
|
||||
case this.modes.CLOCKWISE:
|
||||
case this.modes.COUNTERCLOCKWISE:
|
||||
this._applyPixelRotation(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength,
|
||||
this.currentMode === this.modes.CLOCKWISE
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.CRYSTAL:
|
||||
this._applyPixelCrystal(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.EDGE:
|
||||
this._applyPixelEdge(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级旋转算法
|
||||
*/
|
||||
_applyPixelRotation(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength,
|
||||
clockwise
|
||||
) {
|
||||
// 计算旋转角度
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
|
||||
const baseRotationSpeed = 0.015;
|
||||
const rotationAngle =
|
||||
(clockwise ? 1 : -1) *
|
||||
baseRotationSpeed *
|
||||
strength *
|
||||
(1.0 + timeFactor * 0.3);
|
||||
|
||||
this.accumulatedRotation += rotationAngle;
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
// 距离衰减:内圈快,外圈慢
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = Math.pow(1 - normalizedDistance, 2); // 二次衰减
|
||||
|
||||
// 计算旋转后的源位置
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const newAngle = angle + this.accumulatedRotation * falloff;
|
||||
|
||||
const sourceX = centerX + Math.cos(newAngle) * distance;
|
||||
const sourceY = centerY + Math.sin(newAngle) * distance;
|
||||
|
||||
// 双线性插值采样
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级水晶效果
|
||||
*/
|
||||
_applyPixelCrystal(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
|
||||
const distortionStrength = strength * (1.0 + timeFactor * 0.5);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const crystalRadius = normalizedDistance;
|
||||
|
||||
// 多层波浪扭曲
|
||||
const wave1 = Math.sin(angle * 8 + this.pressDuration * 0.005) * 0.6;
|
||||
const wave2 = Math.cos(angle * 12 + this.pressDuration * 0.003) * 0.4;
|
||||
const waveAngle =
|
||||
angle + (wave1 + wave2) * distortionStrength * falloff;
|
||||
|
||||
const radialMod =
|
||||
1 +
|
||||
Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) *
|
||||
0.3;
|
||||
const modDistance = distance * radialMod;
|
||||
|
||||
const sourceX = centerX + Math.cos(waveAngle) * modDistance;
|
||||
const sourceY = centerY + Math.sin(waveAngle) * modDistance;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
const factor = falloff * distortionStrength * 0.7;
|
||||
|
||||
// 混合原始颜色和扭曲颜色
|
||||
const originalIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = Math.round(
|
||||
srcData[originalIdx] * (1 - factor) + color[0] * factor
|
||||
);
|
||||
dstData[targetIdx + 1] = Math.round(
|
||||
srcData[originalIdx + 1] * (1 - factor) + color[1] * factor
|
||||
);
|
||||
dstData[targetIdx + 2] = Math.round(
|
||||
srcData[originalIdx + 2] * (1 - factor) + color[2] * factor
|
||||
);
|
||||
dstData[targetIdx + 3] = srcData[originalIdx + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级边缘效果
|
||||
*/
|
||||
_applyPixelEdge(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
|
||||
const edgeStrength = strength * (1.0 + timeFactor * 0.4);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const edgeRadius = normalizedDistance;
|
||||
|
||||
const edgeWave =
|
||||
Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) *
|
||||
Math.cos(angle * 6 + this.pressDuration * 0.002);
|
||||
const perpAngle = angle + Math.PI / 2;
|
||||
|
||||
const edgeFactor = edgeWave * falloff * edgeStrength * 0.5;
|
||||
const offsetX = Math.cos(perpAngle) * edgeFactor;
|
||||
const offsetY = Math.sin(perpAngle) * edgeFactor;
|
||||
|
||||
const sourceX = x + offsetX;
|
||||
const sourceY = y + offsetY;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 双线性插值采样
|
||||
*/
|
||||
_bilinearSample(data, width, height, x, y) {
|
||||
if (x < 0 || x >= width - 1 || y < 0 || y >= height - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x1 = Math.floor(x);
|
||||
const y1 = Math.floor(y);
|
||||
const x2 = x1 + 1;
|
||||
const y2 = y1 + 1;
|
||||
|
||||
const fx = x - x1;
|
||||
const fy = y - y1;
|
||||
|
||||
const getPixel = (px, py) => {
|
||||
const idx = (py * width + px) * 4;
|
||||
return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]];
|
||||
};
|
||||
|
||||
const p1 = getPixel(x1, y1);
|
||||
const p2 = getPixel(x2, y1);
|
||||
const p3 = getPixel(x1, y2);
|
||||
const p4 = getPixel(x2, y2);
|
||||
|
||||
return [
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[0] +
|
||||
fx * (1 - fy) * p2[0] +
|
||||
(1 - fx) * fy * p3[0] +
|
||||
fx * fy * p4[0]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[1] +
|
||||
fx * (1 - fy) * p2[1] +
|
||||
(1 - fx) * fy * p3[1] +
|
||||
fx * fy * p4[1]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[2] +
|
||||
fx * (1 - fy) * p2[2] +
|
||||
(1 - fx) * fy * p3[2] +
|
||||
fx * fy * p4[2]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[3] +
|
||||
fx * (1 - fy) * p2[3] +
|
||||
(1 - fx) * fy * p3[3] +
|
||||
fx * fy * p4[3]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格液化算法 - 适用于推拉、捏合、展开模式
|
||||
*/
|
||||
_applyMeshDeformation(x, y, radius, strength) {
|
||||
if (!this.mesh) return this.currentImageData;
|
||||
|
||||
// 使用现有的网格算法处理推拉、捏合、展开
|
||||
const mode = this.currentMode;
|
||||
const { distortion } = this.params;
|
||||
|
||||
this._applyDeformation(x, y, radius, strength, mode, distortion);
|
||||
|
||||
if (this.config.smoothingIterations > 0) {
|
||||
this._smoothMesh();
|
||||
}
|
||||
|
||||
return this._applyMeshToImage();
|
||||
}
|
||||
|
||||
// ...existing mesh methods...
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ export class LiquifyManager {
|
||||
meshResolution: options.meshResolution || 64,
|
||||
// 根据环境选择合适的渲染模式
|
||||
forceCPU: true, // 默认不强制使用CPU
|
||||
forceWebGL: false, // 优先使用WebGL模式
|
||||
forceWebGL: true, // 优先使用WebGL模式
|
||||
webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL
|
||||
layerManager: options.layerManager || null,
|
||||
canvas: options.canvas || null,
|
||||
@@ -87,9 +87,19 @@ export class LiquifyManager {
|
||||
* @param {Number} value 参数值
|
||||
*/
|
||||
setParam(param, value) {
|
||||
console.log(`LiquifyManager 设置参数: ${param}=${value}`);
|
||||
return this.enhancedManager.setParam(param, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("LiquifyManager 批量设置参数:", params);
|
||||
return this.enhancedManager.setParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -120,21 +130,14 @@ export class LiquifyManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保设置正确的模式和参数
|
||||
if (mode) {
|
||||
this.enhancedManager.setMode(mode);
|
||||
}
|
||||
console.log(
|
||||
`LiquifyManager.applyLiquify: 模式=${mode}, 坐标=(${x}, ${y}), 参数=`,
|
||||
params
|
||||
);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.enhancedManager.setParam(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 应用液化变形
|
||||
console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
|
||||
try {
|
||||
// 直接调用EnhancedLiquifyManager的applyLiquify方法
|
||||
// 避免重复设置参数,让EnhancedLiquifyManager处理参数设置
|
||||
const resultData = await this.enhancedManager.applyLiquify(
|
||||
targetObject,
|
||||
mode,
|
||||
@@ -146,6 +149,13 @@ export class LiquifyManager {
|
||||
// 确保返回结果数据
|
||||
if (!resultData) {
|
||||
console.warn("液化变形没有返回结果数据");
|
||||
} else {
|
||||
console.log(
|
||||
"✅ 液化变形成功,返回图像数据尺寸:",
|
||||
resultData.width,
|
||||
"x",
|
||||
resultData.height
|
||||
);
|
||||
}
|
||||
|
||||
return resultData;
|
||||
@@ -180,6 +190,22 @@ export class LiquifyManager {
|
||||
return this.enhancedManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
return this.enhancedManager.startLiquifyOperation(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
return this.enhancedManager.endLiquifyOperation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 液化实时更新器
|
||||
* 负责高效地更新液化效果到画布上,避免频繁创建fabric对象导致的性能问题
|
||||
*/
|
||||
export class LiquifyRealTimeUpdater {
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.targetObject = null;
|
||||
this.isUpdating = false;
|
||||
this.updateQueue = [];
|
||||
this.lastUpdateTime = 0;
|
||||
this.currImage = options.currImage || { value: null };
|
||||
|
||||
// 配置选项
|
||||
this.config = {
|
||||
throttleTime: options.throttleTime || 16, // 60fps
|
||||
maxQueueSize: options.maxQueueSize || 5,
|
||||
useDirectUpdate: options.useDirectUpdate !== false, // 默认启用直接更新
|
||||
imageQuality: options.imageQuality || 1.0, // 图像质量 (0.1-1.0)
|
||||
skipRenderDuringDrag: options.skipRenderDuringDrag || false, // 拖拽时跳过渲染
|
||||
};
|
||||
|
||||
// 临时canvas用于快速渲染
|
||||
this.tempCanvas = document.createElement("canvas");
|
||||
this.tempCtx = this.tempCanvas.getContext("2d");
|
||||
|
||||
// 高质量canvas用于最终输出
|
||||
this.highQualityCanvas = document.createElement("canvas");
|
||||
this.highQualityCtx = this.highQualityCanvas.getContext("2d");
|
||||
|
||||
// 当前缓存的图像数据
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
this.renderingScheduled = false;
|
||||
|
||||
// 优化Canvas画布渲染设置
|
||||
this.canvas.renderOnAddRemove = false; // 禁用自动渲染
|
||||
this.canvas.skipOffscreen = true; // 跳过离屏元素渲染
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置目标对象
|
||||
* @param {Object} fabricObject fabric图像对象
|
||||
*/
|
||||
setTargetObject(fabricObject) {
|
||||
this.targetObject = fabricObject;
|
||||
if (fabricObject && fabricObject._element) {
|
||||
// 设置临时canvas尺寸
|
||||
this.tempCanvas.width = fabricObject.width;
|
||||
this.tempCanvas.height = fabricObject.height;
|
||||
|
||||
// 设置高质量canvas尺寸
|
||||
this.highQualityCanvas.width = fabricObject.width;
|
||||
this.highQualityCanvas.height = fabricObject.height;
|
||||
|
||||
// 配置高质量渲染上下文
|
||||
this.highQualityCtx.imageSmoothingEnabled = true;
|
||||
this.highQualityCtx.imageSmoothingQuality = "high";
|
||||
|
||||
// 配置临时canvas上下文(快速渲染)
|
||||
this.tempCtx.imageSmoothingEnabled = false; // 快速模式关闭平滑
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时更新图像数据到画布
|
||||
* @param {ImageData} imageData 新的图像数据
|
||||
* @param {Boolean} isDrawing 是否正在绘制(拖拽过程中)
|
||||
* @returns {Promise} 更新完成的Promise
|
||||
*/
|
||||
async updateImage(imageData, isDrawing = false) {
|
||||
if (!this.targetObject || !imageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 节流控制
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdateTime < this.config.throttleTime && isDrawing) {
|
||||
// 在绘制过程中进行节流,缓存最新的图像数据
|
||||
this.pendingImageData = imageData;
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdateTime = now;
|
||||
|
||||
if (isDrawing && this.config.useDirectUpdate) {
|
||||
// 拖拽过程中使用快速更新(降低质量以提高性能)
|
||||
this._fastUpdate(imageData);
|
||||
} else {
|
||||
// 拖拽结束后使用完整更新(最高质量)
|
||||
await this._fullUpdate(imageData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能图像质量更新
|
||||
* 根据图像尺寸和设备性能动态调整质量
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @param {Boolean} isDrawing 是否正在绘制
|
||||
* @private
|
||||
*/
|
||||
_getOptimalQuality(imageData, isDrawing) {
|
||||
const pixelCount = imageData.width * imageData.height;
|
||||
|
||||
if (isDrawing) {
|
||||
// 拖拽时根据图像大小调整质量
|
||||
if (pixelCount > 1000000) {
|
||||
// 大于1M像素
|
||||
return 0.7;
|
||||
} else if (pixelCount > 500000) {
|
||||
// 大于500K像素
|
||||
return 0.8;
|
||||
} else {
|
||||
return 0.9;
|
||||
}
|
||||
} else {
|
||||
// 拖拽结束时始终使用最高质量
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速更新 - 直接修改现有对象的图像源
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
_fastUpdate(imageData) {
|
||||
if (!this.targetObject || !this.targetObject._element) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 将ImageData渲染到临时canvas(快速模式)
|
||||
this.tempCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 获取智能质量设置
|
||||
const quality = this._getOptimalQuality(imageData, true);
|
||||
|
||||
// 直接更新fabric对象的图像源(使用PNG格式保持质量)
|
||||
const targetElement = this.targetObject._element;
|
||||
|
||||
// 方案1: 直接设置src属性(最高性能)
|
||||
const dataURL = this.tempCanvas.toDataURL("image/png", quality);
|
||||
|
||||
if (targetElement.src !== dataURL) {
|
||||
targetElement.src = dataURL;
|
||||
|
||||
// 关键优化:直接设置fabric对象为脏状态,但不立即渲染
|
||||
// this.targetObject.dirty = false; // 标记为不需要立即渲染
|
||||
// this.canvas.renderOnAddRemove = true; // 恢复自动渲染
|
||||
// this.renderingScheduled = false; // 重置渲染调度状态
|
||||
this?.scheduleRender?.(); // 调度一次渲染
|
||||
// 使用requestAnimationFrame进行批量渲染优化
|
||||
// if (!this.renderingScheduled && !this.config.skipRenderDuringDrag) {
|
||||
// this.renderingScheduled = true;
|
||||
// requestIdleCallback(() => {
|
||||
// this.canvas.renderAll();
|
||||
// this.renderingScheduled = false;
|
||||
// });
|
||||
// }
|
||||
} else {
|
||||
console.warn(
|
||||
"=================快速更新液化效果时,图像数据未变化,跳过更新"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("快速更新液化效果失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
getImageData(imageData) {
|
||||
// 使用高质量canvas进行最终渲染
|
||||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 生成高质量DataURL(PNG格式,最大质量)
|
||||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整更新 - 创建新的fabric对象
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
async _fullUpdate(imageData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 使用高质量canvas进行最终渲染
|
||||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 生成高质量DataURL(PNG格式,最大质量)
|
||||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||||
|
||||
// 如果DataURL没有变化,跳过更新
|
||||
if (this.cachedDataURL === dataURL) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedDataURL = dataURL;
|
||||
|
||||
// 创建新的fabric图像对象,保持最高质量
|
||||
fabric.Image.fromURL(
|
||||
dataURL,
|
||||
(newImg) => {
|
||||
try {
|
||||
if (!this.targetObject) {
|
||||
console.warn("目标对象为空,跳过更新");
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存原对象信息用于智能查找
|
||||
const originalObjId = this.targetObject.id;
|
||||
const originalObjLayerId = this.targetObject.layerId;
|
||||
|
||||
// 保留原对象的所有变换属性
|
||||
const originalObj = this.targetObject;
|
||||
newImg.set({
|
||||
left: originalObj.left,
|
||||
top: originalObj.top,
|
||||
scaleX: originalObj.scaleX,
|
||||
scaleY: originalObj.scaleY,
|
||||
angle: originalObj.angle,
|
||||
flipX: originalObj.flipX,
|
||||
flipY: originalObj.flipY,
|
||||
opacity: originalObj.opacity,
|
||||
originX: originalObj.originX,
|
||||
originY: originalObj.originY,
|
||||
id: originalObj.id,
|
||||
name: originalObj.name,
|
||||
layerId: originalObj.layerId,
|
||||
selected: false,
|
||||
evented: originalObj.evented,
|
||||
});
|
||||
|
||||
// 临时禁用画布自动渲染
|
||||
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
|
||||
// 智能查找和替换canvas上的对象
|
||||
const allObjects = this.canvas.getObjects();
|
||||
let targetIndex = allObjects.indexOf(originalObj);
|
||||
|
||||
// 如果直接查找失败,尝试通过ID查找
|
||||
if (targetIndex === -1 && originalObjId) {
|
||||
targetIndex = allObjects.findIndex(
|
||||
(obj) => obj.id === originalObjId
|
||||
);
|
||||
if (targetIndex !== -1) {
|
||||
console.log(`通过ID找到目标对象: ${originalObjId}`);
|
||||
// 更新目标对象引用
|
||||
this.targetObject = allObjects[targetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果通过ID查找仍然失败,尝试通过图层ID查找
|
||||
if (targetIndex === -1 && originalObjLayerId) {
|
||||
targetIndex = allObjects.findIndex(
|
||||
(obj) => obj.layerId === originalObjLayerId
|
||||
);
|
||||
if (targetIndex !== -1) {
|
||||
console.log(`通过图层ID找到目标对象: ${originalObjLayerId}`);
|
||||
// 更新目标对象引用
|
||||
this.targetObject = allObjects[targetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
// 找到目标对象,执行替换
|
||||
this.canvas.remove(this.targetObject);
|
||||
this.canvas.insertAt(newImg, targetIndex);
|
||||
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
|
||||
// 更新目标对象引用
|
||||
this.targetObject = newImg;
|
||||
|
||||
// 一次性重新渲染画布
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log(`✅ 液化对象更新成功,位置: ${targetIndex}`);
|
||||
resolve(newImg);
|
||||
} else {
|
||||
// 如果在画布中找不到对象,可能对象已被移除或引用已更新
|
||||
console.warn(
|
||||
"在画布中找不到目标对象,可能已被其他操作移除或替换"
|
||||
);
|
||||
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
|
||||
// 尝试添加新对象到画布末尾
|
||||
this.canvas.add(newImg);
|
||||
this.targetObject = newImg;
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log("🔄 已将新对象添加到画布末尾");
|
||||
resolve(newImg);
|
||||
}
|
||||
} catch (error) {
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
console.error("更新fabric对象时出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
); // 确保跨域支持
|
||||
} catch (error) {
|
||||
console.error("完整更新过程出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理待处理的图像数据
|
||||
* 在拖拽结束后调用,处理可能积压的更新
|
||||
*/
|
||||
async processPendingUpdates() {
|
||||
if (this.pendingImageData && !this.isUpdating) {
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
await this._fullUpdate(this.pendingImageData);
|
||||
this.pendingImageData = null;
|
||||
} catch (error) {
|
||||
console.error("处理待处理更新失败:", error);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this.targetObject = null;
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
this.updateQueue.length = 0;
|
||||
|
||||
// 清理临时canvas
|
||||
if (this.tempCanvas) {
|
||||
this.tempCanvas.width = 0;
|
||||
this.tempCanvas.height = 0;
|
||||
this.tempCanvas = null;
|
||||
this.tempCtx = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
* @returns {Object} 当前的fabric对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.targetObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制进行完整更新
|
||||
* @param {ImageData} imageData 图像数据
|
||||
*/
|
||||
async forceFullUpdate(imageData) {
|
||||
return this._fullUpdate(imageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用拖拽模式 - 暂停渲染以提高性能
|
||||
*/
|
||||
enableDragMode() {
|
||||
this.config.skipRenderDuringDrag = true;
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
console.log("🚀 启用拖拽优化模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用拖拽模式 - 恢复正常渲染
|
||||
*/
|
||||
disableDragMode() {
|
||||
this.config.skipRenderDuringDrag = false;
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
|
||||
// 执行一次完整渲染
|
||||
this.canvas.renderAll();
|
||||
console.log("✅ 恢复正常渲染模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.targetObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图像质量
|
||||
* @param {Number} quality 质量值 (0.1-1.0)
|
||||
*/
|
||||
setImageQuality(quality) {
|
||||
this.config.imageQuality = Math.max(0.1, Math.min(1.0, quality));
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化的批量渲染方法
|
||||
*/
|
||||
scheduleRender() {
|
||||
if (!this.renderingScheduled) {
|
||||
this.renderingScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.canvas.renderAll();
|
||||
this.renderingScheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
// 恢复canvas设置
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
|
||||
// 清理缓存
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
|
||||
// 清理canvas
|
||||
if (this.tempCanvas) {
|
||||
this.tempCanvas.width = 0;
|
||||
this.tempCanvas.height = 0;
|
||||
}
|
||||
|
||||
if (this.highQualityCanvas) {
|
||||
this.highQualityCanvas.width = 0;
|
||||
this.highQualityCanvas.height = 0;
|
||||
}
|
||||
|
||||
console.log("🧹 液化实时更新器资源已清理");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 液化面板状态管理器
|
||||
* 负责管理液化操作的状态、性能优化和用户反馈
|
||||
*/
|
||||
export class LiquifyStateManager {
|
||||
constructor(canvas, realtimeUpdater) {
|
||||
this.canvas = canvas;
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
|
||||
// 状态管理
|
||||
this.isOperating = false;
|
||||
this.isDragging = false;
|
||||
this.operationCount = 0;
|
||||
this.startTime = null;
|
||||
|
||||
// 性能监控
|
||||
this.performanceMetrics = {
|
||||
totalOperations: 0,
|
||||
totalTime: 0,
|
||||
averageTime: 0,
|
||||
maxTime: 0,
|
||||
minTime: Infinity,
|
||||
lastOperationTime: 0,
|
||||
};
|
||||
|
||||
// 用户反馈
|
||||
this.feedbackEnabled = true;
|
||||
this.cursorCache = new Map();
|
||||
|
||||
// 设备性能检测
|
||||
this.devicePerformance = this._detectDevicePerformance();
|
||||
|
||||
console.log(
|
||||
"🎯 液化状态管理器已初始化,设备性能等级:",
|
||||
this.devicePerformance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作
|
||||
*/
|
||||
startOperation() {
|
||||
if (this.isOperating) return;
|
||||
|
||||
this.isOperating = true;
|
||||
this.startTime = performance.now();
|
||||
|
||||
// 根据设备性能调整设置
|
||||
this._adjustPerformanceSettings();
|
||||
|
||||
// 显示操作反馈
|
||||
this._showOperationFeedback();
|
||||
|
||||
console.log("🚀 开始液化操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始拖拽
|
||||
*/
|
||||
startDrag() {
|
||||
if (this.isDragging) return;
|
||||
|
||||
this.isDragging = true;
|
||||
|
||||
// 优化拖拽性能
|
||||
this.realtimeUpdater?.enableDragMode();
|
||||
|
||||
// 更新鼠标样式
|
||||
this._updateCursor("liquifying");
|
||||
|
||||
// 禁用不必要的画布功能
|
||||
this._disableCanvasFeatures();
|
||||
|
||||
console.log("🖱️ 开始拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束拖拽
|
||||
*/
|
||||
async endDrag() {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
// 恢复鼠标样式
|
||||
this._updateCursor("default");
|
||||
|
||||
// 恢复画布功能
|
||||
this._enableCanvasFeatures();
|
||||
|
||||
// 处理待处理的更新
|
||||
if (this.realtimeUpdater) {
|
||||
try {
|
||||
await this.realtimeUpdater.processPendingUpdates();
|
||||
} catch (error) {
|
||||
console.error("处理待处理更新失败:", error);
|
||||
} finally {
|
||||
this.isDragging = false;
|
||||
// 恢复正常模式
|
||||
this.realtimeUpdater?.disableDragMode();
|
||||
|
||||
// 结束液化操作 添加结果到命令中 更新当前激活图层对象
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ 结束拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endOperation() {
|
||||
if (!this.isOperating) return;
|
||||
|
||||
const operationTime = performance.now() - this.startTime;
|
||||
this._updatePerformanceMetrics(operationTime);
|
||||
|
||||
this.isOperating = false;
|
||||
this.operationCount++;
|
||||
|
||||
// 隐藏操作反馈
|
||||
this._hideOperationFeedback();
|
||||
|
||||
console.log(`⏱️ 液化操作完成,耗时: ${operationTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录单次变形操作
|
||||
*/
|
||||
recordDeformation(operationTime) {
|
||||
this.performanceMetrics.totalOperations++;
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime /
|
||||
this.performanceMetrics.totalOperations;
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.lastOperationTime = operationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作性能指标
|
||||
* @param {Object} metrics 性能指标对象
|
||||
*/
|
||||
recordOperationMetrics(metrics) {
|
||||
const {
|
||||
operationTime,
|
||||
operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
} = metrics;
|
||||
|
||||
// 记录基础性能数据
|
||||
this.recordDeformation(operationTime);
|
||||
|
||||
// 记录详细操作信息
|
||||
this.performanceMetrics.lastOperation = {
|
||||
type: operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 根据性能数据动态调整设置
|
||||
this._adaptivePerformanceOptimization(operationTime);
|
||||
|
||||
console.log(
|
||||
`📊 记录性能指标: ${operationType}/${mode}, 耗时: ${operationTime.toFixed(
|
||||
2
|
||||
)}ms, 渲染模式: ${renderMode}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应性能优化
|
||||
* @param {Number} operationTime 操作耗时
|
||||
* @private
|
||||
*/
|
||||
_adaptivePerformanceOptimization(operationTime) {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
// 如果操作耗时过长,动态降低质量或增加节流时间
|
||||
if (operationTime > 50 && this.devicePerformance !== "high") {
|
||||
// 降低图像质量
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality > 0.7) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.max(0.7, currentQuality - 0.1)
|
||||
);
|
||||
console.log("⚡ 自动降低图像质量以提升性能");
|
||||
}
|
||||
|
||||
// 增加节流时间
|
||||
if (this.realtimeUpdater.config.throttleTime < 33) {
|
||||
this.realtimeUpdater.config.throttleTime = Math.min(
|
||||
33,
|
||||
this.realtimeUpdater.config.throttleTime + 8
|
||||
);
|
||||
console.log("⏱️ 自动增加节流时间以提升性能");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果性能很好,可以适当提高质量
|
||||
if (operationTime < 20 && this.devicePerformance === "high") {
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality < 1.0) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.min(1.0, currentQuality + 0.05)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
*/
|
||||
getPerformanceReport() {
|
||||
return {
|
||||
...this.performanceMetrics,
|
||||
devicePerformance: this.devicePerformance,
|
||||
fps: this._calculateFPS(),
|
||||
recommendations: this._generateRecommendations(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户反馈
|
||||
*/
|
||||
setFeedbackEnabled(enabled) {
|
||||
this.feedbackEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this._enableCanvasFeatures();
|
||||
this._updateCursor("default");
|
||||
this.cursorCache.clear();
|
||||
|
||||
console.log("🧹 液化状态管理器已清理");
|
||||
}
|
||||
|
||||
// === 私有方法 ===
|
||||
|
||||
/**
|
||||
* 检测设备性能
|
||||
*/
|
||||
_detectDevicePerformance() {
|
||||
// 检测硬件并发数
|
||||
const cores = navigator.hardwareConcurrency || 4;
|
||||
|
||||
// 检测内存
|
||||
const memory = navigator.deviceMemory || 4;
|
||||
|
||||
// 检测连接类型
|
||||
const connection = navigator.connection;
|
||||
const effectiveType = connection?.effectiveType || "4g";
|
||||
|
||||
// 简单的性能评分算法
|
||||
let score = 0;
|
||||
score += cores * 10;
|
||||
score += memory * 5;
|
||||
|
||||
if (effectiveType === "4g") score += 10;
|
||||
else if (effectiveType === "3g") score += 5;
|
||||
|
||||
// 性能等级
|
||||
if (score >= 70) return "high";
|
||||
if (score >= 40) return "medium";
|
||||
return "low";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备性能调整设置
|
||||
*/
|
||||
_adjustPerformanceSettings() {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
switch (this.devicePerformance) {
|
||||
case "high":
|
||||
this.realtimeUpdater.setImageQuality(1.0);
|
||||
this.realtimeUpdater.config.throttleTime = 8; // 120fps
|
||||
break;
|
||||
case "medium":
|
||||
this.realtimeUpdater.setImageQuality(0.9);
|
||||
this.realtimeUpdater.config.throttleTime = 16; // 60fps
|
||||
break;
|
||||
case "low":
|
||||
this.realtimeUpdater.setImageQuality(0.8);
|
||||
this.realtimeUpdater.config.throttleTime = 33; // 30fps
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示操作反馈
|
||||
*/
|
||||
_showOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
// 添加视觉反馈(可以是加载动画、进度条等)
|
||||
document.body.style.cursor = "wait";
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏操作反馈
|
||||
*/
|
||||
_hideOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
document.body.style.cursor = "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新鼠标样式
|
||||
*/
|
||||
_updateCursor(type) {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
const cursors = {
|
||||
default: "default",
|
||||
liquifying: "crosshair",
|
||||
wait: "wait",
|
||||
"not-allowed": "not-allowed",
|
||||
};
|
||||
|
||||
const cursor = cursors[type] || "default";
|
||||
|
||||
if (this.canvas && this.canvas.upperCanvasEl) {
|
||||
this.canvas.upperCanvasEl.style.cursor = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用画布功能以提高性能
|
||||
*/
|
||||
_disableCanvasFeatures() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 保存原始设置
|
||||
this._originalSettings = {
|
||||
renderOnAddRemove: this.canvas.renderOnAddRemove,
|
||||
skipOffscreen: this.canvas.skipOffscreen,
|
||||
enableRetinaScaling: this.canvas.enableRetinaScaling,
|
||||
};
|
||||
|
||||
// 应用性能优化设置
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
this.canvas.skipOffscreen = true;
|
||||
|
||||
// 低性能设备关闭高分辨率支持
|
||||
if (this.devicePerformance === "low") {
|
||||
this.canvas.enableRetinaScaling = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复画布功能
|
||||
*/
|
||||
_enableCanvasFeatures() {
|
||||
if (!this.canvas || !this._originalSettings) return;
|
||||
|
||||
// 恢复原始设置
|
||||
this.canvas.renderOnAddRemove = this._originalSettings.renderOnAddRemove;
|
||||
this.canvas.skipOffscreen = this._originalSettings.skipOffscreen;
|
||||
this.canvas.enableRetinaScaling =
|
||||
this._originalSettings.enableRetinaScaling;
|
||||
|
||||
this._originalSettings = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新性能指标
|
||||
*/
|
||||
_updatePerformanceMetrics(operationTime) {
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime / (this.operationCount + 1);
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算FPS
|
||||
*/
|
||||
_calculateFPS() {
|
||||
if (this.performanceMetrics.averageTime === 0) return 0;
|
||||
return Math.round(1000 / this.performanceMetrics.averageTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成性能建议
|
||||
*/
|
||||
_generateRecommendations() {
|
||||
const recommendations = [];
|
||||
|
||||
if (this.performanceMetrics.averageTime > 50) {
|
||||
recommendations.push("操作响应较慢,建议降低图像尺寸或关闭高质量模式");
|
||||
}
|
||||
|
||||
if (this.devicePerformance === "low") {
|
||||
recommendations.push("检测到低性能设备,已自动启用性能优化模式");
|
||||
}
|
||||
|
||||
const fps = this._calculateFPS();
|
||||
if (fps < 30) {
|
||||
recommendations.push("帧率较低,建议减少同时进行的操作或降低液化强度");
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 小地图管理器类
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { generateId } from "../../utils/helper";
|
||||
import { OperationType } from "../../utils/layerHelper";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
/**
|
||||
* 液化功能集成测试
|
||||
* 用于在浏览器环境中测试完整的液化工作流程
|
||||
*/
|
||||
|
||||
import { LiquifyCPUManager } from "../managers/liquify/LiquifyCPUManager.js";
|
||||
import { LiquifyWebGLManager } from "../managers/liquify/LiquifyWebGLManager.js";
|
||||
import { LiquifyRealTimeUpdater } from "../managers/liquify/LiquifyRealTimeUpdater.js";
|
||||
import { EnhancedLiquifyManager } from "../managers/liquify/EnhancedLiquifyManager.js";
|
||||
|
||||
// 集成测试结果
|
||||
let testResults = {
|
||||
totalTests: 0,
|
||||
passedTests: 0,
|
||||
failedTests: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建测试图像数据
|
||||
*/
|
||||
function createTestImageData(width = 100, height = 100) {
|
||||
const imageData = new ImageData(width, height);
|
||||
const data = imageData.data;
|
||||
|
||||
// 创建一个简单的渐变图案用于测试
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = (y * width + x) * 4;
|
||||
data[index] = (x / width) * 255; // Red
|
||||
data[index + 1] = (y / height) * 255; // Green
|
||||
data[index + 2] = 128; // Blue
|
||||
data[index + 3] = 255; // Alpha
|
||||
}
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试Canvas
|
||||
*/
|
||||
function createTestCanvas(width = 200, height = 200) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟Fabric对象
|
||||
*/
|
||||
function createMockFabricObject(imageData) {
|
||||
return {
|
||||
left: 50,
|
||||
top: 50,
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
_element: null,
|
||||
calcTransformMatrix: function () {
|
||||
return [this.scaleX, 0, 0, this.scaleY, this.left, this.top];
|
||||
},
|
||||
set: function (props) {
|
||||
Object.assign(this, props);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行单个测试
|
||||
*/
|
||||
async function runTest(testName, testFunction) {
|
||||
testResults.totalTests++;
|
||||
|
||||
try {
|
||||
console.log(`🧪 运行测试: ${testName}`);
|
||||
await testFunction();
|
||||
testResults.passedTests++;
|
||||
console.log(`✅ 测试通过: ${testName}`);
|
||||
} catch (error) {
|
||||
testResults.failedTests++;
|
||||
testResults.errors.push({ testName, error: error.message });
|
||||
console.error(`❌ 测试失败: ${testName}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试CPU管理器基本功能
|
||||
*/
|
||||
async function testCPUManagerBasics() {
|
||||
const manager = new LiquifyCPUManager();
|
||||
const testImageData = createTestImageData();
|
||||
|
||||
// 初始化
|
||||
manager.initialize(testImageData);
|
||||
|
||||
// 设置参数
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
|
||||
// 设置模式
|
||||
manager.setMode("push");
|
||||
|
||||
// 应用变形
|
||||
const result = manager.applyDeformation(50, 50);
|
||||
|
||||
if (
|
||||
!result ||
|
||||
result.width !== testImageData.width ||
|
||||
result.height !== testImageData.height
|
||||
) {
|
||||
throw new Error("CPU管理器变形结果无效");
|
||||
}
|
||||
|
||||
// 测试不同模式
|
||||
const modes = [
|
||||
"push",
|
||||
"clockwise",
|
||||
"counterclockwise",
|
||||
"pinch",
|
||||
"expand",
|
||||
"reconstruct",
|
||||
];
|
||||
for (const mode of modes) {
|
||||
manager.setMode(mode);
|
||||
const modeResult = manager.applyDeformation(25, 25);
|
||||
if (!modeResult) {
|
||||
throw new Error(`模式 ${mode} 变形失败`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试WebGL管理器基本功能
|
||||
*/
|
||||
async function testWebGLManagerBasics() {
|
||||
const manager = new LiquifyWebGLManager();
|
||||
const testImageData = createTestImageData();
|
||||
|
||||
try {
|
||||
// 初始化
|
||||
manager.initialize(testImageData);
|
||||
|
||||
// 检查WebGL是否可用
|
||||
if (!manager.initialized) {
|
||||
console.warn("WebGL不可用,跳过WebGL测试");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置参数
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
|
||||
// 设置模式
|
||||
manager.setMode("push");
|
||||
|
||||
// 应用变形
|
||||
const result = manager.applyDeformation(50, 50);
|
||||
|
||||
if (
|
||||
!result ||
|
||||
result.width !== testImageData.width ||
|
||||
result.height !== testImageData.height
|
||||
) {
|
||||
throw new Error("WebGL管理器变形结果无效");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message.includes("WebGL")) {
|
||||
console.warn("WebGL不支持,跳过WebGL测试");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试实时更新器功能
|
||||
*/
|
||||
async function testRealTimeUpdater() {
|
||||
const canvas = createTestCanvas();
|
||||
const testImageData = createTestImageData();
|
||||
const mockFabricObject = createMockFabricObject(testImageData);
|
||||
|
||||
// 模拟fabric Canvas
|
||||
const fabricCanvas = {
|
||||
getObjects: () => [mockFabricObject],
|
||||
remove: () => {},
|
||||
insertAt: () => {},
|
||||
renderAll: () => {},
|
||||
};
|
||||
|
||||
const updater = new LiquifyRealTimeUpdater(fabricCanvas);
|
||||
|
||||
// 设置目标对象
|
||||
updater.setTargetObject(mockFabricObject);
|
||||
|
||||
// 测试快速更新
|
||||
await updater.updateImage(testImageData, true);
|
||||
|
||||
// 测试完整更新
|
||||
await updater.updateImage(testImageData, false);
|
||||
|
||||
// 测试待处理更新
|
||||
updater.pendingImageData = testImageData;
|
||||
await updater.processPendingUpdates();
|
||||
|
||||
// 清理
|
||||
updater.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试增强液化管理器
|
||||
*/
|
||||
async function testEnhancedLiquifyManager() {
|
||||
const manager = new EnhancedLiquifyManager();
|
||||
const testImageData = createTestImageData();
|
||||
const mockFabricObject = createMockFabricObject(testImageData);
|
||||
|
||||
// 初始化
|
||||
const result = await manager.prepareForLiquify(mockFabricObject);
|
||||
|
||||
if (!result || !result.originalImageData) {
|
||||
throw new Error("增强液化管理器初始化失败");
|
||||
}
|
||||
|
||||
// 设置参数
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
|
||||
// 应用液化
|
||||
const liquifyResult = await manager.applyLiquify(
|
||||
mockFabricObject,
|
||||
"push",
|
||||
{ size: 50, pressure: 0.5, distortion: 0.3, power: 0.8 },
|
||||
50,
|
||||
50
|
||||
);
|
||||
|
||||
if (!liquifyResult) {
|
||||
throw new Error("液化应用失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试坐标转换准确性
|
||||
*/
|
||||
async function testCoordinateConversion() {
|
||||
const testImageData = createTestImageData(200, 200);
|
||||
const mockFabricObject = createMockFabricObject(testImageData);
|
||||
|
||||
// 测试不同的缩放情况
|
||||
const testCases = [
|
||||
{ scaleX: 1, scaleY: 1, flipX: false, flipY: false },
|
||||
{ scaleX: 2, scaleY: 2, flipX: false, flipY: false },
|
||||
{ scaleX: 0.5, scaleY: 0.5, flipX: false, flipY: false },
|
||||
{ scaleX: 1, scaleY: 1, flipX: true, flipY: false },
|
||||
{ scaleX: 1, scaleY: 1, flipX: false, flipY: true },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
Object.assign(mockFabricObject, testCase);
|
||||
|
||||
// 模拟坐标转换函数(简化版)
|
||||
const fabricX = 100,
|
||||
fabricY = 100;
|
||||
|
||||
// 创建变换矩阵模拟
|
||||
const transform = mockFabricObject.calcTransformMatrix();
|
||||
|
||||
// 基本的坐标边界检查
|
||||
const localX = (fabricX - mockFabricObject.left) / mockFabricObject.scaleX;
|
||||
const localY = (fabricY - mockFabricObject.top) / mockFabricObject.scaleY;
|
||||
|
||||
if (
|
||||
localX < -mockFabricObject.width / 2 ||
|
||||
localX > mockFabricObject.width / 2 ||
|
||||
localY < -mockFabricObject.height / 2 ||
|
||||
localY > mockFabricObject.height / 2
|
||||
) {
|
||||
// 坐标在对象外部,这是正常情况
|
||||
continue;
|
||||
}
|
||||
|
||||
// 转换到图像坐标
|
||||
let imageX =
|
||||
(localX + mockFabricObject.width / 2) *
|
||||
(testImageData.width / mockFabricObject.width);
|
||||
let imageY =
|
||||
(localY + mockFabricObject.height / 2) *
|
||||
(testImageData.height / mockFabricObject.height);
|
||||
|
||||
// 处理翻转
|
||||
if (mockFabricObject.flipX) {
|
||||
imageX = testImageData.width - imageX;
|
||||
}
|
||||
if (mockFabricObject.flipY) {
|
||||
imageY = testImageData.height - imageY;
|
||||
}
|
||||
|
||||
// 验证结果在合理范围内
|
||||
if (
|
||||
imageX < 0 ||
|
||||
imageX >= testImageData.width ||
|
||||
imageY < 0 ||
|
||||
imageY >= testImageData.height
|
||||
) {
|
||||
throw new Error(`坐标转换结果超出图像范围: (${imageX}, ${imageY})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试性能表现
|
||||
*/
|
||||
async function testPerformance() {
|
||||
const manager = new LiquifyCPUManager();
|
||||
const testImageData = createTestImageData(400, 400); // 更大的图像
|
||||
|
||||
manager.initialize(testImageData);
|
||||
manager.setParams({
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0.3,
|
||||
power: 0.8,
|
||||
});
|
||||
manager.setMode("push");
|
||||
|
||||
// 测试多次操作的性能
|
||||
const startTime = performance.now();
|
||||
const operationCount = 100;
|
||||
|
||||
for (let i = 0; i < operationCount; i++) {
|
||||
const x = Math.random() * testImageData.width;
|
||||
const y = Math.random() * testImageData.height;
|
||||
manager.applyDeformation(x, y);
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const avgTime = totalTime / operationCount;
|
||||
|
||||
console.log(
|
||||
`性能测试结果: ${operationCount} 次操作,总耗时 ${totalTime.toFixed(
|
||||
2
|
||||
)}ms,平均 ${avgTime.toFixed(2)}ms/次`
|
||||
);
|
||||
|
||||
// 验证性能阈值(每次操作不应超过50ms)
|
||||
if (avgTime > 50) {
|
||||
throw new Error(
|
||||
`性能不达标:平均操作时间 ${avgTime.toFixed(2)}ms 超过阈值 50ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试内存管理
|
||||
*/
|
||||
async function testMemoryManagement() {
|
||||
const initialMemory = performance.memory
|
||||
? performance.memory.usedJSHeapSize
|
||||
: 0;
|
||||
|
||||
// 创建多个管理器实例
|
||||
const managers = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const manager = new LiquifyCPUManager();
|
||||
const testImageData = createTestImageData(200, 200);
|
||||
manager.initialize(testImageData);
|
||||
managers.push(manager);
|
||||
}
|
||||
|
||||
// 销毁所有管理器
|
||||
for (const manager of managers) {
|
||||
if (manager.destroy) {
|
||||
manager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// 强制垃圾回收(如果可用)
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
}
|
||||
|
||||
const finalMemory = performance.memory
|
||||
? performance.memory.usedJSHeapSize
|
||||
: 0;
|
||||
const memoryIncrease = finalMemory - initialMemory;
|
||||
|
||||
console.log(
|
||||
`内存测试: 初始 ${(initialMemory / 1024 / 1024).toFixed(2)}MB, 最终 ${(
|
||||
finalMemory /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)}MB, 增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`
|
||||
);
|
||||
|
||||
// 验证内存增长不超过10MB(基本的内存泄漏检查)
|
||||
if (memoryIncrease > 10 * 1024 * 1024) {
|
||||
console.warn(
|
||||
`潜在内存泄漏: 内存增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行所有集成测试
|
||||
*/
|
||||
export async function runLiquifyIntegrationTests() {
|
||||
console.log("🚀 开始液化功能集成测试...");
|
||||
|
||||
// 重置测试结果
|
||||
testResults = {
|
||||
totalTests: 0,
|
||||
passedTests: 0,
|
||||
failedTests: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
// 基础功能测试
|
||||
await runTest("CPU管理器基本功能", testCPUManagerBasics);
|
||||
await runTest("WebGL管理器基本功能", testWebGLManagerBasics);
|
||||
await runTest("实时更新器功能", testRealTimeUpdater);
|
||||
await runTest("增强液化管理器", testEnhancedLiquifyManager);
|
||||
|
||||
// 高级功能测试
|
||||
await runTest("坐标转换准确性", testCoordinateConversion);
|
||||
await runTest("性能表现", testPerformance);
|
||||
await runTest("内存管理", testMemoryManagement);
|
||||
} catch (error) {
|
||||
console.error("集成测试出现严重错误:", error);
|
||||
}
|
||||
|
||||
// 输出测试结果
|
||||
console.log("\n📊 液化功能集成测试结果:");
|
||||
console.log(`总测试数: ${testResults.totalTests}`);
|
||||
console.log(`通过: ${testResults.passedTests}`);
|
||||
console.log(`失败: ${testResults.failedTests}`);
|
||||
console.log(
|
||||
`成功率: ${(
|
||||
(testResults.passedTests / testResults.totalTests) *
|
||||
100
|
||||
).toFixed(1)}%`
|
||||
);
|
||||
|
||||
if (testResults.errors.length > 0) {
|
||||
console.log("\n❌ 失败的测试:");
|
||||
testResults.errors.forEach(({ testName, error }) => {
|
||||
console.log(` - ${testName}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
return testResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在浏览器控制台中运行测试
|
||||
*/
|
||||
if (typeof window !== "undefined") {
|
||||
window.runLiquifyIntegrationTests = runLiquifyIntegrationTests;
|
||||
console.log("💡 使用 window.runLiquifyIntegrationTests() 运行液化集成测试");
|
||||
}
|
||||
222
src/component/Canvas/CanvasEditor/tests/liquifyTests.js
Normal file
222
src/component/Canvas/CanvasEditor/tests/liquifyTests.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 液化功能测试和验证
|
||||
* 用于测试液化推拉算法和坐标转换修复
|
||||
*/
|
||||
|
||||
// 测试坐标转换函数
|
||||
export function testCoordinateConversion() {
|
||||
console.log("开始测试坐标转换...");
|
||||
|
||||
// 模拟fabric对象
|
||||
const mockFabricObject = {
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 200,
|
||||
height: 200,
|
||||
scaleX: 1.5,
|
||||
scaleY: 1.5,
|
||||
angle: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
calcTransformMatrix: function () {
|
||||
// 简化的变换矩阵计算
|
||||
return [this.scaleX, 0, 0, this.scaleY, this.left, this.top];
|
||||
},
|
||||
};
|
||||
|
||||
// 模拟图像数据
|
||||
const mockImageData = {
|
||||
width: 400,
|
||||
height: 400,
|
||||
};
|
||||
|
||||
// 测试不同的画布坐标
|
||||
const testCoordinates = [
|
||||
{ x: 150, y: 150 }, // 左上角
|
||||
{ x: 200, y: 200 }, // 中心
|
||||
{ x: 250, y: 250 }, // 右下角
|
||||
];
|
||||
|
||||
testCoordinates.forEach((coord, index) => {
|
||||
console.log(`测试坐标 ${index + 1}: (${coord.x}, ${coord.y})`);
|
||||
|
||||
// 这里应该调用实际的坐标转换函数
|
||||
// const imageCoords = _convertFabricCoordsToImageCoords(coord.x, coord.y);
|
||||
|
||||
// 模拟转换结果
|
||||
const expectedImageX =
|
||||
((coord.x - mockFabricObject.left) / mockFabricObject.scaleX +
|
||||
mockFabricObject.width / 2) *
|
||||
(mockImageData.width / mockFabricObject.width);
|
||||
const expectedImageY =
|
||||
((coord.y - mockFabricObject.top) / mockFabricObject.scaleY +
|
||||
mockFabricObject.height / 2) *
|
||||
(mockImageData.height / mockFabricObject.height);
|
||||
|
||||
console.log(
|
||||
` 预期图像坐标: (${expectedImageX.toFixed(2)}, ${expectedImageY.toFixed(
|
||||
2
|
||||
)})`
|
||||
);
|
||||
});
|
||||
|
||||
console.log("坐标转换测试完成");
|
||||
}
|
||||
|
||||
// 测试推拉算法性能
|
||||
export function testPushAlgorithmPerformance() {
|
||||
console.log("开始测试推拉算法性能...");
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// 模拟连续的推拉操作
|
||||
const operations = 100;
|
||||
const movements = [];
|
||||
|
||||
for (let i = 0; i < operations; i++) {
|
||||
// 模拟鼠标移动
|
||||
const x = 200 + Math.sin(i * 0.1) * 50;
|
||||
const y = 200 + Math.cos(i * 0.1) * 50;
|
||||
|
||||
movements.push({ x, y, timestamp: Date.now() });
|
||||
|
||||
// 模拟液化计算
|
||||
const movementLength =
|
||||
i > 0
|
||||
? Math.sqrt(
|
||||
Math.pow(x - movements[i - 1].x, 2) +
|
||||
Math.pow(y - movements[i - 1].y, 2)
|
||||
)
|
||||
: 0;
|
||||
|
||||
if (movementLength > 0.5) {
|
||||
// 模拟变形计算
|
||||
const pressure = 0.8;
|
||||
const power = 0.8;
|
||||
const velocityFactor = Math.min(movementLength * 0.1, 1.0);
|
||||
const pushStrength = pressure * power * velocityFactor * 0.5;
|
||||
|
||||
// 记录计算结果
|
||||
movements[i].strength = pushStrength;
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const totalTime = endTime - startTime;
|
||||
const avgTimePerOperation = totalTime / operations;
|
||||
|
||||
console.log(`推拉算法性能测试结果:`);
|
||||
console.log(` 总操作数: ${operations}`);
|
||||
console.log(` 总耗时: ${totalTime.toFixed(2)}ms`);
|
||||
console.log(` 平均每次操作耗时: ${avgTimePerOperation.toFixed(2)}ms`);
|
||||
console.log(` 预估FPS: ${(1000 / avgTimePerOperation).toFixed(1)}fps`);
|
||||
|
||||
return {
|
||||
totalTime,
|
||||
avgTimePerOperation,
|
||||
estimatedFps: 1000 / avgTimePerOperation,
|
||||
};
|
||||
}
|
||||
|
||||
// 测试缩放一致性
|
||||
export function testScaleConsistency() {
|
||||
console.log("开始测试缩放一致性...");
|
||||
|
||||
// 模拟不同缩放比例的对象
|
||||
const scaleFactors = [0.5, 1.0, 1.5, 2.0, 2.5];
|
||||
|
||||
scaleFactors.forEach((scale) => {
|
||||
console.log(`测试缩放比例: ${scale}`);
|
||||
|
||||
// 模拟fabric对象
|
||||
const fabricObject = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
left: 300,
|
||||
top: 300,
|
||||
};
|
||||
|
||||
// 模拟图像数据(原始尺寸)
|
||||
const imageData = {
|
||||
width: 400,
|
||||
height: 400,
|
||||
};
|
||||
|
||||
// 测试坐标转换
|
||||
const canvasCoord = { x: 350, y: 350 }; // 画布坐标
|
||||
|
||||
// 计算预期的图像坐标(修复后的逻辑)
|
||||
const localX = (canvasCoord.x - fabricObject.left) / fabricObject.scaleX;
|
||||
const localY = (canvasCoord.y - fabricObject.top) / fabricObject.scaleY;
|
||||
|
||||
const imageX =
|
||||
(localX + fabricObject.width / 2) *
|
||||
(imageData.width / fabricObject.width);
|
||||
const imageY =
|
||||
(localY + fabricObject.height / 2) *
|
||||
(imageData.height / fabricObject.height);
|
||||
|
||||
console.log(` 画布坐标: (${canvasCoord.x}, ${canvasCoord.y})`);
|
||||
console.log(` 本地坐标: (${localX.toFixed(2)}, ${localY.toFixed(2)})`);
|
||||
console.log(` 图像坐标: (${imageX.toFixed(2)}, ${imageY.toFixed(2)})`);
|
||||
|
||||
// 验证图像坐标是否在合理范围内
|
||||
const isValid =
|
||||
imageX >= 0 &&
|
||||
imageX < imageData.width &&
|
||||
imageY >= 0 &&
|
||||
imageY < imageData.height;
|
||||
console.log(` 坐标有效性: ${isValid ? "✓" : "✗"}`);
|
||||
});
|
||||
|
||||
console.log("缩放一致性测试完成");
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
export function runAllTests() {
|
||||
console.log("=== 液化功能测试开始 ===");
|
||||
|
||||
try {
|
||||
testCoordinateConversion();
|
||||
console.log("");
|
||||
|
||||
const perfResult = testPushAlgorithmPerformance();
|
||||
console.log("");
|
||||
|
||||
testScaleConsistency();
|
||||
console.log("");
|
||||
|
||||
console.log("=== 液化功能测试完成 ===");
|
||||
console.log(
|
||||
`推荐配置: 节流时间 ${Math.max(
|
||||
16,
|
||||
perfResult.avgTimePerOperation * 2
|
||||
).toFixed(0)}ms`
|
||||
);
|
||||
|
||||
return {
|
||||
coordinateConversion: "通过",
|
||||
performance: perfResult,
|
||||
scaleConsistency: "通过",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("测试过程中出现错误:", error);
|
||||
return {
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 在浏览器控制台中运行测试
|
||||
if (typeof window !== "undefined") {
|
||||
window.liquifyTests = {
|
||||
testCoordinateConversion,
|
||||
testPushAlgorithmPerformance,
|
||||
testScaleConsistency,
|
||||
runAllTests,
|
||||
};
|
||||
|
||||
console.log("液化测试工具已加载,可通过 window.liquifyTests 访问");
|
||||
}
|
||||
820
src/component/Canvas/CanvasEditor/utils/LayerSort.js
Normal file
820
src/component/Canvas/CanvasEditor/utils/LayerSort.js
Normal file
@@ -0,0 +1,820 @@
|
||||
import { ReorderChildLayersCommand } from "../commands/LayerCommands";
|
||||
import { optimizeCanvasRendering } from "./helper";
|
||||
import { findLayerRecursively, LayerType } from "./layerHelper";
|
||||
|
||||
/**
|
||||
* 图层排序工具类
|
||||
* 提供图层排序、重新排列画布对象等功能
|
||||
* 基于fabric.js 5.3.1版本开发
|
||||
*/
|
||||
export class LayerSort {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param {Object} canvas fabric.js画布实例
|
||||
* @param {Object} layers 图层数组响应式引用
|
||||
*/
|
||||
constructor(canvas, layers, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.layers = layers;
|
||||
this.commandManager = options.commandManager || null; // 命令管理器(可选)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新排列画布上的对象以匹配图层顺序
|
||||
* 使用 fabric.js 的 moveTo 方法直接调整对象层级,无需清空画布
|
||||
*/
|
||||
async rearrangeObjects() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
if (canvasObjects.length === 0) return;
|
||||
|
||||
// 使用画布渲染优化
|
||||
await optimizeCanvasRendering(this.canvas, () => {
|
||||
// 计算每个对象应该在的 z-index 位置
|
||||
const objectZIndexMap = this.calculateObjectZIndexes();
|
||||
|
||||
// 按照新的 z-index 排序对象
|
||||
const sortedObjects = canvasObjects
|
||||
.map((obj) => ({
|
||||
object: obj,
|
||||
targetZIndex: objectZIndexMap.get(obj.id) ?? -1,
|
||||
}))
|
||||
.filter((item) => item.targetZIndex >= 0) // 过滤掉无效对象
|
||||
.sort((a, b) => a.targetZIndex - b.targetZIndex);
|
||||
|
||||
// 使用 fabric.js 的 moveTo 方法重新排序
|
||||
sortedObjects.forEach((item, index) => {
|
||||
const currentIndex = this.canvas.getObjects().indexOf(item.object);
|
||||
if (currentIndex !== index && currentIndex !== -1) {
|
||||
// 将对象移动到正确的位置
|
||||
this.canvas.moveTo(item.object, index);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算每个对象在画布中应该的 z-index
|
||||
* 考虑图层类型、组结构和子图层
|
||||
* @returns {Map} 对象ID到z-index的映射
|
||||
*/
|
||||
calculateObjectZIndexes() {
|
||||
const zIndexMap = new Map();
|
||||
let currentZIndex = 0;
|
||||
|
||||
// 按照图层在数组中的顺序从后往前遍历(数组末尾 = 画布底层)
|
||||
for (let i = this.layers.value.length - 1; i >= 0; i--) {
|
||||
const layer = this.layers.value[i];
|
||||
|
||||
// 跳过不可见图层
|
||||
if (!layer.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理不同类型的图层
|
||||
if (layer.isBackground && layer.fabricObject) {
|
||||
// 背景图层对象放在最底层
|
||||
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||
} else if (layer.isFixed && layer.fabricObjects) {
|
||||
// 固定图层对象
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
if (obj?.id) {
|
||||
zIndexMap.set(obj.id, currentZIndex++);
|
||||
}
|
||||
});
|
||||
} else if (!layer.isBackground && !layer.isFixed) {
|
||||
// 普通图层
|
||||
currentZIndex = this.processLayerObjects(
|
||||
layer,
|
||||
currentZIndex,
|
||||
zIndexMap
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return zIndexMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图层对象,包括组和子图层的情况
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {number} currentZIndex 当前z-index
|
||||
* @param {Map} zIndexMap z-index映射表
|
||||
* @returns {number} 更新后的z-index
|
||||
*/
|
||||
processLayerObjects(layer, currentZIndex, zIndexMap) {
|
||||
// 检查是否有子图层(组图层)
|
||||
if (layer.children?.length > 0) {
|
||||
// 处理每个子图层
|
||||
// 按照图层在数组中的顺序从后往前遍历(数组末尾 = 画布底层)
|
||||
for (let i = layer.children.length - 1; i >= 0; i--) {
|
||||
const childLayer = layer.children[i];
|
||||
|
||||
// 跳过不可见图层
|
||||
if (!childLayer.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = childLayer.fabricObjects.length - 1; j >= 0; j--) {
|
||||
const obj = childLayer.fabricObjects[j];
|
||||
if (obj?.id) {
|
||||
zIndexMap.set(obj.id, currentZIndex++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图层本身的对象
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
for (let j = layer.fabricObjects.length - 1; j >= 0; j--) {
|
||||
const obj = layer.fabricObjects[j];
|
||||
if (obj?.id) {
|
||||
zIndexMap.set(obj.id, currentZIndex++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentZIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定图层的子图层,按照正确顺序排列
|
||||
* @param {string} parentLayerId 父图层ID
|
||||
* @returns {Array} 子图层数组
|
||||
*/
|
||||
getChildLayersInOrder(parentLayerId) {
|
||||
// 获取所有子图层
|
||||
const childLayers =
|
||||
this.layers.value.filter((layer) => layer.id === parentLayerId)
|
||||
?.children || [];
|
||||
|
||||
return childLayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取图层
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {Object|null} 图层对象或null
|
||||
*/
|
||||
getLayerById(layerId) {
|
||||
if (!layerId || !this.layers.value) return null;
|
||||
return this.layers.value.find((layer) => layer.id === layerId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重新排列对象(异步版本,适用于大量对象)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rearrangeObjectsAsync() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
if (canvasObjects.length === 0) return;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 使用 requestAnimationFrame 进行异步处理
|
||||
requestAnimationFrame(() => {
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
|
||||
try {
|
||||
const objectZIndexMap = this.calculateObjectZIndexes();
|
||||
|
||||
const sortedObjects = canvasObjects
|
||||
.map((obj) => ({
|
||||
object: obj,
|
||||
targetZIndex: objectZIndexMap.get(obj.id) ?? -1,
|
||||
}))
|
||||
.filter((item) => item.targetZIndex >= 0)
|
||||
.sort((a, b) => a.targetZIndex - b.targetZIndex);
|
||||
|
||||
// 分批处理,避免一次性处理太多对象
|
||||
const batchSize = LayerSortConstants.BATCH_SIZE;
|
||||
let currentBatch = 0;
|
||||
|
||||
const processBatch = () => {
|
||||
const start = currentBatch * batchSize;
|
||||
const end = Math.min(start + batchSize, sortedObjects.length);
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
const item = sortedObjects[i];
|
||||
const currentIndex = this.canvas
|
||||
.getObjects()
|
||||
.indexOf(item.object);
|
||||
if (currentIndex !== i && currentIndex !== -1) {
|
||||
this.canvas.moveTo(item.object, i);
|
||||
}
|
||||
}
|
||||
|
||||
currentBatch++;
|
||||
|
||||
if (end < sortedObjects.length) {
|
||||
// 继续处理下一批
|
||||
requestAnimationFrame(processBatch);
|
||||
} else {
|
||||
// 所有批次处理完成
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
processBatch();
|
||||
} catch (error) {
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
this.canvas.renderAll();
|
||||
console.error("重新排列对象时出错:", error);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证对象顺序是否正确
|
||||
* @returns {boolean} 顺序是否正确
|
||||
*/
|
||||
validateObjectOrder() {
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
const objectZIndexMap = this.calculateObjectZIndexes();
|
||||
|
||||
for (let i = 0; i < canvasObjects.length - 1; i++) {
|
||||
const currentObj = canvasObjects[i];
|
||||
const nextObj = canvasObjects[i + 1];
|
||||
|
||||
const currentZIndex = objectZIndexMap.get(currentObj.id);
|
||||
const nextZIndex = objectZIndexMap.get(nextObj.id);
|
||||
|
||||
if (currentZIndex !== undefined && nextZIndex !== undefined) {
|
||||
if (currentZIndex > nextZIndex) {
|
||||
return false; // 顺序不正确
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 顺序正确
|
||||
}
|
||||
|
||||
/**
|
||||
* 图层排序规则:背景图层 > 固定图层 > 普通图层
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {Array} 排序后的图层数组
|
||||
*/
|
||||
sortLayers(layers = null) {
|
||||
const targetLayers = layers ?? this.layers.value;
|
||||
|
||||
return [...targetLayers].sort((a, b) => {
|
||||
// 如果a是背景图层,它应该排在后面(最底层)
|
||||
if (a.isBackground) return 1;
|
||||
// 如果b是背景图层,它应该排在后面(最底层)
|
||||
if (b.isBackground) return -1;
|
||||
|
||||
// 如果a是固定图层而b不是固定图层,a应该排在后面(固定图层在普通图层下方)
|
||||
if (a.isFixed && !b.isFixed) return 1;
|
||||
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
||||
if (b.isFixed && !a.isFixed) return -1;
|
||||
|
||||
// 其他情况保持原有顺序
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层在排序后的正确插入位置
|
||||
* @param {Object} newLayer 新图层
|
||||
* @param {string} targetLayerId 目标图层ID(插入到该图层之前)
|
||||
* @returns {number} 插入位置索引
|
||||
*/
|
||||
getInsertIndex(newLayer, targetLayerId = null) {
|
||||
if (!targetLayerId) {
|
||||
// 如果没有指定目标图层,根据图层类型决定插入位置
|
||||
if (newLayer.isBackground) {
|
||||
return this.layers.value.length; // 背景图层插入到最后
|
||||
} else if (newLayer.isFixed) {
|
||||
// 固定图层插入到背景图层之前
|
||||
const bgIndex = this.layers.value.findIndex(
|
||||
(layer) => layer.isBackground
|
||||
);
|
||||
return bgIndex !== -1 ? bgIndex : this.layers.value.length;
|
||||
} else {
|
||||
// 普通图层插入到固定图层之前
|
||||
const fixedIndex = this.layers.value.findIndex(
|
||||
(layer) => layer.isFixed
|
||||
);
|
||||
return fixedIndex !== -1 ? fixedIndex : this.layers.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果指定了目标图层,插入到目标图层之前
|
||||
const targetIndex = this.layers.value.findIndex(
|
||||
(layer) => layer.id === targetLayerId
|
||||
);
|
||||
return targetIndex !== -1 ? targetIndex : this.layers.value.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动图层到指定位置
|
||||
* @param {string} layerId 要移动的图层ID
|
||||
* @param {number} newIndex 新位置索引
|
||||
* @returns {boolean} 是否移动成功
|
||||
*/
|
||||
async moveLayerToIndex({ parentId, oldIndex, newIndex, layerId }) {
|
||||
// 检查父图层是否存在
|
||||
// const parentLayer = this.getLayerById(parentId);
|
||||
const { layer: childLayer, parent: parentLayer } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId,
|
||||
parentId
|
||||
);
|
||||
|
||||
if (!parentLayer) {
|
||||
console.warn(`父图层 ${parentId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取所有子图层
|
||||
const childLayers = parentLayer?.children || [];
|
||||
|
||||
// 检查索引有效性
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= childLayers.length ||
|
||||
newIndex >= childLayers.length
|
||||
) {
|
||||
console.warn("子图层排序索引无效");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是同一位置
|
||||
if (oldIndex === newIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证图层ID
|
||||
const layer = childLayers[oldIndex];
|
||||
if (!layer || layer.id !== layerId) {
|
||||
console.warn("子图层ID与索引不匹配");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新父图层的children数组 - 执行命令
|
||||
const command = new ReorderChildLayersCommand({
|
||||
parentId,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
layerId,
|
||||
layers: this.layers,
|
||||
canvas: this.canvas,
|
||||
layerSort: this, // 传入当前实例
|
||||
});
|
||||
|
||||
if (this.commandManager) {
|
||||
await this.commandManager?.execute(command);
|
||||
} else {
|
||||
await command?.execute?.();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的有效移动范围
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {Object} 包含最小和最大索引的对象
|
||||
*/
|
||||
getLayerMoveRange(layerId) {
|
||||
const layer = this.getLayerById(layerId);
|
||||
if (!layer || layer.isBackground || layer.isFixed) {
|
||||
return { minIndex: -1, maxIndex: -1 };
|
||||
}
|
||||
|
||||
// 普通图层只能在普通图层范围内移动
|
||||
const normalLayers = this.layers.value
|
||||
.map((layer, index) => ({ layer, index }))
|
||||
.filter((item) => !item.layer.isBackground && !item.layer.isFixed);
|
||||
|
||||
if (normalLayers.length === 0) {
|
||||
return { minIndex: -1, maxIndex: -1 };
|
||||
}
|
||||
|
||||
return {
|
||||
minIndex: normalLayers[0].index,
|
||||
maxIndex: normalLayers[normalLayers.length - 1].index,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖拽排序图层
|
||||
* @param {number} oldIndex 原索引
|
||||
* @param {number} newIndex 新索引
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
async reorderLayers(oldIndex, newIndex, layerId) {
|
||||
// 检查索引有效性
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= this.layers.value.length ||
|
||||
newIndex >= this.layers.value.length
|
||||
) {
|
||||
console.warn("图层排序索引无效");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是同一位置
|
||||
if (oldIndex === newIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取要移动的图层
|
||||
const layer = this.layers.value[oldIndex];
|
||||
if (!layer || layer.id !== layerId) {
|
||||
console.warn("图层ID与索引不匹配");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是背景层或固定层(不允许排序)
|
||||
if (layer.isBackground || layer.isFixed) {
|
||||
console.warn("背景层和固定层不能参与排序");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查目标位置是否合法(不能移到背景层或固定层的位置)
|
||||
const targetLayer = this.layers.value[newIndex];
|
||||
if (targetLayer && (targetLayer.isBackground || targetLayer.isFixed)) {
|
||||
console.warn("不能移动到背景层或固定层的位置");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行排序
|
||||
const layersArray = [...this.layers.value];
|
||||
const [movedLayer] = layersArray.splice(oldIndex, 1);
|
||||
layersArray.splice(newIndex, 0, movedLayer);
|
||||
|
||||
this.layers.value = layersArray;
|
||||
|
||||
// 重新排列画布对象
|
||||
await this.rearrangeObjects();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子图层排序
|
||||
* @param {string} parentId 父图层ID
|
||||
* @param {number} oldIndex 原索引
|
||||
* @param {number} newIndex 新索引
|
||||
* @param {string} layerId 子图层ID
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
async reorderChildLayers(parentId, oldIndex, newIndex, layerId) {
|
||||
// 检查父图层是否存在
|
||||
// const parentLayer = this.getLayerById(parentId);
|
||||
const { layer: childLayer, parent: parentLayer } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId,
|
||||
parentId
|
||||
);
|
||||
|
||||
if (!parentLayer) {
|
||||
console.warn(`父图层 ${parentId} 不存在`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取所有子图层
|
||||
const childLayers = parentLayer?.children || [];
|
||||
|
||||
// 检查索引有效性
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= childLayers.length ||
|
||||
newIndex >= childLayers.length
|
||||
) {
|
||||
console.warn("子图层排序索引无效");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是同一位置
|
||||
if (oldIndex === newIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证图层ID
|
||||
const layer = childLayers[oldIndex];
|
||||
if (!layer || layer.id !== layerId) {
|
||||
console.warn("子图层ID与索引不匹配");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新父图层的children数组 - 执行命令
|
||||
const command = ReorderChildLayersCommand({
|
||||
parentId,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
layerId,
|
||||
layers: this.layers,
|
||||
canvas: this.canvas,
|
||||
layerSort: this, // 传入当前实例
|
||||
});
|
||||
|
||||
if (this.commandManager) {
|
||||
await this.commandManager?.execute(command);
|
||||
} else {
|
||||
await command?.execute?.();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能排序 - 根据对象类型和位置自动调整图层顺序
|
||||
* @param {Array} targetLayerIds 要排序的图层ID数组
|
||||
* @returns {boolean} 是否排序成功
|
||||
*/
|
||||
async smartSort(targetLayerIds = null) {
|
||||
const layersToSort = targetLayerIds
|
||||
? this.layers.value.filter((layer) => targetLayerIds.includes(layer.id))
|
||||
: this.layers.value.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
);
|
||||
|
||||
if (layersToSort.length <= 1) return true;
|
||||
|
||||
// 按照对象类型和位置进行智能排序
|
||||
layersToSort.sort((a, b) => {
|
||||
const aWeight = this.getLayerSortWeight(a);
|
||||
const bWeight = this.getLayerSortWeight(b);
|
||||
|
||||
if (aWeight !== bWeight) {
|
||||
return bWeight - aWeight; // 权重高的在上层
|
||||
}
|
||||
|
||||
// 权重相同时,按照Y坐标排序(Y值小的在上层)
|
||||
const aY = this.getLayerAverageY(a);
|
||||
const bY = this.getLayerAverageY(b);
|
||||
|
||||
return aY - bY;
|
||||
});
|
||||
|
||||
// 更新图层顺序
|
||||
const sortedLayerIds = layersToSort.map((layer) => layer.id);
|
||||
const otherLayers = this.layers.value.filter(
|
||||
(layer) => !sortedLayerIds.includes(layer.id)
|
||||
);
|
||||
|
||||
// 重新组织图层数组:保持背景层和固定层的位置
|
||||
const newLayers = [];
|
||||
|
||||
// 添加普通图层(已排序)
|
||||
newLayers.push(...layersToSort);
|
||||
|
||||
// 添加其他普通图层
|
||||
otherLayers.forEach((layer) => {
|
||||
if (!layer.isBackground && !layer.isFixed) {
|
||||
newLayers.push(layer);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加固定图层
|
||||
otherLayers.forEach((layer) => {
|
||||
if (layer.isFixed) {
|
||||
newLayers.push(layer);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加背景图层
|
||||
otherLayers.forEach((layer) => {
|
||||
if (layer.isBackground) {
|
||||
newLayers.push(layer);
|
||||
}
|
||||
});
|
||||
|
||||
this.layers.value = newLayers;
|
||||
await this.rearrangeObjects();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的排序权重
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {number} 排序权重
|
||||
*/
|
||||
getLayerSortWeight(layer) {
|
||||
const weightMap = LayerSortConstants.LAYER_PRIORITY;
|
||||
|
||||
if (layer.isBackground) return weightMap[LayerType.BACKGROUND];
|
||||
if (layer.isFixed) return weightMap[LayerType.FIXED];
|
||||
if (layer.children?.length > 0) return weightMap[LayerType.GROUP];
|
||||
|
||||
// 根据对象类型调整权重
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||
const firstObj = layer.fabricObjects[0];
|
||||
if (firstObj.type === "text") return weightMap[LayerType.NORMAL] + 10;
|
||||
if (firstObj.type === "image") return weightMap[LayerType.NORMAL] + 5;
|
||||
}
|
||||
|
||||
return weightMap[LayerType.NORMAL];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层对象的平均Y坐标
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {number} 平均Y坐标
|
||||
*/
|
||||
getLayerAverageY(layer) {
|
||||
let totalY = 0;
|
||||
let count = 0;
|
||||
|
||||
if (layer.fabricObject) {
|
||||
totalY += layer.fabricObject.top || 0;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||
layer.fabricObjects.forEach((obj) => {
|
||||
totalY += obj.top || 0;
|
||||
count++;
|
||||
});
|
||||
}
|
||||
|
||||
return count > 0 ? totalY / count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化图层结构 - 清理空图层、合并相邻相似图层等
|
||||
* @returns {Object} 优化结果统计
|
||||
*/
|
||||
async optimizeLayerStructure() {
|
||||
const stats = {
|
||||
removedEmptyLayers: 0,
|
||||
mergedLayers: 0,
|
||||
reorderedLayers: 0,
|
||||
};
|
||||
|
||||
// 清理空图层
|
||||
const emptyLayers = this.layers.value.filter(
|
||||
(layer) =>
|
||||
!layer.isBackground &&
|
||||
!layer.isFixed &&
|
||||
(!layer.fabricObjects || layer.fabricObjects.length === 0) &&
|
||||
(!layer.children || layer.children.length === 0)
|
||||
);
|
||||
|
||||
emptyLayers.forEach((layer) => {
|
||||
const index = this.layers.value.findIndex((l) => l.id === layer.id);
|
||||
if (index !== -1) {
|
||||
this.layers.value.splice(index, 1);
|
||||
stats.removedEmptyLayers++;
|
||||
}
|
||||
});
|
||||
|
||||
// 重新排序以确保正确的层级关系
|
||||
const wasReordered = this.sortLayers();
|
||||
if (wasReordered) {
|
||||
stats.reorderedLayers = this.layers.value.length;
|
||||
await this.rearrangeObjects();
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图层排序工具实例
|
||||
* @param {Object} canvas fabric.js画布实例
|
||||
* @param {Object} layers 图层数组响应式引用
|
||||
* @returns {LayerSort} 图层排序工具实例
|
||||
*/
|
||||
export const createLayerSort = (canvas, layers, options = {}) => {
|
||||
return new LayerSort(canvas, layers, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* 图层排序相关的常量
|
||||
*/
|
||||
export const LayerSortConstants = {
|
||||
// 图层类型优先级(数值越大优先级越高,在画布中越靠上)
|
||||
LAYER_PRIORITY: {
|
||||
[LayerType.BACKGROUND]: 0,
|
||||
[LayerType.FIXED]: 1,
|
||||
[LayerType.NORMAL]: 2,
|
||||
[LayerType.GROUP]: 2,
|
||||
},
|
||||
|
||||
// 批处理大小
|
||||
BATCH_SIZE: 50,
|
||||
|
||||
// 性能阈值
|
||||
PERFORMANCE_THRESHOLD: {
|
||||
OBJECTS_COUNT: 100, // 超过此数量使用异步处理
|
||||
LAYERS_COUNT: 20, // 超过此数量使用分批处理
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 图层排序辅助函数
|
||||
*/
|
||||
export const LayerSortUtils = {
|
||||
/**
|
||||
* 检查是否需要异步处理
|
||||
* @param {number} objectsCount 对象数量
|
||||
* @param {number} layersCount 图层数量
|
||||
* @returns {boolean} 是否需要异步处理
|
||||
*/
|
||||
shouldUseAsyncProcessing(objectsCount, layersCount) {
|
||||
return (
|
||||
objectsCount > LayerSortConstants.PERFORMANCE_THRESHOLD.OBJECTS_COUNT ||
|
||||
layersCount > LayerSortConstants.PERFORMANCE_THRESHOLD.LAYERS_COUNT
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取图层的排序权重
|
||||
* @param {Object} layer 图层对象
|
||||
* @returns {number} 排序权重
|
||||
*/
|
||||
getLayerSortWeight(layer) {
|
||||
if (layer.isBackground)
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.BACKGROUND];
|
||||
if (layer.isFixed)
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.FIXED];
|
||||
if (layer.children?.length > 0)
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.GROUP];
|
||||
return LayerSortConstants.LAYER_PRIORITY[LayerType.NORMAL];
|
||||
},
|
||||
|
||||
/**
|
||||
* 比较两个图层的排序优先级
|
||||
* @param {Object} layerA 图层A
|
||||
* @param {Object} layerB 图层B
|
||||
* @returns {number} 比较结果
|
||||
*/
|
||||
compareLayerPriority(layerA, layerB) {
|
||||
const weightA = this.getLayerSortWeight(layerA);
|
||||
const weightB = this.getLayerSortWeight(layerB);
|
||||
return weightB - weightA; // 权重高的排在前面(画布上层)
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 图层排序混入方法 - 用于在 LayerManager 中使用
|
||||
*/
|
||||
export const LayerSortMixin = {
|
||||
/**
|
||||
* 初始化图层排序工具
|
||||
*/
|
||||
initLayerSort() {
|
||||
this.layerSort = new LayerSort(this.canvas, this.layers);
|
||||
},
|
||||
|
||||
/**
|
||||
* 重新排列画布对象
|
||||
*/
|
||||
rearrangeCanvasObjects() {
|
||||
if (this.layerSort) {
|
||||
// 检查是否需要异步处理
|
||||
const objectsCount = this.canvas?.getObjects()?.length || 0;
|
||||
const layersCount = this.layers?.value?.length || 0;
|
||||
|
||||
if (LayerSortUtils.shouldUseAsyncProcessing(objectsCount, layersCount)) {
|
||||
return this.layerSort.rearrangeObjectsAsync();
|
||||
} else {
|
||||
this.layerSort.rearrangeObjects();
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 排序图层
|
||||
*/
|
||||
sortLayersWithTool() {
|
||||
if (this.layerSort) {
|
||||
this.layers.value = this.layerSort.sortLayers();
|
||||
return this.layerSort.rearrangeObjects();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 智能排序图层
|
||||
*/
|
||||
smartSortLayers(targetLayerIds = null) {
|
||||
if (this.layerSort) {
|
||||
return this.layerSort.smartSort(targetLayerIds);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 优化图层结构
|
||||
*/
|
||||
optimizeLayerStructure() {
|
||||
if (this.layerSort) {
|
||||
return this.layerSort.optimizeLayerStructure();
|
||||
}
|
||||
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { canvasConfig } from "../config/canvasConfig";
|
||||
|
||||
/**
|
||||
|
||||
@@ -434,25 +434,324 @@ export function createCancellablePromise(executor) {
|
||||
};
|
||||
}
|
||||
|
||||
// 导出所有工具函数
|
||||
export default {
|
||||
deepCompare,
|
||||
deepClone,
|
||||
applyDiff,
|
||||
throttle,
|
||||
debounce,
|
||||
generateId,
|
||||
formatFileSize,
|
||||
formatDuration,
|
||||
isValidCommand,
|
||||
isPromise,
|
||||
safeJSONParse,
|
||||
safeJSONStringify,
|
||||
getObjectDepth,
|
||||
getObjectSize,
|
||||
checkBrowserSupport,
|
||||
delay,
|
||||
retry,
|
||||
batchProcess,
|
||||
createCancellablePromise,
|
||||
/**
|
||||
* 增强版检查对象是否在画布中(包括组内对象)
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} targetObj 目标对象
|
||||
* @returns {Object} { flag: boolean, object: fabric.Object, parent: fabric.Group|null }
|
||||
*/
|
||||
export function objectIsInCanvas(canvas, targetObj) {
|
||||
if (!canvas || !targetObj) {
|
||||
return { flag: false, object: null, parent: null };
|
||||
}
|
||||
|
||||
const targetId = targetObj.id;
|
||||
if (!targetId) {
|
||||
return { flag: false, object: null, parent: null };
|
||||
}
|
||||
|
||||
// 首先检查顶层对象
|
||||
const topLevelObjects = canvas.getObjects();
|
||||
|
||||
// 直接在顶层查找
|
||||
const directMatch = topLevelObjects.find((obj) => obj.id === targetId);
|
||||
if (directMatch) {
|
||||
return { flag: true, object: directMatch, parent: null };
|
||||
}
|
||||
|
||||
// 递归检查组内对象
|
||||
for (const obj of topLevelObjects) {
|
||||
if (obj.type === "group") {
|
||||
const result = findObjectInGroup(obj, targetId);
|
||||
if (result.found) {
|
||||
return { flag: true, object: result.object, parent: obj };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { flag: false, object: null, parent: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 在组中递归查找对象
|
||||
* @param {fabric.Group} group 组对象
|
||||
* @param {string} targetId 目标对象ID
|
||||
* @returns {Object} { found: boolean, object: fabric.Object|null }
|
||||
*/
|
||||
function findObjectInGroup(group, targetId) {
|
||||
if (!group || group.type !== "group" || !group.getObjects) {
|
||||
return { found: false, object: null };
|
||||
}
|
||||
|
||||
const groupObjects = group.getObjects();
|
||||
|
||||
for (const obj of groupObjects) {
|
||||
if (obj.id === targetId) {
|
||||
return { found: true, object: obj };
|
||||
}
|
||||
|
||||
// 递归检查嵌套组
|
||||
if (obj.type === "group") {
|
||||
const nestedResult = findObjectInGroup(obj, targetId);
|
||||
if (nestedResult.found) {
|
||||
return nestedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { found: false, object: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ID查找对象(增强版)
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {string} objectId 对象ID
|
||||
* @returns {Object} { object: fabric.Object|null, parent: fabric.Group|null }
|
||||
*/
|
||||
export function findObjectById(canvas, objectId) {
|
||||
if (!canvas || !objectId) {
|
||||
return { object: null, parent: null };
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, { id: objectId });
|
||||
return { object: result.object, parent: result.parent };
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全移除画布对象(包括组内对象)
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} targetObj 目标对象
|
||||
* @returns {boolean} 是否成功移除
|
||||
*/
|
||||
export function removeCanvasObjectByObject(canvas, targetObj) {
|
||||
if (!canvas || !targetObj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, targetObj);
|
||||
|
||||
if (!result.flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (result.parent) {
|
||||
// 对象在组中,从组中移除
|
||||
result.parent.removeWithUpdate(result.object);
|
||||
} else {
|
||||
// 对象在顶层,直接从画布移除
|
||||
canvas.remove(result.object);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("移除对象时发生错误:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化画布渲染 统一渲染完成后才执行
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {Function} callback 渲染执行函数
|
||||
*/
|
||||
export const optimizeCanvasRendering = async (canvas, callback) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!canvas || typeof callback !== "function") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 暂停渲染以提高性能
|
||||
canvas.skipTargetFind = true;
|
||||
// 开始渲染
|
||||
// 暂停实时渲染和对象查找
|
||||
const wasRenderOnAddRemove = canvas.renderOnAddRemove;
|
||||
canvas.renderOnAddRemove = false; // 禁用自动渲染
|
||||
|
||||
// 等待下一帧渲染完成
|
||||
requestAnimationFrame(async () => {
|
||||
// 恢复渲染设置
|
||||
canvas.skipTargetFind = false;
|
||||
canvas.renderOnAddRemove = wasRenderOnAddRemove;
|
||||
if (isPromise(callback)) {
|
||||
await callback?.();
|
||||
} else {
|
||||
callback?.();
|
||||
}
|
||||
canvas.renderAll(); // 确保画布重新渲染 - 同步渲染
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取对象在画布中的z-index位置
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} targetObj 目标对象
|
||||
* @returns {number} z-index位置,-1表示未找到
|
||||
*/
|
||||
export function getObjectZIndex(canvas, targetObj) {
|
||||
if (!canvas || !targetObj) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, targetObj);
|
||||
if (!result.flag) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (result.parent) {
|
||||
// 对象在组中,返回组在画布中的位置
|
||||
const allObjects = canvas.getObjects();
|
||||
return allObjects.indexOf(result.parent);
|
||||
} else {
|
||||
// 对象在顶层,直接返回在画布中的位置
|
||||
const allObjects = canvas.getObjects();
|
||||
return allObjects.indexOf(result.object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定的z-index位置插入对象
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} object 要插入的对象
|
||||
* @param {number} zIndex z-index位置
|
||||
* @param {boolean} renderAll 是否立即渲染,默认true
|
||||
* @returns {boolean} 是否成功插入
|
||||
*/
|
||||
export function insertObjectAtZIndex(canvas, object, zIndex, renderAll = true) {
|
||||
if (!canvas || !object || zIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保z-index不超过当前对象数量
|
||||
const maxIndex = canvas.getObjects().length;
|
||||
const safeZIndex = Math.min(zIndex, maxIndex);
|
||||
|
||||
canvas.insertAt(object, safeZIndex, false);
|
||||
|
||||
if (renderAll) {
|
||||
canvas.renderAll();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("插入对象到指定z-index位置失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动对象到指定的z-index位置
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} object 要移动的对象
|
||||
* @param {number} zIndex 目标z-index位置
|
||||
* @param {boolean} renderAll 是否立即渲染,默认true
|
||||
* @returns {boolean} 是否成功移动
|
||||
*/
|
||||
export function moveObjectToZIndex(canvas, object, zIndex, renderAll = true) {
|
||||
if (!canvas || !object || zIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = objectIsInCanvas(canvas, object);
|
||||
if (!result.flag) {
|
||||
console.warn("对象不在画布中,无法移动");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保z-index不超过当前对象数量-1(因为当前对象也在其中)
|
||||
const maxIndex = canvas.getObjects().length - 1;
|
||||
const safeZIndex = Math.min(zIndex, maxIndex);
|
||||
|
||||
result.object.moveTo(safeZIndex);
|
||||
|
||||
if (renderAll) {
|
||||
canvas.renderAll();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("移动对象到指定z-index位置失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 交换两个对象的z-index位置
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {fabric.Object} obj1 第一个对象
|
||||
* @param {fabric.Object} obj2 第二个对象
|
||||
* @param {boolean} renderAll 是否立即渲染,默认true
|
||||
* @returns {boolean} 是否成功交换
|
||||
*/
|
||||
export function swapObjectsZIndex(canvas, obj1, obj2, renderAll = true) {
|
||||
if (!canvas || !obj1 || !obj2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const zIndex1 = getObjectZIndex(canvas, obj1);
|
||||
const zIndex2 = getObjectZIndex(canvas, obj2);
|
||||
|
||||
if (zIndex1 === -1 || zIndex2 === -1) {
|
||||
console.warn("其中一个或两个对象不在画布中");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 先移动z-index较大的对象,避免索引冲突
|
||||
if (zIndex1 > zIndex2) {
|
||||
moveObjectToZIndex(canvas, obj2, zIndex1, false);
|
||||
moveObjectToZIndex(canvas, obj1, zIndex2, false);
|
||||
} else {
|
||||
moveObjectToZIndex(canvas, obj1, zIndex2, false);
|
||||
moveObjectToZIndex(canvas, obj2, zIndex1, false);
|
||||
}
|
||||
|
||||
if (renderAll) {
|
||||
canvas.renderAll();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("交换对象z-index位置失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布中所有对象的z-index信息
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @returns {Array} 包含对象ID和z-index的数组
|
||||
*/
|
||||
export function getAllObjectsZIndex(canvas) {
|
||||
if (!canvas) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const objects = canvas.getObjects();
|
||||
return objects.map((obj, index) => ({
|
||||
id: obj.id || `unnamed_${index}`,
|
||||
zIndex: index,
|
||||
type: obj.type,
|
||||
layerId: obj.layerId,
|
||||
object: obj,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按图层ID获取对象的z-index信息
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @param {string} layerId 图层ID
|
||||
* @returns {Array} 属于指定图层的对象z-index信息
|
||||
*/
|
||||
export function getLayerObjectsZIndex(canvas, layerId) {
|
||||
if (!canvas || !layerId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allInfo = getAllObjectsZIndex(canvas);
|
||||
return allInfo.filter((info) => info.layerId === layerId);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { LayerType, OperationType, createBitmapLayer } from "./layerHelper";
|
||||
// 导入新的复合命令
|
||||
import { CreateImageLayerCommand } from "../commands/LayerCommands";
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ChangeFixedImageCommand,
|
||||
AddImageToLayerCommand,
|
||||
} from "../commands/LayerCommands";
|
||||
import { generateId } from "./helper";
|
||||
|
||||
/**
|
||||
* 加载并处理图片
|
||||
@@ -44,6 +45,7 @@ export function loadImage(imageSource, options = {}) {
|
||||
// 设置图片位置 - 默认居中
|
||||
if (options.centerOnCanvas !== false) {
|
||||
fabricImage.set({
|
||||
id: generateId("fabricImage"),
|
||||
left: (options.canvasWidth || 800) / 2,
|
||||
top: (options.canvasHeight || 600) / 2,
|
||||
originX: "center",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const LayerType = {
|
||||
VIDEO: "video", // 视频图层 (预留)
|
||||
AUDIO: "audio", // 音频图层 (预留)
|
||||
FIXED: "fixed", // 固定图层 - 位于背景图层之上,普通图层之下
|
||||
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -461,3 +462,161 @@ export function cloneLayer(layer) {
|
||||
|
||||
return clonedLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找图层(包括子图层)
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @param {Object} parent 父图层(可选,用于内部递归)
|
||||
* @returns {Object|null} 包含layer和parent的对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayerRecursively(layers, layerId, parent = null) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 在当前图层列表中查找
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.id === layerId) {
|
||||
return { layer, parent };
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找子图层
|
||||
if (
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const result = findInChildLayers(layer.children, layerId, layer);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在子图层中递归查找
|
||||
* @param {Array} children 子图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @param {Object} parent 父图层
|
||||
* @returns {Object|null} 包含layer和parent的对象,如果未找到返回null
|
||||
*/
|
||||
export function findInChildLayers(children, layerId, parent) {
|
||||
if (!children || !Array.isArray(children) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
if (child && child.id === layerId) {
|
||||
return { layer: child, parent };
|
||||
}
|
||||
|
||||
// 如果子图层也是组,继续递归查找
|
||||
if (
|
||||
child &&
|
||||
(child.type === "group" || child.type === LayerType.GROUP) &&
|
||||
child.children &&
|
||||
Array.isArray(child.children)
|
||||
) {
|
||||
const result = findInChildLayers(child.children, layerId, child);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单查找图层(仅在顶级图层中查找,不递归子图层)
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @returns {Object|null} 找到的图层对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayer(layers, layerId) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return layers.find((layer) => layer && layer.id === layerId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图层名称查找图层
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerName 要查找的图层名称
|
||||
* @param {boolean} recursive 是否递归查找子图层,默认false
|
||||
* @returns {Object|null} 找到的图层对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayerByName(layers, layerName, recursive = false) {
|
||||
if (!layers || !Array.isArray(layers) || !layerName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.name === layerName) {
|
||||
return layer;
|
||||
}
|
||||
|
||||
// 如果需要递归查找且是组图层
|
||||
if (
|
||||
recursive &&
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const found = findLayerByName(layer.children, layerName, true);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的完整路径(包含父图层信息)
|
||||
* @param {Array} layers 图层数组
|
||||
* @param {string} layerId 要查找的图层ID
|
||||
* @returns {Array} 图层路径数组,从根图层到目标图层
|
||||
*/
|
||||
export function getLayerPath(layers, layerId) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
function findPath(currentLayers, targetId, currentPath = []) {
|
||||
for (const layer of currentLayers) {
|
||||
if (!layer) continue;
|
||||
|
||||
const newPath = [...currentPath, layer];
|
||||
|
||||
if (layer.id === targetId) {
|
||||
return newPath;
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找
|
||||
if (
|
||||
layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children))
|
||||
) {
|
||||
const foundPath = findPath(layer.children, targetId, newPath);
|
||||
if (foundPath.length > 0) {
|
||||
return foundPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return findPath(layers, layerId);
|
||||
}
|
||||
|
||||
204
src/component/Canvas/CanvasEditor/utils/layerUtils.js
Normal file
204
src/component/Canvas/CanvasEditor/utils/layerUtils.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { isArray } from "lodash-es";
|
||||
|
||||
/**
|
||||
* 图层关联工具类
|
||||
* 提供图层与画布对象关联管理的通用方法
|
||||
*/
|
||||
|
||||
/**
|
||||
* 构建单个图层与画布对象的关联关系
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
*/
|
||||
export function buildLayerAssociations(layer, canvasObjects) {
|
||||
if (!layer || !canvasObjects || !isArray(canvasObjects)) return;
|
||||
// 处理单个fabricObject关联
|
||||
if (layer.fabricObject) {
|
||||
// 如果图层已经有关联的fabricObject,确保它的layerId和layerName正确
|
||||
layer.fabricObject =
|
||||
canvasObjects.find((obj) => obj.id === layer.fabricObject.id) || null;
|
||||
}
|
||||
|
||||
if (layer.clippingMask) {
|
||||
// clippingMask 可能是一个fabricObject或组
|
||||
layer.clippingMask =
|
||||
canvasObjects.find((obj) => obj.id === layer.clippingMask.id) || null;
|
||||
}
|
||||
|
||||
// 处理多个fabricObjects关联
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricObject) => {
|
||||
// 确保每个fabricObject的layerId和layerName正确
|
||||
const obj = canvasObjects.find((obj) => obj.id === fabricObject.id);
|
||||
if (obj) {
|
||||
obj.layerId = layer.id; // 确保对象的layerId正确
|
||||
obj.layerName = layer.name; // 确保对象的layerName正确
|
||||
return obj;
|
||||
}
|
||||
return null; // 如果没有找到对象,返回null
|
||||
})
|
||||
.filter((obj) => obj !== null); // 过滤掉null值
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象与图层的关联关系
|
||||
* @param {Object} layerManager 图层管理器实例
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
*/
|
||||
export function restoreObjectLayerAssociations(layers, canvasObjects) {
|
||||
if (!layers || !canvasObjects || !isArray(canvasObjects)) return;
|
||||
layers.forEach((layer) => {
|
||||
buildLayerAssociations(layer, canvasObjects);
|
||||
// 处理子图层
|
||||
if (layer?.children?.length) {
|
||||
restoreObjectLayerAssociations(layer.children, canvasObjects);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为画布对象设置图层信息
|
||||
* @param {Object} fabricObject 画布对象
|
||||
* @param {Object} layer 图层对象
|
||||
*/
|
||||
export function setObjectLayerInfo(fabricObject, layer) {
|
||||
if (!fabricObject || !layer) return;
|
||||
|
||||
fabricObject.layerId = layer.id;
|
||||
fabricObject.layerName = layer.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除画布对象的图层信息
|
||||
* @param {Object} fabricObject 画布对象
|
||||
*/
|
||||
export function clearObjectLayerInfo(fabricObject) {
|
||||
if (!fabricObject) return;
|
||||
|
||||
delete fabricObject.layerId;
|
||||
delete fabricObject.layerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证图层关联关系的完整性
|
||||
* @param {Object} layerManager 图层管理器实例
|
||||
* @param {fabric.Canvas} canvas 画布实例
|
||||
* @returns {Object} 验证结果 { valid: boolean, issues: Array }
|
||||
*/
|
||||
export function validateLayerAssociations(layers, canvasObjects) {
|
||||
const issues = [];
|
||||
|
||||
// 检查画布对象是否都有对应的图层
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.layerId) {
|
||||
const layer = layers.find((l) => l.id === obj.layerId);
|
||||
if (!layer) {
|
||||
issues.push({
|
||||
type: "orphaned_object",
|
||||
objectId: obj.id,
|
||||
layerId: obj.layerId,
|
||||
message: `对象 ${obj.id} 关联的图层 ${obj.layerId} 不存在`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
issues.push({
|
||||
type: "missing_layer_id",
|
||||
objectId: obj.id,
|
||||
message: `对象 ${obj.id} 缺少图层ID关联`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 检查图层是否都有对应的画布对象
|
||||
layers.forEach((layer) => {
|
||||
if (layer.fabricObject && layer.fabricObject.id) {
|
||||
const obj = canvasObjects.find((o) => o.id === layer.fabricObject.id);
|
||||
if (!obj) {
|
||||
issues.push({
|
||||
type: "missing_object",
|
||||
layerId: layer.id,
|
||||
objectId: layer.fabricObject.id,
|
||||
message: `图层 ${layer.id} 关联的对象 ${layer.fabricObject.id} 不存在于画布中`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects.forEach((fabricObj) => {
|
||||
if (fabricObj.id) {
|
||||
const obj = canvasObjects.find((o) => o.id === fabricObj.id);
|
||||
if (!obj) {
|
||||
issues.push({
|
||||
type: "missing_object",
|
||||
layerId: layer.id,
|
||||
objectId: fabricObj.id,
|
||||
message: `图层 ${layer.id} 关联的对象 ${fabricObj.id} 不存在于画布中`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
valid: issues.length === 0,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化layers对象属性,只保留必要的属性
|
||||
* @param {Array} layers 图层数组 simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
|
||||
*/
|
||||
|
||||
export function simplifyLayers(layers) {
|
||||
if (!layers || !isArray(layers)) {
|
||||
console.warn("simplifyLayers 请传入有效的图层数组:", layers);
|
||||
return [];
|
||||
}
|
||||
|
||||
layers.forEach((layer) => {
|
||||
// 处理图层遮罩
|
||||
// 如果clippingMask是一个fabricObject或组,确保它的id正确 // 因为是fabric对象,所以没办法直接获取id,只能通过序列化获取
|
||||
if (layer.clippingMask) {
|
||||
layer.clippingMask = layer.clippingMask?.id || null;
|
||||
}
|
||||
|
||||
// 处理单个fabricObject
|
||||
if (layer.fabricObject) {
|
||||
layer.fabricObject = layer.fabricObject?.id;
|
||||
}
|
||||
// 处理多个fabricObjects
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricObject) => {
|
||||
return fabricObject?.id || null; // 确保每个fabricObject都能转换为对象
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
}
|
||||
// 处理子图层
|
||||
if (layer.children && isArray(layer.children)) {
|
||||
layer.children = simplifyLayers(layer.children);
|
||||
}
|
||||
|
||||
// 只保留必要的属性
|
||||
layer = {
|
||||
id: layer.id,
|
||||
name: layer.name,
|
||||
visible: layer.visible,
|
||||
locked: layer.locked,
|
||||
opacity: layer.opacity,
|
||||
clippingMask: layer.clippingMask || null,
|
||||
fabricObject: layer.fabricObject || null,
|
||||
fabricObjects: layer.fabricObjects || [],
|
||||
children: layer.children || [],
|
||||
isBackground: layer.isBackground || false,
|
||||
ifFixed: layer.ifFixed || false,
|
||||
};
|
||||
});
|
||||
|
||||
return layers;
|
||||
}
|
||||
@@ -109,7 +109,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive, onMounted} from 'vue'
|
||||
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive, onMounted, watch} from 'vue'
|
||||
import { Https } from "@/tool/https";
|
||||
import { useStore } from "vuex";
|
||||
import sketchCategory from "@/component/HomePage/sketchCategory.vue";
|
||||
@@ -165,8 +165,14 @@ export default defineComponent({
|
||||
mannequinStyle:computed(()=>store.state.UserHabit.mannequinStyle),//风格列表
|
||||
sexList:computed(()=>store.state.UserHabit.sex.value),//风格列表
|
||||
ageGroupList:computed(()=>store.state.UserHabit.ageGroup),//风格列表
|
||||
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
|
||||
|
||||
})
|
||||
watch(()=>detailData.selectObject,(newValue,oldValue)=>{
|
||||
detailData.mannequinData.sex = newValue.sex?newValue.sex:'Female'
|
||||
detailData.mannequinData.style = newValue.style?newValue.style:''
|
||||
detailData.mannequinData.ageGroup = newValue.ageGroup?newValue.ageGroup:''
|
||||
},{immediate:true})
|
||||
const getDetailListData = reactive({
|
||||
total:0,
|
||||
pageSize:10,
|
||||
|
||||
@@ -71,6 +71,7 @@ export default defineComponent({
|
||||
selectItem.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
|
||||
},{immediate: true,})
|
||||
watch(()=>detailData.frontBack?.body?.path,(newVal)=>{
|
||||
|
||||
let sacle = 0
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
@@ -82,16 +83,22 @@ export default defineComponent({
|
||||
let value = item.style[key]
|
||||
if(typeof value !== 'number'){
|
||||
value = value.replace('px','')
|
||||
item.style[key] = value
|
||||
}else{
|
||||
item.style[key] = value*sacle+'px'
|
||||
}
|
||||
item.style[key] = value*sacle+'px'
|
||||
// item.style[key] = value*sacle+'px'
|
||||
}
|
||||
for (const key in detailData.frontBack.back[index].style) {
|
||||
if(key == 'zIndex')return
|
||||
let value = detailData.frontBack.back[index].style[key]
|
||||
if(typeof value !== 'number'){
|
||||
value = value.replace('px','')
|
||||
detailData.frontBack.back[index].style[key] = value
|
||||
}else{
|
||||
detailData.frontBack.back[index].style[key] = value*sacle+'px'
|
||||
}
|
||||
detailData.frontBack.back[index].style[key] = value*sacle+'px'
|
||||
// detailData.frontBack.back[index].style[key] = value*sacle+'px'
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -415,7 +422,6 @@ export default defineComponent({
|
||||
}
|
||||
for (const key in data.instance.frontBack.back[index].style) {
|
||||
if(key == 'zIndex')return
|
||||
console.log(data.instance.frontBack.back[index].style[key].replace(/px/g,''))
|
||||
data.instance.frontBack.back[index].style[key] = data.instance.frontBack.back[index].style[key].replace(/px/g,'')*sacle+'px'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -39,16 +39,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <input
|
||||
class="search_input"
|
||||
@input="ifMaximumLength"
|
||||
:placeholder="(scene?.value == 'Slogan' && type_.type2 == 'Printboard')?isSloganHint:$t('Generate.inputContent1')"
|
||||
:maxlength='inputShow?0:9999'
|
||||
v-model="searchPictureName"
|
||||
@keydown.enter="getgenerate()"
|
||||
@click="inputFocus()"
|
||||
@paste="onPaste"
|
||||
/> -->
|
||||
<textarea
|
||||
class="textarea"
|
||||
@input="ifMaximumLength"
|
||||
@@ -82,7 +72,7 @@
|
||||
<!-- <i v-show="!isTextarea" class="fi fi-br-expand" @click.stop="setTextareaShow"></i>
|
||||
<i v-show="isTextarea" class="fi fi-bs-compress" @click.stop="setTextareaShow"></i> -->
|
||||
</div>
|
||||
<div class="input_box_btnBox sketch" v-else>
|
||||
<div class="input_box_btnBox sketch" v-else >
|
||||
<div class="upload_item" v-show="sketchboardList.length > 0">
|
||||
<div
|
||||
class="upload_file_item"
|
||||
@@ -125,9 +115,9 @@
|
||||
>
|
||||
</a-upload>
|
||||
</i>
|
||||
<div :title="$t('Generate.style')">
|
||||
<!-- <div :title="$t('Generate.style')">
|
||||
<generalMenu :dataList="printModelList" :isCanvas="type_.type2 == 'Sketchboard'" @setprintModel="setprintModel" :item="printModel"></generalMenu>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<textarea
|
||||
v-show="isTextarea"
|
||||
@@ -141,18 +131,19 @@
|
||||
<div class="generage_btn_box">
|
||||
<div class="generage_btn started_btn" v-show="!isGenerate">
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getgenerate()"></i>
|
||||
<div class="icon iconfont icon-xiala" v-show="
|
||||
type_.type2 == 'Moodboard' ||
|
||||
(type_.type2 == 'Printboard' && scene?.value == 'Pattern') ||
|
||||
(type_.type2 == 'Sketchboard' && scene?.value == 'generate')" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
<div class="icon iconfont icon-xiala" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState && scene?.value != 'extract'">
|
||||
<div v-for="item in speedList" v-show="(type_.type2 == 'Moodboard' && item?.value != 'flux') || (type_.type2 == 'Sketchboard' && item?.value != 'flux') || type_.type2 == 'Printboard'" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
|
||||
</div>
|
||||
<div class="content" v-show="speedState && scene?.value == 'extract'">
|
||||
<div v-for="item in extractList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="isGenerate && !remGenerate">
|
||||
<i class="fi fi-br-loading" ></i>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="remGenerate" @click="removeGenerate">
|
||||
<div class="generage_btn started_btn" v-show="remGenerate" @click.stop="removeGenerate">
|
||||
{{$t('Generate.Close')}}
|
||||
</div>
|
||||
|
||||
@@ -182,19 +173,22 @@
|
||||
@click="generageAdd(item)"
|
||||
:class="[item.status != 'Success'?'hideEvents':'',item?.checked?'active':'']"
|
||||
>
|
||||
<img v-lazy="item.imgUrl" @click.stop="generageAdd(item)">
|
||||
<sketchCategory v-if="type_.type2 == 'Sketchboard' || type_.type2 == 'Printboard'" :isSpread="type_.type2 == 'Printboard'" :disignTypeList="sketchCatecoryList" :generateList="fileList" :item="item" :driver__="driver__.driver" :driverClass="{'class1': type_.type2 == 'Sketchboard'?'Guide_1_13':'','class2':type_.type2 == 'Sketchboard'?'Guide_1_13_1':''}"></sketchCategory>
|
||||
<img v-if="item?.imgUrl" v-lazy="item.imgUrl" @click.stop="generageAdd(item)">
|
||||
<div v-else class="loading">
|
||||
<a-spin size="large" ></a-spin>
|
||||
</div>
|
||||
<sketchCategory v-show="item?.imgUrl" v-if="type_.type2 == 'Sketchboard' || type_.type2 == 'Printboard'" :isSpread="type_.type2 == 'Printboard'" :disignTypeList="sketchCatecoryList" :generateList="fileList" :item="item" :driver__="driver__.driver" :driverClass="{'class1': type_.type2 == 'Sketchboard'?'Guide_1_13':'','class2':type_.type2 == 'Sketchboard'?'Guide_1_13_1':''}"></sketchCategory>
|
||||
<div
|
||||
v-show="item?.imgUrl"
|
||||
class="delete_like_file_block left1"
|
||||
:class="[driver__.driver?'hideEvents':'',]"
|
||||
>
|
||||
<i v-if="!item.like" class="fi fi-rr-heart" @click.stop="likeFile(item,'like')"></i>
|
||||
<i v-if="!item.like" class="fi fi-rr-heart" @click="likeFile(item,'like')"></i>
|
||||
<i v-else class="fi fi-sr-heart" :adminLike="!!item.like" @click.stop="likeFile(item,'noLike')"></i>
|
||||
</div>
|
||||
<div class="delete_like_file_block left" :class="[driver__.driver?'hideEvents':'']">
|
||||
<div v-show="item?.imgUrl" class="delete_like_file_block left">
|
||||
<i class="fi fi-bs-expand-arrows-alt" @click.stop="scaleImage(index)"></i>
|
||||
</div>
|
||||
<div class="delete_like_file_block" :title="t('LibraryPage.Delete')" @click.stop="deleteGenerate(index)">
|
||||
<div v-show="item?.imgUrl" class="delete_like_file_block" :title="t('LibraryPage.Delete')" @click.stop="deleteGenerate(index)">
|
||||
<span class="icon iconfont icon-shanchu operate_icon"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,6 +288,21 @@ export default defineComponent({
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},{
|
||||
title:'',
|
||||
label:'FLUX',
|
||||
value:'flux',
|
||||
},
|
||||
],
|
||||
extractList:[
|
||||
{
|
||||
title:'This method may produce slight discrepancies between the extracted line art and the original image.',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Note: The extracted line art might have minor variations from the original.',
|
||||
label:'FLUX',
|
||||
value:'flux',
|
||||
},
|
||||
],
|
||||
speedState:false,
|
||||
@@ -311,8 +320,16 @@ export default defineComponent({
|
||||
document.removeEventListener('click',openSpeed)
|
||||
}
|
||||
}
|
||||
watch(()=>props.scene,(newVal,oldVal)=>{
|
||||
if(newVal.value == 'extract'){
|
||||
speed.speedData = speed.extractList[0]
|
||||
}else{
|
||||
speed.speedData = speed.speedList[0]
|
||||
}
|
||||
})
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
return {
|
||||
userDetail,
|
||||
@@ -471,6 +488,7 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
generageAdd(data: any) {
|
||||
if(!data?.imgUrl)return
|
||||
data.type_ = this.type_;
|
||||
data.type_.type1 = data.designType?data.designType:this.type_.type1
|
||||
data.resData = JSON.parse(JSON.stringify(data))
|
||||
@@ -487,12 +505,6 @@ export default defineComponent({
|
||||
data.jsContent1 = this.t('uploadFile.jsContent1',{maxImg:maxImg})
|
||||
this.store.commit("addGenerateMaterialFils", data);
|
||||
// console.log(this.fileList);
|
||||
let moodboard = this.store.state.UploadFilesModule.moodboardGenerateFiles
|
||||
let sketch = this.store.state.UploadFilesModule.sketchGenerateFiles
|
||||
let print = this.store.state.UploadFilesModule.printGenerateFiles
|
||||
if((moodboard.length >= 2 || print.length >= 2 || sketch.length >= 2) && this.driver__.driver){
|
||||
driverObj__.moveNext()
|
||||
}
|
||||
},
|
||||
beforeUpload(file: any) {
|
||||
const isJpgOrPng =
|
||||
@@ -543,94 +555,109 @@ export default defineComponent({
|
||||
})
|
||||
},
|
||||
getgenerate(){
|
||||
if(this.scene?.value == 'extract'){
|
||||
this.imageToSketch()
|
||||
return
|
||||
}
|
||||
// if(this.scene?.value == 'extract'){
|
||||
// this.imageToSketch()
|
||||
// return
|
||||
// }
|
||||
this.isTextarea = false
|
||||
this.isInputFocus = false
|
||||
if(this.isGenerate)return
|
||||
clearInterval(this.remGenerateTime)
|
||||
if(this.searchPictureName){
|
||||
let arr = this.searchPictureName.split(/\s+/).length
|
||||
if(arr > 250){
|
||||
message.info(
|
||||
this.t('Generate.jsContent4')
|
||||
);
|
||||
return
|
||||
let httpsUrl = Https.httpUrls.generatePrepare
|
||||
let data
|
||||
if(this.scene?.value == 'extract'){
|
||||
httpsUrl = Https.httpUrls.imageToSketch
|
||||
if((!this.printModel?.id && !this.printModel?.value) || !this.sketchboardList?.[0]?.id)return message.info(this.t('Generate.jsContent4'));
|
||||
data = {
|
||||
"elementId": this.sketchboardList[0].id,
|
||||
gender:this.workspace.sex,
|
||||
"style": this.printModel.value,
|
||||
"styleImageId": this.printModel?.id?this.printModel?.id:'',
|
||||
modelName:this.speedData.value,//为1就是Print
|
||||
}
|
||||
}else{
|
||||
if(this.sketchboardList?.[0]?.imgUrl){
|
||||
|
||||
if(this.searchPictureName){
|
||||
let arr = this.searchPictureName.split(/\s+/).length
|
||||
if(arr > 250){
|
||||
message.info(
|
||||
this.t('Generate.jsContent4')
|
||||
);
|
||||
return
|
||||
}
|
||||
}else{
|
||||
message.info(
|
||||
this.t('Generate.jsContent5')
|
||||
);
|
||||
return
|
||||
if(this.sketchboardList?.[0]?.imgUrl){
|
||||
|
||||
}else{
|
||||
message.info(
|
||||
this.t('Generate.jsContent5')
|
||||
);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let level2Type = ''
|
||||
let collectionElementId = ''
|
||||
let base64 = ''
|
||||
if(this.sketchboardList?.[0]){
|
||||
collectionElementId = this.sketchboardList[0].id
|
||||
if(this.sketchboardList[0].base64){
|
||||
base64 = this.sketchboardList[0].imgUrl
|
||||
}
|
||||
}
|
||||
let sloganText = ''
|
||||
sloganText = this.searchPictureName
|
||||
if(this.upload.level1Type == "Sketchboard"){
|
||||
level2Type = this.sketchboardList?.[0]?.categoryValue?this.sketchboardList[0].categoryValue:''
|
||||
if(this.workspace.styleName){
|
||||
sloganText = `${this.workspace.styleName},${sloganText}`
|
||||
}
|
||||
}else if(this.upload.level1Type == "Printboard"){
|
||||
level2Type = this.scene?.value
|
||||
if(level2Type == 'Slogan' && this.searchPictureName == ''){
|
||||
sloganText = this.isSloganHint
|
||||
}else if(level2Type == 'Pattern'){
|
||||
sloganText = `${this.printModel.value},${sloganText}`
|
||||
}
|
||||
if(!base64 && level2Type == 'Slogan'){
|
||||
message.info(this.t('Generate.jsContent10'));
|
||||
return
|
||||
}
|
||||
}
|
||||
data = {
|
||||
generateType:'text',
|
||||
designType:'collection',
|
||||
collectionElementId:collectionElementId,
|
||||
level1Type:this.upload.level1Type,
|
||||
level2Type:level2Type,
|
||||
text:sloganText,
|
||||
seed:this.searchPictureSeed,
|
||||
userId:this?.userDetail?.userId,
|
||||
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
modelName:this.speedData.value,//为1就是Print
|
||||
isTestUser:this.driver__.driver?false:this.isTest,
|
||||
gender:this.workspace.sex,
|
||||
sloganBase64:base64,
|
||||
ageGroup:this.workspace.ageGroup
|
||||
}
|
||||
this.generateLevel2Type = data.level2Type
|
||||
}
|
||||
let level2Type = ''
|
||||
let collectionElementId = ''
|
||||
let base64 = ''
|
||||
if(this.sketchboardList?.[0]){
|
||||
collectionElementId = this.sketchboardList[0].id
|
||||
if(this.sketchboardList[0].base64){
|
||||
base64 = this.sketchboardList[0].imgUrl
|
||||
}
|
||||
}
|
||||
let sloganText = ''
|
||||
sloganText = this.searchPictureName
|
||||
if(this.upload.level1Type == "Sketchboard"){
|
||||
level2Type = this.sketchboardList?.[0]?.categoryValue?this.sketchboardList[0].categoryValue:''
|
||||
if(this.workspace.styleName){
|
||||
sloganText = `${this.workspace.styleName},${sloganText}`
|
||||
}
|
||||
}else if(this.upload.level1Type == "Printboard"){
|
||||
level2Type = this.scene?.value
|
||||
if(level2Type == 'Slogan' && this.searchPictureName == ''){
|
||||
sloganText = this.isSloganHint
|
||||
}else if(level2Type == 'Pattern'){
|
||||
sloganText = `${this.printModel.value},${sloganText}`
|
||||
}
|
||||
if(!base64 && level2Type == 'Slogan'){
|
||||
message.info(this.t('Generate.jsContent10'));
|
||||
return
|
||||
}
|
||||
}
|
||||
let data = {
|
||||
generateType:'text',
|
||||
designType:'collection',
|
||||
collectionElementId:collectionElementId,
|
||||
level1Type:this.upload.level1Type,
|
||||
level2Type:level2Type,
|
||||
text:sloganText,
|
||||
seed:this.searchPictureSeed,
|
||||
userId:this?.userDetail?.userId,
|
||||
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
modelName:this.speedData.value,//为1就是Print
|
||||
isTestUser:this.driver__.driver?false:this.isTest,
|
||||
gender:this.workspace.sex,
|
||||
sloganBase64:base64,
|
||||
}
|
||||
this.generateLevel2Type = data.level2Type
|
||||
|
||||
|
||||
this.isGenerate = true
|
||||
this.remGenerateTime = setTimeout(()=>{
|
||||
this.remGenerate = true
|
||||
},10000)
|
||||
Https.axiosPost(Https.httpUrls.generatePrepare, data).then(
|
||||
// this.remGenerateTime = setTimeout(()=>{
|
||||
// },10000)
|
||||
Https.axiosPost(httpsUrl, data).then(
|
||||
(rv) => {
|
||||
// if(data.isTestUser){
|
||||
// if(rv.leftUsageCount >= 1){
|
||||
// message.warning(this.t('Generate.jsContent8',{num:rv.leftUsageCount,str:this.t('collectionModal.Moodboard')}));
|
||||
// }else if(rv.leftUsageCount == 0){
|
||||
// message.warning(this.t('Generate.jsContent9',{str:this.t('collectionModal.Moodboard')}));
|
||||
// this.isGenerate = false
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
if(this.scene?.value == 'extract'){
|
||||
rv = {
|
||||
uniqueId:[rv]
|
||||
}
|
||||
}
|
||||
let rvData = rv.uniqueId.map((item:any)=>{
|
||||
return{taskId:item,status:''}
|
||||
})
|
||||
this.remGenerate = true//出现取消按钮
|
||||
this.fileList.unshift(...rvData)
|
||||
this.setGenerate(rv.uniqueId)
|
||||
|
||||
}
|
||||
).catch(res=>{
|
||||
this.generateLevel2Type = ''
|
||||
@@ -676,7 +703,9 @@ export default defineComponent({
|
||||
if(element.status == 'Success'){
|
||||
element.imgUrl = element.url
|
||||
element.id_ = GO.id++
|
||||
this.fileList.unshift(element)
|
||||
let index = this.fileList.findIndex((item:any)=>item.taskId == element.taskId)
|
||||
this.fileList[index] = element
|
||||
// this.fileList.unshift(element)
|
||||
data = data.filter((item:any) => item !== element.taskId);
|
||||
if(this.type_.type2 == 'Sketchboard'){
|
||||
this.sketchCatecoryList.forEach((itemCategory:any) => {
|
||||
@@ -689,6 +718,10 @@ export default defineComponent({
|
||||
element.categoryValue = this.scene?.value
|
||||
element.category = this.scene?.name
|
||||
}
|
||||
}else if(element.status == 'Fail' || element.status == 'Invalid'){
|
||||
data = data.filter((item:any) => item !== element.taskId);
|
||||
this.fileList = this.fileList.filter((item:any) => item.taskId !== element.taskId);
|
||||
message.info(this.t('Generate.everyTimeEffectPoor'));
|
||||
}
|
||||
});
|
||||
if((data.length == 0)|| (rv.filter((item:any)=>item.status == 'Invalid').length ==data.length)){
|
||||
@@ -718,7 +751,7 @@ export default defineComponent({
|
||||
this.remGenerate = false
|
||||
this.generateLevel2Type = ''
|
||||
});
|
||||
},1000)
|
||||
},5000)
|
||||
},
|
||||
removeGenerate(){
|
||||
//取消操作
|
||||
@@ -737,8 +770,12 @@ export default defineComponent({
|
||||
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
type: type
|
||||
}
|
||||
|
||||
Https.axiosGet(Https.httpUrls.generateStopWaiting, {params:data}).then(
|
||||
(rv) => {
|
||||
this.generateProceedList.forEach((generateProceedListItem:any)=>{
|
||||
this.fileList = this.fileList.filter((item:any) => generateProceedListItem.taskId!== item.taskId);
|
||||
})
|
||||
this.generateProceedList = []
|
||||
}
|
||||
).catch(res=>{
|
||||
@@ -1044,6 +1081,13 @@ export default defineComponent({
|
||||
pointer-events:none;
|
||||
}
|
||||
}
|
||||
.loading{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
img {
|
||||
// width: calc(10rem*1.2);
|
||||
// height: calc(10rem*1.2);
|
||||
|
||||
@@ -534,9 +534,9 @@ export default defineComponent({
|
||||
imageStrength:(100 - imageStrength)/100,
|
||||
}
|
||||
productImgData.isProductimg = true
|
||||
remPrductimgTime = setTimeout(()=>{
|
||||
productImgData.remProductimg = true
|
||||
},10000)
|
||||
// remPrductimgTime = setTimeout(()=>{
|
||||
// productImgData.remProductimg = true
|
||||
// },10000)
|
||||
let url = Https.httpUrls.toProduct
|
||||
if(productimgMenu.value.value == 'Relight'){
|
||||
url = Https.httpUrls.relight
|
||||
@@ -544,6 +544,7 @@ export default defineComponent({
|
||||
productImgData.isShowMark = true
|
||||
Https.axiosPost(url, data).then(
|
||||
(rv) => {
|
||||
productImgData.remProductimg = true
|
||||
productImgData.isShowMark = false
|
||||
let arr:any = []
|
||||
rv.forEach((item:any)=>{
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'">{{$t('ProductImg.MagicTools')}}</span>
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'Relight'">{{$t('ProductImg.relightingTool')}}</span>
|
||||
</div>
|
||||
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>Selection Function</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction" style="margin-bottom: 1rem;">
|
||||
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Similarity')}}</span>
|
||||
</div>
|
||||
@@ -45,10 +50,10 @@
|
||||
</a-slider>
|
||||
<input type="number" readonly v-model="productimgSimilarity">
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.RelightDirection')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
|
||||
<!-- <a-slider class="system_silder"
|
||||
v-model:value="similarity"
|
||||
@afterChange="setSimilarity"
|
||||
@@ -58,10 +63,10 @@
|
||||
</a-slider> -->
|
||||
<a-select style="width: 100%;" v-model:value="productimgRelightDirection" :options="productimgRelightDirectionList"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Highlight')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
|
||||
<a-slider class="system_silder"
|
||||
v-model:value="productimgBrightenValue"
|
||||
:tooltipVisible="false"
|
||||
@@ -92,7 +97,22 @@
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="productImg_content_item_generate_btn input_border">
|
||||
<div class="generage_btn_box" style="margin-left: auto;">
|
||||
<div class="generage_btn started_btn" v-show="!productimgIsProductimg">
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg()"></i>
|
||||
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType != 'Relight'" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="productimgIsProductimg && !productimgRemProductimg">
|
||||
<i class="fi fi-br-loading" ></i>
|
||||
</div>
|
||||
<div class="generage_btn started_btn" v-show="productimgRemProductimg" @click="removeProductimg">
|
||||
{{$t('Generate.Close')}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="productImg_content_item_generate_btn input_border">
|
||||
<div class="input_box">
|
||||
<div v-show="!productimgIsProductimg" class="generage_btn started_btn" @click.stop="getPrductimg">
|
||||
{{ $t('Generate.Generate') }}
|
||||
@@ -104,7 +124,7 @@
|
||||
{{$t('Generate.Close')}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="scaleImage_content_imgBox" :class="{active:isComparison}">
|
||||
@@ -189,6 +209,55 @@ export default defineComponent({
|
||||
productimgRelightDirection:props.productData.RelightDirection,
|
||||
productimgRelightDirectionList:props.productData.RelightDirectionList,
|
||||
})
|
||||
let speed = reactive({
|
||||
speedList:[
|
||||
] as any,
|
||||
speedTypeList:{
|
||||
poseTransfer:[
|
||||
{
|
||||
title:'Generate high-quality images',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},
|
||||
],
|
||||
toPorductImg:[
|
||||
{
|
||||
title:'Generate with high quality',
|
||||
label:'High',
|
||||
relightLabel:'Relight',
|
||||
value:'',
|
||||
},{
|
||||
title:'',
|
||||
label:'FLUX',
|
||||
relightLabel:'Edit',
|
||||
value:'flux',
|
||||
},
|
||||
]
|
||||
},
|
||||
speedState:false,
|
||||
speedData:{
|
||||
title:'Generate high-quality images',
|
||||
relightLabel:'Relight',
|
||||
label:'High',
|
||||
value:'',
|
||||
},
|
||||
})
|
||||
const openSpeed = ()=>{
|
||||
speed.speedState = !speed.speedState
|
||||
if(speed.speedState){
|
||||
document.addEventListener('click',openSpeed)
|
||||
}else{
|
||||
document.removeEventListener('click',openSpeed)
|
||||
}
|
||||
}
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
let scaleImage: any = ref(false);
|
||||
let isShowMark = ref(false)
|
||||
let loadingShow = ref(false)
|
||||
@@ -224,9 +293,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
productimg.productimgIsProductimg = true
|
||||
remPrductimgTime = setTimeout(()=>{
|
||||
productimg.productimgRemProductimg = true
|
||||
},10000)
|
||||
// remPrductimgTime = setTimeout(()=>{
|
||||
// productimg.productimgRemProductimg = true
|
||||
// },10000)
|
||||
let url = Https.httpUrls.relight
|
||||
if(scaleImageList.value[scaleImageIndex.value]?.resultType == 'ToProductImage'){
|
||||
url = Https.httpUrls.toProduct
|
||||
@@ -234,6 +303,7 @@ export default defineComponent({
|
||||
isShowMark.value = true
|
||||
Https.axiosPost(url, data).then(
|
||||
(rv) => {
|
||||
productimg.productimgRemProductimg = true
|
||||
isShowMark.value = false
|
||||
scaleImageList.value[scaleImageIndex.value].imgUrl = '/image/loading.gif'
|
||||
let arr:any = []
|
||||
@@ -381,6 +451,9 @@ export default defineComponent({
|
||||
return {
|
||||
t,
|
||||
...toRefs(productimg),
|
||||
...toRefs(speed),
|
||||
openSpeed,
|
||||
setSpeed,
|
||||
scaleImage,
|
||||
isShowMark,
|
||||
loadingShow,
|
||||
@@ -427,6 +500,12 @@ export default defineComponent({
|
||||
this.scaleImageIndex = index
|
||||
if(dialogueIndex)this.robotAssits = dialogueIndex
|
||||
// let scaleImageList = this.store.state.UploadFilesModule.moodboard
|
||||
if(this.scaleImageList[index].resultType == "PoseTransfer"){
|
||||
this.speedList = this.speedTypeList.poseTransfer
|
||||
}else{
|
||||
this.speedList = this.speedTypeList.toPorductImg
|
||||
}
|
||||
this.speedData = JSON.parse(JSON.stringify(this.speedList[0]))
|
||||
document.addEventListener('keydown',this.setKeydown)
|
||||
},
|
||||
cancelDsign(){
|
||||
|
||||
@@ -81,11 +81,13 @@ export default defineComponent({
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
video{
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: max-content;
|
||||
}
|
||||
.general_video_btn{
|
||||
color: #fff;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal_title_text">
|
||||
<div>Create Cloud Generation Tasks</div>
|
||||
<div>Create Batch Generation Tasks</div>
|
||||
</div>
|
||||
<div class="allUserPoeration_center admin_page">
|
||||
<div class="admin_state_item">
|
||||
@@ -52,12 +52,24 @@
|
||||
placeholder="Please select"
|
||||
:options="objectList"
|
||||
@search="getHistoryProjectList"
|
||||
@change="changeProject"
|
||||
>
|
||||
<template #option="{ value: val, label, icon,updateTime }">
|
||||
<span :title="updateTime.replace('T', ' ')">{{ label }}</span>
|
||||
</template>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Name <span>*</span></span>
|
||||
<input
|
||||
v-model="porjectName"
|
||||
:placeholder="placeholder"
|
||||
@focus="focus"
|
||||
@blur="blur"
|
||||
type="text"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item" v-show="buildType">
|
||||
<span>Quantity <span>*</span></span>
|
||||
<input
|
||||
@@ -72,14 +84,17 @@
|
||||
<div v-show="buildType == 'TO_PRODUCT_IMAGE'" class="admin_state_item ">
|
||||
<span>{{$t('ProductImg.Similarity')}}</span>
|
||||
<div class="sliderAndImput" style="width: 200px">
|
||||
<a-slider class="system_silder"
|
||||
<!-- <a-slider class="system_silder"
|
||||
v-model:value="similarity"
|
||||
range
|
||||
:step="5"
|
||||
|
||||
>
|
||||
</a-slider>
|
||||
<!-- <input type="number" readonly v-model="similarity"> -->
|
||||
</a-slider> -->
|
||||
<div style="display: flex;">
|
||||
<input type="number" readonly v-model="similarity[0]">
|
||||
<div style="margin: 0 1rem;">-</div>
|
||||
<input type="number" readonly v-model="similarity[1]">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="buildType == 'RELIGHT'" class="admin_state_item ">
|
||||
@@ -117,12 +132,18 @@
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="productImg_content_item_imgBox generalScroll upload_item" v-if="buildType && buildType != 'SERIES_DESIGN' && buildType != 'SINGLE_DESIGN'" v-mousewheel>
|
||||
<div class="content_item_imgBox_itemImg" v-for="item in uploadElement" :key="item">
|
||||
<div
|
||||
class="imgBox"
|
||||
@click="()=>item.isChecked = !item.isChecked"
|
||||
>
|
||||
<img :class="[item?.isChecked?'active':'']" :src="item?.url" class="upload_img"/>
|
||||
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content_item_imgBox_itemImg" v-for="(file, index) in fileList" :key="file">
|
||||
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" >
|
||||
<a-spin
|
||||
:indicator="indicator"
|
||||
tip="Uploading..."
|
||||
/>
|
||||
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" style="display: flex;align-items: center;">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<div
|
||||
class="imgBox"
|
||||
@@ -217,9 +238,10 @@ export default defineComponent({
|
||||
exhibitionImgList:[],//选择的图片
|
||||
projectData:null,//批量id
|
||||
objectList:[],
|
||||
porjectName:'',//任务名字
|
||||
//toProduct
|
||||
generateText:'',//输入的内容
|
||||
similarity:[30,60],
|
||||
similarity:[20,40],
|
||||
brightenValue:1,//亮度
|
||||
relightDirection:'Right Light',//打光方向
|
||||
relightDirectionList:[
|
||||
@@ -238,13 +260,43 @@ export default defineComponent({
|
||||
}
|
||||
],
|
||||
fileList:[],
|
||||
uploadElement:[],
|
||||
placeholder:'',
|
||||
})
|
||||
const getUploadElement = ()=>{
|
||||
operations.loadingShow = true
|
||||
let value = {
|
||||
id:operationsData.projectData,
|
||||
moduleList:['uploadElement']
|
||||
}
|
||||
operationsData.placeholder = 'Batch_' + setPlaceholder()
|
||||
Https.axiosPost(Https.httpUrls.getModuleContent,value).then(async (rv)=>{
|
||||
operationsData.uploadElement = rv.uploadElement
|
||||
operations.loadingShow = false
|
||||
}).catch((err)=>{
|
||||
operations.loadingShow = false
|
||||
})
|
||||
}
|
||||
let init = (projectData,buildType)=>{
|
||||
operations.operationsModal = true
|
||||
if(projectData?.id)operationsData.projectData = {label:projectData.name,value:projectData.id}
|
||||
|
||||
clearData()
|
||||
if(projectData?.id){
|
||||
operationsData.projectData = {label:projectData.name,value:projectData.id}
|
||||
getUploadElement()
|
||||
}
|
||||
if(buildType.value)operationsData.buildType = buildType
|
||||
}
|
||||
const clearData = ()=>{
|
||||
operationsData.porjectName = ''
|
||||
operationsData.generateText = ''
|
||||
operationsData.similarity = [20,40]
|
||||
operationsData.brightenValue = 1
|
||||
operationsData.fileList = []
|
||||
operationsData.uploadElement = []
|
||||
}
|
||||
const changeProject = ()=>{
|
||||
getUploadElement()
|
||||
}
|
||||
const changeBuildType = ()=>{
|
||||
// operationsData.exhibitionImgList = []
|
||||
operationsData.projectData = null
|
||||
@@ -255,15 +307,20 @@ export default defineComponent({
|
||||
})
|
||||
getHistoryProjectList()
|
||||
}
|
||||
const getGenerateCloudImgList = ()=>{
|
||||
const getGenerateCloudImgList = (type)=>{
|
||||
let list = []
|
||||
if(operationsData.buildType == 'SINGLE_DESIGN'|| operationsData.buildType == 'SERIES_DESIGN')return list
|
||||
let selectList = operationsData.fileList.filter((item)=>item.isChecked)
|
||||
if(type == 'SINGLE_DESIGN'|| type == 'SERIES_DESIGN')return list
|
||||
let selectList = []
|
||||
let fileList = operationsData.fileList.filter((item)=>item.isChecked)
|
||||
let uploadElement = operationsData.uploadElement.filter((item)=>item.isChecked)
|
||||
if(fileList)selectList.push(...fileList)
|
||||
if(uploadElement)selectList.push(...uploadElement)
|
||||
|
||||
selectList.forEach((item)=>{
|
||||
let obj = {
|
||||
|
||||
}
|
||||
if(operationsData.buildType == 'POSE_TRANSFER'){
|
||||
if(type == 'POSE_TRANSFER'){
|
||||
obj = {
|
||||
poseId:1,
|
||||
productImage:getMinioUrl(item.imgUrl)
|
||||
@@ -271,7 +328,7 @@ export default defineComponent({
|
||||
}else{
|
||||
obj = {
|
||||
elementId:item.id,
|
||||
elementType:item.type
|
||||
elementType:item.type||'ProductElement'
|
||||
}
|
||||
}
|
||||
list.push(obj)
|
||||
@@ -337,10 +394,11 @@ export default defineComponent({
|
||||
"buildType": buildTypeCorresponding[operationsData.buildType],
|
||||
nums: operationsData.numberOfImages,
|
||||
projectId: operationsData.projectData,
|
||||
name:operationsData.porjectName || operationsData.projectData,
|
||||
//productimg
|
||||
toProductImage:{
|
||||
prompt:operationsData.generateText,//输入的内容
|
||||
toProductImageVOList:(operationsData.buildType == 'TO_PRODUCT_IMAGE' || operationsData.buildType == 'RELIGHT')?getGenerateCloudImgList():[],//选择的图片
|
||||
toProductImageVOList:getGenerateCloudImgList(operationsData.buildType),//选择的图片
|
||||
// toProductImageVOList:getPorductImg(),//选择的图片
|
||||
projectId: operationsData.projectData,
|
||||
direction:operationsData.relightDirection,//打光方向
|
||||
@@ -351,7 +409,7 @@ export default defineComponent({
|
||||
},
|
||||
//poseTransform
|
||||
// poseTransform:getPoseTransformData(),
|
||||
poseTransform:operationsData.buildType == 'POSE_TRANSFER'?getGenerateCloudImgList():[],
|
||||
poseTransform:operationsData.buildType == 'POSE_TRANSFER'?getGenerateCloudImgList('POSE_TRANSFER'):[],
|
||||
private: operationsData.projectData,
|
||||
ToProductImageDTO: operationsData.projectData,
|
||||
}
|
||||
@@ -375,7 +433,7 @@ export default defineComponent({
|
||||
// if(data.poseTransform.length == 0)return message.warning("You must first generate results in the 'To Product Image' module before you can use the 'Transfer Pose' cloud generation feature.")
|
||||
}
|
||||
if(operationsData.buildType == 'DESIGN' && !operationsData.projectData)return message.warning('Please select a project')
|
||||
if(!data.buildType || !data.nums || (operationsData.buildType == 'DESIGN' && !operationsData.projectData))return message.warning('Please check the input box marked with *')
|
||||
if(!data.buildType || !data.nums || !data.name || (operationsData.buildType == 'DESIGN' && !operationsData.projectData))return message.warning('Please check the input box marked with *')
|
||||
operations.loadingShow = true
|
||||
Https.axiosPost(Https.httpUrls.designCloud, data).then(
|
||||
(rv) => {
|
||||
@@ -458,11 +516,25 @@ export default defineComponent({
|
||||
bor = false
|
||||
}
|
||||
}
|
||||
const setPlaceholder = ()=>{
|
||||
if(!operationsData.projectData)return ''
|
||||
let index = operationsData.objectList.findIndex(item => item.id === operationsData.projectData)
|
||||
return operationsData.objectList[index].name
|
||||
}
|
||||
const focus = ()=>{
|
||||
if(operationsData.porjectName)return
|
||||
operationsData.porjectName = operationsData.placeholder
|
||||
}
|
||||
const blur = ()=>{
|
||||
if(operationsData.porjectName != operationsData.placeholder)return
|
||||
operationsData.porjectName = ''
|
||||
}
|
||||
return {
|
||||
...toRefs(operations),
|
||||
...toRefs(operationsData),
|
||||
cancelDsign,
|
||||
init,
|
||||
changeProject,
|
||||
focus,
|
||||
blur,
|
||||
setOk,
|
||||
@@ -658,13 +730,17 @@ export default defineComponent({
|
||||
// border-radius: 1.6rem;
|
||||
flex: 1;
|
||||
}
|
||||
> input{
|
||||
border-radius: 1.6rem;
|
||||
width: 4rem;
|
||||
margin-left: 1rem;
|
||||
height: 100%;
|
||||
border-radius: 1rem;
|
||||
>div{
|
||||
input{
|
||||
border-radius: 1.6rem;
|
||||
width: 5rem;
|
||||
padding: 4px 11px 4px;
|
||||
margin-left: 1rem;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,17 +47,35 @@
|
||||
<div class="content">
|
||||
<tr v-for="(row, index) in contentList" :key="index">
|
||||
<td v-for="header in cloudTiltleList" :key="header.value">
|
||||
<span v-show="header.value != 'operation'">
|
||||
<div v-if="header.value != 'operation' && header.value != 'name'">
|
||||
{{header?.fun?header.fun(row[header.value]) : row[header.value]}}
|
||||
</div>
|
||||
<div v-if="header.value == 'name'">
|
||||
<div v-if="row.id == renameId" class="rename">
|
||||
<input type="text" v-model="renameText">
|
||||
<i class="fi fi-br-check" @click="submitRename(row)"></i>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{header?.fun?header.fun(row[header.value]) : row[header.value]}}
|
||||
</div>
|
||||
</div>
|
||||
<span style="color: #007EE5; cursor: pointer; margin-right: 1rem;" v-show="header.value == 'operation'" @click="setRename(row)">
|
||||
Rename
|
||||
</span>
|
||||
<span style="color: #007EE5; cursor: pointer;" v-show="header.value == 'operation'" @click="detailIamge(row)">
|
||||
<span style="color: #007EE5; cursor: pointer; margin-right: 1rem;" v-show="header.value == 'operation'" @click="detailIamge(row)">
|
||||
Review
|
||||
</span>
|
||||
<span style="color: #007EE5; cursor: pointer;" v-show="header.value == 'operation'" @click="deleteRom(row)">
|
||||
Delete
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
<a-pagination style="text-align: center;" @change="pagination" v-model:current="currentPage" :total="total" show-less-items />
|
||||
</div>
|
||||
<div class="mark_loading" v-show="loadingShow">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<createCloud ref="createCloud" :cloudList="generateList.seriesDesign" @getContentList="submitGetContentList"></createCloud>
|
||||
</div>
|
||||
</template>
|
||||
@@ -159,6 +177,9 @@ export default defineComponent({
|
||||
},
|
||||
cloudTiltleList:[
|
||||
{
|
||||
name:'Task Name',
|
||||
value:'name',
|
||||
},{
|
||||
name:'Task type',
|
||||
value:'buildType',
|
||||
fun:(value:any)=>{
|
||||
@@ -213,6 +234,9 @@ export default defineComponent({
|
||||
] as any,
|
||||
objectList:[],
|
||||
isGetContentList:false as any,
|
||||
renameId:-1 as any,
|
||||
renameText:'',
|
||||
loadingShow:false,
|
||||
})
|
||||
const dataDom = reactive({
|
||||
createCloud,
|
||||
@@ -259,7 +283,13 @@ export default defineComponent({
|
||||
store.commit('setCloudList',{str:'relight',list:rv.relight})
|
||||
router.push(`/home/tools?tools=${item.buildType}&id=${item.projectId}&source=batch`)
|
||||
}else if(item.buildType == 'poseTransfer'){
|
||||
store.commit('setCloudList',{str:'poseTransfer',list:rv.poseTransfer})
|
||||
let list = {
|
||||
list:rv.poseTransfer,
|
||||
str:'add',
|
||||
index:-1,
|
||||
}
|
||||
store.commit("setPoseTransfer", list);
|
||||
// store.commit('setCloudList',{str:'poseTransfer',list:rv.poseTransfer})
|
||||
router.push(`/home/tools?tools=${item.buildType}&id=${item.projectId}&source=batch`)
|
||||
}
|
||||
// if(rv.design && rv.design.length > 0){
|
||||
@@ -281,6 +311,7 @@ export default defineComponent({
|
||||
}
|
||||
const pagination = ()=>{
|
||||
data.isGetContentList = true
|
||||
data.renameId = -1
|
||||
getContentList()
|
||||
}
|
||||
let time = null as any
|
||||
@@ -290,7 +321,7 @@ export default defineComponent({
|
||||
let value = {
|
||||
page:data.currentPage,
|
||||
size:data.pageSize,
|
||||
projectId: data.projectData?.value,
|
||||
projectId: data.projectData?.value?data.projectData?.value:'',
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.cloudPage,value).then((rv)=>{
|
||||
data.contentList = rv.content
|
||||
@@ -342,6 +373,40 @@ export default defineComponent({
|
||||
const handleChange = (event:any,value:any)=>{
|
||||
data.createData = value
|
||||
}
|
||||
const setRename = (item:any)=>{
|
||||
data.renameId = item.id
|
||||
data.renameText = item.name
|
||||
}
|
||||
const submitRename = (item:any)=>{
|
||||
data.renameId = -1
|
||||
data.loadingShow = true
|
||||
let value = {
|
||||
id:item.id,
|
||||
name:data.renameText,
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.cloudTaskNameUpdate,value).then((rv)=>{
|
||||
data.loadingShow = false
|
||||
data.renameText = ''
|
||||
|
||||
data.isGetContentList = true
|
||||
getContentList()
|
||||
}).catch((err)=>{
|
||||
data.loadingShow = false
|
||||
})
|
||||
}
|
||||
const deleteRom = (item:any)=>{
|
||||
let value = {
|
||||
id:item.id
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.cloudTaskDelete,value).then((rv)=>{
|
||||
data.loadingShow = false
|
||||
|
||||
data.isGetContentList = true
|
||||
getContentList()
|
||||
}).catch((err)=>{
|
||||
data.loadingShow = false
|
||||
})
|
||||
}
|
||||
onBeforeUnmount(()=>{
|
||||
data.isGetContentList = false
|
||||
})
|
||||
@@ -366,6 +431,9 @@ export default defineComponent({
|
||||
handleChange,
|
||||
getHistoryProjectList,
|
||||
pagination,
|
||||
setRename,
|
||||
submitRename,
|
||||
deleteRom,
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
@@ -452,7 +520,31 @@ export default defineComponent({
|
||||
text-align: center;
|
||||
width: calc(100% / 4);
|
||||
line-height: 4.6rem;
|
||||
font-size: 2.2rem;
|
||||
font-size: 1.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.rename{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
input{
|
||||
height: 100%;
|
||||
padding: .8rem;
|
||||
width: 12rem;
|
||||
}
|
||||
> i{
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ import { useStore } from "vuex";
|
||||
import { Modal,message,Upload,CascaderProps } from 'ant-design-vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { setCookie, getCookie, WriteCookie } from "@/tool/cookie";
|
||||
import { useRouter,useRoute } from 'vue-router'
|
||||
export default defineComponent({
|
||||
components:{
|
||||
},
|
||||
@@ -88,6 +89,7 @@ export default defineComponent({
|
||||
emits:['chatChange'],
|
||||
setup(props,{emit}) {
|
||||
const store = useStore();
|
||||
const route = useRoute()
|
||||
const data = reactive({
|
||||
chatContent:'',
|
||||
openChat:true,
|
||||
@@ -107,6 +109,7 @@ export default defineComponent({
|
||||
watch(()=>data.selectObject.id,(newValue,oldValue)=>{
|
||||
if(newValue && (data.selectObject.httpType == 'SERIES_DESIGN' || data.selectObject.httpType == 'SINGLE_DESIGN')){
|
||||
data.chatList = []
|
||||
if(route.query?.create)return
|
||||
nextTick(()=>{
|
||||
getChatHistory(newValue)
|
||||
})
|
||||
@@ -146,7 +149,7 @@ export default defineComponent({
|
||||
// data.chatList[data.chatList.length-1].content.message+=JSON.parse(event.data).content
|
||||
// }
|
||||
const container = dataDom.chatBox;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
if(container?.scrollHeight)container.scrollTop = container.scrollHeight;
|
||||
|
||||
const eventData = JSON.parse(event.data)
|
||||
if(eventData.type == 'text'){
|
||||
@@ -155,6 +158,11 @@ export default defineComponent({
|
||||
data.chatList[data.chatList.length-1].content.think+=eventData.content
|
||||
}else if(eventData.type == "tools_response"){
|
||||
let nameList = ['moodboard','printboard','sketchboard','generate_color_code']
|
||||
let nameData = {
|
||||
moodboard:'moodBoard',
|
||||
printboard:'printBoard',
|
||||
sketchboard:'sketchBoard',
|
||||
} as any
|
||||
let getData = ''
|
||||
if(nameList.indexOf(eventData.tools_name) > -1){
|
||||
if(data.chatList[data.chatList.length - 1].content.message)data.chatList.push({content:{message:''},role:'system'})
|
||||
@@ -163,16 +171,14 @@ export default defineComponent({
|
||||
getData = 'colorboard'
|
||||
}else{
|
||||
data.chatList[data.chatList.length-1].content.img = JSON.parse(JSON.parse(event.data).content).receiveCollectionElementList
|
||||
getData = eventData.tools_name
|
||||
getData = nameData[eventData.tools_name]
|
||||
}
|
||||
data.chatList.push({content:{message:''},role:'system'})
|
||||
}else{
|
||||
|
||||
}else if(eventData.tools_name == 'design_control_signal'){
|
||||
emit('chatChange',{type:eventData.tools_name,design:true})
|
||||
}
|
||||
emit('chatChange',{type:eventData.type,module:getData})
|
||||
|
||||
}else if(eventData.type == "design_control_signal"){
|
||||
emit('chatChange',{type:eventData.type,design:true})
|
||||
}
|
||||
//emit('chatChange',{type:JSON.parse(event.data).status})
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
:closable="false"
|
||||
:mask="true"
|
||||
:keyboard="false"
|
||||
:destroyOnClose="true"
|
||||
:destroyOnClose="false"
|
||||
:zIndex="1000"
|
||||
>
|
||||
<div class="generalModel_btn">
|
||||
@@ -23,18 +23,33 @@
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collection_title">
|
||||
<div class="collection_title_text">
|
||||
<div v-show="collectionStep === 1">{{ $t('collectionModal.Moodboard') }}</div>
|
||||
<div v-show="collectionStep === 2">{{ $t('collectionModal.Printboard') }}</div>
|
||||
<div v-show="collectionStep === 3">{{ $t('collectionModal.Colorboard') }}</div>
|
||||
<div v-show="collectionStep === 4">{{ $t('collectionModal.Mannquinboard') }}</div>
|
||||
<div v-show="collectionStep === 5">{{ $t('collectionModal.Sketchboard') }}</div>
|
||||
<!-- <div v-show="collectionStep === 5">Markets Sketch</div> -->
|
||||
<!-- <div class="collection_title_text_intro" v-show="collectionStep === 1">{{ $t('collectionModal.MoodCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 2">{{ $t('collectionModal.PrinCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 3">{{ $t('collectionModal.ColorCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 4">{{ $t('collectionModal.SketchCollection') }}</div>
|
||||
<div class="collection_title_text_intro" v-show="collectionStep === 4">{{ $t('collectionModal.SketchCollection') }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="designOpenrtion_content">
|
||||
<!-- <div class="modal_title_text">
|
||||
<div>Setting</div>
|
||||
</div> -->
|
||||
<div class="collectionBox">
|
||||
<MoodboardUpload ref="moodBoard" v-if="openType == 'moodBoard' || collectionStep == 1"></MoodboardUpload>
|
||||
<PrintboardUpload ref="printBoard" v-if="openType == 'printBoard' || collectionStep == 2"></PrintboardUpload>
|
||||
<ColorboardUpload ref="colorBoard" v-if="openType == 'colorBoard' || collectionStep == 3"></ColorboardUpload>
|
||||
<SketchboardUpload ref="sketchBoard" v-if="openType == 'sketchBoard' || collectionStep == 4"></SketchboardUpload>
|
||||
<mannequin ref="mannequin" v-if="openType == 'mannequin' || collectionStep == 5"></mannequin>
|
||||
<MoodboardUpload ref="moodBoard" v-show="openType == 'moodBoard' || collectionStep == 1"></MoodboardUpload>
|
||||
<PrintboardUpload ref="printBoard" v-show="openType == 'printBoard' || collectionStep == 2"></PrintboardUpload>
|
||||
<ColorboardUpload ref="colorBoard" v-show="openType == 'colorBoard' || collectionStep == 3"></ColorboardUpload>
|
||||
<mannequin ref="mannequin" v-show="openType == 'mannequin' || collectionStep == 4"></mannequin>
|
||||
<SketchboardUpload ref="sketchBoard" v-show="openType == 'sketchBoard' || collectionStep == 5"></SketchboardUpload>
|
||||
</div>
|
||||
<div class="collection_page">
|
||||
<div class="collection_page" v-show="isNext">
|
||||
<i v-show="collectionStep > 1" class="fi fi-rr-arrow-small-left" @click="lastStep()"></i>
|
||||
<i v-if="collectionStep < 5" class="fi fi-rr-arrow-small-right Guide_1_8" @click.stop="nextStep()"></i>
|
||||
<i v-else class="fi fi-rr-check Guide_1_14" @click.stop="cleardata()"></i>
|
||||
@@ -47,11 +62,12 @@
|
||||
</a-modal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent,computed,ref,provide,nextTick,createVNode,toRefs, reactive} from 'vue'
|
||||
import { defineComponent,computed,ref,provide,nextTick,inject,toRefs, reactive, onBeforeMount} from 'vue'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { Https } from "@/tool/https";
|
||||
import { useStore } from "vuex";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { rgbToHsv, dataURLtoBlob } from "@/tool/util";
|
||||
import { init } from 'echarts/core';
|
||||
import MoodboardUpload from './collection/MoodboardUpload.vue';
|
||||
import PrintboardUpload from './collection/PrintboardUpload.vue';
|
||||
@@ -64,6 +80,10 @@ export default defineComponent({
|
||||
MoodboardUpload,PrintboardUpload,ColorboardUpload,SketchboardUpload,mannequin,
|
||||
},
|
||||
props:{
|
||||
isNext:{
|
||||
type:Boolean,
|
||||
default:true,
|
||||
},
|
||||
},
|
||||
emits:['getHistory'],
|
||||
setup(props,{emit}) {
|
||||
@@ -74,6 +94,8 @@ export default defineComponent({
|
||||
openType:'',
|
||||
collectionStep:1,
|
||||
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
|
||||
createProbject:inject('createProbject') as any
|
||||
|
||||
})
|
||||
let driver__:any = computed(()=>{
|
||||
return store.state.Guide.guide
|
||||
@@ -83,8 +105,8 @@ export default defineComponent({
|
||||
moodBoard:null as any,
|
||||
printBoard:null as any,
|
||||
colorBoard:null as any,
|
||||
sketchBoard:null as any,
|
||||
mannequin:null as any,
|
||||
sketchBoard:null as any,
|
||||
}) as any
|
||||
const init = (value:any)=>{
|
||||
data.habitSetStyle = true
|
||||
@@ -95,14 +117,16 @@ export default defineComponent({
|
||||
dataDom[value].openSetData()
|
||||
})
|
||||
}
|
||||
let cleardata = ()=>{
|
||||
let cleardata = async ()=>{
|
||||
data.habitSetStyle = false
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = 1
|
||||
if(data.openType)store.dispatch('setAllBoardData',{type:data.openType})
|
||||
saveProject(data.openType)
|
||||
}
|
||||
const saveProject = (str:any)=>{
|
||||
const saveProject = async (str:any)=>{
|
||||
if(str == 'design')return
|
||||
if(!data.selectObject.id && data.createProbject)await data.createProbject()
|
||||
let value:any = {
|
||||
projectId:data.selectObject.id,
|
||||
}
|
||||
@@ -114,11 +138,49 @@ export default defineComponent({
|
||||
})
|
||||
})
|
||||
}
|
||||
let lastStep = ()=>{
|
||||
let getPantongName = ()=>{
|
||||
let colorBoards = store.state.UploadFilesModule.colorBoards;
|
||||
if(!colorBoards || colorBoards?.length == 0) return
|
||||
data.isShowMark = true
|
||||
let value: any = [];
|
||||
for (let v of colorBoards) {
|
||||
let color: any = [v.rgbValue.r, v.rgbValue.g, v.rgbValue.b];
|
||||
let hsv = rgbToHsv(color);
|
||||
v.hsv = hsv[0] + hsv[1] + hsv[2];
|
||||
value.push({
|
||||
h: hsv[0],
|
||||
s: hsv[1],
|
||||
v: hsv[2],
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve: any, reject: any) => {
|
||||
Https.axiosPost(Https.httpUrls.getRgbByHsvBatch, value)
|
||||
.then((rv: any) => {
|
||||
if (rv) {
|
||||
rv.forEach((ele: any, index: number) => {
|
||||
colorBoards[index].id = ele.id;
|
||||
colorBoards[index].tcx = ele.tcx;
|
||||
colorBoards[index].name = ele.name;
|
||||
});
|
||||
store.commit("setColorboardList", colorBoards);
|
||||
resolve();
|
||||
}
|
||||
data.isShowMark = false
|
||||
})
|
||||
.catch((res) => {
|
||||
reject();
|
||||
data.isShowMark = false
|
||||
});
|
||||
});
|
||||
}
|
||||
let lastStep = async ()=>{
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = data.collectionStep - 1
|
||||
setOpenSetData()
|
||||
}
|
||||
let nextStep = ()=>{
|
||||
let nextStep = async ()=>{
|
||||
if(data.collectionStep == 3)await getPantongName()
|
||||
data.collectionStep = data.collectionStep + 1
|
||||
setOpenSetData()
|
||||
}
|
||||
@@ -150,6 +212,42 @@ export default defineComponent({
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.collectionModal{
|
||||
:deep(.ant-modal-body){
|
||||
padding-top: 10rem;
|
||||
|
||||
}
|
||||
.collection_title{
|
||||
top: calc(4rem*1.2);
|
||||
display: flex;
|
||||
font-size: var(--aida-fsize2);
|
||||
font-weight: 900;
|
||||
color: rgba(0,0,0,.65);
|
||||
z-index: 999;
|
||||
align-items: center;
|
||||
width: calc(35rem*1.2);
|
||||
justify-content: space-between;
|
||||
.collection_progress{
|
||||
width: calc(8rem*1.2);
|
||||
height: calc(8rem*1.2);
|
||||
>div{
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
:deep(.ant-progress-inner){
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.collection_title_text{
|
||||
// margin-right: calc(4rem*1.2);
|
||||
}
|
||||
.collection_title_text_intro{
|
||||
font-size: var(--aida-fsize1-4);
|
||||
font-weight: 400;
|
||||
color: rgba(0,0,0,.45);
|
||||
}
|
||||
|
||||
}
|
||||
:deep(.designOpenrtion_content){
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -560,7 +560,7 @@ export default defineComponent({
|
||||
level2Type:'',
|
||||
designType:'',
|
||||
}
|
||||
let arr = JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles)) || []
|
||||
let arr = this.store.state.UploadFilesModule.allBoardData.printboardFiles?JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles)) : []
|
||||
let setboard = {
|
||||
generate:[] as any,
|
||||
material:[] as any,
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'">{{$t('ProductImg.MagicTools')}}</span>
|
||||
<span v-if="scaleImageList[scaleImageIndex]?.resultType == 'Relight'">{{$t('ProductImg.relightingTool')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>Selection Function</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction" style="margin-bottom: 1rem;">
|
||||
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Similarity')}}</span>
|
||||
</div>
|
||||
@@ -44,23 +50,16 @@
|
||||
</a-slider>
|
||||
<input type="number" readonly v-model="productimgSimilarity">
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.RelightDirection')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_Direction">
|
||||
<!-- <a-slider class="system_silder"
|
||||
v-model:value="similarity"
|
||||
@afterChange="setSimilarity"
|
||||
:tooltipVisible="false"
|
||||
:step="5"
|
||||
>
|
||||
</a-slider> -->
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
|
||||
<a-select style="width: 100%;" v-model:value="productimgRelightDirection" :options="productimgRelightDirectionList"></a-select>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Highlight')}}</span>
|
||||
</div>
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight'" class="productImg_content_item_similarity">
|
||||
<div v-show="scaleImageList[scaleImageIndex]?.resultType == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
|
||||
<a-slider class="system_silder"
|
||||
v-model:value="productimgBrightenValue"
|
||||
:tooltipVisible="false"
|
||||
@@ -109,7 +108,7 @@
|
||||
<div class="generage_btn_box" style="margin-left: auto;">
|
||||
<div class="generage_btn started_btn" v-show="!generateSuccess.productimgIsProductimg">
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg()"></i>
|
||||
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType == 'PoseTransfer'" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="icon iconfont icon-xiala" v-show="scaleImageList[scaleImageIndex]?.resultType != 'Relight'" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
@@ -237,19 +236,37 @@ setup(props:any,{emit}) {
|
||||
})
|
||||
let speed = reactive({
|
||||
speedList:[
|
||||
{
|
||||
title:'Generate high-quality images',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},
|
||||
],
|
||||
] as any,
|
||||
speedTypeList:{
|
||||
poseTransfer:[
|
||||
{
|
||||
title:'Generate high-quality images',
|
||||
label:'High',
|
||||
value:'',
|
||||
},{
|
||||
title:'Generate using Wanxiang',
|
||||
label:'WX',
|
||||
value:'wx',
|
||||
},
|
||||
],
|
||||
toPorductImg:[
|
||||
{
|
||||
title:'Generate with high quality',
|
||||
label:'High',
|
||||
relightLabel:'Relight',
|
||||
value:'',
|
||||
},{
|
||||
title:'',
|
||||
label:'FLUX',
|
||||
relightLabel:'Edit',
|
||||
value:'flux',
|
||||
},
|
||||
]
|
||||
},
|
||||
speedState:false,
|
||||
speedData:{
|
||||
title:'Generate high-quality images',
|
||||
relightLabel:'Relight',
|
||||
label:'High',
|
||||
value:'',
|
||||
},
|
||||
@@ -264,6 +281,7 @@ setup(props:any,{emit}) {
|
||||
}
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
let scaleImage: any = ref(false);
|
||||
let isShowMark = ref(false)
|
||||
@@ -316,15 +334,17 @@ setup(props:any,{emit}) {
|
||||
direction:productimg.productimgRelightDirection,
|
||||
prompt:productimg.productimgSearchName,
|
||||
toProductImageVOList:[obj],
|
||||
modelName:speed.speedData.value,
|
||||
brightenValue:productimg.productimgBrightenValue,
|
||||
projectId:productimg.selectObject.id,
|
||||
imageStrength:(100 - imageStrength)/100,
|
||||
ageGroup:productimg.selectObject.ageGroup
|
||||
}
|
||||
// return
|
||||
productimg.generateSuccess.productimgIsProductimg = true
|
||||
productimg.generateSuccess.remPrductimgTime = setTimeout(()=>{
|
||||
productimg.generateSuccess.productimgRemProductimg = true
|
||||
},10000)
|
||||
// productimg.generateSuccess.remPrductimgTime = setTimeout(()=>{
|
||||
// productimg.generateSuccess.productimgRemProductimg = true
|
||||
// },10000)
|
||||
let url = Https.httpUrls.relight
|
||||
if(scaleImageList.value[scaleImageIndex.value]?.resultType == 'ToProductImage'){
|
||||
url = Https.httpUrls.toProduct
|
||||
@@ -337,6 +357,7 @@ setup(props:any,{emit}) {
|
||||
productimg.generateSuccess.isShowMark = true
|
||||
Https.axiosPost(url, data).then(
|
||||
(rv) => {
|
||||
productimg.generateSuccess.productimgRemProductimg = true
|
||||
productimg.generateSuccess.isShowMark = false
|
||||
scaleImageList.value[scaleImageIndex.value].imgUrl = '/image/loading.gif'
|
||||
let arr:any = []
|
||||
@@ -484,7 +505,8 @@ setup(props:any,{emit}) {
|
||||
oldId:productimg.generateSuccess.id,
|
||||
status:productimg.generateSuccess.status,
|
||||
listType:productimg.generateSuccess.listType,
|
||||
isIndex:productimg.generateSuccess.isIndex
|
||||
isIndex:productimg.generateSuccess.isIndex,
|
||||
userLikeSortId:productimg.generateSuccess.userLikeSortId
|
||||
}
|
||||
emit('addGenerateImg',data)
|
||||
|
||||
@@ -565,7 +587,8 @@ methods: {
|
||||
this.generateSuccess.isIndex = index
|
||||
|
||||
}
|
||||
|
||||
// this.
|
||||
this.generateSuccess.userLikeSortId = list[index].userLikeSortId
|
||||
this.generateSuccess.productimgIsProductimg = !!this.generateSuccess.productimgIsProductimg
|
||||
this.generateSuccess.productimgRemProductimg = !!this.generateSuccess.productimgRemProductimg
|
||||
this.generateSuccess.isShowMark = !!this.generateSuccess.isShowMark
|
||||
@@ -595,6 +618,12 @@ methods: {
|
||||
this.scaleImage = true
|
||||
this.isGenerate = false
|
||||
this.scaleImageList = list
|
||||
if(this.scaleImageList[index].resultType == "PoseTransfer"){
|
||||
this.speedList = this.speedTypeList.poseTransfer
|
||||
}else{
|
||||
this.speedList = this.speedTypeList.toPorductImg
|
||||
}
|
||||
this.speedData = JSON.parse(JSON.stringify(this.speedList[0]))
|
||||
// if(this.scaleImageList[index]?.resultType == 'ToProductImage')this.scaleImageList[index].sourceUrl = this.scaleImageList[index].imgUrl
|
||||
this.scaleImageIndex = index
|
||||
if(dialogueIndex)this.robotAssits = dialogueIndex
|
||||
|
||||
@@ -54,8 +54,6 @@
|
||||
</div>
|
||||
<div class="right_content_block_box">
|
||||
<div class="right_content_block" ref="rightContentBlockBox" @mouseenter="mouseenter($event,'like')">
|
||||
|
||||
|
||||
<div class="right_content_body">
|
||||
<div class="right_content_header">
|
||||
<div class="content_header_left">
|
||||
@@ -63,19 +61,23 @@
|
||||
{{ $t('HomeView.SelectedDesign') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="generalModel_state">
|
||||
<!-- <div class="generalModel_state_item smail" style="margin-right: 0;">
|
||||
<div class="generalModel_state" v-show="domHidden || !recycleDomHidden">
|
||||
<div v-show="isUnfold" class="generalModel_state_item smail" style="margin-right: 1rem;">
|
||||
<span>Type :</span>
|
||||
<a-select
|
||||
v-model:value="widthValue.value"
|
||||
v-model:value="resultType"
|
||||
size="large"
|
||||
style="width: 15rem"
|
||||
placeholder="Please select"
|
||||
:options="widthList"
|
||||
@change="upDataLikeListType(500)"
|
||||
:options="resultTypeList"
|
||||
@change="changeResultType"
|
||||
>
|
||||
</a-select>
|
||||
</div> -->
|
||||
</div>
|
||||
<div @click="setUnfold" class="generalModel_state_item smail" style="margin-right: 1rem;">
|
||||
<span>Unfold :</span>
|
||||
<a-switch v-model:checked="isUnfold" @change="setUnfold" />
|
||||
</div>
|
||||
<div class="generalModel_state_item smail" style="margin-right: 0;">
|
||||
<span>Size :</span>
|
||||
<a-select
|
||||
@@ -92,15 +94,19 @@
|
||||
</div>
|
||||
<div class="right_content_img_block scroll_style active">
|
||||
<div class="right_content_img_item" ref="likeItemDom" designType="like" :index="0">
|
||||
<!-- likeDesignCollectionList -->
|
||||
<div class="content_img_block content_img_GetWidth active" :style="likeStyle"
|
||||
v-for="(design, index) in likeDesignCollectionList" :key="design.id || design.designItemId">
|
||||
v-for="(design, index) in selectLikeDesign" :key="design.id || design.designItemId">
|
||||
<div class="content_img_flex"
|
||||
@mousedown.stop="designMousedown(getMousePosition($event,false),design.userLikeSortId || design.id,'like')"
|
||||
@touchstart.passive="designMousedown(getMousePosition($event,true),design.userLikeSortId || design.id,'like')"
|
||||
:class="[design.resultType]"
|
||||
@mouseenter="startHover(getMousePosition($event,false),design)"
|
||||
@mouseleave="endHover(getMousePosition($event,false),design)"
|
||||
@mousedown.stop="designMousedown(getMousePosition($event,false),design,'like')"
|
||||
@touchstart.passive="designMousedown(getMousePosition($event,true),design,'like')"
|
||||
@click="designDetail(
|
||||
design,
|
||||
index,
|
||||
likeDesignCollectionList,
|
||||
selectLikeDesign,
|
||||
'like'
|
||||
)">
|
||||
<img class="content_img"
|
||||
@@ -111,33 +117,36 @@
|
||||
<img class="content_img" v-show="!design?.generateAwait" :src="design.designOutfitUrl||design.url"
|
||||
:key="design.designOutfitUrl" designType="like" :index="index"/>
|
||||
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
|
||||
</div>
|
||||
<div class="btn" v-show="!design?.generateAwait">
|
||||
<i @click.stop="openEditBtn(design.designItemId||design.id)" class="fi fi-br-menu-dots-vertical"></i>
|
||||
<div class="btnOpen" @click.stop="()=>openEditBtnId=-1" :class="{active:openEditBtnId == design.designItemId||openEditBtnId == design.id}">
|
||||
<div class="item" @click="dislikeDesignCollection(design,design.resultType,index)">
|
||||
<div class="text">On Like</div>
|
||||
<div class="icon iconfont icon-jushoucanggift icon_like">
|
||||
|
||||
<!-- <div class="content_img_block_child" @click.stop>
|
||||
<div class="content_img_block content_img_GetWidth active childItem"
|
||||
:style="likeStyle"
|
||||
v-for="childItem,childIndex in design.childList"
|
||||
>
|
||||
<div class="content_img_flex">
|
||||
<img class="content_img" v-show="!design?.generateAwait" :src="childItem.designOutfitUrl||childItem.url" alt="">
|
||||
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
|
||||
</div>
|
||||
<div class="btn" v-show="!childItem?.generateAwait">
|
||||
<i @click.stop="openEditBtn(childItem,childIndex,$event)" class="fi fi-br-menu-dots-vertical"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(likeDesignCollectionList,index,'PoseTransfer','add','like')">
|
||||
<div class="text">Transfer Pose</div>
|
||||
<div class="icon iconfont icon-jushoucanggift icon_like"></div>
|
||||
<div class="content_img_block content_img_GetWidth active childItem"
|
||||
:style="likeStyle"
|
||||
v-for="childItem,childIndex in design.childList"
|
||||
>
|
||||
<div class="content_img_flex">
|
||||
<img class="content_img" v-show="!design?.generateAwait" :src="childItem.designOutfitUrl||childItem.url" alt="">
|
||||
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
|
||||
</div>
|
||||
<div class="btn" v-show="!childItem?.generateAwait">
|
||||
<i @click.stop="openEditBtn(childItem,childIndex,$event)" class="fi fi-br-menu-dots-vertical"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(likeDesignCollectionList,index,'Relight','add','like')">
|
||||
<div class="text">Relight</div>
|
||||
<i class="fi fi fi-br-keyboard-brightness"></i>
|
||||
</div>
|
||||
<div class="item" v-show="design.resultType == 'Design'" @click="setEditDesignType(likeDesignCollectionList,index,'ToProductImage','add','like')">
|
||||
<div class="text">To Product Image</div>
|
||||
<i class="fi fi fi-ss-box-open"></i>
|
||||
</div>
|
||||
<!-- <div class="item" @click="setDeleteDesign(design,index)">
|
||||
<div class="text">Delete</div>
|
||||
<i class="fi fi-rr-trash icon_delete">
|
||||
</i>
|
||||
</div> -->
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="btn" v-show="!design?.generateAwait">
|
||||
<i @click.stop="openEditBtn(design,index,$event)" class="fi fi-br-menu-dots-vertical"></i>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,31 +191,10 @@
|
||||
<a-spin v-show="design?.generateAwait" size="large"></a-spin>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<i @click.stop="openEditBtn(design.designItemId||design.id)" class="fi fi-br-menu-dots-vertical"></i>
|
||||
<div class="btnOpen" @click.stop="()=>openEditBtnId=-1" :class="{active:openEditBtnId == design.designItemId||openEditBtnId == design.id}">
|
||||
<div class="item" @click="likeDesignCollection(design,index,design.resultType)">
|
||||
<div class="text">Like</div>
|
||||
<div class="icon iconfont icon-jushoucang icon_like">
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(designCollectionList,index,'PoseTransfer','add','disLike')">
|
||||
<div class="text">Transfer Pose</div>
|
||||
<div class="icon iconfont icon-jushoucanggift icon_like"></div>
|
||||
</div>
|
||||
<div class="item" v-show="design.resultType == 'ToProductImage'" @click="setEditDesignType(designCollectionList,index,'Relight','add','disLike')">
|
||||
<div class="text">Relight</div>
|
||||
<i class="fi fi fi-br-keyboard-brightness"></i>
|
||||
</div>
|
||||
<div class="item" v-show="design.resultType == 'Design'" @click="setEditDesignType(designCollectionList,index,'ToProductImage','add','disLike')">
|
||||
<div class="text">To Product Image</div>
|
||||
<i class="fi fi fi-ss-box-open"></i>
|
||||
</div>
|
||||
<div class="item" @click="setDeleteDesign(design,index)">
|
||||
<div class="text">Delete</div>
|
||||
<i class="fi fi-rr-trash icon_delete">
|
||||
</i>
|
||||
</div>
|
||||
<div class="icon iconfont icon-jushoucang icon_like" @click="likeDesignCollection(design,index,design.resultType)">
|
||||
</div>
|
||||
<i class="fi fi-rr-trash icon_delete" @click="setDeleteDesign(design,index)">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -219,11 +207,29 @@
|
||||
<span class="icon iconfont icon-xiala" :class="{'active':recycleDomHidden}"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="designBtn" class="btnOpen" @click.stop="()=>openEditBtnId=-1" :class="{active:openEditBtnId == selectEditBtn?.designItemId||openEditBtnId == selectEditBtn?.id}">
|
||||
<div class="item" @click="dislikeDesignCollection(selectEditBtn,selectEditBtn?.resultType,selectEditBtnIndex)">
|
||||
<div class="text">Delete</div>
|
||||
<i class="fi fi-rr-trash icon_delete">
|
||||
</i>
|
||||
</div>
|
||||
<div class="item" v-show="selectEditBtn?.resultType == 'ToProductImage'" @click="setEditDesignType(selectLikeDesign,selectEditBtnIndex,'PoseTransfer','add','like')">
|
||||
<div class="text">Transfer Pose</div>
|
||||
<i class="fi fi fi-rr-play-alt"></i>
|
||||
</div>
|
||||
<div class="item" v-show="selectEditBtn?.resultType == 'ToProductImage'" @click="setEditDesignType(selectLikeDesign,selectEditBtnIndex,'Relight','add','like')">
|
||||
<div class="text">Relight</div>
|
||||
<i class="fi fi fi-br-keyboard-brightness"></i>
|
||||
</div>
|
||||
<div class="item" v-show="selectEditBtn?.resultType == 'Design'" @click="setEditDesignType(selectLikeDesign,selectEditBtnIndex,'ToProductImage','add','like')">
|
||||
<div class="text">To Product Image</div>
|
||||
<i class="fi fi fi-ss-box-open"></i>
|
||||
</div>
|
||||
</div>
|
||||
<collectionModal ref="collectionModal"></collectionModal>
|
||||
<!-- <DesignDetail ref="designDetail" @finishRedesign="finishRedesign"></DesignDetail> -->
|
||||
<!-- 导出缩略图的蒙层 start-->
|
||||
@@ -249,7 +255,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h, ref, computed, toRefs, inject,provide,nextTick,reactive,onBeforeUnmount, toRef} from "vue";
|
||||
import { defineComponent, h, ref, computed, toRefs, inject,provide,nextTick,reactive,onBeforeUnmount, toRef, watch} from "vue";
|
||||
// import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import NewCollectionReview from "@/component/HomePage/NewCollectionReview.vue";
|
||||
import generalCanvas from "@/component/modules/generalCanvas.vue";
|
||||
@@ -376,9 +382,12 @@ export default defineComponent({
|
||||
const dataDom = reactive({
|
||||
collectionModal:null as any,
|
||||
editDesignType:null as any,
|
||||
designBtn:null as any,
|
||||
})
|
||||
const designData = reactive({
|
||||
openEditBtnId:-1,
|
||||
selectEditBtn:null as any,
|
||||
selectEditBtnIndex:-1,
|
||||
likeLoading: false, //喜欢防抖
|
||||
widthList:[
|
||||
{
|
||||
@@ -392,6 +401,39 @@ export default defineComponent({
|
||||
value:400,
|
||||
}
|
||||
],
|
||||
selectDesign:null as any,//当前选择的design
|
||||
isUnfold:false,
|
||||
selectLikeDesign:[] as any,
|
||||
|
||||
//类型
|
||||
resultTypeList:[
|
||||
{
|
||||
label:'All',
|
||||
value:'All',
|
||||
},{
|
||||
label:'Design',
|
||||
value:'Design',
|
||||
},{
|
||||
label:'Product',
|
||||
value:'Product',
|
||||
},
|
||||
// ,{
|
||||
// label:'To Product Image',
|
||||
// value:'ToProductImage',
|
||||
// },{
|
||||
// label:'Relight',
|
||||
// value:'Relight',
|
||||
// },
|
||||
{
|
||||
label:'Pose Transfer',
|
||||
value:'PoseTransfer',
|
||||
},
|
||||
],
|
||||
resultType:'All',
|
||||
oldSelectLikeDesign:null as any
|
||||
})
|
||||
watch(()=>likeDesignCollectionList.value.length,(val)=>{
|
||||
if(val>0)uploadLikeDom()
|
||||
})
|
||||
provide('exportNav',exportNav)
|
||||
let isShowOperate = ref(false)
|
||||
@@ -476,9 +518,11 @@ export default defineComponent({
|
||||
for(let i = 0;i < elArr.length;i++){
|
||||
posiitonData.value.likeElList.push({
|
||||
el: elArr[i],
|
||||
sort: likeDesignCollectionList.value[i].sort - 1,
|
||||
sort: designData.selectLikeDesign[i].sort - 1,
|
||||
// sort: likeDesignCollectionList.value[i].sort - 1,
|
||||
index: i,
|
||||
userLikeSortId:likeDesignCollectionList.value[i].userLikeSortId || likeDesignCollectionList.value[i].id
|
||||
userLikeSortId:designData.selectLikeDesign[i].userLikeSortId || designData.selectLikeDesign[i].id
|
||||
// userLikeSortId:likeDesignCollectionList.value[i].userLikeSortId || likeDesignCollectionList.value[i].id
|
||||
});
|
||||
}
|
||||
moveItem('like')
|
||||
@@ -513,8 +557,10 @@ export default defineComponent({
|
||||
const cancelDeleteDesign = (index:any)=>{
|
||||
store.commit("cancelDeleteDesignCollectionList",index);
|
||||
}
|
||||
const designMousedown = (e:any,Id:number,str:string)=>{
|
||||
const designMousedown = (e:any,design:any,str:string)=>{
|
||||
if(design.resultType != 'Design' || designData.isUnfold)return
|
||||
if(str != 'like' && showDesignMark.value) return
|
||||
let Id = design.userLikeSortId || design.id
|
||||
let arr = str == 'like'? posiitonData.value.likeElList:posiitonData.value.generateElList
|
||||
let item:any = arr.filter((item:any)=>item.userLikeSortId == Id)[0]
|
||||
item.el.style.zIndex = 2;
|
||||
@@ -624,6 +670,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
const setLikeDislLike = (str:string,value:any)=>{
|
||||
designData.selectLikeDesign = []
|
||||
posiitonData.value.likeSelectIndex = 0
|
||||
nextTick(()=>{
|
||||
if(str == 'like'){
|
||||
@@ -688,8 +735,24 @@ export default defineComponent({
|
||||
const openCollection = (str:string)=>{
|
||||
dataDom.collectionModal.init(str)
|
||||
}
|
||||
const openEditBtn = (id:number)=>{
|
||||
designData.openEditBtnId = id
|
||||
const openEditBtn = (design:any,index:number,event:any)=>{
|
||||
if(designData.selectLikeDesign[index].resultType != 'Design'){
|
||||
designData.selectDesign = designData.selectLikeDesign.filter((item:any)=>(item.resultType == 'Design' && item.userLikeSortId == designData.selectLikeDesign[index].parentId))[0]
|
||||
}else{
|
||||
designData.selectDesign = designData.selectLikeDesign[index]
|
||||
}
|
||||
designData.openEditBtnId = design.designItemId||design.id
|
||||
designData.selectEditBtn = design
|
||||
designData.selectEditBtnIndex = index
|
||||
let width = event.target.offsetWidth
|
||||
let height = event.target.offsetHeight
|
||||
let parentRect = dataDom.designBtn.parentElement.getBoundingClientRect()
|
||||
|
||||
let x = event.clientX - parentRect.x - event.offsetX + width
|
||||
let y = event.clientY - parentRect.y - event.offsetY + height
|
||||
dataDom.designBtn.style.left = x+'px'
|
||||
dataDom.designBtn.style.top = y+'px'
|
||||
|
||||
let removeEditBtnId = ()=>{
|
||||
designData.openEditBtnId = -1
|
||||
document.removeEventListener('click',removeEditBtnId)
|
||||
@@ -714,7 +777,6 @@ export default defineComponent({
|
||||
dataDom.editDesignType.isComparison = true
|
||||
}
|
||||
const addGenerateImg = (data:any)=>{
|
||||
console.log(data)
|
||||
if(!data.id)return
|
||||
let obj = {
|
||||
id:data.id,
|
||||
@@ -729,6 +791,7 @@ export default defineComponent({
|
||||
userGroupId:data.userGroupId || data.id,
|
||||
firstFrameUrl:data.firstFrameUrl,
|
||||
gifUrl:data.gifUrl,
|
||||
userLikeSortId:data.userLikeSortId,
|
||||
}
|
||||
let list = []
|
||||
if(data.listType == 'disLike'){
|
||||
@@ -763,16 +826,18 @@ export default defineComponent({
|
||||
list[data.isIndex].designItemId = obj.designItemId
|
||||
list[data.isIndex].sourceUrl = obj.sourceUrl
|
||||
}
|
||||
nextTick().then(()=>{
|
||||
setDesignItemStyle()
|
||||
setSystemDesigner(100)
|
||||
})
|
||||
// nextTick().then(()=>{
|
||||
// // setDesignItemStyle()
|
||||
// // setSystemDesigner(100)
|
||||
// uploadLikeDom()
|
||||
// })
|
||||
}
|
||||
const setNoDesignLike = (design:any,index:any,type:any,str:string) => {
|
||||
let url
|
||||
let data = {
|
||||
toProductImageResultId:[(design.designItemId || design.id)],
|
||||
projectId: store.state.Workspace.probjects.id,
|
||||
collectionSortParentId:designData?.selectDesign?.userLikeSortId || design.parentId,
|
||||
} as any
|
||||
let params = {} as any
|
||||
if(type == 'ToProductImage' || type == 'Relight'){
|
||||
@@ -788,12 +853,14 @@ export default defineComponent({
|
||||
likeOrDislike:'like',
|
||||
transformedId:design.id,
|
||||
projectId:store.state.Workspace.probjects.id,
|
||||
collectionSortParentId:designData?.selectDesign?.userLikeSortId || design.parentId,
|
||||
}
|
||||
}else{
|
||||
params = {
|
||||
likeOrDislike:'dislike',
|
||||
transformedId:design.id,
|
||||
projectId:store.state.Workspace.probjects.id,
|
||||
collectionSortParentId:designData?.selectDesign?.userLikeSortId || design.parentId,
|
||||
}
|
||||
}
|
||||
}else{
|
||||
@@ -807,28 +874,48 @@ export default defineComponent({
|
||||
Https.axiosPost(url, data,{params:params.likeOrDislike?params:''}).then(
|
||||
(rv) => {
|
||||
if(str == 'like'){
|
||||
let value:any = {
|
||||
...design,
|
||||
sort:likeDesignCollectionList.value.length + 1,
|
||||
userLikeGroupId:design.id,
|
||||
}
|
||||
design.groupDetailId = rv.groupDetailId;
|
||||
store.commit(
|
||||
"addLikeDesignCollectionList",
|
||||
value
|
||||
);
|
||||
setLikeDislLike('like',value)
|
||||
if(index != -1)store.commit("deleteDesignCollectionList", index);
|
||||
}else{
|
||||
if(index != -1)store.commit("deleteLikeDesignCollectionList", index);
|
||||
store.commit("addDesignCollectionList", [design]);
|
||||
likeDesignCollectionList.value.forEach((item:any)=>{
|
||||
if(item.sort > design.sort){
|
||||
item.sort-=1
|
||||
if(item.resultType == 'Design' && item.userLikeSortId == designData.selectDesign.userLikeSortId){
|
||||
item.childList.push(design)
|
||||
}
|
||||
})
|
||||
}else{
|
||||
likeDesignCollectionList.value.forEach((item:any)=>{
|
||||
if(item.resultType == 'Design' && item.userLikeSortId == designData.selectDesign.userLikeSortId){
|
||||
item.childList = item.childList.filter((item:any)=>{
|
||||
return item.id != design.id
|
||||
})
|
||||
}
|
||||
})
|
||||
setLikeDislLike('disLike',design)
|
||||
}
|
||||
nextTick().then(()=>{
|
||||
uploadLikeDom()
|
||||
})
|
||||
|
||||
|
||||
//一下代码是生成的design时候也可以toproduct
|
||||
// if(str == 'like'){
|
||||
// let value:any = {
|
||||
// ...design,
|
||||
// sort:likeDesignCollectionList.value.length + 1,
|
||||
// userLikeGroupId:design.id,
|
||||
// }
|
||||
// design.groupDetailId = rv.groupDetailId;
|
||||
// store.commit(
|
||||
// "addLikeDesignCollectionList",
|
||||
// value
|
||||
// );
|
||||
// setLikeDislLike('like',value)
|
||||
// if(index != -1)store.commit("deleteDesignCollectionList", index);
|
||||
// }else{
|
||||
// if(index != -1)store.commit("deleteLikeDesignCollectionList", index);
|
||||
// store.commit("addDesignCollectionList", [design]);
|
||||
// likeDesignCollectionList.value.forEach((item:any)=>{
|
||||
// if(item.sort > design.sort){
|
||||
// item.sort-=1
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
designData.likeLoading = false;
|
||||
}
|
||||
).catch(res=>{
|
||||
@@ -898,6 +985,81 @@ export default defineComponent({
|
||||
const gifPause = (e:any,item:any)=>{
|
||||
e.target.src = item.firstFrame || item.firstFrameUrl//静态图片
|
||||
}
|
||||
const startHover = (e:any,item:any)=>{
|
||||
}
|
||||
const endHover = (e:any,item:any)=>{
|
||||
|
||||
}
|
||||
const setUnfold = ()=>{
|
||||
designData.isUnfold = !designData.isUnfold
|
||||
if(!designData.isUnfold){
|
||||
designData.selectLikeDesign = designData.oldSelectLikeDesign || []
|
||||
}
|
||||
uploadLikeDom()
|
||||
}
|
||||
const uploadLikeDom = ()=>{
|
||||
if(designData.isUnfold){
|
||||
// const parents = designData.selectLikeDesign.filter((item:any) => item.type === 'design');
|
||||
designData.selectLikeDesign = []
|
||||
likeDesignCollectionList.value.sort((a:any,b:any)=>{return a.sort - b.sort})
|
||||
let childLength = 0
|
||||
likeDesignCollectionList.value.forEach((likeItem:any,index:any)=>{
|
||||
let item = JSON.parse(JSON.stringify(likeItem))
|
||||
item.oldSort = item.sort
|
||||
if(item?.childList?.length > 0){
|
||||
item.childList.forEach((childItem:any,childIndex:number)=>{
|
||||
childItem.sort = childIndex + item.sort + childLength
|
||||
designData.selectLikeDesign.push(childItem)
|
||||
})
|
||||
childLength = item.childList.length + childLength
|
||||
}
|
||||
item.sort = childLength + item.sort
|
||||
designData.selectLikeDesign.push(item)
|
||||
})
|
||||
designData.oldSelectLikeDesign = []
|
||||
designData.resultType = 'All'
|
||||
}else{
|
||||
if(designData.selectLikeDesign.length == 0){
|
||||
designData.selectLikeDesign = likeDesignCollectionList.value
|
||||
return
|
||||
}
|
||||
const parents = designData.selectLikeDesign.filter((item:any) => item.resultType === 'Design');
|
||||
parents.map((parent:any) => {
|
||||
parent.sort = parent.oldSort||parent.sort
|
||||
return {
|
||||
...parent,
|
||||
childList: designData.selectLikeDesign.filter((item:any) =>
|
||||
item.parentId === parent.userLikeSortId && item.resultType !== 'Design'
|
||||
)
|
||||
};
|
||||
});
|
||||
designData.selectLikeDesign = parents
|
||||
}
|
||||
nextTick(()=>{
|
||||
setSystemDesigner(0)
|
||||
})
|
||||
}
|
||||
const changeResultType = ()=>{
|
||||
if(designData.resultType != 'All'){
|
||||
if(designData.oldSelectLikeDesign.length == 0)designData.oldSelectLikeDesign = designData.selectLikeDesign
|
||||
if(designData.resultType == 'Product'){
|
||||
designData.selectLikeDesign = designData.oldSelectLikeDesign.filter((item:any) => (item.resultType === 'ToProductImage' || item.resultType === 'Relight'));
|
||||
}else{
|
||||
designData.selectLikeDesign = designData.oldSelectLikeDesign.filter((item:any) => item.resultType === designData.resultType);
|
||||
}
|
||||
designData.selectLikeDesign.forEach((item:any,index:number) => {
|
||||
item.sort = index + 1
|
||||
})
|
||||
}else{
|
||||
designData.selectLikeDesign = designData.oldSelectLikeDesign
|
||||
designData.selectLikeDesign.forEach((item:any,index:number) => {
|
||||
item.sort = index + 1
|
||||
})
|
||||
}
|
||||
nextTick(()=>{
|
||||
setSystemDesigner(0)
|
||||
})
|
||||
}
|
||||
return {
|
||||
store,
|
||||
...toRefs(editDesignType),
|
||||
@@ -952,6 +1114,10 @@ export default defineComponent({
|
||||
setGenerateAwait,
|
||||
gifPlay,
|
||||
gifPause,
|
||||
startHover,
|
||||
endHover,
|
||||
setUnfold,
|
||||
changeResultType,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -1249,6 +1415,7 @@ export default defineComponent({
|
||||
marketingSketchFiles,
|
||||
moodboardPosition,
|
||||
} = this.store.state.UploadFilesModule.allBoardData;
|
||||
console.log(JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData)))
|
||||
this.randomNum()
|
||||
let modelList = [] as any;
|
||||
let workspace:any = this.store.state.Workspace.probjects
|
||||
@@ -1427,21 +1594,17 @@ export default defineComponent({
|
||||
Https.axiosPost(Https.httpUrls.designDislike, data)
|
||||
.then((rv: any) => {
|
||||
if (rv) {
|
||||
this.store.commit("addDesignCollectionList", [design]);
|
||||
this.store.commit(
|
||||
"deleteLikeDesignCollectionList",
|
||||
index
|
||||
);
|
||||
// this.store.commit("addDesignCollectionList", [design]);
|
||||
this.likeDesignCollectionList.forEach((item:any)=>{
|
||||
if(item.sort > design.sort){
|
||||
item.sort-=1
|
||||
}
|
||||
})
|
||||
|
||||
this.store.commit(
|
||||
"deleteLikeDesignCollectionList",
|
||||
index
|
||||
);
|
||||
this.setLikeDislLike('disLike',design)
|
||||
// if (!this.likeDesignCollectionList.length) {
|
||||
// this.store.commit("deleteUserGroupId");
|
||||
// }
|
||||
}
|
||||
this.disLikeLoading = false;
|
||||
})
|
||||
@@ -1768,6 +1931,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.designPage_body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1913,6 +2077,8 @@ export default defineComponent({
|
||||
.content_img_block{
|
||||
width: 16rem;
|
||||
height: 23rem;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2006,7 +2172,8 @@ export default defineComponent({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
transition: top,left .3s;
|
||||
transition: top,left .5s;
|
||||
// transition: top,left .3s;
|
||||
&:hover .btn{
|
||||
display: flex;
|
||||
}
|
||||
@@ -2020,13 +2187,43 @@ export default defineComponent({
|
||||
border: 2px solid #E0E0E0;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
.content_img_block_child{
|
||||
position: absolute;
|
||||
border-radius: 2rem;
|
||||
border: 2px solid #00000070;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
left: -43%;
|
||||
transform: translateX(69%);
|
||||
top: 0rem;
|
||||
> .childItem{
|
||||
> .content_img_flex{
|
||||
border: 2px solid #00000070;
|
||||
}
|
||||
position: relative !important;
|
||||
>img{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.active{
|
||||
.icon_like,.icon_delete{
|
||||
display: block;
|
||||
}
|
||||
.content_img_flex {
|
||||
border: 2px solid #000;
|
||||
&.Design{
|
||||
border: 2px solid #000;
|
||||
}
|
||||
&.PoseTransfer{
|
||||
border: 2px solid #f0d9d0;
|
||||
}
|
||||
&.ToProductImage{
|
||||
border: 2px solid #ccece5;
|
||||
}
|
||||
&.Relight{
|
||||
border: 2px solid #ccece5;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content_img {
|
||||
@@ -2050,39 +2247,7 @@ export default defineComponent({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
> .btnOpen{
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
left: 30px;
|
||||
top: 30px;
|
||||
transform: translateX(-100%);
|
||||
background: #e4e4e7;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
display: none;
|
||||
z-index: 99;
|
||||
&.active{
|
||||
display: block;
|
||||
}
|
||||
> .item{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
&:last-child{
|
||||
margin: 0;
|
||||
}
|
||||
> .text{
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
&:hover{
|
||||
background: #efeff1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon_like,.icon_delete {
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
@@ -2106,7 +2271,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.page_content_bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@@ -2130,5 +2295,38 @@ export default defineComponent({
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.btnOpen{
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
left: 30px;
|
||||
top: 30px;
|
||||
transform: translateX(-100%);
|
||||
background: #e4e4e7;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
display: none;
|
||||
z-index: 999;
|
||||
&.active{
|
||||
display: block;
|
||||
}
|
||||
> .item{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
&:last-child{
|
||||
margin: 0;
|
||||
}
|
||||
> .text{
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
&:hover{
|
||||
background: #efeff1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -328,7 +328,8 @@ export default defineComponent({
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
> .top,> .model{
|
||||
width: 130rem;
|
||||
width: 100%;
|
||||
// width: 130rem;
|
||||
}
|
||||
> .top{
|
||||
display: flex;
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineComponent({
|
||||
components:{
|
||||
design,newPorject,chat
|
||||
},
|
||||
emits:['setTask','newProject'],
|
||||
emits:['setTask','setNewProject'],
|
||||
setup(props,{emit}) {
|
||||
const store = useStore();
|
||||
const route = useRoute()
|
||||
@@ -98,6 +98,11 @@ export default defineComponent({
|
||||
store.commit("setAllBoardDataChoose",{});
|
||||
store.commit("clearShowSketchboard",{});
|
||||
store.commit("clearAllCollection");
|
||||
store.commit("clearAllCloudList");
|
||||
let arr = ['moodBoard','printBoard','sketchBoard','colorBoard']
|
||||
arr.forEach((item:any)=>{
|
||||
store.commit("clearAllBoardData",item);
|
||||
})
|
||||
}
|
||||
const getWorks = (id:any)=>{
|
||||
let value:any = {
|
||||
@@ -214,69 +219,81 @@ export default defineComponent({
|
||||
// })
|
||||
// }
|
||||
const getCollection = (type:string,module:any)=>{
|
||||
let moduleList = [] as any
|
||||
if(type == 'design'){
|
||||
moduleList = ["moodBoard", "printBoard", "colorBoard", "sketchBoard",'design','mannequin']
|
||||
}else{
|
||||
moduleList = ["printBoard", "colorBoard", "sketchBoard",'design','toProduct','relight','poseTransfer','mannequin',"patternMaking3D",'deReconstruction']
|
||||
}
|
||||
if(module)moduleList = [module]
|
||||
let value:any = {
|
||||
"id":data.selectObject.id,
|
||||
"moduleList":moduleList,
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.getModuleContent,value).then(async (rv)=>{
|
||||
|
||||
historyChooseData(rv)//设置历史数据
|
||||
if(rv.mannequin.length>0){
|
||||
let arr = [] as any
|
||||
rv.mannequin.forEach((item:any)=>{
|
||||
arr.push({
|
||||
url:item.url,
|
||||
id:item.mannequinRelationId,
|
||||
type:item.mannequinRelationType,
|
||||
collectionElementId:item.collectionId
|
||||
})
|
||||
})
|
||||
store.commit('setProbject',{model:arr})
|
||||
}
|
||||
let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard']
|
||||
// let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard','toProduct','relight','poseTransfer','mannequin']
|
||||
if(module){
|
||||
let canvasData = ['canvas','deReconstruction']
|
||||
for (let index = 0; index < canvasData.length; index++) {
|
||||
const item = canvasData[index];
|
||||
await getCanvasData(item)
|
||||
}
|
||||
}
|
||||
|
||||
// await setitemData(allBoardData)
|
||||
//还有一个canvas
|
||||
if(rv.boundingBox)store.commit('setShowSketchboard',rv.boundingBox)
|
||||
|
||||
allBoardData.forEach((item)=>{
|
||||
let value = {
|
||||
type:item,
|
||||
objectName:data.selectObject.type,
|
||||
}
|
||||
let arr = ['sketchBoard','moodBoard','printBoard','colorBoard']
|
||||
if(arr.indexOf(item) != -1){
|
||||
// store.dispatch('setAllBoardData',value)
|
||||
}else{
|
||||
store.dispatch('setModularData',value)
|
||||
}
|
||||
})
|
||||
data.isShowMark = false
|
||||
data.dataLoad = false
|
||||
return new Promise((resolve, reject) => {
|
||||
let moduleList = [] as any
|
||||
if(type == 'design'){
|
||||
nextTick(()=>{
|
||||
dataDom.design.openSetData()
|
||||
})
|
||||
moduleList = ["moodBoard", "printBoard", "colorBoard", "sketchBoard",'design','mannequin']
|
||||
}else{
|
||||
moduleList = ["printBoard", "colorBoard", "sketchBoard",'design','toProduct','relight','poseTransfer','mannequin',"patternMaking3D",'deReconstruction','uploadElement']
|
||||
}
|
||||
}).catch(()=>{
|
||||
data.isShowMark = false
|
||||
data.dataLoad = false
|
||||
if(module)moduleList = [module]
|
||||
let value:any = {
|
||||
"id":data.selectObject.id,
|
||||
"moduleList":moduleList,
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.getModuleContent,value).then(async (rv)=>{
|
||||
if(rv.uploadElement){//toproduct、relight、poseTransfer
|
||||
let uploadElementData = {
|
||||
str:'add',
|
||||
list:rv.uploadElement,
|
||||
}
|
||||
store.commit('setUploadElement',uploadElementData)
|
||||
|
||||
}
|
||||
historyChooseData(rv)//设置历史数据
|
||||
if(rv.mannequin.length>0){
|
||||
let arr = [] as any
|
||||
rv.mannequin.forEach((item:any)=>{
|
||||
arr.push({
|
||||
url:item.url,
|
||||
id:item.mannequinRelationId,
|
||||
type:item.mannequinRelationType,
|
||||
collectionElementId:item.collectionId
|
||||
})
|
||||
})
|
||||
store.commit('setProbject',{model:arr})
|
||||
}
|
||||
let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard']
|
||||
// let allBoardData = ['sketchBoard','moodBoard','printBoard','colorBoard','toProduct','relight','poseTransfer','mannequin']
|
||||
if(module){
|
||||
let canvasData = ['canvas','deReconstruction']
|
||||
for (let index = 0; index < canvasData.length; index++) {
|
||||
const item = canvasData[index];
|
||||
await getCanvasData(item)
|
||||
}
|
||||
}
|
||||
|
||||
// await setitemData(allBoardData)
|
||||
//还有一个canvas
|
||||
if(rv.boundingBox)store.commit('setShowSketchboard',rv.boundingBox)
|
||||
|
||||
allBoardData.forEach((item)=>{
|
||||
let value = {
|
||||
type:item,
|
||||
objectName:data.selectObject.type,
|
||||
}
|
||||
let arr = ['sketchBoard','moodBoard','printBoard','colorBoard']
|
||||
if(arr.indexOf(item) != -1){
|
||||
// store.dispatch('setAllBoardData',value)
|
||||
}else{
|
||||
store.dispatch('setModularData',value)
|
||||
}
|
||||
})
|
||||
data.isShowMark = false
|
||||
data.dataLoad = false
|
||||
if(type == 'design'){
|
||||
nextTick(()=>{
|
||||
dataDom.design.openSetData()
|
||||
})
|
||||
}
|
||||
resolve('')
|
||||
}).catch(()=>{
|
||||
data.isShowMark = false
|
||||
data.dataLoad = false
|
||||
resolve('')
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
const getCanvasData = (str:any)=>{
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -312,7 +329,7 @@ export default defineComponent({
|
||||
),
|
||||
generatePrintFiles: [],
|
||||
colorBoards: dealViewChooseColor(
|
||||
dataValue.colorBoard
|
||||
dataValue.colorBoard
|
||||
),
|
||||
sketchboardFiles: dealViewChooseData(
|
||||
dataValue.sketchBoard,"Sketchboard"
|
||||
@@ -320,7 +337,7 @@ export default defineComponent({
|
||||
};
|
||||
if(dataValue.moodBoard?.moodTemplateId)store.commit("setMoodTemplateId", dataValue.moodBoard.moodTemplateId);
|
||||
store.commit("setAllBoardDataChoose", collectionData);
|
||||
store.commit("setShowSketchboard", dataValue.sketchBoards);
|
||||
if(dataValue.sketchBoards)store.commit("setShowSketchboard", dataValue.sketchBoards);
|
||||
let likeDesignCollectionList:any = []
|
||||
if(dataValue.design?.userLikeDetails){
|
||||
dataValue.design?.userLikeDetails.map(
|
||||
@@ -355,7 +372,8 @@ export default defineComponent({
|
||||
}
|
||||
if(dataValue.poseTransfer){
|
||||
let value = {
|
||||
list: dataValue.poseTransfer,
|
||||
list:dataValue.poseTransfer.list,
|
||||
likedList:dataValue.poseTransfer.likedList,
|
||||
str:'add',
|
||||
index:-1,
|
||||
}
|
||||
@@ -363,6 +381,7 @@ export default defineComponent({
|
||||
}
|
||||
if(dataValue.patternMaking3D){
|
||||
let patternMaking3DData = {
|
||||
collectionElementId:dataValue.patternMaking3D.collectionElementId,
|
||||
threeDSimpleId:dataValue.patternMaking3D.threeDSimpleId,
|
||||
url:dataValue.patternMaking3D.printMinioUrl,
|
||||
printMinioUrl:getMinioUrl(dataValue.patternMaking3D.printMinioUrl),
|
||||
@@ -428,16 +447,53 @@ export default defineComponent({
|
||||
});
|
||||
return colorList;
|
||||
}
|
||||
let queue = [] as any
|
||||
let start = false
|
||||
const chatChange = (value:any)=>{
|
||||
if(value.type == 'design_control_signal' && dataDom.design){
|
||||
dataDom.design.designNewCollection()
|
||||
queue.push(
|
||||
{
|
||||
type:'design',
|
||||
fun:dataDom.design.designNewCollection,
|
||||
}
|
||||
)
|
||||
// dataDom.design.designNewCollection()
|
||||
}else{
|
||||
getCollection('design',value.module)
|
||||
queue.push(
|
||||
{
|
||||
type:'promise',
|
||||
fun:getCollection,
|
||||
module:value.module,
|
||||
str:'design'
|
||||
}
|
||||
)
|
||||
// getCollection('design',value.module)
|
||||
}
|
||||
if(!start){
|
||||
startQueue()
|
||||
}
|
||||
}
|
||||
const startQueue = async ()=>{
|
||||
start = true
|
||||
if(queue[0].type){
|
||||
if(queue[0].type == 'promise'){
|
||||
await queue[0].fun(queue[0].str,queue[0].module)
|
||||
}else{
|
||||
await queue[0].fun()
|
||||
}
|
||||
queue.splice(0,1)
|
||||
}
|
||||
if(queue.length>0){
|
||||
startQueue()
|
||||
}else{
|
||||
start = false
|
||||
|
||||
}
|
||||
}
|
||||
const newProject = (value:any)=>{
|
||||
settingGetHistory()
|
||||
router.push(`home?history=${value.id}`)
|
||||
emit('setNewProject')
|
||||
router.push(`home?history=${value.id}&create=true`)
|
||||
data.chatData = value
|
||||
|
||||
}
|
||||
|
||||
@@ -37,7 +37,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="list printList" v-show="libraryOrModel == 'print'">
|
||||
<selectPrint ref="selectPrint" @setPrint="setPrint"></selectPrint>
|
||||
<!-- <selectPrint ref="selectPrint" @setPrint="setPrint"></selectPrint> -->
|
||||
<div v-for="item in allBoardData.printboardFiles" class="item" :class="{active:item.id == patternMaking3D.collectionElementId}" @click="setSelectPrint(item)">
|
||||
<img :src="item.imgUrl" alt="">
|
||||
</div>
|
||||
<div class="item add">
|
||||
<div class="icon" @click="openSetPrint">
|
||||
<i class="fi fi-br-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model" v-show="selectModel.id != -1">
|
||||
@@ -54,7 +62,7 @@
|
||||
<img v-show="!isFront" :src="selectModel.threeDLayoutList?.[0]?.url" alt="">
|
||||
<img v-show="isFront" :src="selectModel.threeDLayoutList?.[1]?.url" alt="">
|
||||
</div>
|
||||
<threeBox v-if="imgOrThree" @saveProject="saveProject" ref="threeBox"></threeBox>
|
||||
<threeBox v-if="imgOrThree" @threeSaveProject="saveProject" ref="threeBox"></threeBox>
|
||||
</div>
|
||||
<div class="gallery_btn" v-show="!imgOrThree" @click="setImgOrThree(true)">3D view</div>
|
||||
<div class="gallery_btn" v-show="imgOrThree" @click="setImgOrThree(false)">Img view</div>
|
||||
@@ -74,7 +82,7 @@
|
||||
<div class="download">
|
||||
<download ref="download"></download>
|
||||
</div>
|
||||
<scaleImage ref="scaleImage"></scaleImage>
|
||||
<collection ref="collection" :isNext="false"></collection>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
@@ -87,10 +95,10 @@ import threeBox from "./three.vue"
|
||||
import download from "./download.vue"
|
||||
import {getMinioUrl} from '@/tool/util'
|
||||
import scaleImage from "@/component/HomePage/scaleImage.vue";
|
||||
import selectPrint from './selectPrint.vue'
|
||||
import collection from '@/component/home/design/collection.vue'
|
||||
export default defineComponent({
|
||||
components:{
|
||||
threeBox,download,scaleImage,selectPrint
|
||||
threeBox,download,scaleImage,collection
|
||||
},
|
||||
props:{
|
||||
},
|
||||
@@ -105,8 +113,10 @@ export default defineComponent({
|
||||
// return store.state.UserHabit.printType
|
||||
// }),
|
||||
selectObject:computed(()=>store.state.Workspace.probjects),//选择的项目
|
||||
patternMaking3D:computed(()=>store.state.HomeStoreModule.patternMaking3D),//选择的项目
|
||||
modelList:[] as any,
|
||||
sex:'Female',
|
||||
allBoardData:computed(()=>store.state.UploadFilesModule.allBoardData),
|
||||
sexList:computed(()=>store.state.UserHabit.sex.value),
|
||||
isShowMark:false,
|
||||
isNoData:false,
|
||||
@@ -118,13 +128,16 @@ export default defineComponent({
|
||||
maskShow:false,
|
||||
libraryOrModel:'model'
|
||||
})
|
||||
watch(()=>data.patternMaking3D.threeDSimpleId,(newValue,oldValue)=>{
|
||||
if(newValue)openSetData()
|
||||
})
|
||||
const sexChange = ()=>{
|
||||
data.modelList = []
|
||||
data.currentPage = 0,
|
||||
data.isNoData = false
|
||||
data.isShowMark = false
|
||||
}
|
||||
const createProbject:any = inject('createProbject')
|
||||
const createProbject:any = inject('createProbject') as any
|
||||
const setSelectModel = async (item:any)=>{
|
||||
if(!data.selectObject.id)await createProbject()
|
||||
data.isShowMark = true
|
||||
@@ -141,7 +154,7 @@ export default defineComponent({
|
||||
// dataDom.threeBox.openSetData()
|
||||
// }
|
||||
let stateData = {
|
||||
threeDSimpleId:item.id
|
||||
threeDSimpleId:item.threeDSimpleId
|
||||
}
|
||||
store.commit('setPatternMaking3D',stateData)
|
||||
saveProject()
|
||||
@@ -149,10 +162,22 @@ export default defineComponent({
|
||||
data.isShowMark = false
|
||||
})
|
||||
}
|
||||
const setSelectPrint = async (item:any)=>{
|
||||
if(!data.selectObject.id)await createProbject()
|
||||
let patternMaking3DData = {
|
||||
collectionElementId:item.id,
|
||||
url:item.imgUrl,
|
||||
printMinioUrl:getMinioUrl(item.imgUrl),
|
||||
};
|
||||
store.commit('setPatternMaking3D',patternMaking3DData)
|
||||
if(!dataDom.threeBox)dataDom.threeBox.addMaterial({url:item.imgUrl})
|
||||
saveProject()
|
||||
}
|
||||
const dataDom = reactive({
|
||||
threeBox:null as any,
|
||||
download:null as any,
|
||||
scaleImage:null as any,
|
||||
collection:null as any,
|
||||
})
|
||||
const openSetData = ()=>{
|
||||
nextTick(()=>{
|
||||
@@ -240,16 +265,18 @@ export default defineComponent({
|
||||
})
|
||||
})
|
||||
},2000)
|
||||
|
||||
}
|
||||
onMounted(()=>{
|
||||
openSetData()
|
||||
})
|
||||
const openSetPrint = ()=>{
|
||||
dataDom.collection.init('printBoard')
|
||||
}
|
||||
return{
|
||||
...toRefs(dataDom),
|
||||
...toRefs(data),
|
||||
sexChange,
|
||||
setSelectModel,
|
||||
setSelectPrint,
|
||||
openSetData,
|
||||
getModelList,
|
||||
setImgOrThree,
|
||||
@@ -257,6 +284,8 @@ export default defineComponent({
|
||||
setLibraryOrModel,
|
||||
openScaleImage,
|
||||
setPrint,
|
||||
openSetPrint,
|
||||
saveProject,
|
||||
}
|
||||
},
|
||||
directives:{
|
||||
@@ -374,6 +403,46 @@ export default defineComponent({
|
||||
border: 2px solid #000;
|
||||
}
|
||||
}
|
||||
&.printList{
|
||||
>.item{
|
||||
width: calc(100% / 4 - 1rem);
|
||||
margin:.5rem;
|
||||
aspect-ratio: 1 / 1;
|
||||
&.active{
|
||||
opacity: .7;
|
||||
transform: scale(.9);
|
||||
// border: 2px solid #000;
|
||||
// border-radius: 2rem;
|
||||
// overflow: hidden;
|
||||
}
|
||||
> img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.add{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
>.icon{
|
||||
border: calc(0.3rem*1.2) solid #000;
|
||||
border-radius: calc(1rem*1.2);
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
i{
|
||||
font-weight: 600;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> .model ,> .flatPatterm{
|
||||
|
||||
@@ -1,700 +0,0 @@
|
||||
<template>
|
||||
<div class="printboard_upload_modal">
|
||||
|
||||
<div class="printboard_left_upload">
|
||||
<div class="left_upload_header">
|
||||
<div class="upload_header_item">
|
||||
<div class="switch_type_list">
|
||||
<div
|
||||
|
||||
class="switch_type_item"
|
||||
:class="[openClick == 1 ? 'select_swtich' : '']"
|
||||
>
|
||||
<span @click.stop="open(1)">{{ $t('PrintboardUpload.Upload') }}</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
class="switch_type_item"
|
||||
:class="[openClick == 2 ? 'select_swtich' : '']"
|
||||
>
|
||||
<span @click.stop="open(2)">{{ $t('PrintboardUpload.Library') }}</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
class="switch_type_item Guide_1_2_1"
|
||||
:class="[openClick == 3 ? 'select_swtich' : '']"
|
||||
>
|
||||
<span @click.stop="open(3)">{{ $t('PrintboardUpload.Generate') }}</span>
|
||||
|
||||
</div>
|
||||
<div v-show="openClick == 3" class="generalMenu_printModel printMenu">
|
||||
<div @click.stop="openPrintModel"><span>{{ scene.name }}<i class="icon iconfont icon-xiala" :class="{forbidden:openMenu}"></i></span> </div>
|
||||
<ul v-show="openMenu">
|
||||
<li
|
||||
v-for="item,index in printCatecoryList"
|
||||
class="printModel_item"
|
||||
@click.stop="setSceneList(item)"
|
||||
:title="item.value == 'Pattern'?$t('PrintboardUpload.PatternTitle'):
|
||||
item.value == 'Logo'?$t('PrintboardUpload.LogoTitle'):
|
||||
item.value == 'Slogan'?$t('PrintboardUpload.SloganTitle'):''"
|
||||
>{{ item.name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="openClick == 1" class="printboard_body">
|
||||
<div class="upload_img_body">
|
||||
<div class="upload_item">
|
||||
<div :class="['upload_file_item']" v-for="(file, index) in fileList" :key="file">
|
||||
<div class="upload_file_img_block">
|
||||
<div class="upload_file_item_content" v-show="file?.status === 'uploading'">
|
||||
<a-spin :indicator="indicator" tip="Uploading..."/>
|
||||
</div>
|
||||
<div class="upload_file_item_content" v-show="file?.status === 'done'" @click="setSetchboardGenerate(file.resData)">
|
||||
<img v-lazy="file.imgUrl" class="upload_img" :key="file.imgUrl">
|
||||
<sketchCategory :disignTypeList="printCatecoryList" :generateList="fileList" :isSetSketchCategory="true" :item="file" ></sketchCategory>
|
||||
|
||||
<div class="delete_like_file_block" @click.stop="deleteFile(file)">
|
||||
<span
|
||||
class="icon iconfont icon-shanchu"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="pin_block" v-show="file?.status === 'done'">
|
||||
<a-checkbox v-model:checked="file.pin">{{ $t('PrintboardUpload.PIN') }}</a-checkbox>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
<div class="upload_file_item upload_component" v-show="printboardList.length < 16">
|
||||
<a-upload
|
||||
:action="uploadUrl + '/api/element/upload'"
|
||||
:capture="null"
|
||||
list-type="picture-card"
|
||||
:before-upload="beforeUpload"
|
||||
:data="{
|
||||
...upload
|
||||
}"
|
||||
:headers="{Authorization:token}"
|
||||
v-model:file-list="fileList"
|
||||
:customRequest="function(){}"
|
||||
:maxCount="16 - printboardList.length+fileList.length"
|
||||
accept=".jpg,.png,.jpeg,.bmp"
|
||||
@change="fileUploadChange"
|
||||
>
|
||||
<div
|
||||
class="upload_tip_block"
|
||||
v-show="printboardList.length < 16"
|
||||
>
|
||||
<i class="fi fi-br-upload"></i>
|
||||
<!-- <img class="upload_img_icon" src="@/assets/images/homePage/add_file.png"> -->
|
||||
</div>
|
||||
</a-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Material v-show="openClick == 2"
|
||||
ref="Material"
|
||||
msg="Printboard"
|
||||
@setLibrary = setSetchboardGenerate
|
||||
@confirmSelect="confirmSelect"
|
||||
:disignTypeList="printCatecoryList"
|
||||
></Material>
|
||||
<Generate v-show="openClick == 3" ref="Generate" @setLibrary = setSetchboardGenerate :scene="scene" :sketchCatecoryList="printCatecoryList" msg="Printboard"></Generate>
|
||||
</div>
|
||||
<!-- 取消请求 -->
|
||||
<scaleImage ref="scaleImage"></scaleImage>
|
||||
<Cropper ref="Cropper" @handleCropperSuccess="handleCropperSuccess" @closeCropper="deletUploadFile()" :cropperFileData="cropperFileData" :isUpload="isUpload"></Cropper>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent,h,ref,computed,inject,createVNode,provide, nextTick } from 'vue'
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import {getCookie} from '@/tool/cookie'
|
||||
import {getUploadUrl} from '@/tool/util'
|
||||
import {useStore} from 'vuex'
|
||||
import { Https } from "@/tool/https";
|
||||
import { Modal,message,Upload} from 'ant-design-vue';
|
||||
import Cropper from '@/component/HomePage/Cropper.vue'
|
||||
import Material from '@/component/HomePage/Material.vue'
|
||||
import Generate from "@/component/HomePage/Generate.vue";
|
||||
import GO from "@/tool/GO";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { openGuide,driverObj__ } from "@/tool/guide";
|
||||
import scaleImage from "@/component/HomePage/scaleImage.vue";
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import sketchCategory from "@/component/HomePage/sketchCategory.vue";
|
||||
import axios from 'axios'
|
||||
import generalMenu from "@/component/HomePage/generalMenu.vue";
|
||||
import { overflow } from 'html2canvas/dist/types/css/property-descriptors/overflow';
|
||||
|
||||
export default defineComponent({
|
||||
components:{
|
||||
Cropper,
|
||||
Material,
|
||||
Generate,
|
||||
scaleImage,
|
||||
generalMenu,
|
||||
sketchCategory,
|
||||
},
|
||||
emits: ['setPrint'],
|
||||
setup(){
|
||||
let store:any =useStore()
|
||||
let fileList:any = ref([]),//选中的文件id数据
|
||||
printImgList:any = ref([]), //print的印花图片
|
||||
moodBoards:any = computed(()=>{return store.state.UploadFilesModule.moodboard})
|
||||
let openClick: any = ref(1);
|
||||
let {t} = useI18n()
|
||||
let isTest = ref()
|
||||
let userInfo:any = {}
|
||||
let workspace:any = ref({})
|
||||
let scene = ref({
|
||||
name:'Pattern',
|
||||
value:'Pattern'
|
||||
})
|
||||
let openMenu = ref(false)
|
||||
let printCatecoryList:any = computed(()=>{
|
||||
return store.state.UserHabit.printType
|
||||
})
|
||||
return {
|
||||
fileList,
|
||||
printImgList,
|
||||
moodBoards,
|
||||
openClick,
|
||||
t,
|
||||
isTest,
|
||||
userInfo,
|
||||
workspace,
|
||||
scene,
|
||||
openMenu,
|
||||
printCatecoryList,
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
|
||||
},
|
||||
watch:{
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
swtich_type:'upload',
|
||||
indicator : h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: '2.4rem*1.2)',
|
||||
},
|
||||
spin: true,
|
||||
}),
|
||||
upload:{
|
||||
isPin:0,
|
||||
gender:'',
|
||||
level1Type:'Printboard',
|
||||
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
token:'',
|
||||
uploadUrl:'',
|
||||
store:useStore(),
|
||||
cropperFileData:{name:'',uid:''}, //裁剪的原始文件数据
|
||||
currentFileNum:0, //当前上传的文件数
|
||||
isUpload:false,
|
||||
printboardList:computed(()=>{
|
||||
return useStore().state.UploadFilesModule.printboard
|
||||
}),
|
||||
isTextarea:false,
|
||||
isInputFocus:false,
|
||||
isUseGenerate:true
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
let userInfo:any = getCookie("userInfo")
|
||||
this.userInfo = JSON.parse(userInfo);
|
||||
this.token = getCookie('token') || ''
|
||||
let isTest:any = getCookie('isTest')
|
||||
this.isTest =JSON.parse(isTest)
|
||||
this.uploadUrl = getUploadUrl()
|
||||
this.workspace = computed(()=>{
|
||||
return this.store?.state?.Workspace?.workspace
|
||||
})
|
||||
},
|
||||
methods:{
|
||||
setSetchboardGenerate(item:any){
|
||||
this.$emit('setPrint',item)
|
||||
},
|
||||
open(num: Number) {
|
||||
this.openClick = num;
|
||||
let material:any = this.$refs.Material
|
||||
if(num == 2){
|
||||
material.init('Printboard')
|
||||
}
|
||||
if(num == 3){
|
||||
this.scene = this.printCatecoryList[0]
|
||||
}
|
||||
},
|
||||
fileUploadChange(data:any){
|
||||
let file = data.file
|
||||
|
||||
// let res = JSON.parse(data?.xhr?.response);
|
||||
file.id_ = GO.id++
|
||||
file.type_ = {
|
||||
type1:'upload',
|
||||
type2:'Printboard'
|
||||
};
|
||||
file.pin = false;
|
||||
// file.id = res.data.id?res.data.id:""
|
||||
let Cropper:any = this.$refs.Cropper
|
||||
if(this.currentFileNum === 1){
|
||||
var reader = new FileReader();
|
||||
|
||||
reader.onload = (e:any) => {
|
||||
let data_new;
|
||||
if (typeof e.target.result === 'object') {
|
||||
// 把Array Buffer转化为blob 如果是base64不需要
|
||||
data_new = window.URL.createObjectURL(new Blob([e.target.result]));
|
||||
} else {
|
||||
data_new = e.target.result;
|
||||
}
|
||||
Cropper.getOptionImg(data_new)
|
||||
|
||||
|
||||
};
|
||||
// 转化为base64
|
||||
// reader.readAsDataURL(file)
|
||||
// 转化为blob
|
||||
reader.readAsArrayBuffer(file.originFileObj);
|
||||
this.cropperFileData = file
|
||||
Cropper.changeShowModal(true)
|
||||
}else{
|
||||
this.customRequest(file)
|
||||
}
|
||||
},
|
||||
|
||||
beforeUpload(file:any,fileList:any){
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg' || file.type === 'image/bmp';
|
||||
if (!isJpgOrPng) {
|
||||
message.info(this.t('PrintboardUpload.jsContent1'));
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt2M) {
|
||||
message.info(this.t('PrintboardUpload.jsContent2'));
|
||||
}
|
||||
if(isJpgOrPng && isLt2M){
|
||||
this.currentFileNum = fileList.length
|
||||
}else{
|
||||
return (isJpgOrPng && isLt2M) || Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
deleteFile(item:any){
|
||||
// this.fileList.splice(item, 1)
|
||||
// this.store.commit('setPrintboardFile',this.fileList)
|
||||
if(item.type_.type1 == 'generate' || item.type_.type1 == 'material'){
|
||||
item.jsContent1 = this.t('uploadFile.jsContent1',{maxImg:16})
|
||||
this.store.commit("addGenerateMaterialFils", item);
|
||||
}else{
|
||||
this.fileList = this.store.state.UploadFilesModule.printboardFiles
|
||||
let moodboard
|
||||
this.store.state.UploadFilesModule.printboardFiles.forEach((items:any,index:Number)=>{
|
||||
if(items.id_ == item.id_){
|
||||
moodboard = index
|
||||
|
||||
}
|
||||
})
|
||||
this.fileList.splice(moodboard,1)
|
||||
this.store.commit("setPrintboardFile", this.fileList);
|
||||
}
|
||||
},
|
||||
likeFile(item:any,str:string){
|
||||
if(str == 'like'){
|
||||
let data = {
|
||||
generateDetailId:item.id,
|
||||
level1Type:"Printboard",
|
||||
level2Type: item.categoryValue,
|
||||
gender:this.workspace.sexEnum.value,
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.generateLike, data).then(
|
||||
(rv) => {
|
||||
item.like = true
|
||||
}
|
||||
).catch(res=>{
|
||||
});
|
||||
}else{
|
||||
let data = {
|
||||
generateDetailId:item.id,
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
Https.axiosGet(Https.httpUrls.generateDislike, {params:data}).then(
|
||||
(rv) => {
|
||||
item.like = false
|
||||
}
|
||||
).catch(res=>{
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
customRequest(data:any){
|
||||
let new_data = {
|
||||
...this.upload,
|
||||
file:data.originFileObj
|
||||
}
|
||||
let fileUid = data.uid
|
||||
Https.axiosPost('/api/element/upload', new_data,{headers:{'Content-Type': 'multipart/form-data'}}).then(
|
||||
(rv: any) => {
|
||||
if (rv) {
|
||||
for(let file of this.fileList){
|
||||
if(fileUid === file.uid){
|
||||
file.status = 'done'
|
||||
file.imgUrl = rv.url
|
||||
file.pin = false
|
||||
file.id = rv.id
|
||||
file.category = this.printCatecoryList[0].name
|
||||
file.categoryValue = this.printCatecoryList[0].value
|
||||
file.resData = rv
|
||||
|
||||
}
|
||||
}
|
||||
let fileList = this.fileList.filter((v:any)=> v.status === 'done')
|
||||
this.store.commit('setPrintboardFile',fileList)
|
||||
}
|
||||
}
|
||||
).catch((res)=>{
|
||||
let index = -1
|
||||
this.fileList.forEach((ele:any,index1:any) => {
|
||||
if(fileUid === ele.uid){
|
||||
index = index1
|
||||
}
|
||||
});
|
||||
if(index > -1){
|
||||
this.fileList.splice(index, 1)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
closeCropper(type:any){
|
||||
if(this.isUpload){
|
||||
return
|
||||
}
|
||||
if(type == 'error'){
|
||||
let index = -1
|
||||
this.fileList.forEach((ele:any,index1:any) => {
|
||||
if(this.cropperFileData.uid === ele.uid){
|
||||
index = index1
|
||||
}
|
||||
});
|
||||
if(index > -1){
|
||||
this.fileList.splice(index, 1)
|
||||
}
|
||||
}
|
||||
let Cropper:any = this.$refs.Cropper
|
||||
Cropper.closeCropper()
|
||||
|
||||
},
|
||||
|
||||
deletUploadFile(){
|
||||
let index = -1
|
||||
this.fileList.forEach((ele:any,index1:any) => {
|
||||
if(this.cropperFileData.uid === ele.uid){
|
||||
index = index1
|
||||
}
|
||||
});
|
||||
if(index > -1){
|
||||
this.fileList.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
handleCropperSuccess(event:any){
|
||||
let {file, fileData} =event
|
||||
let new_data = {
|
||||
...this.upload,
|
||||
file:file
|
||||
}
|
||||
if(this.isUpload){
|
||||
return
|
||||
}
|
||||
this.isUpload = true
|
||||
const hide = message.loading('loading', 0);
|
||||
Https.axiosPost('/api/element/upload', new_data,{headers:{'Content-Type': 'multipart/form-data'}}).then(
|
||||
(rv: any) => {
|
||||
for(let file of this.fileList){
|
||||
if(fileData.uid === file.uid){
|
||||
file.status = 'done'
|
||||
file.imgUrl = rv.url
|
||||
file.id = rv.id
|
||||
file.resData = rv
|
||||
file.category = this.printCatecoryList[0].name
|
||||
file.categoryValue = this.printCatecoryList[0].value
|
||||
}
|
||||
}
|
||||
let fileList = this.fileList.filter((v:any)=> v.status === 'done')
|
||||
|
||||
this.isUpload = false
|
||||
this.closeCropper('success')
|
||||
this.cropperFileData = {name:'',uid:''}
|
||||
// this.store.commit('setPrintboardFile',fileList)
|
||||
hide()
|
||||
}
|
||||
).catch(res=>{
|
||||
let index = -1
|
||||
this.fileList.forEach((ele:any,index1:any) => {
|
||||
if(fileData.uid === ele.uid){
|
||||
index = index1
|
||||
}
|
||||
});
|
||||
if(index > -1){
|
||||
this.fileList.splice(index, 1)
|
||||
}
|
||||
this.cropperFileData = {name:'',uid:''}
|
||||
this.isUpload = false
|
||||
this.closeCropper('error')
|
||||
hide()
|
||||
});
|
||||
},
|
||||
|
||||
recollection(){
|
||||
|
||||
let arr = JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.printboardFiles))
|
||||
let setboard = {
|
||||
generate:[] as any,
|
||||
material:[] as any,
|
||||
moodboard:[] as any,
|
||||
}
|
||||
arr.forEach((v:any)=>{
|
||||
v.pin = v.pin == 1?true:false
|
||||
this.printCatecoryList.forEach((item:any) => {
|
||||
if(v.level2Type == item.value){
|
||||
v.category=item.name
|
||||
v.categoryValue=item.value
|
||||
}
|
||||
});
|
||||
if(v.type_.type1 == 'generate'){
|
||||
setboard.generate.push(v)
|
||||
}else if(v.type_.type1 == 'material'){
|
||||
setboard.material.push(v)
|
||||
}else{
|
||||
setboard.moodboard.push(v)
|
||||
}
|
||||
})
|
||||
this.store.commit("setPrintboardGenerateFiles", setboard.generate);
|
||||
this.store.commit("setPrintboardMaterialFiles", setboard.material);
|
||||
this.store.commit("setPrintboardFile", setboard.moodboard);
|
||||
this.fileList = setboard.moodboard
|
||||
// this.printImgList = JSON.parse(JSON.stringify(this.store.state.UploadFilesModule.allBoardData.generatePrintFiles))
|
||||
this.store.commit('setPrintboardFile',this.fileList)
|
||||
// this.store.commit('setGeneratePrintFile',this.printImgList)
|
||||
|
||||
},
|
||||
|
||||
confirmSelect(event:any){
|
||||
for(let item of event){
|
||||
let data = {
|
||||
imgUrl:item.url,
|
||||
resData:item,
|
||||
pin:false,
|
||||
id:item.id,
|
||||
status:'done',
|
||||
}
|
||||
if(this.fileList.length == 15){
|
||||
message.warning(this.t('PrintboardUpload.jsContent3'))
|
||||
break
|
||||
}
|
||||
this.fileList.push(data)
|
||||
}
|
||||
this.store.commit('setPrintboardFile',this.fileList)
|
||||
|
||||
},
|
||||
scaleImage(index:any){
|
||||
let scaleImage:any = this.$refs.scaleImage
|
||||
scaleImage.isLike = false
|
||||
scaleImage.init(this.printboardList,index)
|
||||
},
|
||||
setSceneList(data:any){
|
||||
if(this.scene.value === data.value) return
|
||||
this.scene = data
|
||||
let generate:any = this.$refs.Generate
|
||||
generate.sketchboardList = []
|
||||
generate.searchPictureName = ''
|
||||
this.openMenu = false
|
||||
},
|
||||
openPrintModel(){
|
||||
if(this.openMenu)return
|
||||
document.addEventListener('click',this.removePrintModel)
|
||||
this.openMenu = true
|
||||
},
|
||||
removePrintModel(){
|
||||
this.openMenu = false
|
||||
document.removeEventListener('click',this.removePrintModel)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.printboard_upload_modal{
|
||||
// padding: calc(1rem*1.2) calc(1rem*1.2) calc(1.8rem*1.2) calc(1rem*1.2);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
.printboard_left_upload{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
.switch_type_list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 11;
|
||||
.switch_type_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// padding: 0 2rem*1.2);
|
||||
height: calc(4rem*1.2);
|
||||
border-radius: calc(0.8rem*1.2);
|
||||
line-height: calc(4rem*1.2);
|
||||
font-size: var(--aida-fsize1-8);
|
||||
// margin-right: 2.2rem*1.2);
|
||||
margin-right: calc(8rem*1.2);
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
transform-origin: left;
|
||||
transform: scale(1);
|
||||
transition: 0.3s all;
|
||||
&.switch_type_item::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
background: #000;
|
||||
height: calc(.3rem*1.2);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: calc(.6rem*1.2);
|
||||
width: 0px;
|
||||
transition: 0.3s all;
|
||||
}
|
||||
&.select_swtich {
|
||||
color: #000;
|
||||
// font-weight: 900;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
&.select_swtich::before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.switch_icon {
|
||||
font-size: calc(1.8rem*1.2);
|
||||
margin-right: calc(0.8rem*1.2);
|
||||
}
|
||||
}
|
||||
.printMenu{
|
||||
margin-right: 0;
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
margin-left: 2rem;
|
||||
>div{
|
||||
width: 14rem;
|
||||
font-size: var(--aida-fsize1-6);
|
||||
border: 0;
|
||||
i{
|
||||
transition: all .3s;
|
||||
display: inline-block;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.forbidden{
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
ul{
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
.switch_type_item:nth-child(3){
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.printboard_body{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// height: calc(100% - 5rem*1.2));
|
||||
flex: 1;
|
||||
padding-top: calc(2.5rem*1.2);
|
||||
height: calc(30rem*1.2);
|
||||
overflow-x: hidden;
|
||||
&.printboard_body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.upload_img_body{
|
||||
height: calc(100% - 3rem*1.2);
|
||||
overflow-y: auto;
|
||||
margin-bottom: calc(1rem*1.2);
|
||||
&.upload_img_body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.upload_file_item{
|
||||
margin: 0 calc(2rem*1.2) calc(2rem*1.2) 0;
|
||||
// margin: 0 2rem*1.2) 2rem*1.2) 0;
|
||||
display: inline-block;
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
border: 1px solid #f5f5f5;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
img{
|
||||
width: auto;
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.upload_file_img_block{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.upload_component{
|
||||
}
|
||||
.checked{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
&.modal_img_item:hover .checked{
|
||||
display: flex;
|
||||
}
|
||||
.upload_file_item_content{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.upload_img{
|
||||
display: block;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
&:hover .delete_like_file_block{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -63,7 +63,7 @@ export default defineComponent({
|
||||
},
|
||||
props:{
|
||||
},
|
||||
emits:['saveProject'],
|
||||
emits:['threeSaveProject'],
|
||||
setup(props,{emit}) {
|
||||
const store = useStore();
|
||||
const data = reactive({
|
||||
@@ -265,8 +265,6 @@ export default defineComponent({
|
||||
texture.anisotropy = 32; // 提高纹理清晰度
|
||||
data.group?.traverse((child:any) => {
|
||||
if (child.isMesh) {
|
||||
// console.log(child.name)
|
||||
// 5. 创建新材质(根据需求选择材质类型)
|
||||
const textureWidth = texture.image.width;
|
||||
const textureHeight = texture.image.height;
|
||||
const box = new THREE.Box3().setFromObject(child);
|
||||
@@ -278,9 +276,6 @@ export default defineComponent({
|
||||
if(patternMaking3D.x)data.repeat.x = patternMaking3D.x
|
||||
if(patternMaking3D.y)data.repeat.y = patternMaking3D.y
|
||||
|
||||
// texture.repeat.set(1, 1); // 纹理重复次数
|
||||
texture.repeat.set(2 - data.repeat.x, 2 - data.repeat.y); // 纹理重复次数
|
||||
|
||||
const newMaterial = new THREE.MeshStandardMaterial({
|
||||
map: texture, // 基础颜色贴图
|
||||
roughness: 0.7, // 表面粗糙度 (0-1)
|
||||
@@ -338,7 +333,6 @@ export default defineComponent({
|
||||
y:data.repeat.y,
|
||||
}
|
||||
store.commit('setPatternMaking3D',value)
|
||||
|
||||
})
|
||||
}
|
||||
const addModel = async (url:any)=>{
|
||||
@@ -421,23 +415,23 @@ export default defineComponent({
|
||||
await setModel(modeUrl)
|
||||
let patternMaking3D = store.state.HomeStoreModule.patternMaking3D
|
||||
if(patternMaking3D.url)await addMaterial({url:patternMaking3D.url})
|
||||
changeRepeat(null)
|
||||
data.load.state = false
|
||||
}
|
||||
const changeRepeat = (e:any)=>{
|
||||
if(data.isLock)data.repeat.x = e
|
||||
if(data.isLock)data.repeat.y = e
|
||||
if(data.isLock && e)data.repeat.x = e
|
||||
if(data.isLock && e)data.repeat.y = e
|
||||
clearTimeout(dataTime.updataRepeat)
|
||||
dataTime.updataRepeat = setTimeout(()=>{
|
||||
data.repeat.x = data.repeat.x == 6 ? 5.999 : data.repeat.x
|
||||
data.repeat.y = data.repeat.y == 6 ? 5.999 : data.repeat.y
|
||||
|
||||
data.textureLoader.repeat.set(6 - data.repeat.x,6 - data.repeat.y); // 纹理重复次数
|
||||
let value = {
|
||||
x:data.repeat.x,
|
||||
y:data.repeat.y,
|
||||
}
|
||||
store.commit('setPatternMaking3D',value)
|
||||
emit('saveProject')
|
||||
emit('threeSaveProject')
|
||||
},1000)
|
||||
}
|
||||
const setLock = ()=>{
|
||||
|
||||
@@ -10,13 +10,23 @@
|
||||
<div class="text">Choose design to transfer</div>
|
||||
</div>
|
||||
<div class="imgBox" v-mousewheel>
|
||||
<div class="item" :class="{active:item.id == selectImg.id}" v-for="item in currentList" @click="selectImgItem(item)">
|
||||
<img :src="item.imgUrl || item.url" alt="">
|
||||
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
|
||||
</div>
|
||||
<div class="item" :class="{active:item.id == selectImg.id}" v-for="item in fileList" @click="selectImgItem(item)">
|
||||
<!-- <div class="item" :class="{active:item.id == selectImg.id}" v-for="item in currentList" @click="selectImgItem(item)">
|
||||
<img :src="item.imgUrl || item.url" alt="">
|
||||
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
|
||||
</div> -->
|
||||
<div class="item" :class="{active:item.id == selectImg.id}" v-for="item,index in fileList" @click="selectImgItem(item)">
|
||||
<div class="" v-if="item.status == 'uploading'" style="display: flex;align-items: center;">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<img v-show="item.status != 'uploading'" :src="item.imgUrl || item.url" alt="">
|
||||
<div v-show="item.status != 'uploading'" class="btnBox">
|
||||
<div :class="{active:item.isChecked}">
|
||||
<i class="fi fi-br-check"></i>
|
||||
</div>
|
||||
<div @click.stop="setUploadDelete(item,index)">
|
||||
<i class="fi fi-rr-trash icon_delete"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload_item item">
|
||||
<div class="upload_file_item">
|
||||
@@ -43,7 +53,6 @@
|
||||
</a-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="poses">
|
||||
@@ -107,7 +116,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent,computed,ref,inject,nextTick,createVNode,toRefs, reactive, onMounted} from 'vue'
|
||||
import { defineComponent,computed,ref,inject,nextTick,watch,createVNode,toRefs, reactive, onMounted} from 'vue'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { Https } from "@/tool/https";
|
||||
import { useStore } from "vuex";
|
||||
@@ -144,8 +153,8 @@ export default defineComponent({
|
||||
projectId:computed(()=>store.state.Workspace.probjects.id)
|
||||
},
|
||||
waitList:[],
|
||||
likeList:computed(()=>store.state.HomeStoreModule.poseTransfer),
|
||||
noLikeList:[ ],
|
||||
likeList:computed(()=>store.state.HomeStoreModule.poseTransfer.likedList),
|
||||
noLikeList:computed(()=>store.state.HomeStoreModule.poseTransfer.list),
|
||||
isGenerate:false,//判断是否正在进行generate
|
||||
remGenerate:false,
|
||||
removeGenerate:false,
|
||||
@@ -153,6 +162,16 @@ export default defineComponent({
|
||||
poseList:[],
|
||||
selectPose:null as any,
|
||||
})
|
||||
watch(()=>store.state.HomeStoreModule.uploadElement.length,(newVal,oldVal)=>{
|
||||
data.fileList = store.state.HomeStoreModule.uploadElement
|
||||
})
|
||||
watch(()=>store.state.HomeStoreModule.poseTransfer.list.length,(newVal,oldVal)=>{
|
||||
let list = store.state.HomeStoreModule.poseTransfer.list
|
||||
data.isGenerate = true
|
||||
data.remGenerate = true
|
||||
let taskIdList = list.filter((item:any)=>!item.id)
|
||||
if(taskIdList.length > 0)setGenerate(taskIdList[0].taskId)
|
||||
})
|
||||
let speed = reactive({
|
||||
speedList:[
|
||||
{
|
||||
@@ -180,6 +199,10 @@ export default defineComponent({
|
||||
scaleVideo:null as any,
|
||||
})
|
||||
const selectImgItem = (item:any,)=>{
|
||||
if(item.isChecked){
|
||||
item.isChecked = !item.isChecked
|
||||
return
|
||||
}
|
||||
data.selectImg = item
|
||||
data.fileList.forEach((listItem:any)=>listItem.isChecked = false)
|
||||
data.currentList.forEach((listItem:any)=>listItem.isChecked = false)
|
||||
@@ -194,18 +217,17 @@ export default defineComponent({
|
||||
if(data.poseList.length == 0){
|
||||
getPoseList()
|
||||
}
|
||||
setCloudImg()
|
||||
}
|
||||
const setCloudImg = ()=>{
|
||||
let arr = store.state.UploadFilesModule.cloudList
|
||||
let list = JSON.parse(JSON.stringify(arr.poseTransfer)) || []
|
||||
list.forEach((item:any)=>{
|
||||
item.url = item.firstFrameUrl
|
||||
data.noLikeList.unshift(item)
|
||||
})
|
||||
|
||||
store.commit('clearCloudList','poseTransfer')
|
||||
// setCloudImg()
|
||||
}
|
||||
// const setCloudImg = ()=>{
|
||||
// let arr = store.state.UploadFilesModule.cloudList
|
||||
// let list = JSON.parse(JSON.stringify(arr.poseTransfer)) || []
|
||||
// list.forEach((item:any)=>{
|
||||
// item.url = item.firstFrameUrl
|
||||
// data.noLikeList.unshift(item)
|
||||
// })
|
||||
// store.commit('clearCloudList','poseTransfer')
|
||||
// }
|
||||
const gifPlay = (e:any,item:any)=>{
|
||||
e.target.src = item.gif//使用gif图片
|
||||
}
|
||||
@@ -234,9 +256,8 @@ export default defineComponent({
|
||||
if(data.isGenerate)return
|
||||
|
||||
data.isGenerate = true
|
||||
data.remGenerateTime = setTimeout(()=>{
|
||||
data.remGenerate = true
|
||||
},10000)
|
||||
// data.remGenerateTime = setTimeout(()=>{
|
||||
// },10000)
|
||||
let value = {
|
||||
poseId:data.selectPose,
|
||||
projectId:store.state.Workspace.probjects.id,
|
||||
@@ -244,6 +265,7 @@ export default defineComponent({
|
||||
modelName:speed.speedData.value,
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.poseTransform,value).then((rv)=>{
|
||||
data.remGenerate = true
|
||||
data.noLikeList.unshift({taskId:rv})
|
||||
setGenerate(rv)
|
||||
}).catch((res:any)=>{
|
||||
@@ -254,6 +276,7 @@ export default defineComponent({
|
||||
}
|
||||
const setGenerate = (dataList:any)=>{
|
||||
let list:any = dataList
|
||||
data.waitList = list
|
||||
let dataNum = dataList.length
|
||||
let state = true
|
||||
data.generateTime = setInterval(()=>{
|
||||
@@ -271,30 +294,24 @@ export default defineComponent({
|
||||
element.url = element.firstFrameUrl
|
||||
let index = data.noLikeList.findIndex((obj:any) => obj.taskId === element.taskId);
|
||||
data.noLikeList[index] = element
|
||||
console.log(list)
|
||||
list = ''
|
||||
// if(list?.filter)list = list?.filter((item:any) => item !== element.taskId);
|
||||
store.dispatch('getCredits')
|
||||
}else if(element.status == 'Fail'){
|
||||
|
||||
let index = data.noLikeList.findIndex((obj:any) => obj.taskId === element.taskId);
|
||||
data.noLikeList.splice(index,1)
|
||||
clearInterval(data.generateTime)
|
||||
clearInterval(data.remGenerateTime)
|
||||
data.remGenerate = false
|
||||
data.isGenerate = false
|
||||
store.dispatch('getCredits')
|
||||
}
|
||||
});
|
||||
data.waitList = list
|
||||
if((!list)|| (rv.filter((item:any)=>item.status == 'Invalid').length ==list.length)){
|
||||
if(rv.filter((item:any)=>item.status == 'Invalid').length ==dataNum){
|
||||
message.info(t('Generate.effectPoor'));
|
||||
}else{
|
||||
}
|
||||
|
||||
// this.store.dispatch('getCredits')
|
||||
clearInterval(data.generateTime)
|
||||
clearInterval(data.remGenerateTime)
|
||||
data.remGenerate = false
|
||||
data.isGenerate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
).catch(res=>{
|
||||
data.noLikeList = data.noLikeList.filter((item:any)=>item.taskId != list)
|
||||
let index = data.noLikeList.findIndex((obj:any) => obj.taskId === list);
|
||||
data.noLikeList.splice(index,1)
|
||||
clearInterval(data.generateTime)
|
||||
clearInterval(data.remGenerateTime)
|
||||
data.isGenerate = false
|
||||
@@ -417,7 +434,7 @@ export default defineComponent({
|
||||
(rv) => {
|
||||
if(str == 'like'){
|
||||
let value = {
|
||||
list:[item],
|
||||
likedList:[item],
|
||||
str:'add',
|
||||
index:-1,
|
||||
}
|
||||
@@ -425,7 +442,7 @@ export default defineComponent({
|
||||
data.noLikeList.splice(index,1)
|
||||
}else{
|
||||
let value = {
|
||||
list:[item],
|
||||
likedList:[item],
|
||||
str:'splice',
|
||||
index:index,
|
||||
}
|
||||
@@ -455,6 +472,19 @@ export default defineComponent({
|
||||
}
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
const setUploadDelete = (item:any,index:any)=>{
|
||||
let value = {
|
||||
id:item.id
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.toProductImageElementDelete,{},{params:value}).then((rv)=>{
|
||||
let storeData = {
|
||||
str:'delete',
|
||||
index,
|
||||
}
|
||||
store.commit('setUploadElement',storeData)
|
||||
})
|
||||
}
|
||||
return{
|
||||
...toRefs(speed),
|
||||
@@ -476,6 +506,7 @@ export default defineComponent({
|
||||
selectPose,
|
||||
openSpeed,
|
||||
setSpeed,
|
||||
setUploadDelete,
|
||||
}
|
||||
},
|
||||
directives:{
|
||||
@@ -512,6 +543,7 @@ export default defineComponent({
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-right: 3.8rem;
|
||||
width: 39rem;
|
||||
&.active{
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
@@ -562,14 +594,13 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
> .configuratioBox > .configuratio{
|
||||
width: 31.8rem;
|
||||
width: 100%;
|
||||
padding-bottom: 1rem;
|
||||
> .title{
|
||||
// font-size: 2.6rem;
|
||||
}
|
||||
> .content{
|
||||
// margin-top: 4rem;
|
||||
margin-top: 1.8rem;
|
||||
> .selectImg,> .poses{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -602,6 +633,40 @@ export default defineComponent({
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
}
|
||||
> .btnBox{
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> div{
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
border: 1px solid;
|
||||
align-items: center;
|
||||
border-radius: .5rem;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 1rem;
|
||||
> i{
|
||||
display: flex;
|
||||
}
|
||||
>.fi-br-check{
|
||||
color: #fff;
|
||||
display: none;
|
||||
}
|
||||
&.active{
|
||||
background: #000;
|
||||
> i{
|
||||
display: flex;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> img{
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
@@ -627,8 +692,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
> .likeBox ,> .noLikeBox{
|
||||
|
||||
margin-top: 1.8rem;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
> .element{
|
||||
|
||||
@@ -10,56 +10,57 @@
|
||||
<!-- <div class="productImg_content_item_title productImg_content_item_title_menu">
|
||||
<generalMenu class="productImg_content_item_title_menubtn" :class="{hideEvents:driver__.driver}" :dataList="productimgMenuList" @setprintModel="setproduct" :item="productimgMenu"></generalMenu>
|
||||
</div> -->
|
||||
<div class="productImg_content_item_title">{{$t('ProductImg.SelectCollection')}}</div>
|
||||
<div class="productImg_content_item_imgBox generalScroll upload_item" v-mousewheel>
|
||||
<div class="content_item_imgBox_itemImg" v-for="item,index in selectList[productimgMenu.value]" :key="item.id" >
|
||||
<img @click="setGenerate(item)" v-lazy="item.designOutfitUrl?item.designOutfitUrl:item.url" alt="" :class="[item?.isChecked?'active':'']">
|
||||
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
|
||||
<div class="selectImg">
|
||||
<div class="head">
|
||||
<div class="text">{{$t('ProductImg.SelectCollection')}}</div>
|
||||
</div>
|
||||
<div class="content_item_imgBox_itemImg" v-for="(file, index) in fileList[productimgMenu.value]" :key="file">
|
||||
<div class="upload_file_item_content" v-show="file?.status === 'uploading'" >
|
||||
<a-spin
|
||||
:indicator="indicator"
|
||||
tip="Uploading..."
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="imgBox"
|
||||
v-show="file?.status === 'done'"
|
||||
|
||||
>
|
||||
<img @click="setGenerate(file)" :class="[file?.isChecked?'active':'']" :src="file?.imgUrl" class="upload_img"/>
|
||||
<a-checkbox v-model:checked="file.isChecked"></a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload_file_item upload_component">
|
||||
<!-- :action="uploadUrl + '/api/history/toProductImageElementUpload'" -->
|
||||
<a-upload
|
||||
:action="uploadUrl + '/api/history/toProductImageElementUpload'"
|
||||
list-type="picture-card"
|
||||
:capture="null"
|
||||
:data="{
|
||||
...upload,
|
||||
}"
|
||||
:headers="{ Authorization: token }"
|
||||
:before-upload="beforeUpload"
|
||||
v-model:file-list="fileList[productimgMenu.value]"
|
||||
:multiple="true"
|
||||
:maxCount="8"
|
||||
accept=".jpg,.png,.jpeg,.bmp"
|
||||
@change="(file) => fileUploadChange(file)"
|
||||
>
|
||||
<div
|
||||
class="upload_tip_block"
|
||||
>
|
||||
<i class="fi fi-br-upload"></i>
|
||||
<!-- <img class="upload_img_icon" src="@/assets/images/homePage/add_file.png"> -->
|
||||
<div class="imgBox" v-mousewheel>
|
||||
<!-- <div class="item" :class="[item?.isChecked?'active':'']" v-for="item in selectList[productimgMenu.value]" @click="setGenerate(item)">
|
||||
<img :src="item.imgUrl || item.url" alt="">
|
||||
<a-checkbox v-model:checked="item.isChecked"></a-checkbox>
|
||||
</div> -->
|
||||
<div class="item" :class="[item?.isChecked?'active':'']" v-for="item,index in fileList[productimgMenu.value]" @click="setGenerate(item)">
|
||||
<div class="" v-if="item.status == 'uploading'" style="display: flex;align-items: center;">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</a-upload>
|
||||
<img v-show="item.status != 'uploading'" :src="item.imgUrl || item.url" alt="">
|
||||
<div v-show="item.status != 'uploading'" class="btnBox">
|
||||
<div :class="{active:item.isChecked}">
|
||||
<i class="fi fi-br-check"></i>
|
||||
</div>
|
||||
<div @click.stop="setUploadDelete(item,index)">
|
||||
<i class="fi fi-rr-trash icon_delete"></i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <a-checkbox v-model:checked="item.isChecked"></a-checkbox> -->
|
||||
</div>
|
||||
<div class="upload_item item">
|
||||
<div class="upload_file_item">
|
||||
<a-upload
|
||||
:action="uploadUrl + '/api/history/toProductImageElementUpload'"
|
||||
list-type="picture-card"
|
||||
:capture="null"
|
||||
:data="{
|
||||
...upload,
|
||||
}"
|
||||
:headers="{ Authorization: token }"
|
||||
:before-upload="beforeUpload"
|
||||
v-model:file-list="fileList[productimgMenu.value]"
|
||||
:multiple="true"
|
||||
accept=".jpg,.png,.jpeg,.bmp"
|
||||
@change="(file) => fileUploadChange(file)"
|
||||
>
|
||||
<div
|
||||
class="upload_tip_block"
|
||||
>
|
||||
<i class="fi fi-br-upload"></i>
|
||||
<!-- <img class="upload_img_icon" src="@/assets/images/homePage/add_file.png"> -->
|
||||
</div>
|
||||
</a-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-show="productimgMenu.value == 'ToProductImage'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Similarity')}}</span>
|
||||
</div>
|
||||
@@ -74,15 +75,21 @@
|
||||
<input type="number" readonly v-model="similarity">
|
||||
</div>
|
||||
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.RelightDirection')}}</span>
|
||||
<span>Selection Function</span>
|
||||
</div>
|
||||
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_Direction">
|
||||
<a-select style="width: 100%;" v-model:value="speedData.value" :options="speedList" :field-names="{ label: 'relightLabel', value: 'value' }"></a-select>
|
||||
</div>
|
||||
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.RelightDirection')}}</span>
|
||||
</div>
|
||||
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_Direction">
|
||||
<a-select style="width: 100%;" v-model:value="RelightDirection" :options="RelightDirectionList"></a-select>
|
||||
</div>
|
||||
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_title productImg_content_item_title_similarity">
|
||||
<span>{{$t('ProductImg.Highlight')}}</span>
|
||||
</div>
|
||||
<div v-show="productimgMenu.value == 'Relight'" class="productImg_content_item_similarity">
|
||||
<div v-show="productimgMenu.value == 'Relight' && speedData.value != 'flux'" class="productImg_content_item_similarity">
|
||||
<a-slider class="system_silder"
|
||||
v-model:value="brightenValue"
|
||||
:tooltipVisible="false"
|
||||
@@ -114,8 +121,12 @@
|
||||
</div>
|
||||
<div class="productImg_content_item_generate_btn input_border">
|
||||
<div class="generage_btn_box ">
|
||||
<div v-show="!isProductimg" class="generage_btn started_btn" @click.stop="getPrductimg">
|
||||
{{ $t('Generate.Generate') }}
|
||||
<div v-show="!isProductimg" class="generage_btn started_btn">
|
||||
<i class="fi fi-bs-magic-wand" style="background-color: #000; font-size: 2.3rem; flex: 1;margin: 0;" @click="getPrductimg"></i>
|
||||
<div v-show="productimgMenu.value != 'Relight'" class="icon iconfont icon-xiala" :class="{active:speedState}" @click.stop="openSpeed"></div>
|
||||
<div class="content" v-show="speedState">
|
||||
<div v-for="item in speedList" :key="item.value" :class="{active:item.value == speedData.value}" @click="setSpeed(item)" :title="item.title">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isProductimg && !remProductimg" class="generage_btn started_btn" @click="getPrductimg">
|
||||
<i class="fi fi-br-loading"></i>
|
||||
@@ -135,32 +146,8 @@
|
||||
{{ $t('ProductImg.SelectedDesign') }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div class="productImg_right_item" v-for="item,index in generateList" :key="item"> -->
|
||||
<!-- <div class="productImg_right_item_imgBox" v-if="item?.status != 'Success' && !item.imgUrl">
|
||||
<img class="loadingImg" src="@/assets/images/homePage/loading.gif" alt="">
|
||||
</div>
|
||||
|
||||
<div class="productImg_right_item_imgBox" v-else>
|
||||
<img :src="item.url" alt="">
|
||||
<div class="productImg_right_item_iconRight">
|
||||
<div class="productImg_right_item_like" @click.stop="likeFile(item,'like',index)">
|
||||
<i class="fi fi-rr-heart"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="productImg_right_item_iconLeft" >
|
||||
<div class="productImg_right_item_scale" @click.stop="setScaleImage(generateList,index,true)">
|
||||
<i class="fi fi-bs-expand-arrows-alt"></i>
|
||||
</div>
|
||||
<div v-show="item.resultType != 'Relight'" class="productImg_right_item_men" @click.stop="setMenuShow(item)">
|
||||
<i class="fi fi-rr-circle-ellipsis"></i>
|
||||
<ul v-show="item.menuShow">
|
||||
<li v-for="menuItem,index in productimgMenuList" v-show="index != 0" @click.stop="setMenu(menuItem,item)" :key="menuItem.value">{{ menuItem.label }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="content">
|
||||
<generalDrag ref="generalDragLeft" @setBtn="selectSetBtn" :isDelete="false" :isLike="true" :list="likeList[productimgMenu.value]"></generalDrag>
|
||||
<generalDrag ref="generalDragLeft" :type="productimgMenu.value" @setBtn="selectSetBtn" :isDelete="false" :isLike="true" :list="likeList[productimgMenu.value]"></generalDrag>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
@@ -180,30 +167,8 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<generalDrag ref="generalDragRight" @setBtn="generateSetBtn" :list="generateList" :showMark="isShowMark"></generalDrag>
|
||||
<generalDrag ref="generalDragRight" :type="productimgMenu.value" @setBtn="generateSetBtn" :list="generateList" :showMark="isShowMark"></generalDrag>
|
||||
</div>
|
||||
<!-- <div class="mark_loading" v-show="isShowMark">
|
||||
<a-spin size="large" />
|
||||
</div> -->
|
||||
<!-- <div class="productImg_right_item" v-for="item,index in likeList[productimgMenu.value]" :key="item" >
|
||||
<img :src="item.url" alt="">
|
||||
<div class="productImg_right_item_iconRight">
|
||||
<div class="productImg_right_item_like" @click.stop="likeFile(item,'noLike',index)">
|
||||
<i class="fi fi-sr-heart"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="productImg_right_item_iconLeft">
|
||||
<div class="productImg_right_item_scale" @click.stop="setScaleImage(likeList[productimgMenu.value],index,false)">
|
||||
<i class="fi fi-bs-expand-arrows-alt"></i>
|
||||
</div>
|
||||
<div v-show="item.resultType != 'Relight'" class="productImg_right_item_menu" :title="$t('ProductImg.moreTitle')" @click.stop="setMenuShow(item)">
|
||||
<i class="fi fi-rr-circle-ellipsis"></i>
|
||||
<ul v-show="item.menuShow">
|
||||
<li v-for="menuItem,index in productimgMenuList" v-show="index != 0" @click.stop="setMenu(menuItem,item)" :key="menuItem.value">{{ menuItem.label }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +188,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { LoadingOutlined } from "@ant-design/icons-vue";
|
||||
import { defineComponent,watch,createVNode, h, ref ,toRefs,computed,reactive,triggerRef, nextTick, inject} from "vue";
|
||||
import { defineComponent,watch,createVNode, h, ref ,toRefs,computed,reactive,triggerRef, nextTick, inject, onMounted} from "vue";
|
||||
import { Https } from "@/tool/https";
|
||||
import { getCookie,setCookie } from "@/tool/cookie";
|
||||
// import domTurnImg from '@/tool/domTurnImg'
|
||||
@@ -238,7 +203,7 @@ import generalMenu from "@/component/HomePage/generalMenu.vue";
|
||||
import UpgradePlan from "@/component/HomePage/UpgradePlan.vue";
|
||||
import generalDrag from '@/component/modules/generalDrag.vue';
|
||||
import { List } from "echarts";
|
||||
|
||||
import { useRouter,useRoute } from 'vue-router'
|
||||
|
||||
export default defineComponent({
|
||||
components:{
|
||||
@@ -252,10 +217,13 @@ export default defineComponent({
|
||||
return store.state.UserHabit.userDetail
|
||||
})
|
||||
const {t} = useI18n()
|
||||
const upload:any = ref({})
|
||||
const upload:any = ref({
|
||||
projectId:computed(()=>store.state.Workspace.probjects.id)
|
||||
})
|
||||
let driver__:any = computed(()=>{
|
||||
return store.state.Guide.guide
|
||||
})
|
||||
const route = useRoute()
|
||||
const createProbject:any = inject('createProbject')
|
||||
let productImgData:any = reactive({
|
||||
isShowMark:false,
|
||||
@@ -279,7 +247,43 @@ export default defineComponent({
|
||||
button:{
|
||||
left:false,
|
||||
right:false,
|
||||
}
|
||||
},
|
||||
})
|
||||
let speed = reactive({
|
||||
speedList:[
|
||||
{
|
||||
title:'Generate with high quality',
|
||||
label:'High',
|
||||
relightLabel:'Relight',
|
||||
value:'',
|
||||
},{
|
||||
title:'',
|
||||
label:'FLUX',
|
||||
relightLabel:'Edit',
|
||||
value:'flux',
|
||||
},
|
||||
],
|
||||
speedState:false,
|
||||
speedData:{
|
||||
title:'Generate with high quality',
|
||||
relightLabel:'Relight',
|
||||
label:'High',
|
||||
value:'',
|
||||
},
|
||||
})
|
||||
//watch立即执行一次
|
||||
// watch(()=>productImgData.selectObject.id,(newVal,oldVal)=>{
|
||||
// createProbject.generateList = []
|
||||
// },{immediate: true })
|
||||
watch(() => route.query.id,
|
||||
(query:any, oldQuery:any) => {
|
||||
if(oldQuery && query != oldQuery){
|
||||
productImgData.generateList = []
|
||||
}
|
||||
},
|
||||
);
|
||||
watch(()=>store.state.HomeStoreModule.uploadElement.length,(newVal,oldVal)=>{
|
||||
productImgData.fileList[props.productimgMenu.value] = store.state.HomeStoreModule.uploadElement
|
||||
})
|
||||
const productImgDom = reactive({
|
||||
generalDragLeft:null as any,
|
||||
@@ -326,13 +330,10 @@ export default defineComponent({
|
||||
}else if(props.productimgMenu.value == 'Relight'){
|
||||
selectList.value['Relight'] = JSON.parse(JSON.stringify(selectDesignList.value.toProduct))
|
||||
}
|
||||
upload.value = {
|
||||
projectId:productImgData.selectObject.id
|
||||
}
|
||||
|
||||
userlikeGroupId = selectDesignList.value.userlikeGroupId
|
||||
// getLikeProductImage(selectDesignList.value.userlikeGroupId)
|
||||
productImgDom.generalDragLeft.setItemPosition()
|
||||
|
||||
setCloudImg()
|
||||
}
|
||||
const setCloudImg = ()=>{
|
||||
@@ -430,6 +431,18 @@ export default defineComponent({
|
||||
let setGenerate = (item:any)=>{
|
||||
item.isChecked = !item.isChecked
|
||||
}
|
||||
const setUploadDelete = (item:any,index:any)=>{
|
||||
let value = {
|
||||
id:item.id
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.toProductImageElementDelete,{},{params:value}).then((rv)=>{
|
||||
let storeData = {
|
||||
str:'delete',
|
||||
index,
|
||||
}
|
||||
store.commit('setUploadElement',storeData)
|
||||
})
|
||||
}
|
||||
let likeFile = (item:any,str:any,index:any) =>{
|
||||
let url
|
||||
let data = {
|
||||
@@ -492,7 +505,7 @@ export default defineComponent({
|
||||
productImgData.fileList[props.productimgMenu.value].forEach((item:any)=>{
|
||||
if(item.isChecked){
|
||||
obj.elementId = item.id
|
||||
obj.elementType = item.type
|
||||
obj.elementType = item.type || 'ProductElement'
|
||||
selectArr.push(JSON.parse(JSON.stringify(obj)))
|
||||
}
|
||||
})
|
||||
@@ -521,14 +534,15 @@ export default defineComponent({
|
||||
toProductImageVOList:selectArr,
|
||||
// userLikeGroupId:upload.value.userlikeGroupId,
|
||||
projectId:productImgData.selectObject.id,
|
||||
modelName:speed.speedData.value,
|
||||
direction:RelightDirection.value,
|
||||
brightenValue:productImgData.brightenValue,
|
||||
imageStrength:(100 - imageStrength)/100,
|
||||
}
|
||||
productImgData.isProductimg = true
|
||||
remPrductimgTime = setTimeout(()=>{
|
||||
productImgData.remProductimg = true
|
||||
},10000)
|
||||
// remPrductimgTime = setTimeout(()=>{
|
||||
// productImgData.remProductimg = true
|
||||
// },10000)
|
||||
let url = Https.httpUrls.toProduct
|
||||
if(props.productimgMenu.value == 'Relight'){
|
||||
url = Https.httpUrls.relight
|
||||
@@ -536,12 +550,12 @@ export default defineComponent({
|
||||
productImgData.isShowMark = true
|
||||
Https.axiosPost(url, data).then(
|
||||
(rv) => {
|
||||
productImgData.remProductimg = true
|
||||
productImgData.isShowMark = false
|
||||
let arr:any = []
|
||||
rv.forEach((item:any)=>{
|
||||
arr.push(item.taskId)
|
||||
})
|
||||
|
||||
productImgData.generateList.unshift(...rv)
|
||||
setPrductimg(arr)
|
||||
}
|
||||
@@ -699,6 +713,8 @@ export default defineComponent({
|
||||
const setLikeZoom = (item:any,str:string,index:number,list:List) =>{
|
||||
if(str == 'zoom'){
|
||||
setScaleImage(list,index,true)
|
||||
}else if(str == 'copy'){
|
||||
setCopy(item)
|
||||
}else{
|
||||
likeFile(item,str,index)
|
||||
}
|
||||
@@ -707,13 +723,12 @@ export default defineComponent({
|
||||
productImgData.likeList[props.productimgMenu.value].forEach((item:any,index:number)=>{
|
||||
if(item.id == id){
|
||||
let selectStr = ''
|
||||
str == 'zoom'?selectStr = 'zoom':selectStr ='noLike'
|
||||
setLikeZoom(item,selectStr,index,productImgData.likeList[props.productimgMenu.value])
|
||||
str == 'like'?(str = 'noLike'):str
|
||||
setLikeZoom(item,str,index,productImgData.likeList[props.productimgMenu.value])
|
||||
}
|
||||
})
|
||||
}
|
||||
const generateSetBtn = (id:any,str:string)=>{
|
||||
console.log(str)
|
||||
if(str == 'delete'){
|
||||
productImgData.generateList = productImgData.generateList.filter((v:any) => v.id != id);
|
||||
}else{
|
||||
@@ -721,12 +736,21 @@ export default defineComponent({
|
||||
if(item.id == id){
|
||||
let selectStr = ''
|
||||
str == 'zoom'?selectStr = 'zoom':selectStr ='like'
|
||||
setLikeZoom(item,selectStr,index,productImgData.generateList)
|
||||
setLikeZoom(item,str,index,productImgData.generateList)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
const setCopy = (item:any)=>{
|
||||
let value = {
|
||||
id:item.id
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.convertRelightElement,{},{params:value}).then((rv)=>{
|
||||
if(!productImgData.fileList[props.productimgMenu.value]?.length)productImgData.fileList[props.productimgMenu.value] = []
|
||||
productImgData.fileList[props.productimgMenu.value].unshift(rv)
|
||||
})
|
||||
}
|
||||
const setItemPosition = ()=>{
|
||||
productImgDom.generalDragLeft.setItemPosition()
|
||||
productImgDom.generalDragRight.setItemPosition()
|
||||
@@ -749,94 +773,109 @@ export default defineComponent({
|
||||
textarea.style.height = newHeight + 'px';
|
||||
textarea.scrollTop = scrollTop;
|
||||
}
|
||||
const openSpeed = ()=>{
|
||||
speed.speedState = !speed.speedState
|
||||
if(speed.speedState){
|
||||
document.addEventListener('click',openSpeed)
|
||||
}else{
|
||||
document.removeEventListener('click',openSpeed)
|
||||
}
|
||||
}
|
||||
const setSpeed = (item:any)=>{
|
||||
speed.speedData = item
|
||||
speed.speedState = false
|
||||
}
|
||||
return {
|
||||
upload,
|
||||
driver__,
|
||||
openSetData,
|
||||
...toRefs(productImgData),
|
||||
...toRefs(productImgDom),
|
||||
...toRefs(speed),
|
||||
productimgMenuList,
|
||||
RelightDirectionList,
|
||||
RelightDirection,
|
||||
|
||||
RelightDirectionList,
|
||||
RelightDirection,
|
||||
|
||||
selectList,
|
||||
setproduct,
|
||||
fileUploadChange,
|
||||
beforeUpload,
|
||||
deleteFile,
|
||||
setGenerate,
|
||||
likeFile,
|
||||
getPrductimg,
|
||||
removeProductimg,
|
||||
scaleImage,
|
||||
setScaleImage,
|
||||
setMenu,
|
||||
setMenuShow,
|
||||
setSimilarity,
|
||||
setTask,
|
||||
selectSetBtn,
|
||||
generateSetBtn,
|
||||
setItemPosition,
|
||||
setSize,
|
||||
ifMaximumLength,
|
||||
};
|
||||
},
|
||||
directives:{
|
||||
mousewheel:{
|
||||
mounted (el) {
|
||||
el.addEventListener('wheel',(e:WheelEvent)=>{
|
||||
let num = 0
|
||||
if(e.deltaY > 0){
|
||||
num = 25
|
||||
}else{
|
||||
num = -25
|
||||
}
|
||||
el.scrollBy(num, 0);
|
||||
},{ passive: true })
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
indicator: h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: "2.4rem",
|
||||
selectList,
|
||||
setproduct,
|
||||
fileUploadChange,
|
||||
beforeUpload,
|
||||
deleteFile,
|
||||
setGenerate,
|
||||
setUploadDelete,
|
||||
likeFile,
|
||||
getPrductimg,
|
||||
removeProductimg,
|
||||
scaleImage,
|
||||
setScaleImage,
|
||||
setMenu,
|
||||
setMenuShow,
|
||||
setSimilarity,
|
||||
setTask,
|
||||
selectSetBtn,
|
||||
generateSetBtn,
|
||||
setItemPosition,
|
||||
setSize,
|
||||
ifMaximumLength,
|
||||
openSpeed,
|
||||
setSpeed,
|
||||
};
|
||||
},
|
||||
directives:{
|
||||
mousewheel:{
|
||||
mounted (el) {
|
||||
el.addEventListener('wheel',(e:WheelEvent)=>{
|
||||
let num = 0
|
||||
if(e.deltaY > 0){
|
||||
num = 25
|
||||
}else{
|
||||
num = -25
|
||||
}
|
||||
el.scrollBy(num, 0);
|
||||
},{ passive: true })
|
||||
}
|
||||
},
|
||||
spin: true,
|
||||
}),
|
||||
// moodTemplateId: "", //模板id
|
||||
token: "",
|
||||
uploadUrl: "",
|
||||
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.token = getCookie("token") || "";
|
||||
this.uploadUrl = getUploadUrl();
|
||||
},
|
||||
methods: {
|
||||
// init(list:any,index:any,dialogueIndex:any){
|
||||
|
||||
// },
|
||||
// cancelDsign(){
|
||||
// this.productImg = false
|
||||
// // this.productImgList = []
|
||||
// // this.productImgIndex = 0
|
||||
// },
|
||||
// download(){
|
||||
// // downloadIamge(this.productImgList[this.productImgIndex].imgUrl)
|
||||
// },
|
||||
// setScaleImageIndex(index:any){
|
||||
// // this.productImgIndex = index
|
||||
// // console.log(this.productImgIndex);
|
||||
|
||||
// },
|
||||
// LikeFile(item:any,str:string){
|
||||
// let parent:any = this.$parent
|
||||
// parent.likeFile(item,str)
|
||||
// },
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
indicator: h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: "2.4rem",
|
||||
},
|
||||
spin: true,
|
||||
}),
|
||||
// moodTemplateId: "", //模板id
|
||||
token: "",
|
||||
uploadUrl: "",
|
||||
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.token = getCookie("token") || "";
|
||||
this.uploadUrl = getUploadUrl();
|
||||
},
|
||||
methods: {
|
||||
// init(list:any,index:any,dialogueIndex:any){
|
||||
|
||||
// },
|
||||
// cancelDsign(){
|
||||
// this.productImg = false
|
||||
// // this.productImgList = []
|
||||
// // this.productImgIndex = 0
|
||||
// },
|
||||
// download(){
|
||||
// // downloadIamge(this.productImgList[this.productImgIndex].imgUrl)
|
||||
// },
|
||||
// setScaleImageIndex(index:any){
|
||||
// // this.productImgIndex = index
|
||||
// // console.log(this.productImgIndex);
|
||||
|
||||
// },
|
||||
// LikeFile(item:any,str:string){
|
||||
// let parent:any = this.$parent
|
||||
// parent.likeFile(item,str)
|
||||
// },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -890,59 +929,145 @@ methods: {
|
||||
.productImg_content_item_intro{
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.productImg_content_item_imgBox{
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
width: auto;
|
||||
margin-bottom: 2rem;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
.content_item_imgBox_itemImg{
|
||||
display: flex;
|
||||
margin-right: 1rem;
|
||||
position: relative;
|
||||
height: 36rem;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
img{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
object-fit: contain;
|
||||
opacity: .5;
|
||||
&.active{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.ant-checkbox-wrapper{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
&.content_item_imgBox_itemImg:hover{
|
||||
.content_item_imgBox_itemImg_delete{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.content_item_imgBox_itemImg_delete{
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,.2);
|
||||
position: absolute;
|
||||
i{
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
// .productImg_content_item_imgBox{
|
||||
// display: flex;
|
||||
// overflow-x: auto;
|
||||
// width: auto;
|
||||
// margin-bottom: 2rem;
|
||||
// align-items: center;
|
||||
// flex-wrap: nowrap;
|
||||
// .content_item_imgBox_itemImg{
|
||||
// display: flex;
|
||||
// margin-right: 1rem;
|
||||
// position: relative;
|
||||
// height: 36rem;
|
||||
// max-height: 100%;
|
||||
// position: relative;
|
||||
// img{
|
||||
// height: 100%;
|
||||
// width: 100%;
|
||||
// cursor: pointer;
|
||||
// object-fit: contain;
|
||||
// opacity: .5;
|
||||
// &.active{
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// .ant-checkbox-wrapper{
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
// top: 0;
|
||||
// }
|
||||
// &.content_item_imgBox_itemImg:hover{
|
||||
// .content_item_imgBox_itemImg_delete{
|
||||
// display: block;
|
||||
// cursor: pointer;
|
||||
// }
|
||||
// }
|
||||
// .content_item_imgBox_itemImg_delete{
|
||||
// display: none;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// background: rgba(0,0,0,.2);
|
||||
// position: absolute;
|
||||
// i{
|
||||
// position: absolute;
|
||||
// left: 50%;
|
||||
// top: 50%;
|
||||
// transform: translate(-50%,-50%);
|
||||
// color: #fff;
|
||||
// }
|
||||
// }
|
||||
|
||||
// }
|
||||
// .content_item_imgBox_itemImg:last-child{
|
||||
// margin-right: 0;
|
||||
// }
|
||||
// }
|
||||
.selectImg{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .imgBox{
|
||||
flex: 1;
|
||||
max-height: 45rem;
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
// &.active
|
||||
|
||||
> .item{
|
||||
margin-right: 1rem;
|
||||
width: calc(100% / 2 - .5rem);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
height: 25rem;
|
||||
position: relative;
|
||||
// &.active{
|
||||
// border: 2px solid;
|
||||
// }
|
||||
:deep(.ant-checkbox-wrapper){
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
}
|
||||
> .btnBox{
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> div{
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
border: 1px solid;
|
||||
align-items: center;
|
||||
border-radius: .5rem;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 1rem;
|
||||
> i{
|
||||
display: flex;
|
||||
}
|
||||
>.fi-br-check{
|
||||
color: #fff;
|
||||
display: none;
|
||||
}
|
||||
&.active{
|
||||
background: #000;
|
||||
> i{
|
||||
display: flex;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> img{
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
> .upload_item{
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.content_item_imgBox_itemImg:last-child{
|
||||
margin-right: 0;
|
||||
> .head{
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
> .text{
|
||||
display: inline-block;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.productImg_left,.productImg_right{
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
<div class="zoom" v-if="item.url" @click.stop="()=>$emit('setBtn',item.id,'zoom')">
|
||||
<i class="fi fi-bs-expand-arrows-alt"></i>
|
||||
</div>
|
||||
<div class="copy" v-if="type == 'Relight' || type == 'ToProductImage' || type == 'PoseTransfer'" @click.stop="()=>$emit('setBtn',item.id,'copy')">
|
||||
<i class="fi fi-sr-copy-alt"></i>
|
||||
</div>
|
||||
<div class="delete" v-if="item.url && isDelete" @click.stop="()=>$emit('setBtn',item.id,'delete')">
|
||||
<i class="fi fi-rr-trash icon_delete"></i>
|
||||
</div>
|
||||
@@ -51,6 +54,7 @@ export default defineComponent({
|
||||
showMark:{type:Boolean,default:false},
|
||||
isLike:{type:Boolean,default:false},
|
||||
isDelete:{type:Boolean,default:true},
|
||||
type:{type:String,default:''},
|
||||
},
|
||||
emits:['setBtn','setSort'],
|
||||
setup(props,{emit}) {
|
||||
|
||||
Reference in New Issue
Block a user