网页端:平台桥与部署修复(宝塔/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>
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* 平台抽象层 - 网页实现
|
||||
* 使用 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 = '';
|
||||
})();
|
||||
Reference in New Issue
Block a user