Web 端本地缓存、首页与会员弹窗体验优化
新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -39,18 +39,243 @@
|
||||
return FILE_PREFIX + id + ':v' + version;
|
||||
}
|
||||
|
||||
function soonIsTemplateKey(key) {
|
||||
return !!(key && typeof key === 'string' && key.indexOf(TEMPLATE_PREFIX) === 0);
|
||||
}
|
||||
|
||||
function soonNeedsSaveDialog(key) {
|
||||
return !key || soonIsTemplateKey(key);
|
||||
}
|
||||
|
||||
function soonDefaultNewSoonName() {
|
||||
var d = new Date();
|
||||
function pad(n) { return n < 10 ? '0' + n : String(n); }
|
||||
var yy = String(d.getFullYear()).slice(-2);
|
||||
var stamp = yy + pad(d.getMonth() + 1) + pad(d.getDate()) +
|
||||
pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds());
|
||||
return 'design' + stamp + '.soon';
|
||||
}
|
||||
|
||||
function soonEnsureSoonExt(name) {
|
||||
var n = String(name || 'design.soon').trim();
|
||||
if (!/\.soon$/i.test(n)) n = (n.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
return n;
|
||||
}
|
||||
|
||||
function soonSessionSlug(nameOrKey) {
|
||||
var s = String(nameOrKey || 'design').replace(/^soondesign_session:/, '').split(/[/\\]/).pop();
|
||||
s = s.replace(/\.soon$/i, '');
|
||||
s = s.replace(/-\d{10,}$/, '');
|
||||
return s || 'design';
|
||||
}
|
||||
|
||||
function soonMakeSessionKey(nameOrKey) {
|
||||
return 'soondesign_session:' + soonSessionSlug(nameOrKey);
|
||||
}
|
||||
|
||||
function soonSessionDisplayName(keyOrName) {
|
||||
return soonEnsureSoonExt(soonSessionSlug(keyOrName));
|
||||
}
|
||||
|
||||
function soonBuildSavePreviewPic(canvas) {
|
||||
var THUMB_MAX = 102400;
|
||||
if (!canvas || typeof canvas.getObjects !== 'function') return '';
|
||||
var hidden = [];
|
||||
canvas.getObjects().forEach(function (obj) {
|
||||
if (obj.isGuideLine) {
|
||||
hidden.push(obj);
|
||||
obj.visible = false;
|
||||
}
|
||||
});
|
||||
if (hidden.length) canvas.renderAll();
|
||||
var mult = 0.18;
|
||||
var quality = 0.72;
|
||||
var url = '';
|
||||
for (var i = 0; i < 3; i++) {
|
||||
try {
|
||||
var candidate = canvas.toDataURL({ format: 'jpeg', quality: quality, multiplier: mult });
|
||||
if (candidate && candidate.indexOf('data:image/') === 0 && candidate.length <= THUMB_MAX) {
|
||||
url = candidate;
|
||||
break;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
quality -= 0.18;
|
||||
mult -= 0.04;
|
||||
}
|
||||
hidden.forEach(function (obj) { obj.visible = true; });
|
||||
if (hidden.length) canvas.renderAll();
|
||||
return url;
|
||||
}
|
||||
|
||||
function soonApplySavePreview(con_o, canvas) {
|
||||
if (!con_o) return con_o;
|
||||
var pic = soonBuildSavePreviewPic(canvas);
|
||||
if (pic) con_o.frontDisplayPic = pic;
|
||||
return con_o;
|
||||
}
|
||||
|
||||
function soonSaveDialogDefaultPath(openKey) {
|
||||
if (soonNeedsSaveDialog(openKey)) return soonDefaultNewSoonName();
|
||||
return openKey || undefined;
|
||||
}
|
||||
|
||||
function soonEnsureSaveFileName(fp) {
|
||||
var name = String(fp || 'design.soon');
|
||||
if (typeof window !== 'undefined' && window.platformBridge && window.fs == null) {
|
||||
return soonEnsureSoonExt(name);
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.path && window.path.extname) {
|
||||
if (window.path.extname(name) !== '.soon') {
|
||||
name = (name.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
}
|
||||
} else if (!/\.soon$/i.test(name)) {
|
||||
name = (name.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
}
|
||||
return soonEnsureSoonExt(name);
|
||||
}
|
||||
|
||||
function soonReportSaveError(err) {
|
||||
if (typeof window.soonShowApiError === 'function') {
|
||||
window.soonShowApiError({
|
||||
status: err && err.status,
|
||||
message: (err && err.message) || '保存失败',
|
||||
code: err && err.code
|
||||
});
|
||||
} else {
|
||||
soonToast((err && err.message) || '保存失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function soonWriteSoonContentWeb(fp, content, prevOpenKey, onDone, onError) {
|
||||
if (!window.platformBridge || !window.platformBridge.writeFile) return false;
|
||||
window.platformBridge.writeFile(fp, content).then(function (res) {
|
||||
if (typeof onDone === 'function') onDone(res, prevOpenKey);
|
||||
}).catch(function (err) {
|
||||
if (typeof onError === 'function') onError(err);
|
||||
else soonReportSaveError(err);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function soonDownloadBlob(filename, content, mime) {
|
||||
var name = soonEnsureSoonExt(filename || 'design.soon');
|
||||
var blob = content instanceof Blob
|
||||
? content
|
||||
: new Blob([content], { type: mime || 'application/json' });
|
||||
var a = document.createElement('a');
|
||||
a.download = name;
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
function soonDownloadRecentItem(item) {
|
||||
if (!item) return Promise.resolve();
|
||||
var key = item.filePath || item.key || '';
|
||||
var name = soonEnsureSoonExt(item.name || 'design.soon');
|
||||
var kind = item.kind || (typeof window.soonRecentKindFromKey === 'function'
|
||||
? window.soonRecentKindFromKey(key) : 'local');
|
||||
|
||||
if (kind === 'template') {
|
||||
var tpl = soonParseTemplateKey(key);
|
||||
if (!tpl) {
|
||||
soonToast('无法下载模板', 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
|
||||
if (!base) {
|
||||
soonToast('下载地址未配置', 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return fetch(base + '/templates/' + tpl.id + '/file', { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('download_failed');
|
||||
return r.text();
|
||||
})
|
||||
.then(function (text) {
|
||||
soonDownloadBlob(name, text, 'application/json');
|
||||
})
|
||||
.catch(function () {
|
||||
soonToast('模板下载失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
if (kind === 'cloud') {
|
||||
function tryCloudApiDownload() {
|
||||
if (!item.fileId || !soonGetAccessToken()) return Promise.resolve(false);
|
||||
var bridge = window.platformBridge;
|
||||
if (!bridge || typeof bridge.downloadCloudFile !== 'function') return Promise.resolve(false);
|
||||
return bridge.downloadCloudFile(item.fileId, name).then(function () { return true; }).catch(function () {
|
||||
soonToast('下载失败', 'error');
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (key && typeof window.soonLocalGet === 'function') {
|
||||
return window.soonLocalGet(key).then(function (hit) {
|
||||
if (hit && hit.json) {
|
||||
soonDownloadBlob(name, JSON.stringify(hit.json), 'application/json');
|
||||
return;
|
||||
}
|
||||
return tryCloudApiDownload().then(function (done) {
|
||||
if (!done) soonToast('请先打开或保存该文件', 'warn');
|
||||
});
|
||||
});
|
||||
}
|
||||
return tryCloudApiDownload().then(function (done) {
|
||||
if (!done) soonToast('请先打开或保存该文件', 'warn');
|
||||
});
|
||||
}
|
||||
|
||||
if (key.indexOf('soondesign_session:') === 0 || kind === 'local') {
|
||||
if (typeof window.soonLocalGet !== 'function') {
|
||||
soonToast('本地缓存不可用', 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return window.soonLocalGet(key).then(function (hit) {
|
||||
if (!hit || !hit.json) {
|
||||
soonToast('请先打开或保存该文件', 'warn');
|
||||
return;
|
||||
}
|
||||
soonDownloadBlob(name, JSON.stringify(hit.json), 'application/json');
|
||||
});
|
||||
}
|
||||
|
||||
soonToast('无法下载该文件', 'warn');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function soonLookupRecentCloudName(fileId) {
|
||||
if (!fileId || typeof window.soonRecentList !== 'function') return '';
|
||||
var items = window.soonRecentList();
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].fileId === fileId && items[i].name) return items[i].name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function soonResolveCloudFileName(fileKey) {
|
||||
if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) return '';
|
||||
var parsed = soonParseFileKey(fileKey);
|
||||
if (!parsed) return '';
|
||||
var meta = window._soonFileMeta;
|
||||
if (meta && meta.id === parsed.id && meta.name) return meta.name;
|
||||
var recentName = soonLookupRecentCloudName(parsed.id);
|
||||
if (recentName) return recentName;
|
||||
return '';
|
||||
}
|
||||
|
||||
function soonNormalizeSoonName(pathOrName) {
|
||||
var n = String(pathOrName || 'design.soon');
|
||||
if (n.indexOf(FILE_PREFIX) === 0) {
|
||||
var meta = window._soonFileMeta;
|
||||
return (meta && meta.name) ? meta.name : 'design.soon';
|
||||
var resolved = soonResolveCloudFileName(n);
|
||||
if (resolved) return resolved;
|
||||
return 'design.soon';
|
||||
}
|
||||
if (n.indexOf(TEMPLATE_PREFIX) === 0) {
|
||||
var tpl = soonParseTemplateKey(n);
|
||||
return tpl ? ('template-' + tpl.id + '.soon') : 'template.soon';
|
||||
return soonDefaultNewSoonName();
|
||||
}
|
||||
if (n.indexOf('soondesign_session:') === 0) {
|
||||
n = n.replace(/^soondesign_session:/, '');
|
||||
return soonSessionDisplayName(n);
|
||||
}
|
||||
n = n.split(/[/\\]/).pop();
|
||||
if (!/\.soon$/i.test(n)) n = (n.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
@@ -128,10 +353,12 @@
|
||||
var parsed = soonParseFileKey(fileKey);
|
||||
if (!parsed) return;
|
||||
var prev = window._soonFileMeta || {};
|
||||
var name = (opts && opts.name) || (prev.id === parsed.id ? prev.name : '') || '';
|
||||
if (!name) name = soonLookupRecentCloudName(parsed.id);
|
||||
window._soonFileMeta = {
|
||||
id: parsed.id,
|
||||
version: parsed.version != null ? parsed.version : prev.version,
|
||||
name: (opts && opts.name) || (prev.id === parsed.id ? prev.name : '') || ''
|
||||
name: name
|
||||
};
|
||||
}
|
||||
|
||||
@@ -301,11 +528,15 @@
|
||||
|
||||
function soonPutSoonSession(j, fileName) {
|
||||
if (!j) return '';
|
||||
var hint = (fileName || 'design').replace(/\.soon$/i, '');
|
||||
var key = 'soondesign_session:' + hint + '-' + Date.now();
|
||||
var displayName = soonEnsureSoonExt(fileName || 'design.soon');
|
||||
var key = soonMakeSessionKey(displayName);
|
||||
if (typeof window.soonLocalCacheAndThumb === 'function') {
|
||||
window.soonLocalCacheAndThumb(key, j, { source: 'session', name: displayName });
|
||||
try { sessionStorage.setItem(key, 'idb'); } catch (e) { /* ignore */ }
|
||||
return key;
|
||||
}
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(j));
|
||||
try { localStorage.setItem(key, JSON.stringify(j)); } catch (e2) { /* ignore quota */ }
|
||||
return key;
|
||||
} catch (e) {
|
||||
return '';
|
||||
@@ -318,6 +549,9 @@
|
||||
soonToast('无法打开文件(存储空间不足)', 'error');
|
||||
return '';
|
||||
}
|
||||
if (typeof window.soonRecentUpsertFromOpen === 'function') {
|
||||
window.soonRecentUpsertFromOpen(key, j);
|
||||
}
|
||||
soonScheduleCloudImport(key, fileName);
|
||||
var type = soonSoonTypeFromJson(j);
|
||||
if (window.platformBridge && window.platformBridge.openDesignPage) {
|
||||
@@ -369,25 +603,55 @@
|
||||
if (!pending || pending.sessionKey !== currentFileKey) return;
|
||||
try { sessionStorage.removeItem(PENDING_IMPORT_KEY); } catch (e) { /* ignore */ }
|
||||
|
||||
var jsonRaw;
|
||||
try {
|
||||
jsonRaw = sessionStorage.getItem(currentFileKey);
|
||||
if (!jsonRaw && typeof localStorage !== 'undefined') jsonRaw = localStorage.getItem(currentFileKey);
|
||||
} catch (e) {
|
||||
return;
|
||||
function loadJsonForImport() {
|
||||
if (typeof window.soonLocalGet === 'function') {
|
||||
return window.soonLocalGet(currentFileKey).then(function (hit) {
|
||||
if (hit && hit.json) return hit.json;
|
||||
try {
|
||||
var raw = sessionStorage.getItem(currentFileKey);
|
||||
if (raw && raw !== 'idb') return JSON.parse(raw);
|
||||
} catch (e) { /* ignore */ }
|
||||
return null;
|
||||
});
|
||||
}
|
||||
try {
|
||||
var raw = sessionStorage.getItem(currentFileKey);
|
||||
if (raw && raw !== 'idb') return JSON.parse(raw);
|
||||
} catch (e) { /* ignore */ }
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (!jsonRaw) return;
|
||||
|
||||
var bridge = window.platformBridge;
|
||||
if (!bridge || typeof bridge.importSoonFile !== 'function') return;
|
||||
|
||||
bridge.importSoonFile(pending.name || 'design.soon', jsonRaw).then(function (res) {
|
||||
if (res && res.fileKey && typeof window.openAs !== 'undefined' && window.openAs) {
|
||||
window.openAs.name = res.fileKey;
|
||||
}
|
||||
loadJsonForImport().then(function (jsonObj) {
|
||||
if (!jsonObj) return;
|
||||
var oldKey = currentFileKey;
|
||||
return bridge.importSoonFile(pending.name || 'design.soon', jsonObj).then(function (res) {
|
||||
if (res && res.fileKey && typeof window.openAs !== 'undefined' && window.openAs) {
|
||||
window.openAs.name = res.fileKey;
|
||||
}
|
||||
if (res && res.fileKey && typeof window.soonLocalRenameKey === 'function') {
|
||||
window.soonLocalRenameKey(oldKey, res.fileKey, { source: 'cloud', name: res.name || pending.name });
|
||||
}
|
||||
try {
|
||||
sessionStorage.removeItem(oldKey);
|
||||
localStorage.removeItem(oldKey);
|
||||
} catch (e) { /* ignore */ }
|
||||
if (typeof window.soonRecentOnCloudSave === 'function') {
|
||||
window.soonRecentOnCloudSave(res, oldKey, soonSoonTypeFromJson(jsonObj));
|
||||
}
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (err && err.status) return;
|
||||
soonToast((err && err.message) ? err.message : '云端登记失败,可稍后保存重试', 'warn');
|
||||
if (typeof window.soonShowApiError === 'function') {
|
||||
window.soonShowApiError({
|
||||
status: err && err.status,
|
||||
message: (err && err.message) || '云端登记失败,可稍后保存重试',
|
||||
code: err && err.code
|
||||
});
|
||||
} else {
|
||||
soonToast((err && err.message) || '云端登记失败,可稍后保存重试', 'warn');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -462,13 +726,13 @@
|
||||
return tplParsed ? ('模板 #' + tplParsed.id) : pathOrKey;
|
||||
}
|
||||
if (pathOrKey.indexOf(FILE_PREFIX) === 0) {
|
||||
var meta = window._soonFileMeta;
|
||||
if (meta && meta.name) return meta.name;
|
||||
var resolved = soonResolveCloudFileName(pathOrKey);
|
||||
if (resolved) return resolved;
|
||||
var parsed = soonParseFileKey(pathOrKey);
|
||||
return parsed ? ('文件 #' + parsed.id) : pathOrKey;
|
||||
}
|
||||
if (pathOrKey.indexOf('soondesign_session:') === 0) {
|
||||
return pathOrKey.replace(/^soondesign_session:/, '');
|
||||
return soonSessionDisplayName(pathOrKey);
|
||||
}
|
||||
return String(pathOrKey).split(/[/\\]/).pop();
|
||||
}
|
||||
@@ -503,6 +767,7 @@
|
||||
window.soonParseFileKey = soonParseFileKey;
|
||||
window.soonMakeFileKey = soonMakeFileKey;
|
||||
window.soonNormalizeSoonName = soonNormalizeSoonName;
|
||||
window.soonResolveCloudFileName = soonResolveCloudFileName;
|
||||
window.soonApplyCloudMeta = soonApplyCloudMeta;
|
||||
window.soonBindFileMeta = soonBindFileMeta;
|
||||
window.soonSyncOpenNavigation = soonSyncOpenNavigation;
|
||||
@@ -529,6 +794,21 @@
|
||||
window.soonParseApiError = soonParseApiError;
|
||||
window.soonShowApiError = soonShowApiError;
|
||||
window.soonDisplayFileName = soonDisplayFileName;
|
||||
window.soonIsTemplateKey = soonIsTemplateKey;
|
||||
window.soonNeedsSaveDialog = soonNeedsSaveDialog;
|
||||
window.soonDefaultNewSoonName = soonDefaultNewSoonName;
|
||||
window.soonEnsureSoonExt = soonEnsureSoonExt;
|
||||
window.soonSessionSlug = soonSessionSlug;
|
||||
window.soonMakeSessionKey = soonMakeSessionKey;
|
||||
window.soonSessionDisplayName = soonSessionDisplayName;
|
||||
window.soonBuildSavePreviewPic = soonBuildSavePreviewPic;
|
||||
window.soonApplySavePreview = soonApplySavePreview;
|
||||
window.soonSaveDialogDefaultPath = soonSaveDialogDefaultPath;
|
||||
window.soonEnsureSaveFileName = soonEnsureSaveFileName;
|
||||
window.soonReportSaveError = soonReportSaveError;
|
||||
window.soonWriteSoonContentWeb = soonWriteSoonContentWeb;
|
||||
window.soonDownloadBlob = soonDownloadBlob;
|
||||
window.soonDownloadRecentItem = soonDownloadRecentItem;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', soonHandlePayReturn);
|
||||
|
||||
Reference in New Issue
Block a user