diff --git a/plugins/Image-Toolbox/core/src/CanvasManager.js b/plugins/Image-Toolbox/core/src/CanvasManager.js
index e2e5bc5c..7fb91279 100644
--- a/plugins/Image-Toolbox/core/src/CanvasManager.js
+++ b/plugins/Image-Toolbox/core/src/CanvasManager.js
@@ -1,4 +1,4 @@
-import eventBus from './EventBus.js';
+import eventBus from './EventBus.js';
const CLIP_PATH_SERIALIZED_PROPS = ['clipPath', 'absolutePositioned', 'inverted'];
@@ -23,6 +23,8 @@ class CanvasManager {
this.zoomLevel = 1;
this._historySaveTimer = null;
this._isCropMode = false;
+ this._resizeObserver = null;
+ this._boundResize = null;
}
// ── 生命周期 ──
@@ -48,6 +50,14 @@ class CanvasManager {
* 销毁画布,释放内存
*/
destroy() {
+ if (this._resizeObserver) {
+ this._resizeObserver.disconnect();
+ this._resizeObserver = null;
+ }
+ if (this._boundResize) {
+ window.removeEventListener('resize', this._boundResize);
+ this._boundResize = null;
+ }
if (this._historySaveTimer) {
clearTimeout(this._historySaveTimer);
}
@@ -96,7 +106,7 @@ class CanvasManager {
fabricImg.width, fabricImg.height, fabricImg.type);
this.originalImage = fabricImg;
- fabricImg._originalImage = true;
+ this._applyBackgroundImageProps(fabricImg);
this.canvas.clear();
this.canvas.add(fabricImg);
this.canvas.renderAll();
@@ -149,11 +159,11 @@ class CanvasManager {
const index = this.canvas.getObjects().indexOf(this.originalImage);
this.canvas.remove(this.originalImage);
this.originalImage = fabricImg;
- fabricImg._originalImage = true;
+ this._applyBackgroundImageProps(fabricImg);
this.canvas.insertAt(fabricImg, index >= 0 ? index : 0);
} else {
this.originalImage = fabricImg;
- fabricImg._originalImage = true;
+ this._applyBackgroundImageProps(fabricImg);
this.canvas.insertAt(fabricImg, 0);
}
this.canvas.renderAll();
@@ -162,6 +172,29 @@ class CanvasManager {
});
}
+ /**
+ * 给作为背景的 fabric.Image 设置通用属性:
+ * - 标记为 _originalImage(序列化/恢复时识别)
+ * - 允许选中/接收事件(用于图层面板和调色工具定位背景图层)
+ * - 显示变换控制点,允许像普通图层一样拖拽移动、缩放、旋转
+ * @param {fabric.Image} fabricImg
+ */
+ _applyBackgroundImageProps(fabricImg) {
+ fabricImg._originalImage = true;
+ fabricImg.set({
+ selectable: true,
+ evented: true,
+ hasControls: true,
+ hasBorders: true,
+ lockMovementX: false,
+ lockMovementY: false,
+ lockRotation: false,
+ lockScalingX: false,
+ lockScalingY: false,
+ });
+ fabricImg.setCoords();
+ }
+
/**
* 获取画布当前图片 DataURL
* @param {object} [options] - 导出选项
@@ -197,6 +230,7 @@ class CanvasManager {
left: (cw - img.width * scale) / 2,
top: (ch - img.height * scale) / 2,
});
+ img.setCoords();
this.canvas.renderAll();
}
@@ -288,18 +322,28 @@ class CanvasManager {
'id',
'selectable',
'evented',
+ 'hasControls',
+ 'hasBorders',
+ 'lockMovementX',
+ 'lockMovementY',
+ 'lockRotation',
+ 'lockScalingX',
+ 'lockScalingY',
'absolutePositioned',
'inverted',
'objectCaching',
'strokeLineCap',
'strokeLineJoin',
+ '_strokePosition',
'_layerName',
'_layerNameAuto',
'_layerBaseName',
'_layerKind',
+ '_layerShapeType',
'_layerColorPresetName',
'_layerWidthPresetName',
'_layerPresetName',
+ '_layerLocked',
'_mosaicDynamic',
'_mosaicMode',
'_mosaicSize',
@@ -326,31 +370,37 @@ class CanvasManager {
return;
}
- // 提取 canvas 级别的 clipPath
+ // 提取 canvas 级别的 clipPath,避免修改历史栈里的原始快照对象。
const canvasClipPathData = json._canvasClipPath;
- delete json._canvasClipPath;
+ const snapshot = { ...json };
+ delete snapshot._canvasClipPath;
+
+ this.canvas.loadFromJSON(snapshot, () => {
+ const finishRestore = () => {
+ // 恢复 originalImage 引用
+ const objs = this.canvas.getObjects();
+ this.originalImage = objs.find(o => o.type === 'image' && o._originalImage) || objs[0];
+ if (this.originalImage?.type === 'image') {
+ this._applyBackgroundImageProps(this.originalImage);
+ }
+ this.canvas.renderAll();
+ eventBus.emit('canvas:restored');
+ resolve();
+ };
- this.canvas.loadFromJSON(json, () => {
- // 恢复 canvas.clipPath
if (canvasClipPathData) {
fabric.util.enlivenObjects([canvasClipPathData], (objects) => {
this.canvas.clipPath = objects[0] || null;
if (this.canvas.clipPath) {
this.canvas.clipPath.absolutePositioned = true;
}
- this.canvas.renderAll();
+ finishRestore();
});
} else {
// 快照中没有 clipPath → 清除画布上已有的(撤消裁切的关键)
this.canvas.clipPath = null;
+ finishRestore();
}
-
- // 恢复 originalImage 引用
- const objs = this.canvas.getObjects();
- this.originalImage = objs.find(o => o.type === 'image' && o._originalImage) || objs[0];
- this.canvas.renderAll();
- eventBus.emit('canvas:restored');
- resolve();
});
});
}
@@ -441,17 +491,16 @@ class CanvasManager {
});
// 窗口大小变化
- window.addEventListener('resize', () => {
- this._updateCanvasSize();
- });
+ this._boundResize = () => this._updateCanvasSize();
+ window.addEventListener('resize', this._boundResize);
// 使用 ResizeObserver 监听容器变化
const container = this.canvas.wrapperEl?.parentElement;
if (container && window.ResizeObserver) {
- const observer = new ResizeObserver(() => {
+ this._resizeObserver = new ResizeObserver(() => {
this._updateCanvasSize();
});
- observer.observe(container);
+ this._resizeObserver.observe(container);
}
}
}
diff --git a/plugins/Image-Toolbox/core/src/HistoryManager.js b/plugins/Image-Toolbox/core/src/HistoryManager.js
index 1cc6471a..d4fb3315 100644
--- a/plugins/Image-Toolbox/core/src/HistoryManager.js
+++ b/plugins/Image-Toolbox/core/src/HistoryManager.js
@@ -24,6 +24,9 @@ class HistoryManager {
const json = this._cm.toJSON();
if (!json) return;
+ const last = this.undoStack[this.undoStack.length - 1];
+ if (last && this._isSameSnapshot(last, json)) return;
+
this.undoStack.push(json);
// 限制栈大小
@@ -46,14 +49,23 @@ class HistoryManager {
this._isRestoring = true;
- // 保存当前状态到重做栈
const currentJson = this._cm.toJSON();
+ let prevJson = this.undoStack.pop();
+
+ while (prevJson && currentJson && this._isSameSnapshot(prevJson, currentJson) && this.undoStack.length > 0) {
+ prevJson = this.undoStack.pop();
+ }
+
+ if (!prevJson || (currentJson && this._isSameSnapshot(prevJson, currentJson))) {
+ this._isRestoring = false;
+ this._notify();
+ return;
+ }
+
if (currentJson) {
this.redoStack.push(currentJson);
}
- // 恢复上一个状态
- const prevJson = this.undoStack.pop();
try {
await this._restoreState(prevJson);
} catch (err) {
@@ -137,6 +149,17 @@ class HistoryManager {
undoCount: this.undoStack.length,
});
}
+
+ _isSameSnapshot(a, b) {
+ if (a === b) return true;
+ if (!a || !b) return false;
+
+ try {
+ return JSON.stringify(a) === JSON.stringify(b);
+ } catch (err) {
+ return false;
+ }
+ }
}
export default HistoryManager;
diff --git a/plugins/Image-Toolbox/core/src/LayerManager.js b/plugins/Image-Toolbox/core/src/LayerManager.js
index 4fe3092a..276d2070 100644
--- a/plugins/Image-Toolbox/core/src/LayerManager.js
+++ b/plugins/Image-Toolbox/core/src/LayerManager.js
@@ -1,4 +1,4 @@
-import eventBus from './EventBus.js';
+import eventBus from './EventBus.js';
/**
* 图层管理器 — 管理 Fabric.js 物件的 z-order、显隐、锁定
@@ -7,6 +7,7 @@
class LayerManager {
constructor(canvasManager) {
this._cm = canvasManager;
+ this._cm.layerManager = this;
this._layers = []; // 图层元数据 [{ id, name, visible, locked, fabricObj }]
this._idCounter = 0;
}
@@ -43,12 +44,14 @@ class LayerManager {
meta.fabricObj = obj;
this._refreshMetaName(meta, obj, false, newLayers, currentObjects);
}
+ this._refreshMetaState(meta, obj, false);
meta.zIndex = objects.length - 1 - i;
newLayers.push(meta);
}
// 背景图层始终在列表末尾(面板最底部)
if (this._cm.originalImage) {
+ this._ensureBackgroundSelectable(this._cm.originalImage);
let bgMeta = oldLayers.find(l => l.fabricObj === this._cm.originalImage) || null;
if (!bgMeta) {
bgMeta = this._createMeta(this._cm.originalImage, true, newLayers, currentObjects);
@@ -56,6 +59,7 @@ class LayerManager {
bgMeta.fabricObj = this._cm.originalImage;
this._refreshMetaName(bgMeta, this._cm.originalImage, true, newLayers, currentObjects);
}
+ this._refreshMetaState(bgMeta, this._cm.originalImage, true);
bgMeta.zIndex = 0;
newLayers.push(bgMeta);
}
@@ -106,13 +110,15 @@ class LayerManager {
const meta = {
id,
name: nameInfo.name,
- visible: obj.visible !== false,
- locked: isBackground ? true : (!obj.selectable && !obj.evented),
+ visible: true,
+ locked: false,
fabricObj: obj,
zIndex: 0,
isBackground,
};
+ this._refreshMetaState(meta, obj, isBackground);
+
if (!isBackground) {
this._setObjectLayerName(obj, meta.name, nameInfo.auto, nameInfo.baseName);
}
@@ -120,6 +126,39 @@ class LayerManager {
return meta;
}
+ _refreshMetaState(meta, obj, isBackground = false) {
+ meta.visible = obj.visible !== false;
+ meta.locked = this._resolveLockedState(obj, isBackground);
+ meta.isBackground = !!isBackground;
+ }
+
+ _resolveLockedState(obj, isBackground = false) {
+ if (isBackground) return true;
+ if (typeof obj?._layerLocked === 'boolean') return obj._layerLocked;
+
+ // 旧版本把工具激活期间创建的图层也标成 selectable/evented=false。
+ // 没有明确锁定标记时按未锁定处理,避免移动/框选工具无法选中这些图层。
+ return false;
+ }
+
+ _ensureBackgroundSelectable(obj) {
+ if (!obj) return;
+
+ obj._originalImage = true;
+ obj.set({
+ selectable: true,
+ evented: true,
+ hasControls: true,
+ hasBorders: true,
+ lockMovementX: false,
+ lockMovementY: false,
+ lockRotation: false,
+ lockScalingX: false,
+ lockScalingY: false,
+ });
+ obj.setCoords();
+ }
+
_refreshMetaName(meta, obj, isBackground = false, newLayers = null, currentObjects = null) {
const nameInfo = this._resolveLayerName(obj, isBackground, newLayers, meta, currentObjects);
meta.name = nameInfo.name;
@@ -378,6 +417,7 @@ class LayerManager {
if (!meta || meta.isBackground) return;
meta.locked = !!locked;
+ meta.fabricObj._layerLocked = meta.locked;
meta.fabricObj.set({
selectable: !meta.locked,
evented: !meta.locked,
@@ -398,9 +438,13 @@ class LayerManager {
const meta = this._layers.find(l => l.id === layerId);
if (!meta) return;
- // 即使图层被锁定也触发事件(让橡皮擦等工具能响应图层切换),
- // 但不调用 setActiveObject(避免误操作锁定图层)。
- if (!meta.locked) {
+ // 即使图层被锁定也触发事件(让橡皮擦等工具能响应图层切换)。
+ // 普通锁定图层不调用 setActiveObject(避免误操作);
+ // 但背景图层允许选中和变换;图层锁定只限制删除、改名和排序。
+ if (!meta.locked || meta.isBackground) {
+ if (meta.isBackground) {
+ this._ensureBackgroundSelectable(meta.fabricObj);
+ }
this._cm.canvas.setActiveObject(meta.fabricObj);
this._cm.canvas.renderAll();
}
diff --git a/plugins/Image-Toolbox/core/src/ToolManager.js b/plugins/Image-Toolbox/core/src/ToolManager.js
index 3f23ce27..b4a916a3 100644
--- a/plugins/Image-Toolbox/core/src/ToolManager.js
+++ b/plugins/Image-Toolbox/core/src/ToolManager.js
@@ -1,7 +1,8 @@
-import eventBus from './EventBus.js';
+import eventBus from './EventBus.js';
import SelectModule from './modules/SelectModule.js';
import MosaicModule from './modules/MosaicModule.js';
import CropModule from './modules/CropModule.js';
+import ColorModule from './modules/ColorModule.js';
import BrushModule from './modules/BrushModule.js';
import EraserModule from './modules/EraserModule.js';
import TextModule from './modules/TextModule.js';
@@ -21,7 +22,7 @@ class ToolManager {
* @param {import('./HistoryManager.js').default} historyManager
* @param {object} [options]
* @param {Array} [options.tools] - 外部注入的工具定义列表
- * @param {import('./interfaces/HostAdapter.js').default} [options.host]
+ * @param {object} [options.host]
*/
constructor(canvasManager, historyManager, options = {}) {
this._cm = canvasManager;
@@ -45,7 +46,7 @@ class ToolManager {
/**
* 注入 host adapter。
- * @param {import('./interfaces/HostAdapter.js').default} host
+ * @param {object} host
*/
setHost(host) {
this._host = host;
@@ -69,7 +70,7 @@ class ToolManager {
name: 'mosaic',
label: '马赛克',
icon: 'mosaic',
- group: 'edit',
+ group: 'redact',
shortcut: 'M',
module: MosaicModule,
defaultOptions: { mode: 'mosaic', drawMode: 'rect', mosaicSize: 12, blurRadius: 8, brushSize: 20 },
@@ -84,6 +85,16 @@ class ToolManager {
module: CropModule,
});
+ this.registerTool({
+ name: 'color',
+ label: '调色',
+ icon: 'color',
+ group: 'adjust',
+ shortcut: 'A',
+ module: ColorModule,
+ defaultOptions: { filterScope: 'all' },
+ });
+
this.registerTool({
name: 'brush',
label: '画笔',
@@ -221,6 +232,7 @@ class ToolManager {
destroy() {
Object.values(this._modules).forEach(m => {
if (m.deactivate) m.deactivate();
+ if (m.destroy) m.destroy();
});
this._modules = {};
this._currentTool = null;
diff --git a/plugins/Image-Toolbox/core/src/index.js b/plugins/Image-Toolbox/core/src/index.js
index 6d041dfe..049c0311 100644
--- a/plugins/Image-Toolbox/core/src/index.js
+++ b/plugins/Image-Toolbox/core/src/index.js
@@ -1,17 +1,5 @@
// ═══════════════════════════════════════════════════════
// @img-toolbox/core — 公共无环境依赖入口
-// 这里只导出平台无关的状态和接口;Fabric/browser 运行时见 ./runtime/fabric.js
// ═══════════════════════════════════════════════════════
-// ── 基础设施 ──
export { default as eventBus, EventBus } from './EventBus.js';
-export { default as EditorContext } from './EditorContext.js';
-
-// ── 状态存储 ──
-export { default as HistoryStore } from './HistoryStore.js';
-export { default as LayerStore } from './LayerStore.js';
-export { default as ToolRegistry } from './ToolRegistry.js';
-
-// ── 接口 ──
-export { default as HostAdapter } from './interfaces/HostAdapter.js';
-export { default as EditorEngineAdapter } from './interfaces/EditorEngineAdapter.js';
diff --git a/plugins/Image-Toolbox/core/src/modules/BaseModule.js b/plugins/Image-Toolbox/core/src/modules/BaseModule.js
index 5c79e625..7d554e28 100644
--- a/plugins/Image-Toolbox/core/src/modules/BaseModule.js
+++ b/plugins/Image-Toolbox/core/src/modules/BaseModule.js
@@ -75,13 +75,33 @@ class BaseModule {
const saved = this._savedInteractivity.get(obj);
if (saved) {
obj.set({ selectable: saved.selectable, evented: saved.evented });
- } else {
- obj.set({ selectable: true, evented: true });
}
+ // 不在保存映射中的对象(如工具激活期间新增的图层)保持原样,
+ // 不做任何修改,避免错误地解锁被用户主动锁定的图层
});
this._savedInteractivity.clear();
}
+ /**
+ * 启用可编辑图层的交互性,保留用户主动锁定的图层和临时辅助对象状态。
+ */
+ _enableEditableLayerInteractivity() {
+ const canvas = this.canvasManager.canvas;
+ if (!canvas) return;
+
+ const layerManager = this.canvasManager.layerManager;
+ const objects = canvas.getObjects();
+ objects.forEach(obj => {
+ if (obj.excludeFromLayer || obj.excludeFromHistory) return;
+
+ const meta = layerManager?.getLayerByObject?.(obj) || null;
+ const locked = meta ? meta.locked : obj._layerLocked === true;
+ if (locked && !meta?.isBackground && obj !== this.canvasManager.originalImage && !obj._originalImage) return;
+
+ obj.set({ selectable: true, evented: true });
+ });
+ }
+
/**
* 获取属性面板 HTML(子类可选实现)
* @returns {string}
diff --git a/plugins/Image-Toolbox/core/src/modules/BrushModule.js b/plugins/Image-Toolbox/core/src/modules/BrushModule.js
index 7b81fe73..6fb5fa2b 100644
--- a/plugins/Image-Toolbox/core/src/modules/BrushModule.js
+++ b/plugins/Image-Toolbox/core/src/modules/BrushModule.js
@@ -1,5 +1,6 @@
import BaseModule from './BaseModule.js';
import eventBus from '../EventBus.js';
+import { clamp, escapeAttr, normalizeColor, requestRender as _requestRender } from '../utils/helpers.js';
/**
* 画笔模块 - 使用 Fabric 自由绘制生成可编辑的 path 图层。
@@ -61,7 +62,7 @@ class BrushModule extends BaseModule {
}
setColor(color) {
- this.options.color = this._normalizeColor(color, this.options.color);
+ this.options.color = normalizeColor(color, this.options.color);
this._applyBrushOptions();
this._updateCursorPreviewStyle();
}
@@ -95,7 +96,7 @@ class BrushModule extends BaseModule {
}
getOptionsBarHTML() {
- const color = this._normalizeColor(this.options.color);
+ const color = normalizeColor(this.options.color);
const width = this.options.width;
return `
@@ -121,7 +122,7 @@ class BrushModule extends BaseModule {
画笔工具
-
+
@@ -257,13 +258,7 @@ class BrushModule extends BaseModule {
}
_requestRender() {
- const canvas = this.canvasManager.canvas;
- if (!canvas) return;
- if (typeof canvas.requestRenderAll === 'function') {
- canvas.requestRenderAll();
- } else {
- canvas.renderAll();
- }
+ _requestRender(this.canvasManager.canvas);
}
_ensureBrush() {
@@ -284,7 +279,7 @@ class BrushModule extends BaseModule {
}
_getColorPresetButton(preset, label, color, currentColor) {
- const normalized = this._normalizeColor(color);
+ const normalized = normalizeColor(color);
const active = currentColor === normalized ? ' active' : '';
return `
+
+
+
+
+
+
+
+ ${this._getSelectOption('outside', '外部', active._strokePosition || 'outside')}
+ ${this._getSelectOption('inside', '内部', active._strokePosition || 'outside')}
+
+
@@ -217,6 +234,10 @@ class PropertyPanel {
+
+
+
+
@@ -272,6 +293,7 @@ class PropertyPanel {
const moduleAction = target.dataset.moduleAction;
const modulePreset = target.dataset.modulePreset;
if (!prop && !moduleProp && !moduleAction && !modulePreset) return;
+ // 普通属性控件只在 input/change 事件中处理;click 事件仅放行模块动作/预设
if (e.type === 'click' && !moduleAction && !modulePreset) return;
const module = this._tm.getCurrentModule();
@@ -369,9 +391,16 @@ class PropertyPanel {
case 'underline':
active.set('underline', value);
break;
+ case 'linethrough':
+ active.set('linethrough', value);
+ break;
case 'textAlign':
active.set('textAlign', value);
break;
+ case 'strokePosition':
+ active.set('paintFirst', value === 'inside' ? 'fill' : 'stroke');
+ active.set('_strokePosition', value);
+ break;
default:
return;
}
@@ -415,7 +444,8 @@ class PropertyPanel {
const handled = module.onToolPropertyChange(prop, value, { eventType: e.type });
if (target.type === 'range' && target.nextElementSibling) {
- target.nextElementSibling.textContent = `${value}${target.dataset.valueSuffix || 'px'}`;
+ const suffix = target.dataset.valueSuffix != null ? target.dataset.valueSuffix : 'px';
+ target.nextElementSibling.textContent = `${value}${suffix}`;
}
if (handled !== false && target.dataset.refreshProperty === 'true') {
@@ -642,21 +672,20 @@ class PropertyPanel {
_clamp(value, min, max) {
if (!Number.isFinite(value)) return min;
- return Math.max(min, Math.min(max, value));
+ return clamp(value, min, max);
}
_escapeHTML(value) {
- return String(value ?? '').replace(/[&<>"']/g, ch => ({
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": ''',
- }[ch]));
+ return escapeHTML(value);
}
_escapeAttr(value) {
- return this._escapeHTML(value);
+ return escapeAttr(value);
+ }
+
+ destroy() {
+ this._eventBusUnsubscribers.forEach(unsub => unsub());
+ this._eventBusUnsubscribers = [];
}
}
diff --git a/plugins/Image-Toolbox/src/ui/SidePanelTabs.js b/plugins/Image-Toolbox/src/ui/SidePanelTabs.js
index 69e1ad15..f6296612 100644
--- a/plugins/Image-Toolbox/src/ui/SidePanelTabs.js
+++ b/plugins/Image-Toolbox/src/ui/SidePanelTabs.js
@@ -20,6 +20,7 @@ class SidePanelTabs {
this._lm = layerManager;
this._activeTab = this._getInitialTab();
this._layout = this._getInitialLayout();
+ this._eventBusUnsubscribers = [];
this._render();
this._bindEvents();
@@ -59,9 +60,11 @@ class SidePanelTabs {
this._activateTab(tabBtn.dataset.panelTab, true);
});
- eventBus.on('layers:updated', () => this._updateLayerCount());
- eventBus.on('image:loaded', () => this._updateLayerCount());
- eventBus.on('canvas:restored', () => this._updateLayerCount());
+ this._eventBusUnsubscribers.push(
+ eventBus.on('layers:updated', () => this._updateLayerCount()),
+ eventBus.on('image:loaded', () => this._updateLayerCount()),
+ eventBus.on('canvas:restored', () => this._updateLayerCount())
+ );
}
applyLayout(layout, persist = true) {
@@ -135,6 +138,11 @@ class SidePanelTabs {
const saved = localStorage.getItem(SIDE_PANEL_LAYOUT_KEY);
return VALID_LAYOUTS.has(saved) ? saved : SIDE_PANEL_LAYOUTS.TABS;
}
+
+ destroy() {
+ this._eventBusUnsubscribers.forEach(unsub => unsub());
+ this._eventBusUnsubscribers = [];
+ }
}
export default SidePanelTabs;
diff --git a/plugins/Image-Toolbox/src/ui/StatusBar.js b/plugins/Image-Toolbox/src/ui/StatusBar.js
index 7e64385f..4b0b1bba 100644
--- a/plugins/Image-Toolbox/src/ui/StatusBar.js
+++ b/plugins/Image-Toolbox/src/ui/StatusBar.js
@@ -9,6 +9,7 @@ class StatusBar {
this._el = containerEl;
this._cm = canvasManager;
this._lm = layerManager;
+ this._eventBusUnsubscribers = [];
this._bindEvents();
this._render();
@@ -32,24 +33,30 @@ class StatusBar {
_bindEvents() {
// Update size after image load.
- eventBus.on('image:loaded', (img) => {
- this._updateSize(img);
- });
+ this._eventBusUnsubscribers.push(
+ eventBus.on('image:loaded', (img) => {
+ this._updateSize(img);
+ })
+ );
// Update size after canvas changes.
- eventBus.on('canvas:objectModified', () => {
- if (this._cm.originalImage) {
- this._updateSize(this._cm.originalImage);
- }
- });
+ this._eventBusUnsubscribers.push(
+ eventBus.on('canvas:objectModified', () => {
+ if (this._cm.originalImage) {
+ this._updateSize(this._cm.originalImage);
+ }
+ })
+ );
// Update layer count when layers change.
- eventBus.on('layers:updated', (layers) => {
- const countEl = this._el.querySelector('#status-layers');
- if (countEl) {
- countEl.textContent = `图层: ${layers ? layers.length : this._lm.getCount()}`;
- }
- });
+ this._eventBusUnsubscribers.push(
+ eventBus.on('layers:updated', (layers) => {
+ const countEl = this._el.querySelector('#status-layers');
+ if (countEl) {
+ countEl.textContent = `图层: ${layers ? layers.length : this._lm.getCount()}`;
+ }
+ })
+ );
// 导出按钮
this._el.addEventListener('click', (e) => {
@@ -69,6 +76,11 @@ class StatusBar {
sizeEl.textContent = `${w} × ${h} px`;
}
}
+
+ destroy() {
+ this._eventBusUnsubscribers.forEach(unsub => unsub());
+ this._eventBusUnsubscribers = [];
+ }
}
export default StatusBar;
diff --git a/plugins/Image-Toolbox/src/ui/Toolbar.js b/plugins/Image-Toolbox/src/ui/Toolbar.js
index d6dda046..b2b79f97 100644
--- a/plugins/Image-Toolbox/src/ui/Toolbar.js
+++ b/plugins/Image-Toolbox/src/ui/Toolbar.js
@@ -1,4 +1,5 @@
import { eventBus } from '../../core/src/index.js';
+import { escapeHTML, escapeAttr } from '../../core/src/utils/helpers.js';
/**
* Toolbar UI component.
@@ -11,12 +12,14 @@ class Toolbar {
this._host = host;
this._currentTool = 'select';
this._user = this._getHostUser();
+ this._eventBusUnsubscribers = [];
// SVG 图标模板
this._icons = {
select: ``,
mosaic: ``,
crop: ``,
+ color: ``,
brush: ``,
eraser: ``,
text: ``,
@@ -36,7 +39,7 @@ class Toolbar {
let currentGroup = null;
// Render tool groups in a stable order.
- const groupOrder = ['edit', 'annotate', 'view', 'action'];
+ const groupOrder = ['edit', 'adjust', 'redact', 'annotate', 'view', 'action'];
for (const group of groupOrder) {
const groupTools = tools.filter(t => t.group === group);
if (groupTools.length === 0) continue;
@@ -120,15 +123,15 @@ class Toolbar {
}, true);
// 监听工具切换事件(外部触发)
- eventBus.on('tool:changed', (toolName) => {
- this._currentTool = toolName;
- this._updateActive();
- });
-
- // 监听历史状态变化,更新撤销/重做按钮
- eventBus.on('history:changed', ({ canUndo, canRedo }) => {
- this._updateHistoryButtons(canUndo, canRedo);
- });
+ this._eventBusUnsubscribers.push(
+ eventBus.on('tool:changed', (toolName) => {
+ this._currentTool = toolName;
+ this._updateActive();
+ }),
+ eventBus.on('history:changed', ({ canUndo, canRedo }) => {
+ this._updateHistoryButtons(canUndo, canRedo);
+ })
+ );
}
_updateHistoryButtons(canUndo, canRedo) {
@@ -205,18 +208,16 @@ class Toolbar {
}
_escapeAttr(value) {
- return String(value)
- .replace(/&/g, '&')
- .replace(/"/g, '"')
- .replace(//g, '>');
+ return escapeAttr(value);
}
_escapeHTML(value) {
- return String(value)
- .replace(/&/g, '&')
- .replace(//g, '>');
+ return escapeHTML(value);
+ }
+
+ destroy() {
+ this._eventBusUnsubscribers.forEach(unsub => unsub());
+ this._eventBusUnsubscribers = [];
}
}