Files
SoonDesign/lib/platform/web.js
T
24kycj 2e6578247e 网页端:平台桥与部署修复(宝塔/Nginx)
- lib/platform:网页 Electron 统一 bridge/web/electron,子目录 API 基路径、返回首页跳站点根、路径末尾斜杠兼容
- index/design*.web.html:网页入口,web.js 加缓存参数避免旧脚本缓存
- api:PHP 读写 soon;文档说明目录权限与 Nginx
- lib/design*、lib/index:与 platformBridge 对接及网页侧逻辑
- 公共资源 JsBarcode/jr-qrcode;文档与 package/README/.gitignore 等同步更新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 18:57:53 +08:00

342 lines
14 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.
/**
* 平台抽象层 - 网页实现
* 使用 LocalStorage、File Picker、Blob 下载、navigator 等 Web API。
*/
(function () {
'use strict';
var HISTORY_KEY = 'soondesign_history';
var VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '3.2.101';
/** 站点目录前缀(支持部署在子目录 / 宝塔子路径):去掉入口 HTML 文件名 */
function getAppBasePathname() {
var p = (typeof window !== 'undefined' && window.location && window.location.pathname) ? window.location.pathname : '/';
// Nginx/宝塔可能把页面写成 /design2.web.html/ ,末尾斜杠会导致匹配不到 .html,误判站点根为「目录 URL」→ 回首页 404
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;
}
// API 相对站点根目录(与 api/README 中「相对网站根目录的 /api/」一致)
var SERVER_API_BASE = (typeof window !== 'undefined' && window.location)
? (window.location.origin + getAppBasePathname() + 'api/')
: '/api/';
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);
}
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 });
}
},
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('.soon') > 0 && typeof fetch !== 'undefined') {
var isServerEnv = typeof window !== 'undefined' && window.location &&
(window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1' && window.location.protocol !== 'file:');
if (isServerEnv) {
return fetch(SERVER_API_BASE + 'read_file.php?fileName=' + encodeURIComponent(key))
.then(function(response) {
if (!response.ok) return null;
return response.json();
})
.catch(function() { return null; });
}
}
return Promise.resolve(null);
},
showOpenDialog: function (options) {
return new Promise(function (resolve) {
var input = document.createElement('input');
input.type = 'file';
// 根据 options.filters 动态设置 accept
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(/^\./, '');
// 图片类型使用 image/* MIME 类型,其他使用扩展名
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 {
// 默认:.soon 文件
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 = raw
? (raw.indexOf('soondesign_session:') === 0 ? raw.substring('soondesign_session:'.length) : raw.split(/[/\\]/).pop())
: 'design.soon';
return new Promise(function (resolve) {
if (typeof window.showSaveFilePicker !== 'function') {
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;
if (fp.indexOf('.soon') === -1 || fp.slice(-5).toLowerCase() !== '.soon') {
fp = (fp.replace(/\.soon$/i, '') || 'design') + '.soon';
}
resolve({ canceled: false, filePath: fp, useDownload: true });
return;
}
window.showSaveFilePicker({
suggestedName: defaultName,
types: [{ description: 'SoonDesign', accept: { 'application/json': ['.soon'] } }]
}).then(function (handle) {
var filePath = (handle && handle.name) ? handle.name : defaultName;
resolve({ canceled: false, filePath: filePath, fileHandle: handle });
}).catch(function () {
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() : ''));
// File System Access API(浏览器原生文件保存)
if (pathOrHandle && pathOrHandle.createWritable) {
return pathOrHandle.createWritable().then(function (w) {
if (isBlob) w.write(content);
else w.write(str);
return w.close();
});
}
// 服务器环境:上传到服务器
var name = typeof pathOrHandle === 'string' ? pathOrHandle : (isBlob ? 'output.pdf' : 'design.soon');
var isServerEnv = typeof window !== 'undefined' && window.location &&
(window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1' && window.location.protocol !== 'file:');
if (isServerEnv && !isBlob && typeof fetch !== 'undefined') {
// 上传到服务器
var formData = new FormData();
formData.append('fileName', name);
formData.append('fileContent', str);
return fetch(SERVER_API_BASE + 'save_file.php', {
method: 'POST',
body: formData
}).then(function(response) {
return response.json();
}).then(function(result) {
if (result.success) {
return Promise.resolve();
} else {
throw new Error(result.error || '保存失败');
}
}).catch(function(err) {
// 如果服务器保存失败,降级为下载
console.warn('服务器保存失败,降级为下载:', err);
var a = document.createElement('a');
a.download = name;
a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(str);
a.click();
return Promise.resolve();
});
}
// 本地环境或 Blob:触发下载
var a = document.createElement('a');
a.download = name;
if (isBlob) a.href = URL.createObjectURL(content);
else a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(str);
a.click();
if (isBlob && a.href) URL.revokeObjectURL(a.href);
return Promise.resolve();
},
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));
} catch (e) {}
}
var fileParam = file ? encodeURIComponent(file) : '';
navigateToPage('design' + t + '.web.html?file=' + fileParam + '&type=' + t);
},
openFirstPage: function () {
var loc = typeof window !== 'undefined' ? window.location : null;
if (!loc) return;
// 显式拼站点根(与 getAppBasePathname 一致),避免「路径末尾斜杠 / URL API」与 navigateToPage('.') 在个别环境下的解析差异
loc.href = loc.origin + getAppBasePathname();
},
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('help/User Manual.pdf', window.location.href).href, '_blank');
} catch (e) {
window.open('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 = '';
})();