Files
SoonDesign/frontend-web/js/platform/web.js
T
24kycj ebe191b06d 永久会员与模板库后台化:预览门控、轻量首页与 Admin CRUD
- 会员改为永久激活方案;云端文件不再拦截非会员;预览弹窗内导出/打印才校验
- 首页最近文件支持本地记录,登录后与云端合并;移除独立会员页与订阅页
- 模板库入库管理:soon_templates 表、Admin 上传 CRUD、/templates 轻量列表与按需下载

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 23:52:56 +08:00

477 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 (!getAccessToken()) {
var empty = { items: [], total: 0 };
if (typeof pageOrLimit === 'object' && pageOrLimit) {
empty.page = pageOrLimit.page || 1;
empty.size = pageOrLimit.size || 12;
}
return Promise.resolve(empty);
}
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'));
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'));
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'));
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';
}
var name = typeof pathOrHandle === 'string' ? pathOrHandle : 'design.soon';
var cloudRef = parseCloudRef(name);
var fileName = normalizeSoonName(name);
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'));
}
}
if (!requireCloudAuth('保存')) {
return Promise.reject(new Error('unauthorized'));
}
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 = '';
})();