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