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
+217 -73
View File
@@ -186,7 +186,9 @@
}
function downloadCloudFile(id, fileName) {
if (!requireCloudAuth('下载')) return Promise.reject(new Error('unauthorized'));
if (!getAccessToken()) {
return Promise.reject(new Error('unauthorized'));
}
return authedFetch('files/' + id + '/download', { headers: { Accept: 'application/octet-stream' } })
.then(function (response) {
if (!response.ok) {
@@ -202,6 +204,52 @@
});
}
function isSoonSaveDialog(options) {
if (options && options.soonFile === true) return true;
if (options && options.soonFile === false) return false;
var raw = (options && options.defaultPath) ? String(options.defaultPath) : '';
if (!raw) return true;
if (typeof window.soonIsTemplateKey === 'function' && window.soonIsTemplateKey(raw)) return true;
if (raw.indexOf('soondesign_file:') === 0 || raw.indexOf('soondesign_session:') === 0) return true;
var filters = options && options.filters;
if (filters && Array.isArray(filters)) {
for (var i = 0; i < filters.length; i++) {
var exts = filters[i].extensions;
if (exts && exts.indexOf('soon') >= 0) return true;
}
return false;
}
return /\.soon$/i.test(raw.split(/[/\\]/).pop() || '');
}
function genericSaveDialogName(raw) {
var n = String(raw || 'file').trim();
return n.split(/[/\\]/).pop() || n || 'file';
}
function resolveSaveDialogName(name, options, isSoon) {
var n = String(name || '').trim();
if (!n) n = isSoon ? 'design.soon' : 'file';
if (isSoon) {
return typeof window.soonEnsureSoonExt === 'function'
? window.soonEnsureSoonExt(n)
: normalizeSoonName(n);
}
return genericSaveDialogName(n);
}
function defaultSoonSaveDialogName(raw) {
if (!raw || (typeof window.soonIsTemplateKey === 'function' && window.soonIsTemplateKey(raw))) {
return typeof window.soonDefaultNewSoonName === 'function'
? window.soonDefaultNewSoonName()
: normalizeSoonName('design.soon');
}
if (raw.indexOf('soondesign_file:') === 0 && typeof window.soonDisplayFileName === 'function') {
return window.soonDisplayFileName(raw);
}
return normalizeSoonName(raw);
}
var bridge = {
readHistory: function () {
try {
@@ -234,61 +282,121 @@
});
}
var key = typeof pathOrHandle === 'string' ? pathOrHandle : '';
if (key.indexOf('soondesign_session:') === 0) {
try {
var j = sessionStorage.getItem(key);
if (!j && typeof localStorage !== 'undefined') j = localStorage.getItem(key);
return Promise.resolve(j ? JSON.parse(j) : null);
} catch (e) { return Promise.resolve(null); }
}
if (key && key.indexOf('soondesign_file:') === 0 && typeof fetch !== 'undefined') {
var fileMatch = key.match(/^soondesign_file:(\d+)/);
if (fileMatch && getAccessToken()) {
return authedFetch('files/' + fileMatch[1], { headers: { Accept: 'application/json' } })
.then(function (response) {
if (!response.ok) return null;
return response.json();
})
.then(function (payload) {
if (!payload || !payload.ok || !payload.data) return null;
var raw = payload.data.json;
if (typeof raw === 'string') {
try { return JSON.parse(raw); } catch (e) { return null; }
}
return raw && typeof raw === 'object' ? raw : null;
})
.catch(function () { return null; });
var cacheKey = key;
function cacheHitThen(hit) {
if (!hit || !hit.json) return null;
if (typeof window.soonLocalIsTemplateStale === 'function' &&
window.soonLocalIsTemplateStale(cacheKey, hit.meta)) {
if (typeof window.soonLocalRemove === 'function') {
return window.soonLocalRemove(cacheKey).then(function () { return null; });
}
return Promise.resolve(null);
}
if (cacheKey.indexOf('soondesign_file:') === 0 && hit.meta && hit.meta.name &&
typeof window.soonBindFileMeta === 'function') {
window.soonBindFileMeta(cacheKey, { name: hit.meta.name });
}
return Promise.resolve(hit.json);
}
if (key && key.indexOf('soondesign_template:') === 0 && typeof fetch !== 'undefined') {
var tplMatch = key.match(/^soondesign_template:(\d+)/);
if (tplMatch) {
var apiBase = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
if (!apiBase) return Promise.resolve(null);
return fetch(apiBase + '/templates/' + tplMatch[1], { headers: { Accept: 'application/json' } })
.then(function (response) {
if (!response.ok) return null;
return response.json();
})
.then(function (payload) {
if (!payload || !payload.ok || !payload.data) return null;
if (payload.data.id) {
window._soonTemplateMeta = {
id: payload.data.id,
function afterNetworkJson(json, meta) {
if (!json) return null;
if (typeof window.soonLocalCacheAndThumb === 'function') {
return window.soonLocalCacheAndThumb(cacheKey, json, meta || {}).then(function () { return json; });
}
return json;
}
if (key && typeof window.soonLocalGet === 'function') {
return window.soonLocalGet(cacheKey).then(function (hit) {
return cacheHitThen(hit).then(function (cached) {
if (cached) return cached;
return fetchFromNetwork(key, cacheKey);
});
}).catch(function () { return fetchFromNetwork(key, cacheKey); });
}
return fetchFromNetwork(key, cacheKey);
function fetchFromNetwork(key, cacheKey) {
if (key.indexOf('soondesign_session:') === 0) {
if (typeof window.soonLocalGet === 'function') {
return window.soonLocalGet(cacheKey).then(function (hit) {
if (hit && hit.json) return hit.json;
try {
var j = sessionStorage.getItem(key);
if (!j && typeof localStorage !== 'undefined') j = localStorage.getItem(key);
if (!j || j === 'idb') return null;
var parsed = JSON.parse(j);
return afterNetworkJson(parsed, { source: 'session', name: key });
} catch (e) { return null; }
});
}
try {
var j = sessionStorage.getItem(key);
if (!j && typeof localStorage !== 'undefined') j = localStorage.getItem(key);
if (!j || j === 'idb') return Promise.resolve(null);
return Promise.resolve(JSON.parse(j));
} catch (e) { return Promise.resolve(null); }
}
if (key && key.indexOf('soondesign_file:') === 0 && typeof fetch !== 'undefined') {
var fileMatch = key.match(/^soondesign_file:(\d+)/);
if (fileMatch && getAccessToken()) {
return authedFetch('files/' + fileMatch[1], { headers: { Accept: 'application/json' } })
.then(function (response) {
if (!response.ok) return null;
return response.json();
})
.then(function (payload) {
if (!payload || !payload.ok || !payload.data) return null;
var raw = payload.data.json;
var parsed;
if (typeof raw === 'string') {
try { parsed = JSON.parse(raw); } catch (e) { return null; }
} else {
parsed = raw && typeof raw === 'object' ? raw : null;
}
if (!parsed) return null;
if (typeof window.soonApplyCloudMeta === 'function') {
window.soonApplyCloudMeta(payload.data);
}
return afterNetworkJson(parsed, {
source: 'cloud',
name: payload.data.name || '',
type: payload.data.type
};
}
var raw = payload.data.json;
if (typeof raw === 'string') {
try { return JSON.parse(raw); } catch (e) { return null; }
}
return raw && typeof raw === 'object' ? raw : null;
})
.catch(function () { return null; });
type: parsed.soonType || 1,
updatedAt: payload.data.updated_at || ''
});
})
.catch(function () { return null; });
}
}
if (key && key.indexOf('soondesign_template:') === 0 && typeof fetch !== 'undefined') {
var tplMatch = key.match(/^soondesign_template:(\d+)/);
if (tplMatch) {
var apiBase = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
if (!apiBase) return Promise.resolve(null);
return fetch(apiBase + '/templates/' + tplMatch[1] + '/file', { headers: { Accept: 'application/json' } })
.then(function (response) {
if (!response.ok) return null;
return response.text();
})
.then(function (text) {
if (!text) return null;
var parsed;
try { parsed = JSON.parse(text); } catch (e) { return null; }
var tplMeta = window._soonTemplateMeta || {};
return afterNetworkJson(parsed, {
source: 'template',
name: tplMeta.name || '',
type: tplMeta.type || (parsed.soonType || 1),
updatedAt: tplMeta.updated_at || ''
});
})
.catch(function () { return null; });
}
}
return Promise.resolve(null);
}
return Promise.resolve(null);
},
showOpenDialog: function (options) {
return new Promise(function (resolve) {
@@ -327,16 +435,12 @@
},
showSaveDialog: function (options) {
var raw = (options && options.defaultPath) ? options.defaultPath : '';
var defaultName = normalizeSoonName(raw || 'design.soon');
if (typeof window.soonDisplayFileName === 'function' &&
(raw.indexOf('soondesign_file:') === 0 || raw.indexOf('soondesign_template:') === 0)) {
defaultName = window.soonDisplayFileName(raw);
}
var title = (options && options.title) ? options.title : '保存到云端';
var isSoon = isSoonSaveDialog(options);
var defaultName = isSoon ? defaultSoonSaveDialogName(raw) : genericSaveDialogName(raw);
var title = (options && options.title) ? options.title : (isSoon ? '保存到云端' : '保存文件');
return new Promise(function (resolve) {
if (typeof layer === 'undefined' || !layer.open) {
var fpFallback = defaultName;
resolve({ canceled: false, filePath: fpFallback, useCloud: true });
resolve({ canceled: false, filePath: defaultName, useCloud: !!isSoon });
return;
}
var esc = function (s) {
@@ -359,10 +463,9 @@
yes: function (index) {
var input = document.getElementById('soonSaveDialogInput');
var name = input && input.value ? String(input.value).trim() : defaultName;
if (!name) name = defaultName;
name = normalizeSoonName(name);
name = resolveSaveDialogName(name, options, isSoon);
layer.close(index);
resolve({ canceled: false, filePath: name, useCloud: true });
resolve({ canceled: false, filePath: name, useCloud: !!isSoon });
},
btn2: function (index) {
layer.close(index);
@@ -397,21 +500,52 @@
var cloudRef = parseCloudRef(name);
var fileName = normalizeSoonName(name);
var parsedJson;
try { parsedJson = JSON.parse(str); } catch (e) { parsedJson = null; }
function cacheWriteForKey(targetKey, meta) {
if (!parsedJson || typeof window.soonLocalCacheAndThumb !== 'function') return Promise.resolve();
return window.soonLocalCacheAndThumb(targetKey, parsedJson, meta || {});
}
function afterCloudSave(res, prevKey) {
if (!res || !res.fileKey || !parsedJson) return res;
var meta = { source: 'cloud', name: res.name || fileName, type: parsedJson.soonType || 1 };
if (prevKey && prevKey !== res.fileKey && typeof window.soonLocalRenameKey === 'function') {
return window.soonLocalRenameKey(prevKey, res.fileKey, meta).then(function () { return res; });
}
return cacheWriteForKey(res.fileKey, meta).then(function () { return res; });
}
if (!getAccessToken()) {
if (cloudRef && cloudRef.id) {
requireCloudAuth('保存');
return Promise.reject(new Error('unauthorized'));
}
var sessionKey = name.indexOf('soondesign_session:') === 0
? name
: 'soondesign_session:' + fileName.replace(/\.soon$/i, '') + '-' + Date.now();
try {
sessionStorage.setItem(sessionKey, str);
try { localStorage.setItem(sessionKey, str); } catch (e2) { /* ignore quota */ }
return Promise.resolve({ fileKey: sessionKey, name: fileName, version: 0 });
} catch (e) {
return Promise.reject(new Error('save_failed'));
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 };
}
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 cacheWriteForKey(sessionKey, { source: 'session', name: fileName }).then(function () {
return finishSessionSave();
}).catch(function () {
return Promise.reject(new Error('save_failed'));
});
}
if (!requireCloudAuth('保存')) {
@@ -423,10 +557,20 @@
if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) {
ver = window._soonFileMeta.version;
}
return updateCloudFile(cloudRef.id, fileName, str, ver);
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);
});
}
return createCloudFile(fileName, str);
var prevKey = name.indexOf('soondesign_session:') === 0 ? name : '';
return createCloudFile(fileName, str).then(function (res) {
return afterCloudSave(res, prevKey);
});
},
readFile: function (pathOrHandle) {
if (pathOrHandle && pathOrHandle.getFile) {