重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
(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 = '';
|
||||
})();
|
||||
Reference in New Issue
Block a user