customer-chat-sdk 1.0.34 → 1.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ScreenshotManager.d.ts +42 -2
- package/dist/core/ScreenshotManager.d.ts.map +1 -1
- package/dist/customer-sdk.cjs.js +421 -45
- package/dist/customer-sdk.esm.js +421 -45
- package/dist/customer-sdk.min.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/customer-sdk.esm.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import html2canvas from 'html2canvas';
|
|
2
|
+
|
|
1
3
|
// 直接使用base64字符串,避免打包后路径问题
|
|
2
4
|
const iconImage = 'data:image/png;base64,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';
|
|
3
5
|
class IconManager {
|
|
@@ -6894,25 +6896,62 @@ class ScreenshotManager {
|
|
|
6894
6896
|
}
|
|
6895
6897
|
}
|
|
6896
6898
|
let dataUrl;
|
|
6897
|
-
// 等待一小段时间,确保 DOM 更新完成(减少等待时间)
|
|
6898
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
6899
6899
|
// 根据选择的引擎进行截图
|
|
6900
|
-
if (selectedEngine === '
|
|
6900
|
+
if (selectedEngine === 'html2canvas') {
|
|
6901
|
+
// html2canvas 需要更长的等待时间确保样式加载
|
|
6902
|
+
// 额外等待样式和字体加载完成
|
|
6903
|
+
await Promise.all([
|
|
6904
|
+
this.waitForStylesAndFonts(),
|
|
6905
|
+
this.waitForFonts()
|
|
6906
|
+
]);
|
|
6907
|
+
// 再等待一段时间,确保样式完全应用
|
|
6908
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
6909
|
+
dataUrl = await this.takeScreenshotWithHtml2Canvas(this.targetElement);
|
|
6910
|
+
}
|
|
6911
|
+
else if (selectedEngine === 'snapdom') {
|
|
6912
|
+
// 等待一小段时间,确保 DOM 更新完成
|
|
6913
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
6901
6914
|
dataUrl = await this.takeScreenshotWithSnapdom(this.targetElement);
|
|
6902
6915
|
}
|
|
6903
6916
|
else {
|
|
6904
6917
|
// 默认使用 modern-screenshot
|
|
6918
|
+
// 等待一小段时间,确保 DOM 更新完成
|
|
6919
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
6905
6920
|
dataUrl = await this.takeScreenshotWithModernScreenshot(this.targetElement);
|
|
6906
6921
|
}
|
|
6907
6922
|
const timestamp = Date.now();
|
|
6908
6923
|
// 更新状态
|
|
6909
6924
|
this.screenshotCount++;
|
|
6910
6925
|
this.lastScreenshotTime = timestamp;
|
|
6911
|
-
//
|
|
6926
|
+
// 管理历史记录(限制内存占用)
|
|
6927
|
+
// base64 字符串很大,需要严格控制历史记录数量
|
|
6912
6928
|
if (this.screenshotHistory.length >= this.options.maxHistory) {
|
|
6913
|
-
|
|
6929
|
+
// 删除最旧的截图,释放内存
|
|
6930
|
+
const removed = this.screenshotHistory.shift();
|
|
6931
|
+
// 强制 GC(如果可能)
|
|
6932
|
+
if (removed && removed.length > 1000000) { // 大于1MB的字符串
|
|
6933
|
+
if (!this.options.silentMode) {
|
|
6934
|
+
console.log(`📸 清理旧截图,释放内存: ${Math.round(removed.length * 0.75 / 1024)} KB`);
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6914
6937
|
}
|
|
6915
6938
|
this.screenshotHistory.push(dataUrl);
|
|
6939
|
+
// 如果历史记录总大小超过限制,清理最旧的
|
|
6940
|
+
let totalSize = this.screenshotHistory.reduce((sum, item) => sum + item.length, 0);
|
|
6941
|
+
const maxTotalSize = 50 * 1024 * 1024; // 最大50MB
|
|
6942
|
+
while (this.screenshotHistory.length > 0 && totalSize > maxTotalSize) {
|
|
6943
|
+
const removed = this.screenshotHistory.shift();
|
|
6944
|
+
if (removed) {
|
|
6945
|
+
const removedSize = removed.length;
|
|
6946
|
+
totalSize -= removedSize;
|
|
6947
|
+
if (!this.options.silentMode) {
|
|
6948
|
+
console.warn(`📸 ⚠️ 历史记录总大小超过限制,清理最旧截图: ${Math.round(removedSize * 0.75 / 1024)} KB`);
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
else {
|
|
6952
|
+
break;
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6916
6955
|
// 打印基本信息
|
|
6917
6956
|
const base64Data = dataUrl.split(',')[1];
|
|
6918
6957
|
if (!this.options.silentMode) {
|
|
@@ -7062,8 +7101,206 @@ class ScreenshotManager {
|
|
|
7062
7101
|
throw error;
|
|
7063
7102
|
}
|
|
7064
7103
|
}
|
|
7104
|
+
/**
|
|
7105
|
+
* 使用 html2canvas 截图
|
|
7106
|
+
*
|
|
7107
|
+
* 优势:
|
|
7108
|
+
* - 处理 SVG 和本地资源更快(不需要复杂的 Worker 通信)
|
|
7109
|
+
* - 兼容性好,支持更多 CSS 特性
|
|
7110
|
+
* - 跨域处理相对简单
|
|
7111
|
+
*
|
|
7112
|
+
* 劣势:
|
|
7113
|
+
* - 在主线程执行,可能阻塞 UI(但处理速度快,影响较小)
|
|
7114
|
+
* - 不支持 Worker 模式
|
|
7115
|
+
*
|
|
7116
|
+
* 适用场景:
|
|
7117
|
+
* - 页面包含大量 SVG 图标
|
|
7118
|
+
* - 本地资源较多
|
|
7119
|
+
* - 需要快速截图
|
|
7120
|
+
*/
|
|
7121
|
+
async takeScreenshotWithHtml2Canvas(element) {
|
|
7122
|
+
if (!this.options.silentMode) {
|
|
7123
|
+
console.log('📸 使用 html2canvas 引擎截图...');
|
|
7124
|
+
}
|
|
7125
|
+
try {
|
|
7126
|
+
// html2canvas 需要确保样式完全加载,额外等待
|
|
7127
|
+
// 等待所有样式表加载完成
|
|
7128
|
+
await this.waitForAllStylesLoaded();
|
|
7129
|
+
// 等待字体加载完成
|
|
7130
|
+
await this.waitForFonts();
|
|
7131
|
+
// 等待 DOM 完全渲染
|
|
7132
|
+
await new Promise(resolve => {
|
|
7133
|
+
requestAnimationFrame(() => {
|
|
7134
|
+
requestAnimationFrame(() => {
|
|
7135
|
+
setTimeout(() => resolve(), 100);
|
|
7136
|
+
});
|
|
7137
|
+
});
|
|
7138
|
+
});
|
|
7139
|
+
// 检查元素是否存在和可见
|
|
7140
|
+
const rect = element.getBoundingClientRect();
|
|
7141
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
7142
|
+
throw new Error('元素尺寸为 0,无法截图');
|
|
7143
|
+
}
|
|
7144
|
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
7145
|
+
const isLowEndDevice = navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4;
|
|
7146
|
+
// 计算压缩后的尺寸
|
|
7147
|
+
let elementWidth = element.scrollWidth || element.clientWidth || element.offsetWidth;
|
|
7148
|
+
let elementHeight = element.scrollHeight || element.clientHeight || element.offsetHeight;
|
|
7149
|
+
if (element === document.body || element === document.documentElement) {
|
|
7150
|
+
elementWidth = Math.max(element.scrollWidth, element.offsetWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth, window.innerWidth);
|
|
7151
|
+
elementHeight = Math.max(element.scrollHeight, element.offsetHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight, window.innerHeight);
|
|
7152
|
+
}
|
|
7153
|
+
const { width, height } = this.calculateCompressedSize(elementWidth, elementHeight, this.options.maxWidth, this.options.maxHeight);
|
|
7154
|
+
const finalWidth = width < elementWidth ? width : Math.min(elementWidth, this.options.maxWidth);
|
|
7155
|
+
const finalHeight = height < elementHeight ? height : Math.min(elementHeight, this.options.maxHeight);
|
|
7156
|
+
// html2canvas 质量设置(0-1)
|
|
7157
|
+
const finalQuality = isMobile || isLowEndDevice
|
|
7158
|
+
? Math.max(this.options.quality * 0.65, 0.2)
|
|
7159
|
+
: this.options.quality;
|
|
7160
|
+
// html2canvas 配置选项
|
|
7161
|
+
const options = {
|
|
7162
|
+
// 基本配置
|
|
7163
|
+
backgroundColor: '#ffffff',
|
|
7164
|
+
scale: this.options.scale !== 1 ? (isMobile ? 0.7 : this.options.scale) : (isMobile ? 0.7 : 1),
|
|
7165
|
+
useCORS: this.options.enableCORS,
|
|
7166
|
+
allowTaint: !this.options.enableCORS, // 如果启用 CORS,不允许 taint
|
|
7167
|
+
logging: !this.options.silentMode,
|
|
7168
|
+
width: finalWidth,
|
|
7169
|
+
height: finalHeight,
|
|
7170
|
+
// 关键配置:确保样式正确渲染
|
|
7171
|
+
foreignObjectRendering: false, // 禁用 foreignObject,使用传统渲染方式(更稳定)
|
|
7172
|
+
onclone: (_clonedDoc, _element) => {
|
|
7173
|
+
// 在克隆的文档中,确保所有样式都正确应用
|
|
7174
|
+
// html2canvas 会自动处理样式,这里不需要手动复制
|
|
7175
|
+
// 如果需要,可以在这里添加额外的样式处理逻辑
|
|
7176
|
+
// 复制所有样式表到克隆的文档
|
|
7177
|
+
const originalStyleSheets = Array.from(document.styleSheets);
|
|
7178
|
+
originalStyleSheets.forEach((originalSheet) => {
|
|
7179
|
+
try {
|
|
7180
|
+
// 检查样式表是否可访问
|
|
7181
|
+
if (originalSheet.cssRules) {
|
|
7182
|
+
// 样式表可访问,html2canvas 会自动处理
|
|
7183
|
+
}
|
|
7184
|
+
}
|
|
7185
|
+
catch (e) {
|
|
7186
|
+
// 跨域样式表无法访问,html2canvas 会尝试其他方式
|
|
7187
|
+
}
|
|
7188
|
+
});
|
|
7189
|
+
},
|
|
7190
|
+
// 性能优化
|
|
7191
|
+
removeContainer: true, // 截图后移除临时容器
|
|
7192
|
+
imageTimeout: this.options.imageLoadTimeout || 5000,
|
|
7193
|
+
// 忽略某些元素(可选,提升性能)
|
|
7194
|
+
ignoreElements: (element) => {
|
|
7195
|
+
// 忽略隐藏元素
|
|
7196
|
+
const style = window.getComputedStyle(element);
|
|
7197
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
7198
|
+
return true;
|
|
7199
|
+
}
|
|
7200
|
+
return false;
|
|
7201
|
+
},
|
|
7202
|
+
};
|
|
7203
|
+
// html2canvas 不支持直接的 proxy 选项,需要通过 onclone 钩子处理图片
|
|
7204
|
+
// 如果配置了代理服务器,在克隆时替换图片 URL
|
|
7205
|
+
if (this.options.useProxy && this.options.proxyUrl && this.options.proxyUrl.trim() !== '') {
|
|
7206
|
+
options.onclone = (clonedDoc) => {
|
|
7207
|
+
// 在克隆的文档中,替换所有跨域图片的 src
|
|
7208
|
+
const images = clonedDoc.querySelectorAll('img');
|
|
7209
|
+
images.forEach((img) => {
|
|
7210
|
+
const originalSrc = img.getAttribute('src');
|
|
7211
|
+
if (!originalSrc)
|
|
7212
|
+
return;
|
|
7213
|
+
// 检查是否是跨域图片
|
|
7214
|
+
try {
|
|
7215
|
+
const imgUrl = new URL(originalSrc, window.location.href);
|
|
7216
|
+
if (imgUrl.origin === window.location.origin) {
|
|
7217
|
+
return; // 同源图片,不需要处理
|
|
7218
|
+
}
|
|
7219
|
+
}
|
|
7220
|
+
catch {
|
|
7221
|
+
// URL 解析失败,可能是相对路径,继续处理
|
|
7222
|
+
}
|
|
7223
|
+
// 检查缓存
|
|
7224
|
+
const cachedDataUrl = this.getCachedImage(originalSrc);
|
|
7225
|
+
if (cachedDataUrl) {
|
|
7226
|
+
img.src = cachedDataUrl;
|
|
7227
|
+
return;
|
|
7228
|
+
}
|
|
7229
|
+
// 对于跨域图片,使用代理 URL
|
|
7230
|
+
// html2canvas 会自动处理,但我们可以预先处理
|
|
7231
|
+
// 注意:html2canvas 会自己处理图片加载,这里主要是为了缓存
|
|
7232
|
+
});
|
|
7233
|
+
};
|
|
7234
|
+
}
|
|
7235
|
+
if (!this.options.silentMode) {
|
|
7236
|
+
console.log(`📸 html2canvas 配置: 尺寸 ${finalWidth}x${finalHeight}, 质量 ${finalQuality.toFixed(2)}, 缩放 ${options.scale}`);
|
|
7237
|
+
}
|
|
7238
|
+
// 执行截图
|
|
7239
|
+
const canvas = await html2canvas(element, options);
|
|
7240
|
+
// 根据输出格式转换
|
|
7241
|
+
let mimeType = 'image/png';
|
|
7242
|
+
let finalQualityForExport = undefined;
|
|
7243
|
+
if (this.options.outputFormat === 'webp' && !isMobile) {
|
|
7244
|
+
try {
|
|
7245
|
+
const testCanvas = document.createElement('canvas');
|
|
7246
|
+
testCanvas.width = 1;
|
|
7247
|
+
testCanvas.height = 1;
|
|
7248
|
+
const testDataUrl = testCanvas.toDataURL('image/webp');
|
|
7249
|
+
if (testDataUrl.indexOf('webp') !== -1) {
|
|
7250
|
+
mimeType = 'image/webp';
|
|
7251
|
+
finalQualityForExport = finalQuality;
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
catch {
|
|
7255
|
+
mimeType = 'image/jpeg';
|
|
7256
|
+
finalQualityForExport = finalQuality;
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
else if (this.options.outputFormat === 'jpeg') {
|
|
7260
|
+
mimeType = 'image/jpeg';
|
|
7261
|
+
finalQualityForExport = finalQuality;
|
|
7262
|
+
}
|
|
7263
|
+
// 转换为 data URL
|
|
7264
|
+
const dataUrl = mimeType === 'image/png'
|
|
7265
|
+
? canvas.toDataURL(mimeType)
|
|
7266
|
+
: canvas.toDataURL(mimeType, finalQualityForExport);
|
|
7267
|
+
// 验证结果
|
|
7268
|
+
if (!dataUrl || dataUrl.length < 100) {
|
|
7269
|
+
throw new Error('生成的截图数据无效或过短');
|
|
7270
|
+
}
|
|
7271
|
+
if (!this.options.silentMode) {
|
|
7272
|
+
console.log(`📸 html2canvas 截图成功!格式: ${this.options.outputFormat}, 尺寸: ${canvas.width}x${canvas.height}`);
|
|
7273
|
+
}
|
|
7274
|
+
return dataUrl;
|
|
7275
|
+
}
|
|
7276
|
+
catch (error) {
|
|
7277
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7278
|
+
if (!this.options.silentMode) {
|
|
7279
|
+
console.error('📸 html2canvas 截图失败:', errorMessage);
|
|
7280
|
+
if (errorMessage.includes('CORS') || errorMessage.includes('cross-origin')) {
|
|
7281
|
+
console.warn('📸 💡 建议:配置 proxyUrl 选项处理跨域图片');
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
throw error;
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7065
7287
|
/**
|
|
7066
7288
|
* 使用 modern-screenshot 截图(启用 Worker)
|
|
7289
|
+
*
|
|
7290
|
+
* 优势:
|
|
7291
|
+
* - 使用 Worker,不阻塞主线程 UI
|
|
7292
|
+
* - 支持并发处理
|
|
7293
|
+
* - 适合复杂页面
|
|
7294
|
+
*
|
|
7295
|
+
* 劣势:
|
|
7296
|
+
* - 处理 SVG 和本地资源较慢(Worker 通信开销)
|
|
7297
|
+
* - 配置相对复杂
|
|
7298
|
+
* - 需要处理 Worker URL
|
|
7299
|
+
*
|
|
7300
|
+
* 适用场景:
|
|
7301
|
+
* - 复杂页面,需要不阻塞 UI
|
|
7302
|
+
* - 需要高质量截图
|
|
7303
|
+
* - 页面资源较少
|
|
7067
7304
|
*/
|
|
7068
7305
|
async takeScreenshotWithModernScreenshot(element) {
|
|
7069
7306
|
if (!this.options.silentMode) {
|
|
@@ -7356,37 +7593,49 @@ class ScreenshotManager {
|
|
|
7356
7593
|
throw new Error('无法获取 canvas context');
|
|
7357
7594
|
}
|
|
7358
7595
|
const img = new Image();
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7596
|
+
let convertedDataUrl;
|
|
7597
|
+
try {
|
|
7598
|
+
await new Promise((resolve, reject) => {
|
|
7599
|
+
img.onload = () => {
|
|
7600
|
+
canvas.width = img.width;
|
|
7601
|
+
canvas.height = img.height;
|
|
7602
|
+
ctx.drawImage(img, 0, 0);
|
|
7603
|
+
resolve();
|
|
7604
|
+
};
|
|
7605
|
+
img.onerror = reject;
|
|
7606
|
+
img.src = dataUrl;
|
|
7607
|
+
});
|
|
7608
|
+
let mimeType = 'image/jpeg';
|
|
7609
|
+
// 使用与 createContext 相同的质量设置
|
|
7610
|
+
let conversionQuality = finalQuality;
|
|
7611
|
+
if (this.options.outputFormat === 'webp' && !isMobile) {
|
|
7612
|
+
try {
|
|
7613
|
+
const testCanvas = document.createElement('canvas');
|
|
7614
|
+
testCanvas.width = 1;
|
|
7615
|
+
testCanvas.height = 1;
|
|
7616
|
+
const testDataUrl = testCanvas.toDataURL('image/webp');
|
|
7617
|
+
if (testDataUrl.indexOf('webp') !== -1) {
|
|
7618
|
+
mimeType = 'image/webp';
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7621
|
+
catch {
|
|
7622
|
+
mimeType = 'image/jpeg';
|
|
7380
7623
|
}
|
|
7381
7624
|
}
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7625
|
+
// 使用优化后的质量进行格式转换
|
|
7626
|
+
convertedDataUrl = mimeType === 'image/png'
|
|
7627
|
+
? canvas.toDataURL(mimeType)
|
|
7628
|
+
: canvas.toDataURL(mimeType, conversionQuality);
|
|
7629
|
+
}
|
|
7630
|
+
finally {
|
|
7631
|
+
// 清理资源,释放内存
|
|
7632
|
+
img.src = ''; // 清除图片引用
|
|
7633
|
+
img.onload = null;
|
|
7634
|
+
img.onerror = null;
|
|
7635
|
+
canvas.width = 0; // 清空 canvas
|
|
7636
|
+
canvas.height = 0;
|
|
7637
|
+
ctx.clearRect(0, 0, 0, 0); // 清除绘制内容
|
|
7385
7638
|
}
|
|
7386
|
-
// 使用优化后的质量进行格式转换
|
|
7387
|
-
const convertedDataUrl = mimeType === 'image/png'
|
|
7388
|
-
? canvas.toDataURL(mimeType)
|
|
7389
|
-
: canvas.toDataURL(mimeType, conversionQuality);
|
|
7390
7639
|
return convertedDataUrl;
|
|
7391
7640
|
}
|
|
7392
7641
|
return dataUrl;
|
|
@@ -7407,15 +7656,34 @@ class ScreenshotManager {
|
|
|
7407
7656
|
throw error;
|
|
7408
7657
|
}
|
|
7409
7658
|
finally {
|
|
7410
|
-
//
|
|
7659
|
+
// 每次截图后立即清理 context,释放 Worker 和内存
|
|
7660
|
+
// 这是防止内存泄漏的关键步骤
|
|
7411
7661
|
if (this.screenshotContext) {
|
|
7412
7662
|
try {
|
|
7413
7663
|
destroyContext(this.screenshotContext);
|
|
7664
|
+
if (!this.options.silentMode) {
|
|
7665
|
+
console.log('📸 ✅ modern-screenshot context 已清理');
|
|
7666
|
+
}
|
|
7414
7667
|
}
|
|
7415
7668
|
catch (e) {
|
|
7416
|
-
|
|
7669
|
+
if (!this.options.silentMode) {
|
|
7670
|
+
console.warn('📸 ⚠️ 清理 context 失败:', e);
|
|
7671
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
finally {
|
|
7674
|
+
// 确保 context 引用被清除
|
|
7675
|
+
this.screenshotContext = null;
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
// 强制触发垃圾回收(如果可能)
|
|
7679
|
+
// 注意:这需要浏览器支持,不是所有浏览器都有效
|
|
7680
|
+
if (typeof window !== 'undefined' && window.gc && typeof window.gc === 'function') {
|
|
7681
|
+
try {
|
|
7682
|
+
window.gc();
|
|
7683
|
+
}
|
|
7684
|
+
catch {
|
|
7685
|
+
// 忽略 GC 错误
|
|
7417
7686
|
}
|
|
7418
|
-
this.screenshotContext = null;
|
|
7419
7687
|
}
|
|
7420
7688
|
}
|
|
7421
7689
|
}
|
|
@@ -7990,15 +8258,42 @@ class ScreenshotManager {
|
|
|
7990
8258
|
}
|
|
7991
8259
|
}
|
|
7992
8260
|
/**
|
|
7993
|
-
* 等待 CSS
|
|
8261
|
+
* 等待 CSS 和字体加载完成
|
|
8262
|
+
* html2canvas 需要确保所有样式表都加载完成才能正确截图
|
|
7994
8263
|
*/
|
|
7995
8264
|
async waitForStylesAndFonts() {
|
|
7996
|
-
//
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8265
|
+
// 等待所有样式表加载完成
|
|
8266
|
+
const styleSheets = Array.from(document.styleSheets);
|
|
8267
|
+
const styleSheetPromises = styleSheets.map((sheet) => {
|
|
8268
|
+
return new Promise((resolve) => {
|
|
8269
|
+
try {
|
|
8270
|
+
// 检查样式表是否已加载
|
|
8271
|
+
// 尝试访问 cssRules,如果成功说明样式表已加载
|
|
8272
|
+
const rules = sheet.cssRules;
|
|
8273
|
+
if (rules) {
|
|
8274
|
+
resolve();
|
|
8275
|
+
}
|
|
8276
|
+
else {
|
|
8277
|
+
// 如果样式表还在加载,等待一下
|
|
8278
|
+
setTimeout(() => resolve(), 100);
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
catch (e) {
|
|
8282
|
+
// 跨域样式表可能无法访问,忽略错误
|
|
8000
8283
|
resolve();
|
|
8001
|
-
}
|
|
8284
|
+
}
|
|
8285
|
+
});
|
|
8286
|
+
});
|
|
8287
|
+
await Promise.all(styleSheetPromises);
|
|
8288
|
+
// 使用 requestAnimationFrame 确保 DOM 已渲染
|
|
8289
|
+
await new Promise((resolve) => {
|
|
8290
|
+
requestAnimationFrame(() => {
|
|
8291
|
+
requestAnimationFrame(() => {
|
|
8292
|
+
// 额外等待,确保样式应用完成
|
|
8293
|
+
setTimeout(() => {
|
|
8294
|
+
resolve();
|
|
8295
|
+
}, 100); // 增加到100ms,确保样式加载
|
|
8296
|
+
});
|
|
8002
8297
|
});
|
|
8003
8298
|
});
|
|
8004
8299
|
}
|
|
@@ -8017,6 +8312,52 @@ class ScreenshotManager {
|
|
|
8017
8312
|
// 忽略错误
|
|
8018
8313
|
}
|
|
8019
8314
|
}
|
|
8315
|
+
/**
|
|
8316
|
+
* 等待所有样式表加载完成(html2canvas 专用)
|
|
8317
|
+
*/
|
|
8318
|
+
async waitForAllStylesLoaded() {
|
|
8319
|
+
const styleSheets = Array.from(document.styleSheets);
|
|
8320
|
+
const promises = [];
|
|
8321
|
+
styleSheets.forEach((sheet) => {
|
|
8322
|
+
promises.push(new Promise((resolve) => {
|
|
8323
|
+
try {
|
|
8324
|
+
// 尝试访问样式表规则,如果成功说明样式表已加载
|
|
8325
|
+
const rules = sheet.cssRules;
|
|
8326
|
+
if (rules) {
|
|
8327
|
+
resolve();
|
|
8328
|
+
}
|
|
8329
|
+
else {
|
|
8330
|
+
// 等待样式表加载
|
|
8331
|
+
const checkInterval = setInterval(() => {
|
|
8332
|
+
try {
|
|
8333
|
+
if (sheet.cssRules) {
|
|
8334
|
+
clearInterval(checkInterval);
|
|
8335
|
+
resolve();
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
catch {
|
|
8339
|
+
// 跨域样式表无法访问,直接 resolve
|
|
8340
|
+
clearInterval(checkInterval);
|
|
8341
|
+
resolve();
|
|
8342
|
+
}
|
|
8343
|
+
}, 50);
|
|
8344
|
+
// 超时保护(5秒)
|
|
8345
|
+
setTimeout(() => {
|
|
8346
|
+
clearInterval(checkInterval);
|
|
8347
|
+
resolve();
|
|
8348
|
+
}, 5000);
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
catch (e) {
|
|
8352
|
+
// 跨域样式表无法访问,直接 resolve
|
|
8353
|
+
resolve();
|
|
8354
|
+
}
|
|
8355
|
+
}));
|
|
8356
|
+
});
|
|
8357
|
+
await Promise.all(promises);
|
|
8358
|
+
// 额外等待,确保样式应用
|
|
8359
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
8360
|
+
}
|
|
8020
8361
|
/**
|
|
8021
8362
|
* 计算压缩后的尺寸
|
|
8022
8363
|
*/
|
|
@@ -8233,6 +8574,11 @@ class ScreenshotManager {
|
|
|
8233
8574
|
}
|
|
8234
8575
|
// 清理图片代理缓存
|
|
8235
8576
|
this.imageProxyCache.clear();
|
|
8577
|
+
// 清理截图历史记录(释放大量内存)
|
|
8578
|
+
this.screenshotHistory.length = 0;
|
|
8579
|
+
// 清理图片下载队列
|
|
8580
|
+
this.imageDownloadQueue.clear();
|
|
8581
|
+
this.activeDownloads.clear();
|
|
8236
8582
|
}
|
|
8237
8583
|
/**
|
|
8238
8584
|
* 获取缓存的图片(检查是否过期)
|
|
@@ -8254,8 +8600,34 @@ class ScreenshotManager {
|
|
|
8254
8600
|
}
|
|
8255
8601
|
/**
|
|
8256
8602
|
* 设置缓存的图片(带时间戳)
|
|
8603
|
+
* 添加内存大小限制,防止缓存无限增长
|
|
8257
8604
|
*/
|
|
8258
8605
|
setCachedImage(url, dataUrl) {
|
|
8606
|
+
// 估算当前缓存总大小(MB)
|
|
8607
|
+
let totalSizeMB = 0;
|
|
8608
|
+
this.imageProxyCache.forEach((cached) => {
|
|
8609
|
+
totalSizeMB += cached.dataUrl.length * 0.75 / (1024 * 1024); // base64 转字节再转MB
|
|
8610
|
+
});
|
|
8611
|
+
// 添加新项的大小
|
|
8612
|
+
const newItemSizeMB = dataUrl.length * 0.75 / (1024 * 1024);
|
|
8613
|
+
const maxCacheSizeMB = 100; // 最大100MB内存缓存
|
|
8614
|
+
// 如果超过限制,清理最旧的缓存
|
|
8615
|
+
if (totalSizeMB + newItemSizeMB > maxCacheSizeMB) {
|
|
8616
|
+
const sortedEntries = Array.from(this.imageProxyCache.entries())
|
|
8617
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp); // 按时间排序
|
|
8618
|
+
let currentSizeMB = totalSizeMB;
|
|
8619
|
+
for (const [key, value] of sortedEntries) {
|
|
8620
|
+
if (currentSizeMB + newItemSizeMB <= maxCacheSizeMB) {
|
|
8621
|
+
break;
|
|
8622
|
+
}
|
|
8623
|
+
const itemSizeMB = value.dataUrl.length * 0.75 / (1024 * 1024);
|
|
8624
|
+
this.imageProxyCache.delete(key);
|
|
8625
|
+
currentSizeMB -= itemSizeMB;
|
|
8626
|
+
if (!this.options.silentMode) {
|
|
8627
|
+
console.log(`📸 清理内存缓存(超过限制): ${key.substring(0, 50)}...`);
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8630
|
+
}
|
|
8259
8631
|
this.imageProxyCache.set(url, {
|
|
8260
8632
|
dataUrl,
|
|
8261
8633
|
timestamp: Date.now()
|
|
@@ -8282,9 +8654,10 @@ class ScreenshotManager {
|
|
|
8282
8654
|
}
|
|
8283
8655
|
/**
|
|
8284
8656
|
* 定期清理过期缓存(可选,在截图时也会自动清理)
|
|
8657
|
+
* 增加清理频率,防止内存积累
|
|
8285
8658
|
*/
|
|
8286
8659
|
startCacheCleanup() {
|
|
8287
|
-
// 每5
|
|
8660
|
+
// 每2分钟清理一次过期缓存(从5分钟改为2分钟,更频繁)
|
|
8288
8661
|
setInterval(() => {
|
|
8289
8662
|
this.cleanExpiredCache();
|
|
8290
8663
|
// 如果启用 IndexedDB,也清理 IndexedDB 缓存
|
|
@@ -8295,9 +8668,12 @@ class ScreenshotManager {
|
|
|
8295
8668
|
}
|
|
8296
8669
|
if (!this.options.silentMode) {
|
|
8297
8670
|
const memoryCacheSize = this.imageProxyCache.size;
|
|
8298
|
-
|
|
8671
|
+
const memoryCacheSizeMB = Array.from(this.imageProxyCache.values())
|
|
8672
|
+
.reduce((sum, cached) => sum + cached.dataUrl.length * 0.75 / (1024 * 1024), 0);
|
|
8673
|
+
const historySizeMB = this.screenshotHistory.reduce((sum, item) => sum + item.length * 0.75 / (1024 * 1024), 0);
|
|
8674
|
+
console.log(`📸 清理过期缓存,内存缓存: ${memoryCacheSize} 项,${memoryCacheSizeMB.toFixed(2)} MB,历史记录: ${historySizeMB.toFixed(2)} MB`);
|
|
8299
8675
|
}
|
|
8300
|
-
},
|
|
8676
|
+
}, 120000); // 2分钟(从5分钟改为2分钟,更频繁清理)
|
|
8301
8677
|
}
|
|
8302
8678
|
/**
|
|
8303
8679
|
* 获取状态
|