Web 端本地缓存、首页与会员弹窗体验优化
新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
})();
|
||||
Reference in New Issue
Block a user