Web 端本地缓存、首页与会员弹窗体验优化

新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-11 11:05:44 +08:00
parent 877fd278c2
commit 4ce4486b26
33 changed files with 3190 additions and 1143 deletions
+118
View File
@@ -38,4 +38,122 @@
var n = String(rel || '').replace(/^\/+/, '');
return window.SOON_JS_BASE + n;
};
/** 等 HTMLImage 解码完成后再 toDataURL / setSrc,避免间歇性空白背景 */
window.soonFabricImageWhenReady = function (image, cb, skipEmbed) {
if (!image) {
if (typeof cb === 'function') cb(null);
return;
}
function finalize() {
if (skipEmbed) {
if (typeof cb === 'function') cb(image);
return;
}
try {
var dataUrl = image.toDataURL();
if (dataUrl && dataUrl.length > 500) {
image.setSrc(dataUrl, function (img) {
if (typeof cb === 'function') cb(img || image);
});
return;
}
} catch (e) { /* ignore */ }
if (typeof cb === 'function') cb(image);
}
var el = image.getElement ? image.getElement() : null;
if (!el) {
finalize();
return;
}
if (el.complete && el.naturalWidth > 0) {
finalize();
return;
}
el.onload = function () { finalize(); };
el.onerror = function () {
if (typeof cb === 'function') cb(null);
};
};
window.soonFabricImageFromAsset = function (assetName, onReady, onFail, retryLeft, skipEmbed) {
if (typeof fabric === 'undefined' || !fabric.Image) {
if (typeof onFail === 'function') onFail();
return;
}
var retries = retryLeft == null ? 2 : retryLeft;
var url = window.soonAsset(assetName);
fabric.Image.fromURL(url, function (image) {
if (!image || !image.width) {
if (retries > 0) {
setTimeout(function () {
window.soonFabricImageFromAsset(assetName, onReady, onFail, retries - 1, skipEmbed);
}, 150);
return;
}
if (typeof onFail === 'function') onFail();
return;
}
function deliver(img) {
if (img && typeof onReady === 'function') onReady(img);
else if (retries > 0) {
setTimeout(function () {
window.soonFabricImageFromAsset(assetName, onReady, onFail, retries - 1, skipEmbed);
}, 150);
} else if (typeof onFail === 'function') onFail();
}
if (skipEmbed) {
window.soonFabricImageWhenReady(image, deliver, true);
return;
}
window.soonFabricImageWhenReady(image, deliver);
}, null, { crossOrigin: 'anonymous' });
};
window.soonRunWhenCanvasReady = function (selector, fn) {
function tryRun() {
var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
var w = el ? (el.clientWidth || el.width || 0) : 0;
if ((!w || w < 50) && window.SOON_DEPLOY_CONFIG) {
requestAnimationFrame(function () {
requestAnimationFrame(tryRun);
});
return;
}
if (typeof fn === 'function') fn(w || 800);
}
tryRun();
};
var SOON_LOCALE_CODES = { zh: 1, ozh: 1, en: 1 };
window.soonNormalizeLocaleCode = function (loc) {
if (!loc) return 'zh';
if (SOON_LOCALE_CODES[loc]) return loc;
var s = String(loc);
if (s.indexOf('zh') === 0) return s.indexOf('TW') >= 0 ? 'ozh' : 'zh';
return 'en';
};
window.soonResolveLocale = function (cb) {
if (typeof cb !== 'function') return;
var stored;
try { stored = localStorage.getItem('lang'); } catch (e) { stored = null; }
if (stored && SOON_LOCALE_CODES[stored]) {
cb(stored);
return;
}
var bridge = window.platformBridge;
if (!bridge || typeof bridge.getLocale !== 'function') {
cb('zh');
return;
}
var ret;
try { ret = bridge.getLocale(); } catch (e) { cb('zh'); return; }
if (ret && typeof ret.then === 'function') {
ret.then(function (loc) { cb(window.soonNormalizeLocaleCode(loc)); }).catch(function () { cb('zh'); });
return;
}
cb(window.soonNormalizeLocaleCode(ret));
};
})();
+305 -25
View File
@@ -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);
+17 -3
View File
@@ -141,6 +141,8 @@
}
_gate.plan = plan;
if (statusEl) statusEl.textContent = '';
var priceEl = modal.querySelector('.soon-activate-price__value');
if (priceEl) priceEl.textContent = '¥' + planPriceDisplay(plan);
updatePayButton(modal);
}).catch(function () {
if (statusEl) statusEl.textContent = '网络错误,请稍后重试';
@@ -205,24 +207,36 @@
try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ }
}
_gate = { index: null, payIndex: null, plan: null, gateOpts: opts };
var width = Math.min(520, window.innerWidth - 24);
var width = Math.min(480, window.innerWidth - 32);
layer.open({
type: 1,
skin: 'soon-layer',
title: false,
closeBtn: 1,
fixed: true,
offset: 'auto',
shade: [0.68, '#000'],
shadeClose: true,
maxWidth: width,
area: [width + 'px', 'auto'],
content: shellHtml(opts.reason, null),
success: function (layero, index) {
var layerEl = layero && layero[0] ? layero[0] : layero;
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--subscribe');
if (layerEl && layerEl.classList) {
layerEl.classList.add('soon-layer--activate');
}
var content = layerEl && layerEl.querySelector ? layerEl.querySelector('.layui-layer-content') : null;
if (content) content.style.padding = '0';
if (content) {
content.style.padding = '0';
content.style.overflow = 'visible';
}
var modal = layerEl.querySelector('.soon-activate-modal');
_gate.index = index;
_gate.gateOpts = opts;
if (modal) bindModal(modal);
requestAnimationFrame(function () {
if (typeof layer.style === 'function') layer.style(index);
});
},
end: function () {
var payIdx = _gate.payIndex;
+31 -17
View File
@@ -37,21 +37,29 @@
}
function shellHtml(reason) {
var mailIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16v12H4z"/><path d="M4 8l8 5 8-5"/></svg>';
var lockIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="10" rx="1"/><path d="M8 11V8a4 4 0 118 0v3"/></svg>';
return '<div class="soon-login-gate">' +
'<header class="soon-subscribe-modal__head">' +
'<span class="soon-subscribe-modal__eyebrow">登录</span>' +
'<h3 class="soon-subscribe-modal__title">登录后继续</h3>' +
'<p class="soon-subscribe-modal__desc">预览内' + esc(reason || '导出或打印') + '需先登录账号。</p>' +
'<header class="soon-login-gate__head">' +
'<span class="soon-login-gate__eyebrow">登录</span>' +
'<h3 class="soon-login-gate__title">登录后继续</h3>' +
'<p class="soon-login-gate__desc">预览内' + esc(reason || '导出或打印') + '成品需先登录账号。</p>' +
'</header>' +
'<form class="soon-login-gate__form" data-role="login-form">' +
'<div class="soon-input-wrap"><input type="email" name="email" required class="soon-input" placeholder="邮箱" autocomplete="email"></div>' +
'<div class="soon-input-wrap"><input type="password" name="password" required class="soon-input" placeholder="密码" autocomplete="current-password"></div>' +
'<p class="soon-login-gate__error" data-role="login-error" style="display:none"></p>' +
'<button type="submit" class="soon-btn soon-btn--block">登录</button>' +
'</form>' +
'<div class="soon-input-wrap soon-login-gate__field">' +
'<span class="soon-input-icon" aria-hidden="true">' + mailIcon + '</span>' +
'<input type="email" name="email" required class="soon-input soon-login-gate__input" placeholder="邮箱" autocomplete="email">' +
'</div>' +
'<div class="soon-input-wrap soon-login-gate__field">' +
'<span class="soon-input-icon" aria-hidden="true">' + lockIcon + '</span>' +
'<input type="password" name="password" required class="soon-input soon-login-gate__input" placeholder="密码" autocomplete="current-password">' +
'</div>' +
'<p class="soon-login-gate__error" data-role="login-error" hidden></p>' +
'<button type="submit" class="soon-btn soon-btn--block soon-login-gate__submit">登录</button>' +
'<p class="soon-login-gate__links">还没有账号?<a href="register.web.html" target="_blank" rel="noopener">立即注册</a></p>' +
'<footer class="soon-subscribe-modal__foot">' +
'<button type="button" class="soon-subscribe-modal__stay" data-action="stay">继续设计</button>' +
'</form>' +
'<footer class="soon-login-gate__foot">' +
'<button type="button" class="soon-login-gate__stay" data-action="stay">继续设计</button>' +
'</footer></div>';
}
@@ -77,7 +85,7 @@
form.addEventListener('submit', function (e) {
e.preventDefault();
if (errEl) {
errEl.style.display = 'none';
errEl.hidden = true;
errEl.textContent = '';
}
var email = (form.email && form.email.value || '').trim();
@@ -90,7 +98,7 @@
if (!j.ok || !j.data || !j.data.access_token) {
if (errEl) {
errEl.textContent = (j && j.message) || '登录失败';
errEl.style.display = 'block';
errEl.hidden = false;
}
return;
}
@@ -109,7 +117,7 @@
}).catch(function () {
if (errEl) {
errEl.textContent = '网络错误,请稍后重试';
errEl.style.display = 'block';
errEl.hidden = false;
}
});
});
@@ -122,20 +130,26 @@
try { layer.close(_gate.index); } catch (e) { /* ignore */ }
}
_gate.opts = opts;
var width = Math.min(420, window.innerWidth - 24);
var width = Math.min(400, window.innerWidth - 32);
layer.open({
type: 1,
skin: 'soon-layer',
title: false,
closeBtn: 1,
shade: [0.62, '#000'],
shadeClose: true,
area: [width + 'px', 'auto'],
offset: 'auto',
content: shellHtml(opts.reason),
success: function (layero, index) {
var layerEl = layero && layero[0] ? layero[0] : layero;
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--subscribe');
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--login-gate');
var content = layerEl && layerEl.querySelector ? layerEl.querySelector('.layui-layer-content') : null;
if (content) content.style.padding = '0';
if (content) {
content.style.padding = '0';
content.style.overflow = 'visible';
content.style.background = 'transparent';
}
var modal = layerEl.querySelector('.soon-login-gate');
_gate.index = index;
_gate.opts = opts;
+12 -2
View File
@@ -473,22 +473,29 @@
displayChannels: channels,
});
var payCtrl = null;
var width = Math.min(480, window.innerWidth - 32);
var idx = layer.open({
type: 1,
skin: 'soon-layer',
title: false,
closeBtn: 1,
fixed: true,
offset: 'auto',
shade: [0.72, '#000'],
shadeClose: !!opts.shadeClose,
area: ['480px'],
maxWidth: width,
area: [width + 'px', 'auto'],
content: html,
success: function (layero) {
success: function (layero, index) {
var layerEl = layero && layero[0] ? layero[0] : layero;
if (layerEl && layerEl.classList) {
layerEl.classList.add('soon-layer--pay');
var layerContent = layerEl.querySelector('.layui-layer-content');
if (layerContent) {
layerContent.style.padding = '0';
layerContent.style.overflow = 'visible';
layerContent.style.maxHeight = 'none';
layerContent.style.background = 'transparent';
}
}
var sheet = layerEl.querySelector('.soon-pay-sheet');
@@ -505,6 +512,9 @@
},
onBack: opts.onBack,
});
requestAnimationFrame(function () {
if (typeof layer.style === 'function') layer.style(index);
});
},
end: function () {
if (payCtrl) payCtrl.destroy();
+35
View File
@@ -19,8 +19,43 @@
}
}
function initClearCache() {
var btn = document.getElementById('auth_clear_cache');
if (!btn) return;
btn.addEventListener('click', function (e) {
e.preventDefault();
if (typeof layui === 'undefined' || typeof window.soonClearLocalDesignCache !== 'function') return;
layui.use('layer', function () {
var layer = layui.layer;
layer.open({
type: 1,
skin: 'soon-layer',
title: '清除本地缓存',
content: '<div class="soon-dialog-body">将清除最近文件与本地缓存,不会退出登录。未上传的本地编辑将丢失。</div>',
btn: ['确定清除', '取消'],
btnAlign: 'r',
area: ['320px', 'auto'],
shadeClose: true,
yes: function (index) {
layer.close(index);
window.soonClearLocalDesignCache().then(function () {
if (typeof window.soonToast === 'function') window.soonToast('已清除本地缓存', 'success');
else layer.msg('已清除本地缓存', { icon: 1, time: 1200 });
setTimeout(function () {
if (/design[12]\.web\.html/i.test(location.pathname || '')) location.href = 'index.web.html';
else location.reload();
}, 400);
});
}
});
});
});
}
function initTopbar(options) {
options = options || {};
initClearCache();
var tok = localStorage.getItem('soon_access') || '';
var login = document.getElementById('auth_login');
var reg = document.getElementById('auth_register');
+2 -1
View File
@@ -37,7 +37,8 @@
function toolsBlock(lang, linksHtml) {
return '<div class="soon-portal-topbar__tools">' +
lang +
(linksHtml ? '<nav class="soon-portal-topbar__links" aria-label="导航">' + linksHtml + '</nav>' : '') +
(linksHtml ? '<nav class="soon-portal-topbar__links" aria-label="导航">' + linksHtml +
'<a href="#" id="auth_clear_cache" class="soon-portal-topbar__link">清除缓存</a></nav>' : '') +
'</div>';
}
+244
View File
@@ -0,0 +1,244 @@
(function () {
'use strict';
var DB_NAME = 'soondesign_local';
var DB_VERSION = 1;
var MAX_BLOBS = 20;
var THUMB_MAX_BYTES = 102400;
var dbPromise = null;
var idbAvailable = typeof indexedDB !== 'undefined';
function soonExtractThumbFromJson(json) {
if (!json || !json.frontDisplayPic || typeof json.frontDisplayPic !== 'string') return '';
var candidate = json.frontDisplayPic.trim();
if (candidate.indexOf('data:image/') !== 0) return '';
if (candidate.length > THUMB_MAX_BYTES) return '';
return candidate;
}
function txDone(tx) {
return new Promise(function (resolve, reject) {
tx.oncomplete = function () { resolve(); };
tx.onerror = function () { reject(tx.error || new Error('idb_tx_error')); };
tx.onabort = function () { reject(tx.error || new Error('idb_tx_abort')); };
});
}
function reqPromise(req) {
return new Promise(function (resolve, reject) {
req.onsuccess = function () { resolve(req.result); };
req.onerror = function () { reject(req.error || new Error('idb_req_error')); };
});
}
function soonLocalOpen() {
if (!idbAvailable) return Promise.resolve(null);
if (dbPromise) return dbPromise;
dbPromise = new Promise(function (resolve, reject) {
var req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = function (e) {
var db = e.target.result;
if (!db.objectStoreNames.contains('blobs')) {
db.createObjectStore('blobs', { keyPath: 'cacheKey' });
}
if (!db.objectStoreNames.contains('thumbs')) {
db.createObjectStore('thumbs', { keyPath: 'cacheKey' });
}
};
req.onsuccess = function () { resolve(req.result); };
req.onerror = function () {
idbAvailable = false;
dbPromise = null;
reject(req.error || new Error('idb_open_failed'));
};
}).catch(function () {
idbAvailable = false;
dbPromise = null;
return null;
});
return dbPromise;
}
function soonLocalGet(cacheKey) {
if (!cacheKey || !idbAvailable) return Promise.resolve(null);
return soonLocalOpen().then(function (db) {
if (!db) return null;
var tx = db.transaction('blobs', 'readonly');
return reqPromise(tx.objectStore('blobs').get(cacheKey)).then(function (row) {
if (!row || row.json == null) return null;
return {
json: row.json,
meta: {
name: row.name,
type: row.type,
source: row.source,
updatedAt: row.updatedAt,
savedAt: row.savedAt,
bytes: row.bytes
}
};
});
}).catch(function () { return null; });
}
function soonLocalEvictLRU(max) {
max = max || MAX_BLOBS;
return soonLocalOpen().then(function (db) {
if (!db) return;
var tx = db.transaction(['blobs', 'thumbs'], 'readwrite');
var blobStore = tx.objectStore('blobs');
return reqPromise(blobStore.getAll()).then(function (rows) {
if (!rows || rows.length <= max) return txDone(tx);
rows.sort(function (a, b) { return (a.savedAt || 0) - (b.savedAt || 0); });
var toRemove = rows.length - max;
for (var i = 0; i < toRemove; i++) {
var k = rows[i].cacheKey;
blobStore.delete(k);
tx.objectStore('thumbs').delete(k);
}
return txDone(tx);
});
}).catch(function () { /* ignore */ });
}
function soonLocalPut(cacheKey, json, meta) {
if (!cacheKey || json == null || !idbAvailable) return Promise.resolve(false);
meta = meta || {};
var jsonObj = typeof json === 'string' ? (function () {
try { return JSON.parse(json); } catch (e) { return null; }
})() : json;
if (!jsonObj) return Promise.resolve(false);
var bytes = 0;
try { bytes = JSON.stringify(jsonObj).length; } catch (e) { bytes = 0; }
var row = {
cacheKey: cacheKey,
json: jsonObj,
name: meta.name || '',
type: meta.type != null ? meta.type : (jsonObj.soonType || 1),
source: meta.source || '',
updatedAt: meta.updatedAt || '',
savedAt: Date.now(),
bytes: bytes
};
return soonLocalOpen().then(function (db) {
if (!db) return false;
var tx = db.transaction('blobs', 'readwrite');
tx.objectStore('blobs').put(row);
return txDone(tx).then(function () {
return soonLocalEvictLRU(MAX_BLOBS).then(function () { return true; });
});
}).catch(function () { return false; });
}
function soonLocalPutThumb(cacheKey, dataUrl) {
if (!cacheKey || !dataUrl || !idbAvailable) return Promise.resolve(false);
if (String(dataUrl).length > THUMB_MAX_BYTES) return Promise.resolve(false);
return soonLocalOpen().then(function (db) {
if (!db) return false;
var tx = db.transaction('thumbs', 'readwrite');
tx.objectStore('thumbs').put({ cacheKey: cacheKey, dataUrl: dataUrl, savedAt: Date.now() });
return txDone(tx).then(function () { return true; });
}).catch(function () { return false; });
}
function soonLocalGetThumb(cacheKey) {
if (!cacheKey || !idbAvailable) return Promise.resolve('');
return soonLocalOpen().then(function (db) {
if (!db) return '';
var tx = db.transaction('thumbs', 'readonly');
return reqPromise(tx.objectStore('thumbs').get(cacheKey)).then(function (row) {
return (row && row.dataUrl) ? row.dataUrl : '';
});
}).catch(function () { return ''; });
}
function soonLocalRemove(cacheKey) {
if (!cacheKey || !idbAvailable) return Promise.resolve();
return soonLocalOpen().then(function (db) {
if (!db) return;
var tx = db.transaction(['blobs', 'thumbs'], 'readwrite');
tx.objectStore('blobs').delete(cacheKey);
tx.objectStore('thumbs').delete(cacheKey);
return txDone(tx);
}).catch(function () { /* ignore */ });
}
function soonLocalRenameKey(oldKey, newKey, meta) {
if (!oldKey || !newKey || oldKey === newKey) return Promise.resolve(false);
return soonLocalGet(oldKey).then(function (hit) {
if (!hit) return false;
return soonLocalPut(newKey, hit.json, Object.assign({}, hit.meta, meta || {})).then(function (ok) {
if (!ok) return false;
return soonLocalGetThumb(oldKey).then(function (thumb) {
var chain = Promise.resolve();
if (thumb) chain = soonLocalPutThumb(newKey, thumb);
return chain.then(function () { return soonLocalRemove(oldKey); }).then(function () { return true; });
});
});
}).catch(function () { return false; });
}
function soonLocalCacheAndThumb(cacheKey, json, meta) {
var thumb = soonExtractThumbFromJson(json);
return soonLocalPut(cacheKey, json, meta).then(function (ok) {
if (!ok) return false;
if (thumb) return soonLocalPutThumb(cacheKey, thumb).then(function () { return true; });
return true;
});
}
function soonLocalIsTemplateStale(cacheKey, blobMeta) {
if (!cacheKey || cacheKey.indexOf('soondesign_template:') !== 0) return false;
var tplMeta = window._soonTemplateMeta;
if (!tplMeta || !tplMeta.updated_at) return false;
if (!blobMeta || !blobMeta.updatedAt) return false;
return String(tplMeta.updated_at) !== String(blobMeta.updatedAt);
}
window.soonLocalOpen = soonLocalOpen;
window.soonLocalGet = soonLocalGet;
window.soonLocalPut = soonLocalPut;
window.soonLocalPutThumb = soonLocalPutThumb;
window.soonLocalGetThumb = soonLocalGetThumb;
window.soonLocalRemove = soonLocalRemove;
window.soonLocalRenameKey = soonLocalRenameKey;
window.soonLocalEvictLRU = soonLocalEvictLRU;
window.soonLocalCacheAndThumb = soonLocalCacheAndThumb;
window.soonLocalIsTemplateStale = soonLocalIsTemplateStale;
window.soonExtractThumbFromJson = soonExtractThumbFromJson;
var LS_DROP = ['soondesign_recent', 'soondesign_history', 'soondesign_recent_migrated'];
var SS_DROP = ['soondesign_open_file', 'soondesign_open_type', 'soondesign_open_meta', 'soondesign_pending_import'];
function dropStorageKeys(storage, prefix) {
if (!storage) return;
try {
var rm = [];
for (var i = 0; i < storage.length; i++) {
var k = storage.key(i);
if (k && k.indexOf(prefix) === 0) rm.push(k);
}
rm.forEach(function (k) { storage.removeItem(k); });
} catch (e) { /* ignore */ }
}
function soonClearLocalDesignCache() {
try {
LS_DROP.forEach(function (k) { localStorage.removeItem(k); });
dropStorageKeys(localStorage, 'soondesign_session:');
SS_DROP.forEach(function (k) { sessionStorage.removeItem(k); });
dropStorageKeys(sessionStorage, 'soondesign_session:');
} catch (e) { /* ignore */ }
dbPromise = null;
if (typeof indexedDB === 'undefined') return Promise.resolve();
return new Promise(function (resolve) {
var done = function () { resolve(); };
try {
var req = indexedDB.deleteDatabase(DB_NAME);
req.onsuccess = req.onerror = req.onblocked = done;
} catch (e2) { done(); }
});
}
window.soonClearLocalDesignCache = soonClearLocalDesignCache;
})();
+217
View File
@@ -0,0 +1,217 @@
(function () {
'use strict';
var RECENT_KEY = 'soondesign_recent';
var MIGRATED_KEY = 'soondesign_recent_migrated';
var HISTORY_KEY = 'soondesign_history';
var MAX_ITEMS = 20;
function nowTimeStr() {
var d = new Date();
function pad(n) { return n < 10 ? '0' + n : String(n); }
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' +
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
function readRaw() {
try {
var raw = localStorage.getItem(RECENT_KEY);
if (!raw) return { version: 1, items: [] };
var j = JSON.parse(raw);
if (!j || !Array.isArray(j.items)) return { version: 1, items: [] };
return j;
} catch (e) {
return { version: 1, items: [] };
}
}
function writeRaw(data) {
try {
localStorage.setItem(RECENT_KEY, JSON.stringify(data));
return true;
} catch (e) {
return false;
}
}
function soonRecentKindFromKey(key) {
if (!key) return 'local';
if (key.indexOf('soondesign_file:') === 0) return 'cloud';
if (key.indexOf('soondesign_template:') === 0) return 'template';
return 'local';
}
function parseFileId(key) {
var m = String(key).match(/^soondesign_file:(\d+)/);
return m ? parseInt(m[1], 10) : null;
}
function soonRecentList() {
return readRaw().items.slice();
}
function soonRecentRemove(key) {
if (!key) return;
var data = readRaw();
data.items = data.items.filter(function (it) { return it.key !== key; });
writeRaw(data);
}
function soonRecentUpsert(item) {
if (!item || !item.key) return;
var key = item.key;
var kind = item.kind || soonRecentKindFromKey(key);
var type = item.type != null ? item.type : 1;
var name = item.name || '';
if (!name && typeof window.soonDisplayFileName === 'function') {
name = window.soonDisplayFileName(key);
}
var entry = {
key: key,
name: name || 'design.soon',
type: type,
kind: kind,
fileId: item.fileId != null ? item.fileId : parseFileId(key),
thumbRef: item.thumbRef || key,
time: item.time || nowTimeStr()
};
var data = readRaw();
data.items = data.items.filter(function (it) { return it.key !== key; });
data.items.unshift(entry);
if (data.items.length > MAX_ITEMS) data.items = data.items.slice(0, MAX_ITEMS);
writeRaw(data);
}
function soonRecentUpsertFromOpen(fileKey, json) {
if (!fileKey) return;
var type = 1;
if (json) {
type = json.soonType ? json.soonType : (json.backBlackPic ? 2 : 1);
}
var name = '';
if (typeof window.soonDisplayFileName === 'function') name = window.soonDisplayFileName(fileKey);
var tplMeta = window._soonTemplateMeta;
if (tplMeta && tplMeta.name && fileKey.indexOf('soondesign_template:') === 0) {
name = tplMeta.name;
if (tplMeta.type) type = tplMeta.type;
}
var fileMeta = window._soonFileMeta;
if (fileMeta && fileMeta.name && fileKey.indexOf('soondesign_file:') === 0) {
name = fileMeta.name;
}
soonRecentUpsert({
key: fileKey,
name: name,
type: type,
kind: soonRecentKindFromKey(fileKey),
fileId: parseFileId(fileKey),
thumbRef: fileKey,
time: nowTimeStr(),
updated_at: (tplMeta && tplMeta.updated_at) ? tplMeta.updated_at : undefined
});
}
function soonRecentMigrateFromHistory() {
try {
if (localStorage.getItem(MIGRATED_KEY) === '1') return;
} catch (e) { return; }
var histRaw;
try {
histRaw = localStorage.getItem(HISTORY_KEY);
} catch (e) { return; }
if (!histRaw) {
try { localStorage.setItem(MIGRATED_KEY, '1'); } catch (e2) { /* ignore */ }
return;
}
var hist;
try {
hist = JSON.parse(histRaw);
} catch (e) {
try { localStorage.setItem(MIGRATED_KEY, '1'); } catch (e2) { /* ignore */ }
return;
}
var list = (hist && Array.isArray(hist.history)) ? hist.history : [];
list.forEach(function (h) {
if (!h || !h.path) return;
soonRecentUpsert({
key: h.path,
name: typeof window.soonDisplayFileName === 'function' ? window.soonDisplayFileName(h.path) : h.path,
type: h.type || 1,
kind: soonRecentKindFromKey(h.path),
fileId: parseFileId(h.path),
thumbRef: h.path,
time: h.time || nowTimeStr()
});
});
try { localStorage.setItem(MIGRATED_KEY, '1'); } catch (e) { /* ignore */ }
}
function soonRecentEnrichFromCloud(items, cloudItems) {
if (!items || !items.length || !cloudItems || !cloudItems.length) return items;
var byId = {};
cloudItems.forEach(function (c) {
if (c && c.id != null) byId[c.id] = c;
});
return items.map(function (it) {
if (!it.fileId || !byId[it.fileId]) return it;
var c = byId[it.fileId];
var copy = Object.assign({}, it);
if (c.name) copy.name = c.name;
var curKey = it.filePath || it.key;
if (c.version != null && typeof window.soonMakeFileKey === 'function') {
var newKey = window.soonMakeFileKey(c.id, c.version);
if (newKey !== curKey) {
soonRecentRemove(curKey);
copy.filePath = newKey;
copy.key = newKey;
copy.thumbRef = newKey;
soonRecentUpsert({
key: newKey,
name: copy.name,
type: copy.type,
kind: soonRecentKindFromKey(newKey),
fileId: c.id,
thumbRef: newKey
});
}
}
return copy;
});
}
function soonRecentOnCloudSave(res, prevKey, type) {
if (!res || !res.fileKey) return;
var dropKeys = {};
if (prevKey && prevKey !== res.fileKey) dropKeys[prevKey] = 1;
if (res.fileId != null) {
readRaw().items.forEach(function (it) {
if (it.fileId === res.fileId && it.key !== res.fileKey) dropKeys[it.key] = 1;
});
}
Object.keys(dropKeys).forEach(function (k) {
soonRecentRemove(k);
if (typeof window.soonLocalRemove === 'function') window.soonLocalRemove(k);
});
var name = res.name || '';
if (!name && typeof window.soonDisplayFileName === 'function') {
name = window.soonDisplayFileName(res.fileKey);
}
soonRecentUpsert({
key: res.fileKey,
name: name || 'design.soon',
type: type != null ? type : 1,
kind: soonRecentKindFromKey(res.fileKey),
fileId: res.fileId,
thumbRef: res.fileKey
});
}
window.soonRecentList = soonRecentList;
window.soonRecentRemove = soonRecentRemove;
window.soonRecentUpsert = soonRecentUpsert;
window.soonRecentUpsertFromOpen = soonRecentUpsertFromOpen;
window.soonRecentMigrateFromHistory = soonRecentMigrateFromHistory;
window.soonRecentKindFromKey = soonRecentKindFromKey;
window.soonRecentEnrichFromCloud = soonRecentEnrichFromCloud;
window.soonRecentOnCloudSave = soonRecentOnCloudSave;
})();