Files
SoonDesign/frontend-web/js/platform/web.js
T
24kycj 88c6ce8ccc 重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 18:17:39 +08:00

462 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
'use strict';
var HISTORY_KEY = 'soondesign_history';
var VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '3.2.101';
function getAppBasePathname() {
var p = (typeof window !== 'undefined' && window.location && window.location.pathname) ? window.location.pathname : '/';
while (p.length > 1 && p.endsWith('/')) {
p = p.slice(0, -1);
}
if (/\.web\.html$/i.test(p) || /\.html$/i.test(p)) {
p = p.replace(/[^/]+$/, '');
}
if (!p.endsWith('/')) p += '/';
return p;
}
var SERVER_API_BASE = (function () {
var cfg = (typeof window !== 'undefined' && window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base);
if (cfg) return String(cfg).replace(/\/+$/, '') + '/';
if (typeof window === 'undefined' || !window.location) return '/api/v1/';
return window.location.origin + getAppBasePathname() + 'api/v1/';
})();
function getAccessToken() {
if (typeof window.soonGetAccessToken === 'function') return window.soonGetAccessToken();
try { return localStorage.getItem('soon_access') || ''; } catch (e) { return ''; }
}
function authedFetch(path, opts) {
var url = SERVER_API_BASE + String(path).replace(/^\/+/, '');
if (typeof window.soonAuthedFetch === 'function') {
return window.soonAuthedFetch(url, opts);
}
opts = opts || {};
opts.headers = Object.assign({}, opts.headers || {});
if (!opts.headers['Authorization'] && !opts.headers['authorization']) {
var t = getAccessToken();
if (t) opts.headers['Authorization'] = 'Bearer ' + t;
}
return fetch(url, opts);
}
var JSON_HEADERS = {
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json'
};
function parseCloudRef(p) {
return typeof window.soonParseFileKey === 'function' ? window.soonParseFileKey(p) : null;
}
function normalizeSoonName(p) {
return typeof window.soonNormalizeSoonName === 'function'
? window.soonNormalizeSoonName(p)
: String(p || 'design.soon');
}
function packCloudResult(j) {
if (!j || !j.ok || !j.data) {
return Promise.reject(new Error('cloud_empty_response'));
}
var fileKey = typeof window.soonApplyCloudMeta === 'function'
? window.soonApplyCloudMeta(j.data)
: null;
if (!fileKey) {
var d = j.data;
window._soonFileMeta = { id: d.id, version: d.version, name: d.name };
fileKey = 'soondesign_file:' + d.id + ':v' + d.version;
}
return { fileId: j.data.id, version: j.data.version, fileKey: fileKey, name: j.data.name };
}
function handleCloudResponse(response, bodyText) {
if (response.ok) {
try { return Promise.resolve(JSON.parse(bodyText)); } catch (e) { return Promise.resolve(null); }
}
var info = typeof window.soonParseApiError === 'function'
? window.soonParseApiError(response, bodyText)
: { status: response.status, message: '操作失败' };
if (typeof window.soonShowApiError === 'function') window.soonShowApiError(info);
var err = new Error(info.message || 'cloud_error');
err.status = info.status;
err.code = info.code;
return Promise.reject(err);
}
function requireCloudAuth(actionLabel) {
if (getAccessToken()) return true;
if (typeof window.soonRequireLogin === 'function') return window.soonRequireLogin(actionLabel);
return false;
}
function navigateToPage(pathWithQuery) {
try {
location.href = new URL(pathWithQuery, window.location.href).href;
} catch (e) {
location.href = pathWithQuery;
}
}
function getLocale() {
var lang = typeof navigator !== 'undefined' ? (navigator.language || navigator.browserLanguage || '') : '';
if (lang.indexOf('zh') === 0) return lang.indexOf('TW') >= 0 ? 'ozh' : 'zh';
return 'en';
}
function getSystemFonts() {
var list = [
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia',
'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong',
'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB'
];
if (typeof document !== 'undefined' && document.fonts && document.fonts.forEach) {
var set = {};
document.fonts.forEach(function (f) {
var name = (f.family || '').replace(/^["']|["']$/g, '');
if (name) set[name] = 1;
});
list = Object.keys(set).length ? Object.keys(set).sort() : list;
}
return Promise.resolve(list);
}
function listCloudFiles(pageOrLimit, sizeOrOffset) {
if (!requireCloudAuth('查看文件')) return Promise.reject(new Error('unauthorized'));
var url;
var fallback = { items: [], total: 0 };
if (typeof pageOrLimit === 'object' && pageOrLimit) {
var page = pageOrLimit.page || 1;
var size = pageOrLimit.size || 12;
url = 'files?page=' + page + '&size=' + size;
fallback = { items: [], total: 0, page: page, size: size };
} else {
var lim = pageOrLimit || 50;
var off = sizeOrOffset || 0;
url = 'files?limit=' + lim + '&offset=' + off;
fallback = { items: [], limit: lim, offset: off, total: 0 };
}
return authedFetch(url, { headers: { Accept: 'application/json' } })
.then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); })
.then(function (j) { return (j && j.ok && j.data) ? j.data : fallback; });
}
function createCloudFile(name, jsonStr) {
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('保存')) {
return Promise.reject(new Error('membership_required'));
}
return authedFetch('files', {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({ name: normalizeSoonName(name), json: jsonStr })
})
.then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); })
.then(packCloudResult);
}
function updateCloudFile(id, name, jsonStr, version) {
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('保存')) {
return Promise.reject(new Error('membership_required'));
}
var body = { name: normalizeSoonName(name), json: jsonStr };
if (version != null) body.version = version;
return authedFetch('files/' + id, {
method: 'PUT',
headers: JSON_HEADERS,
body: JSON.stringify(body)
})
.then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); })
.then(packCloudResult);
}
function deleteCloudFile(id) {
if (!requireCloudAuth('删除')) return Promise.reject(new Error('unauthorized'));
return authedFetch('files/' + id, { method: 'DELETE', headers: { Accept: 'application/json' } })
.then(function (r) { return r.text().then(function (t) { return handleCloudResponse(r, t); }); });
}
function importSoonFile(name, jsonObj) {
var jsonStr = typeof jsonObj === 'string' ? jsonObj : JSON.stringify(jsonObj);
return createCloudFile(name, jsonStr);
}
function downloadCloudFile(id, fileName) {
if (!requireCloudAuth('下载')) return Promise.reject(new Error('unauthorized'));
if (typeof window.soonRequireMember === 'function' && !window.soonRequireMember('下载')) {
return Promise.reject(new Error('membership_required'));
}
return authedFetch('files/' + id + '/download', { headers: { Accept: 'application/octet-stream' } })
.then(function (response) {
if (!response.ok) {
return response.text().then(function (t) { return handleCloudResponse(response, t); });
}
return response.blob().then(function (blob) {
var a = document.createElement('a');
a.download = normalizeSoonName(fileName || 'design.soon');
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href);
});
});
}
var bridge = {
readHistory: function () {
try {
var raw = localStorage.getItem(HISTORY_KEY);
return Promise.resolve(raw ? JSON.parse(raw) : { history: [] });
} catch (e) {
return Promise.resolve({ history: [] });
}
},
writeHistory: function (data) {
try {
localStorage.setItem(HISTORY_KEY, JSON.stringify(data));
return Promise.resolve({ success: true });
} catch (e) {
return Promise.resolve({ success: false, error: e.message });
}
},
listCloudFiles: listCloudFiles,
createCloudFile: createCloudFile,
updateCloudFile: updateCloudFile,
deleteCloudFile: deleteCloudFile,
importSoonFile: importSoonFile,
downloadCloudFile: downloadCloudFile,
requireCloudAuth: requireCloudAuth,
readJsonFile: function (pathOrHandle) {
if (!pathOrHandle) return Promise.resolve(null);
if (typeof pathOrHandle === 'object' && pathOrHandle.text) {
return pathOrHandle.text().then(function (t) {
try { return JSON.parse(t); } catch (e) { return null; }
});
}
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] + '/download', { headers: { Accept: 'application/json' } })
.then(function (response) {
if (!response.ok) return null;
return response.text().then(function (text) {
try { return JSON.parse(text); } catch (e) { return null; }
});
})
.catch(function () { return null; });
}
}
return Promise.resolve(null);
},
showOpenDialog: function (options) {
return new Promise(function (resolve) {
var input = document.createElement('input');
input.type = 'file';
if (options && options.filters && Array.isArray(options.filters) && options.filters.length > 0) {
var acceptList = [];
options.filters.forEach(function (filter) {
if (filter.extensions && Array.isArray(filter.extensions)) {
filter.extensions.forEach(function (ext) {
var cleanExt = ext.replace(/^\./, '');
if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'].indexOf(cleanExt.toLowerCase()) >= 0) {
var mimeType = 'image/' + (cleanExt.toLowerCase() === 'jpg' ? 'jpeg' : cleanExt.toLowerCase());
if (acceptList.indexOf(mimeType) < 0) acceptList.push(mimeType);
} else {
var extWithDot = '.' + cleanExt;
if (acceptList.indexOf(extWithDot) < 0) acceptList.push(extWithDot);
}
});
}
});
input.accept = acceptList.length > 0 ? acceptList.join(',') : '';
} else {
input.accept = '.soon,application/json';
}
input.style.display = 'none';
input.onchange = function () {
var f = input.files && input.files[0];
document.body.removeChild(input);
if (!f) resolve({ canceled: true });
else resolve({ canceled: false, filePaths: [], files: [f], file: f });
};
document.body.appendChild(input);
input.click();
});
},
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) {
defaultName = window.soonDisplayFileName(raw);
}
return new Promise(function (resolve) {
var name = typeof prompt === 'function' ? prompt('保存为文件名(如 xxx.soon', defaultName) : defaultName;
if (name === null) {
resolve({ canceled: true });
return;
}
var fp = (name && String(name).trim()) ? String(name).trim() : defaultName;
fp = normalizeSoonName(fp);
resolve({ canceled: false, filePath: fp, useCloud: true });
});
},
writeFile: function (pathOrHandle, content) {
var isBlob = content instanceof Blob;
var str = typeof content === 'string' ? content : (isBlob ? null : (content && content.toString ? content.toString() : ''));
if (isBlob) {
var blobName = typeof pathOrHandle === 'string' ? pathOrHandle : 'output.bin';
var a = document.createElement('a');
a.download = blobName;
a.href = URL.createObjectURL(content);
a.click();
URL.revokeObjectURL(a.href);
return Promise.resolve();
}
if (pathOrHandle && pathOrHandle.createWritable) {
pathOrHandle = typeof pathOrHandle.name === 'string' ? pathOrHandle.name : 'design.soon';
}
if (!requireCloudAuth('保存')) {
return Promise.reject(new Error('unauthorized'));
}
var name = typeof pathOrHandle === 'string' ? pathOrHandle : 'design.soon';
var cloudRef = parseCloudRef(name);
var fileName = normalizeSoonName(name);
if (cloudRef && cloudRef.id) {
var ver = cloudRef.version;
if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) {
ver = window._soonFileMeta.version;
}
return updateCloudFile(cloudRef.id, fileName, str, ver);
}
return createCloudFile(fileName, str);
},
readFile: function (pathOrHandle) {
if (pathOrHandle && pathOrHandle.getFile) {
return pathOrHandle.getFile().then(function (f) {
return new Promise(function (res, rej) {
var r = new FileReader();
r.onload = function () { res(r.result); };
r.onerror = rej;
r.readAsArrayBuffer(f);
});
});
}
if (pathOrHandle && pathOrHandle.arrayBuffer) {
return pathOrHandle.arrayBuffer();
}
return Promise.reject(new Error('No file'));
},
getAppVersion: function () { return Promise.resolve(VERSION); },
getLocale: getLocale,
getUserDataPath: function () { return ''; },
getSystemFonts: getSystemFonts,
openDesignPage: function (file, type) {
var t = type || 1;
if (file && typeof sessionStorage !== 'undefined') {
try {
sessionStorage.setItem('soondesign_open_file', file);
sessionStorage.setItem('soondesign_open_type', String(t));
if (file.indexOf('soondesign_file:') === 0 && window._soonFileMeta) {
sessionStorage.setItem('soondesign_open_meta', JSON.stringify(window._soonFileMeta));
}
} catch (e) {}
}
var fileParam = file ? encodeURIComponent(file) : '';
navigateToPage('design' + t + '.web.html?file=' + fileParam + '&type=' + t);
},
openFirstPage: function () {
navigateToPage('index.web.html');
},
onClose: function (callback) {
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', callback);
}
},
runClose: function () {
if (typeof window !== 'undefined' && window.close) window.close();
},
openHelp: function () {
try {
window.open(new URL('../assets/help/User Manual.pdf', window.location.href).href, '_blank');
} catch (e) {
window.open('../assets/help/User Manual.pdf', '_blank');
}
},
printPdf: function (urlOrBlob) {
var url = urlOrBlob;
if (urlOrBlob && typeof urlOrBlob === 'object' && !(urlOrBlob instanceof String)) {
url = URL.createObjectURL(urlOrBlob);
}
var w = window.open(url, '_blank');
if (w) w.onload = function () { w.print(); };
},
getScaleRate: function () {
return typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;
},
clipboard: {
readText: function () {
return navigator.clipboard && navigator.clipboard.readText ? navigator.clipboard.readText() : Promise.resolve('');
},
writeText: function (text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
return Promise.resolve();
}
}
};
window.platformBridge = bridge;
var pathStub = {
join: function () { return [].slice.call(arguments).join('/').replace(/\/+/g, '/'); }
};
window.sysAPI = {
readHistory: bridge.readHistory,
writeHistory: bridge.writeHistory,
readJsonFile: bridge.readJsonFile,
getAppVersion: bridge.getAppVersion
};
window.dialog = {
showOpenDialog: bridge.showOpenDialog,
showSaveDialog: bridge.showSaveDialog
};
window.path = pathStub;
window.fs = null;
window.remote = null;
var loc = getLocale();
window.ipcRenderer = {
send: function (ch, a1, a2) {
if (ch === 'get-sys-language') { window._sysLanPending = true; setTimeout(function () { if (window._sysLanCb && window._sysLanPending) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
if (ch === 'open-first-page' && bridge.openFirstPage) { bridge.openFirstPage(); }
if (ch === 'open-design-page' && bridge.openDesignPage && (a1 !== undefined || a2 !== undefined)) { bridge.openDesignPage(a1 || '', a2 || 1); }
if (ch === 'open-help-file' && bridge.openHelp) { bridge.openHelp(); }
if (ch === 'run-close' && bridge.runClose) { bridge.runClose(); }
},
on: function (ch, cb) {
if (ch === 'sys-lan') { window._sysLanCb = cb; if (window._sysLanPending) setTimeout(function () { if (window._sysLanCb) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
if (ch === 'close') window._closeCb = cb;
}
};
window.clipboard = bridge.clipboard;
window.exePath = '';
window.fullPath = '';
})();