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:
+175
-57
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user