Files
SoonDesign/frontend-web/js/common/soon-local-store.js
T
24kycj 4ce4486b26 Web 端本地缓存、首页与会员弹窗体验优化
新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 11:05:44 +08:00

245 lines
9.0 KiB
JavaScript

(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;
})();