网页端:平台桥与部署修复(宝塔/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:
24kycj
2026-05-09 18:57:53 +08:00
parent ee6ef462ca
commit 2e6578247e
27 changed files with 6606 additions and 3542 deletions
+44
View File
@@ -0,0 +1,44 @@
/**
* 平台抽象层 - 统一接口定义
* 桌面端由 lib/platform/electron.js 实现,网页端由 lib/platform/web.js 实现。
* 业务代码通过 window.platformBridge 或兼容的 window.sysAPI / window.dialog 调用。
*/
(function () {
'use strict';
// 若已存在(由 electron 或 web 注入),则不覆盖
if (typeof window.platformBridge !== 'undefined') {
return;
}
// 默认空实现,避免未注入时报错(仅作占位,实际运行前必须注入 electron 或 web 实现)
function notImplemented() {
console.warn('[platform] bridge not injected');
return Promise.reject(new Error('Platform bridge not available'));
}
window.platformBridge = {
readHistory: notImplemented,
writeHistory: notImplemented,
readJsonFile: notImplemented,
showOpenDialog: notImplemented,
showSaveDialog: notImplemented,
writeFile: notImplemented,
readFile: notImplemented,
getAppVersion: function () { return Promise.resolve('0.0.0'); },
getLocale: function () { return 'zh'; },
getUserDataPath: function () { return ''; },
getSystemFonts: function () { return Promise.resolve([]); },
openDesignPage: function () {},
openFirstPage: function () {},
onClose: function () {},
runClose: function () {},
openHelp: function () {},
printPdf: notImplemented,
getScaleRate: function () { return typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1; },
clipboard: {
readText: function () { return Promise.resolve(''); },
writeText: function () { return Promise.resolve(); }
}
};
})();
+145
View File
@@ -0,0 +1,145 @@
/**
* 平台抽象层 - Electron 实现
* 仅在存在 require 且可加载 electron 时使用;封装 IPC、dialog、fs、remote。
*/
(function () {
'use strict';
if (typeof require === 'undefined') {
return;
}
var ipcRenderer, dialog, path, fs, remote, clipboard;
try {
ipcRenderer = require('electron').ipcRenderer;
dialog = require('@electron/remote').dialog;
path = require('path');
fs = require('fs');
remote = require('@electron/remote');
clipboard = require('electron').clipboard;
} catch (e) {
console.warn('[platform] Electron modules not available', e);
return;
}
var exePath = remote.app.getPath('userData');
var historyFilePath = path.join(exePath, 'data.json');
var bridge = {
readHistory: function () {
return ipcRenderer.invoke('history:read');
},
writeHistory: function (data) {
return ipcRenderer.invoke('history:write', data);
},
readJsonFile: function (filePath) {
if (!filePath) return Promise.resolve(null);
return ipcRenderer.invoke('file:read-json', filePath);
},
showOpenDialog: function (options) {
return dialog.showOpenDialog(options).then(function (result) {
return { canceled: result.canceled, filePaths: result.filePaths };
});
},
showSaveDialog: function (options) {
return dialog.showSaveDialog(options);
},
writeFile: function (filePath, content) {
return new Promise(function (resolve, reject) {
var buf = Buffer.isBuffer(content) ? content : (typeof content === 'string' ? Buffer.from(content, 'utf8') : Buffer.from(content));
fs.writeFile(filePath, buf, 'utf8', function (err) {
if (err) reject(err);
else resolve();
});
});
},
writeFileSync: function (filePath, content) {
fs.writeFileSync(filePath, content, 'utf8');
},
readFile: function (filePath) {
return new Promise(function (resolve, reject) {
fs.readFile(filePath, function (err, data) {
if (err) reject(err);
else resolve(data);
});
});
},
readFileSync: function (filePath) {
return fs.readFileSync(filePath);
},
existsSync: function (p) {
return fs.existsSync(p);
},
getAppVersion: function () {
return ipcRenderer.invoke('app:get-version');
},
getLocale: function () {
return remote.app.getLocale();
},
getUserDataPath: function () {
return exePath;
},
getSystemFonts: function () {
return new Promise(function (resolve, reject) {
ipcRenderer.once('font-list', function (ev, fonts) {
resolve(fonts || []);
});
ipcRenderer.send('get-sys-fonts');
setTimeout(function () {
resolve([]);
}, 10000);
});
},
openDesignPage: function (file, type) {
ipcRenderer.send('open-design-page', file || 'empty', type || '1');
},
openFirstPage: function () {
ipcRenderer.send('open-first-page');
},
onClose: function (callback) {
ipcRenderer.on('close', callback);
},
runClose: function () {
ipcRenderer.send('run-close');
},
openHelp: function () {
ipcRenderer.send('open-help-file');
},
printPdf: function (urlOrBlob) {
ipcRenderer.send('print-pdf', urlOrBlob);
},
getScaleRate: function () {
return typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;
},
clipboard: {
readText: function () {
return Promise.resolve(clipboard.readText());
},
writeText: function (text) {
clipboard.writeText(text);
return Promise.resolve();
}
}
};
window.platformBridge = bridge;
// 兼容现有命名
window.sysAPI = {
readHistory: bridge.readHistory,
writeHistory: bridge.writeHistory,
readJsonFile: bridge.readJsonFile,
getAppVersion: bridge.getAppVersion
};
window.dialog = {
showOpenDialog: bridge.showOpenDialog,
showSaveDialog: bridge.showSaveDialog
};
window.path = path;
window.fs = fs;
window.remote = remote;
window.ipcRenderer = ipcRenderer;
window.clipboard = clipboard;
window.exePath = exePath;
window.fullPath = historyFilePath;
})();
+341
View File
@@ -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 = '';
})();