/** * 平台抽象层 - 网页实现 * 使用 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 = ''; })();