web-上传组件、富文本组件

This commit is contained in:
2025-10-20 11:25:34 +08:00
parent f137d7d720
commit 2f1835bdbf
12 changed files with 1608 additions and 445 deletions

View File

@@ -8,14 +8,24 @@ import Quill from 'quill';
interface ResizeOptions {
modules?: string[];
onResizeEnd?: (element: HTMLElement) => void; // 拉伸结束回调
}
interface ResizeState {
startX: number;
startY: number;
startWidth: number;
startHeight: number;
aspectRatio: number;
position: string;
}
export class ImageResize {
quill: any;
options: ResizeOptions;
overlay: HTMLElement | null = null;
img: HTMLImageElement | HTMLVideoElement | null = null;
handle: HTMLElement | null = null;
element: HTMLImageElement | HTMLVideoElement | null = null;
resizeState: ResizeState | null = null;
constructor(quill: any, options: ResizeOptions = {}) {
this.quill = quill;
@@ -25,23 +35,26 @@ export class ImageResize {
// 等待编辑器完全初始化
setTimeout(() => {
// 监听编辑器点击事件
this.quill.root.addEventListener('click', this.handleClick.bind(this));
// 点击编辑器外部时隐藏 resize 控件
document.addEventListener('click', this.handleDocumentClick.bind(this));
this.initEventListeners();
console.log('✅ ImageResize 事件监听器已添加');
}, 100);
}
private initEventListeners() {
// 监听编辑器点击事件
this.quill.root.addEventListener('click', this.handleClick.bind(this));
// 点击编辑器外部时隐藏 resize 控件
document.addEventListener('click', this.handleDocumentClick.bind(this));
}
handleClick(e: MouseEvent) {
const target = e.target as HTMLElement;
console.log('🖱️ 点击事件:', target.tagName, target);
// 检查是否点击了图片或视频
if (target.tagName === 'IMG' || target.tagName === 'VIDEO') {
if (this.isResizableElement(target)) {
console.log('📷 检测到图片/视频点击,显示缩放控件');
this.showResizer(target as HTMLImageElement | HTMLVideoElement);
} else if (!this.overlay || !this.overlay.contains(target)) {
@@ -50,6 +63,10 @@ export class ImageResize {
}
}
private isResizableElement(element: HTMLElement): boolean {
return element.tagName === 'IMG' || element.tagName === 'VIDEO';
}
handleDocumentClick(e: MouseEvent) {
const target = e.target as HTMLElement;
@@ -60,7 +77,7 @@ export class ImageResize {
}
showResizer(element: HTMLImageElement | HTMLVideoElement) {
this.img = element;
this.element = element;
console.log('🎯 显示缩放控件,元素:', element);
@@ -72,6 +89,14 @@ export class ImageResize {
if (!this.overlay) return;
// 更新遮罩层位置和大小
this.updateOverlayPosition(element);
console.log('✅ 缩放控件已显示');
}
private updateOverlayPosition(element: HTMLImageElement | HTMLVideoElement) {
if (!this.overlay) return;
const rect = element.getBoundingClientRect();
const containerRect = this.quill.root.getBoundingClientRect();
@@ -87,15 +112,14 @@ export class ImageResize {
this.overlay.style.top = `${rect.top - containerRect.top + this.quill.root.scrollTop}px`;
this.overlay.style.width = `${rect.width}px`;
this.overlay.style.height = `${rect.height}px`;
console.log('✅ 缩放控件已显示');
}
hideResizer() {
if (this.overlay) {
this.overlay.style.display = 'none';
}
this.img = null;
this.element = null;
this.resizeState = null;
}
createOverlay() {
@@ -199,87 +223,124 @@ export class ImageResize {
e.preventDefault();
e.stopPropagation();
if (!this.img || !this.overlay) return;
if (!this.element || !this.overlay) return;
const startX = e.clientX;
const startY = e.clientY;
const startWidth = this.img.offsetWidth;
const startHeight = this.img.offsetHeight;
const aspectRatio = startWidth / startHeight;
// 初始化拉伸状态
this.resizeState = {
startX: e.clientX,
startY: e.clientY,
startWidth: this.element.offsetWidth,
startHeight: this.element.offsetHeight,
aspectRatio: this.element.offsetWidth / this.element.offsetHeight,
position
};
const handleMouseMove = (moveEvent: MouseEvent) => {
if (!this.img || !this.overlay) return;
if (!this.element || !this.overlay || !this.resizeState) return;
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
let newWidth = startWidth;
let newHeight = startHeight;
// 根据拉伸方向计算新尺寸
if (position.includes('e')) {
newWidth = startWidth + deltaX;
} else if (position.includes('w')) {
newWidth = startWidth - deltaX;
}
if (position.includes('s')) {
newHeight = startHeight + deltaY;
} else if (position.includes('n')) {
newHeight = startHeight - deltaY;
}
// 保持纵横比(对于对角拉伸)
if (position.length === 2) {
newHeight = newWidth / aspectRatio;
}
// 限制最小尺寸
if (newWidth < 50) newWidth = 50;
if (newHeight < 50) newHeight = 50;
// 应用新尺寸(同时设置 style 和属性)
this.img.style.width = `${newWidth}px`;
this.img.style.height = `${newHeight}px`;
this.img.setAttribute('width', `${newWidth}`);
this.img.setAttribute('height', `${newHeight}`);
// 更新遮罩层
this.overlay.style.width = `${newWidth}px`;
this.overlay.style.height = `${newHeight}px`;
this.updateElementSize(moveEvent);
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
// 强制触发 Quill 的 text-change 事件
// 使用 Quill 的内部方法
console.log('🔄 尝试触发 text-change 事件');
if (this.quill.emitter) {
this.quill.emitter.emit('text-change', {
oldRange: null,
range: null,
source: 'user'
});
console.log('✅ text-change 事件已触发');
} else {
console.log('❌ quill.emitter 不存在');
}
// 备用方案:直接触发 DOM 事件
console.log('🔄 尝试触发 input 事件');
const event = new Event('input', { bubbles: true });
this.quill.root.dispatchEvent(event);
console.log('✅ input 事件已触发');
// 拉伸结束,触发更新
this.onResizeEnd();
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
private updateElementSize(moveEvent: MouseEvent) {
if (!this.element || !this.overlay || !this.resizeState) return;
const deltaX = moveEvent.clientX - this.resizeState.startX;
const deltaY = moveEvent.clientY - this.resizeState.startY;
let newWidth = this.resizeState.startWidth;
let newHeight = this.resizeState.startHeight;
// 根据拉伸方向计算新尺寸
if (this.resizeState.position.includes('e')) {
newWidth = this.resizeState.startWidth + deltaX;
} else if (this.resizeState.position.includes('w')) {
newWidth = this.resizeState.startWidth - deltaX;
}
if (this.resizeState.position.includes('s')) {
newHeight = this.resizeState.startHeight + deltaY;
} else if (this.resizeState.position.includes('n')) {
newHeight = this.resizeState.startHeight - deltaY;
}
// 保持纵横比(对于对角拉伸)
if (this.resizeState.position.length === 2) {
newHeight = newWidth / this.resizeState.aspectRatio;
}
// 限制最小尺寸
if (newWidth < 50) newWidth = 50;
if (newHeight < 50) newHeight = 50;
// 应用新尺寸
this.applyElementSize(newWidth, newHeight);
// 更新遮罩层
this.overlay.style.width = `${newWidth}px`;
this.overlay.style.height = `${newHeight}px`;
}
private applyElementSize(width: number, height: number) {
if (!this.element) return;
// 同时设置 style 和属性
this.element.style.width = `${width}px`;
this.element.style.height = `${height}px`;
this.element.setAttribute('width', `${width}`);
this.element.setAttribute('height', `${height}`);
}
private onResizeEnd() {
if (!this.element) return;
console.log('🔄 拉伸结束触发HTML更新');
// 调用自定义回调
if (this.options.onResizeEnd) {
this.options.onResizeEnd(this.element);
}
// 强制触发 Quill 的内容更新
this.triggerQuillUpdate();
}
private triggerQuillUpdate() {
// 方案1: 使用 Quill 的内部方法
if (this.quill.emitter) {
this.quill.emitter.emit('text-change', {
oldRange: null,
range: null,
source: 'user'
});
console.log('✅ Quill text-change 事件已触发');
}
// 方案2: 触发 DOM 事件
const inputEvent = new Event('input', { bubbles: true });
this.quill.root.dispatchEvent(inputEvent);
console.log('✅ DOM input 事件已触发');
// 方案3: 强制更新 Quill 内容
setTimeout(() => {
if (this.quill.update) {
this.quill.update();
console.log('✅ Quill 内容已更新');
}
}, 0);
}
destroy() {
if (this.overlay) {
this.overlay.remove();