Files
SoonDesign/frontend-web/js/platform/web.js
T
24kycj 877fd278c2 Web 端数据交互与 design2 布局修复
统一门户 JSON 契约:新增 GET /templates/{id},模板/云文件/保存/打开走虚拟 key;
layer 保存弹层替代 prompt,本地 .soon 已登录后台 POST 登记;修复保存静默失败与 design2 全宽布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 01:20:34 +08:00

540 lines
21 KiB
JavaScript

(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], { 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; });
}
}
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,
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; });
}
}
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 || raw.indexOf('soondesign_template:') === 0)) {
defaultName = window.soonDisplayFileName(raw);
}
var title = (options && options.title) ? options.title : '保存到云端';
return new Promise(function (resolve) {
if (typeof layer === 'undefined' || !layer.open) {
var fpFallback = defaultName;
resolve({ canceled: false, filePath: fpFallback, useCloud: true });
return;
}
var esc = function (s) {
return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
};
var html = '<div class="soon-dialog-body" style="padding:16px 20px;">' +
'<label style="display:block;margin-bottom:8px;color:#b8c0c8;font-size:13px;">文件名</label>' +
'<input type="text" id="soonSaveDialogInput" class="layui-input" value="' + esc(defaultName) + '" ' +
'style="background:#2b3036;border-color:rgba(255,255,255,0.08);color:#eef1f4;">' +
'</div>';
layer.open({
type: 1,
skin: 'soon-layer',
title: title,
area: ['360px', 'auto'],
shadeClose: false,
content: html,
btn: ['确定', '取消'],
btnAlign: 'r',
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);
layer.close(index);
resolve({ canceled: false, filePath: name, useCloud: true });
},
btn2: function (index) {
layer.close(index);
resolve({ canceled: true });
},
cancel: function (index) {
layer.close(index);
resolve({ canceled: 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;
var key = file || '';
if (typeof window.soonSyncOpenNavigation === 'function') {
window.soonSyncOpenNavigation(key, t);
}
var fileParam = key ? encodeURIComponent(key) : '';
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.soonShowSaveDialog = 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 = '';
})();