fix(web): 本地优先保存与 design 页体验修复

- 保存先写本地缓存,登录后可选云端同步;退出登录不再因文件操作跳转登录

- 修复 Layui 遮罩残留、design2 保存后线条、toast 被 finally 提前关闭

- 保存过程恢复 loading spinner;成功提示 2.5 秒

- 云端 payload 瘦身与体积超限提示;后端 schema 迁移与 FileService 容错

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-24 19:24:34 +08:00
parent 4d19d7e814
commit 5fb269cc0a
36 changed files with 894 additions and 358 deletions
+175 -57
View File
@@ -47,6 +47,57 @@
'Accept': 'application/json'
};
var CLOUD_JSON_LIMIT = 16 * 1024 * 1024 - 65536;
var CLOUD_STRIP_KEYS = [
'frontColorPic', 'backColorPic', 'frontBlackPic', 'backBlackPic', 'backDisplayPic'
];
var CLOUD_REGEN_IMAGE_TYPES = { 2: 1, 8: 1, 9: 1 };
function slimCloudFabricSide(data, sideKey, metaKey) {
var canvas = data[sideKey];
var metaList = data[metaKey];
if (!canvas || !Array.isArray(canvas.objects)) return;
canvas.objects.forEach(function (obj, i) {
if (!obj || typeof obj.src !== 'string') return;
var meta = Array.isArray(metaList) && i > 0 ? metaList[i - 1] : null;
var t = meta && meta.type;
if (CLOUD_REGEN_IMAGE_TYPES[t]) delete obj.src;
});
}
function slimCloudPayload(data) {
CLOUD_STRIP_KEYS.forEach(function (key) { delete data[key]; });
var preview = data.frontDisplayPic;
if (typeof preview === 'string' && preview.length > 120000) delete data.frontDisplayPic;
slimCloudFabricSide(data, 'f', 'fo');
slimCloudFabricSide(data, 'b', 'bo');
return data;
}
function formatPayloadTooLargeMessage(sizeBytes) {
var mb = Math.max(0.1, Math.round(sizeBytes / 1024 / 1024 * 10) / 10);
return '设计文件约 ' + mb + 'MB,超过云端上传限制。请减少或压缩自定义图片;'
+ '若仍失败,请让管理员将 MySQL max_allowed_packet 调至 32M 以上。';
}
function prepareCloudJsonStr(jsonStr) {
if (typeof jsonStr !== 'string') jsonStr = String(jsonStr || '{}');
if (jsonStr.length <= CLOUD_JSON_LIMIT) return jsonStr;
var slim = jsonStr;
try {
var data = slimCloudPayload(JSON.parse(jsonStr));
slim = JSON.stringify(data);
if (slim.length <= CLOUD_JSON_LIMIT) return slim;
delete data.frontDisplayPic;
slim = JSON.stringify(data);
if (slim.length <= CLOUD_JSON_LIMIT) return slim;
} catch (e) { /* ignore */ }
var err = new Error(formatPayloadTooLargeMessage(slim.length));
err.code = 'payload_too_large';
err.status = 413;
throw err;
}
function parseCloudRef(p) {
return typeof window.soonParseFileKey === 'function' ? window.soonParseFileKey(p) : null;
}
@@ -83,13 +134,12 @@
var err = new Error(info.message || 'cloud_error');
err.status = info.status;
err.code = info.code;
err.apiNotified = true;
return Promise.reject(err);
}
function requireCloudAuth(actionLabel) {
if (getAccessToken()) return true;
if (typeof window.soonRequireLogin === 'function') return window.soonRequireLogin(actionLabel);
return false;
return !!getAccessToken();
}
function navigateToPage(pathWithQuery) {
@@ -106,21 +156,41 @@
return 'en';
}
function getSystemFonts() {
var list = [
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia',
'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong',
'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB'
];
var WEB_FONT_FALLBACK = [
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia',
'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong',
'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB',
'Helvetica Neue', 'Segoe UI', 'Tahoma', 'STHeiti', 'STSong', 'STKaiti', 'STFangsong'
];
function mergeFontNames() {
var set = {};
WEB_FONT_FALLBACK.forEach(function (name) { set[name] = 1; });
if (typeof document !== 'undefined' && document.fonts && document.fonts.forEach) {
var set = {};
document.fonts.forEach(function (f) {
var name = (f.family || '').replace(/^["']|["']$/g, '');
if (name) set[name] = 1;
});
list = Object.keys(set).length ? Object.keys(set).sort() : list;
}
return Promise.resolve(list);
return Object.keys(set).sort();
}
function getSystemFonts() {
if (typeof window !== 'undefined' && window.queryLocalFonts) {
return window.queryLocalFonts().then(function (fonts) {
var set = {};
WEB_FONT_FALLBACK.forEach(function (name) { set[name] = 1; });
(fonts || []).forEach(function (f) {
var name = (f.fullName || f.family || '').replace(/^["']|["']$/g, '');
if (name) set[name] = 1;
});
var merged = Object.keys(set).sort();
return merged.length ? merged : mergeFontNames();
}).catch(function () {
return mergeFontNames();
});
}
return Promise.resolve(mergeFontNames());
}
function listCloudFiles(pageOrLimit, sizeOrOffset) {
@@ -152,6 +222,11 @@
function createCloudFile(name, jsonStr) {
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
try {
jsonStr = prepareCloudJsonStr(jsonStr);
} catch (e) {
return Promise.reject(e);
}
return authedFetch('files', {
method: 'POST',
headers: JSON_HEADERS,
@@ -197,6 +272,11 @@
function updateCloudFile(id, name, jsonStr, version) {
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
try {
jsonStr = prepareCloudJsonStr(jsonStr);
} catch (e) {
return Promise.reject(e);
}
var body = { name: normalizeSoonName(name), json: jsonStr };
if (version != null) body.version = version;
return authedFetch('files/' + id, {
@@ -511,14 +591,23 @@
var name = input && input.value ? String(input.value).trim() : defaultName;
name = resolveSaveDialogName(name, options, isSoon);
layer.close(index);
if (typeof window.soonCleanupLayerMask === 'function') {
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
}
resolve({ canceled: false, filePath: name, useCloud: !!isSoon });
},
btn2: function (index) {
layer.close(index);
if (typeof window.soonCleanupLayerMask === 'function') {
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
}
resolve({ canceled: true });
},
cancel: function (index) {
layer.close(index);
if (typeof window.soonCleanupLayerMask === 'function') {
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
}
resolve({ canceled: true });
}
});
@@ -563,59 +652,75 @@
return cacheWriteForKey(res.fileKey, meta).then(function () { return res; });
}
if (!getAccessToken()) {
if (cloudRef && cloudRef.id) {
requireCloudAuth('保存');
return Promise.reject(new Error('unauthorized'));
function resolveLocalSaveKey() {
if (name.indexOf('soondesign_session:') === 0) return name;
if (name.indexOf('soondesign_file:') === 0) return name;
if (typeof window.soonMakeSessionKey === 'function') {
return window.soonMakeSessionKey(name);
}
var sessionKey = typeof window.soonMakeSessionKey === 'function'
? window.soonMakeSessionKey(name)
: (name.indexOf('soondesign_session:') === 0
? name
: 'soondesign_session:' + fileName.replace(/\.soon$/i, ''));
var prevSessionKey = name.indexOf('soondesign_session:') === 0 ? name : '';
function finishSessionSave() {
try { sessionStorage.setItem(sessionKey, 'idb'); } catch (e) { /* ignore */ }
return { fileKey: sessionKey, name: fileName, version: 0 };
return 'soondesign_session:' + fileName.replace(/\.soon$/i, '');
}
function localSaveMeta(localKey) {
if (localKey.indexOf('soondesign_file:') === 0) {
return { source: 'cloud', name: fileName, type: parsedJson ? parsedJson.soonType : 1 };
}
if (prevSessionKey && prevSessionKey !== sessionKey &&
typeof window.soonLocalRenameKey === 'function') {
return cacheWriteForKey(sessionKey, { source: 'session', name: fileName }).then(function () {
return window.soonLocalRenameKey(prevSessionKey, sessionKey, { source: 'session', name: fileName })
.then(finishSessionSave);
}).catch(function () {
return Promise.reject(new Error('save_failed'));
return { source: 'session', name: fileName, type: parsedJson ? parsedJson.soonType : 1 };
}
function finishLocalSave(localKey) {
if (localKey.indexOf('soondesign_session:') === 0) {
try { sessionStorage.setItem(localKey, 'idb'); } catch (e) { /* ignore */ }
}
return { fileKey: localKey, name: fileName, version: 0, localOnly: true };
}
function saveLocalFirst() {
var localKey = resolveLocalSaveKey();
var prevKey = (name.indexOf('soondesign_session:') === 0 || name.indexOf('soondesign_file:') === 0) ? name : '';
var meta = localSaveMeta(localKey);
if (prevKey && prevKey !== localKey && typeof window.soonLocalRenameKey === 'function') {
return cacheWriteForKey(localKey, meta).then(function () {
return window.soonLocalRenameKey(prevKey, localKey, meta).then(function () {
return finishLocalSave(localKey);
});
});
}
return cacheWriteForKey(sessionKey, { source: 'session', name: fileName }).then(function () {
return finishSessionSave();
return cacheWriteForKey(localKey, meta).then(function () {
return finishLocalSave(localKey);
});
}
function syncCloudIfAuthed(localResult) {
if (!getAccessToken()) return Promise.resolve(localResult);
if (cloudRef && cloudRef.id) {
var ver = cloudRef.version;
if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) {
ver = window._soonFileMeta.version;
}
var cloudSaveName = fileName;
if (typeof window.soonResolveCloudFileName === 'function') {
var resolved = window.soonResolveCloudFileName(name);
if (resolved) cloudSaveName = resolved;
}
return updateCloudFile(cloudRef.id, cloudSaveName, str, ver).then(function (res) {
return afterCloudSave(res, localResult.fileKey || name);
}).catch(function () {
localResult.syncFailed = true;
return localResult;
});
}
var prevKey = localResult.fileKey || name;
return createCloudFile(fileName, str).then(function (res) {
return afterCloudSave(res, prevKey);
}).catch(function () {
return Promise.reject(new Error('save_failed'));
localResult.syncFailed = true;
return localResult;
});
}
if (!requireCloudAuth('保存')) {
return Promise.reject(new Error('unauthorized'));
}
if (cloudRef && cloudRef.id) {
var ver = cloudRef.version;
if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) {
ver = window._soonFileMeta.version;
}
if (typeof window.soonResolveCloudFileName === 'function') {
var cloudSaveName = window.soonResolveCloudFileName(name);
if (cloudSaveName) fileName = cloudSaveName;
}
var prevCloudKey = name;
return updateCloudFile(cloudRef.id, fileName, str, ver).then(function (res) {
return afterCloudSave(res, prevCloudKey);
});
}
var prevKey = name.indexOf('soondesign_session:') === 0 ? name : '';
return createCloudFile(fileName, str).then(function (res) {
return afterCloudSave(res, prevKey);
return saveLocalFirst().then(syncCloudIfAuthed).catch(function () {
return Promise.reject(new Error('save_failed'));
});
},
readFile: function (pathOrHandle) {
@@ -713,6 +818,11 @@
window.ipcRenderer = {
send: function (ch, a1, a2) {
if (ch === 'get-sys-language') { window._sysLanPending = true; setTimeout(function () { if (window._sysLanCb && window._sysLanPending) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
if (ch === 'get-sys-fonts' && bridge.getSystemFonts) {
bridge.getSystemFonts().then(function (fonts) {
if (typeof window._fontListCb === 'function') window._fontListCb(null, fonts || []);
});
}
if (ch === 'open-first-page' && bridge.openFirstPage) { bridge.openFirstPage(); }
if (ch === 'open-design-page' && bridge.openDesignPage && (a1 !== undefined || a2 !== undefined)) { bridge.openDesignPage(a1 || '', a2 || 1); }
if (ch === 'open-help-file' && bridge.openHelp) { bridge.openHelp(); }
@@ -720,6 +830,14 @@
},
on: function (ch, cb) {
if (ch === 'sys-lan') { window._sysLanCb = cb; if (window._sysLanPending) setTimeout(function () { if (window._sysLanCb) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
if (ch === 'font-list') {
window._fontListCb = cb;
if (bridge.getSystemFonts) {
bridge.getSystemFonts().then(function (fonts) {
if (typeof window._fontListCb === 'function') window._fontListCb(null, fonts || []);
});
}
}
if (ch === 'close') window._closeCb = cb;
}
};