@@ -376,9 +376,11 @@
var pendingFile = null;
var imgNW, imgNH; // 原图像素尺寸
var imgDW, imgDH; // 图片缩放后显示尺寸(zoom=1 基准)
- var zoom = 1; // 图片缩放系数 1.0 ~ 3.0
+ var zoom = 1; // 图片缩放系数
+ var minZoom = 1; // 最小缩放系数(超大图限制选区不超过 3840px)
var panX = 0, panY = 0; // 图片平移偏移量
var fSize, fLeft, fTop; // 裁切框的固定屏幕像素:边长、左、顶
+ var CROP_MAX_PX = 3840; // 选区最大像素限制
// 更新裁切框固定参数(图片加载时计算一次,之后不变)
function initFrame() {
@@ -418,6 +420,11 @@
cropMask.style.top = fTop + 'px';
cropMask.style.width = fSize + 'px';
cropMask.style.height = fSize + 'px';
+
+ // 实时显示选区像素尺寸
+ var p = getCropParams();
+ var infoEl = document.getElementById('cropInfo');
+ if (infoEl) infoEl.textContent = p.size + ' × ' + p.size + ' px';
}
function openCrop(file) {
@@ -431,7 +438,11 @@
var fit = Math.min(sw / imgNW, sh / imgNH);
imgDW = Math.round(imgNW * fit);
imgDH = Math.round(imgNH * fit);
- zoom = 1;
+ // 限制选框最大像素:原图短边超过 3840 时,抬高最小缩放
+ minZoom = Math.min(imgNW, imgNH) > CROP_MAX_PX
+ ? Math.min(imgNW, imgNH) / CROP_MAX_PX
+ : 1;
+ zoom = minZoom;
panX = 0;
panY = 0;
initFrame();
@@ -514,7 +525,7 @@
cropStage.addEventListener('wheel', function (e) {
e.preventDefault();
if (panning) return;
- var newZoom = Math.max(1, Math.min(zoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1), 3));
+ var newZoom = Math.max(minZoom, Math.min(zoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1), 3));
if (newZoom === zoom) return;
zoom = newZoom;
clampPan();
@@ -526,42 +537,80 @@
if (e.target === cropOverlay) closeCrop();
});
+ // 客户端裁剪 + 压缩:用 Canvas 裁出选区,转 JPEG 后上传,大幅节省带宽
+ function cropOnClient(file, x, y, size) {
+ return new Promise(function (resolve) {
+ var url = URL.createObjectURL(file);
+ var img = new Image();
+ img.onload = function () {
+ URL.revokeObjectURL(url);
+ var out = Math.min(size, 2048); // 不超过 2048px,避免 Canvas 内存过大
+ var canvas = document.createElement('canvas');
+ canvas.width = out;
+ canvas.height = out;
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(img, x, y, size, size, 0, 0, out, out);
+ canvas.toBlob(function (blob) {
+ resolve({
+ file: new File([blob], 'avatar.jpg', { type: blob.type || 'image/jpeg' }),
+ cropX: 0,
+ cropY: 0,
+ cropSize: out
+ });
+ }, 'image/jpeg', 0.88);
+ };
+ img.onerror = function () {
+ URL.revokeObjectURL(url);
+ // Canvas 处理失败时回退:直接上传原文件 + 原坐标
+ resolve({ file: file, cropX: x, cropY: y, cropSize: size });
+ };
+ img.src = url;
+ });
+ }
+
cropConfirmBtn.addEventListener('click', function () {
if (!pendingFile) return;
var params = getCropParams();
- var formData = new FormData();
- formData.append('avatar', pendingFile);
- formData.append('crop_x', params.x);
- formData.append('crop_y', params.y);
- formData.append('crop_size', params.size);
cropConfirmBtn.disabled = true;
cropConfirmBtn.textContent = '处理中...';
showSettingsToast('上传中...');
- var xhr = new XMLHttpRequest();
- xhr.open('POST', '/api/settings/avatar', true);
- xhr.onreadystatechange = function () {
- if (xhr.readyState !== 4) return;
+ cropOnClient(pendingFile, params.x, params.y, params.size).then(function (result) {
+ var formData = new FormData();
+ formData.append('avatar', result.file, result.file.name);
+ formData.append('crop_x', result.cropX);
+ formData.append('crop_y', result.cropY);
+ formData.append('crop_size', result.cropSize);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', '/api/settings/avatar', true);
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState !== 4) return;
+ cropConfirmBtn.disabled = false;
+ cropConfirmBtn.textContent = '确认';
+ closeCrop();
+
+ try {
+ var res = JSON.parse(xhr.responseText);
+ if (res.success) {
+ showSettingsToast(res.message || '头像已更新');
+ if (res.data && res.data.url && avatarPreview.tagName === 'IMG') {
+ avatarPreview.src = res.data.url + '?t=' + Date.now();
+ }
+ } else {
+ showSettingsToast(res.message || '上传失败', true);
+ }
+ } catch (err) {
+ showSettingsToast('上传失败', true);
+ }
+ };
+ xhr.send(formData);
+ }).catch(function () {
cropConfirmBtn.disabled = false;
cropConfirmBtn.textContent = '确认';
- closeCrop();
-
- try {
- var res = JSON.parse(xhr.responseText);
- if (res.success) {
- showSettingsToast(res.message || '头像已更新');
- if (res.data && res.data.url && avatarPreview.tagName === 'IMG') {
- avatarPreview.src = res.data.url + '?t=' + Date.now();
- }
- } else {
- showSettingsToast(res.message || '上传失败', true);
- }
- } catch (err) {
- showSettingsToast('上传失败', true);
- }
- };
- xhr.send(formData);
+ showSettingsToast('处理失败,请重试', true);
+ });
});
if (avatarWrap && avatarInput) {
diff --git a/templates/MetaLab-2026/static/css/settings.css b/templates/MetaLab-2026/static/css/settings.css
index 54376e0..cd021f3 100644
--- a/templates/MetaLab-2026/static/css/settings.css
+++ b/templates/MetaLab-2026/static/css/settings.css
@@ -362,6 +362,13 @@ body {
border-bottom: 1px solid var(--color-border);
}
+.crop-info {
+ font-size: .75rem;
+ color: var(--color-text-secondary);
+ font-weight: 400;
+ margin-left: .5rem;
+}
+
.crop-stage {
position: relative;
width: min(85vw, 480px);