重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构 - 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐 - 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作 - 新增管理后台、支付与云端文件 API,更新 README 与项目规范 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
// 平台桥由 js/platform/electron.js 注入
|
||||
var ipcRenderer = typeof window !== 'undefined' ? window.ipcRenderer : null;
|
||||
var dialog = typeof window !== 'undefined' ? window.dialog : null;
|
||||
|
||||
// 辅助函数:获取文件名 (保留扩展名)
|
||||
function get_filename(filePath) {
|
||||
if (!filePath) return "";
|
||||
return filePath.split(/[/\\]/).pop();
|
||||
}
|
||||
|
||||
// 辅助函数:HTML 属性转义,避免 path 中的 "&<> 破坏属性导致点击传参错误
|
||||
function escapeAttr(s) {
|
||||
if (s == null) return "";
|
||||
return String(s).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
// Electron 历史曾被误写 soondesign_session: 前缀,读取时还原为磁盘路径
|
||||
function isWebPortal() {
|
||||
return !!(window.platformBridge && !ipcRenderer);
|
||||
}
|
||||
function resolveDiskPath(p) {
|
||||
if (!p) return p;
|
||||
const prefix = 'soondesign_session:';
|
||||
if (ipcRenderer && p.indexOf(prefix) === 0) return p.substring(prefix.length);
|
||||
return p;
|
||||
}
|
||||
|
||||
let s_lan = "zh";
|
||||
|
||||
function language_str(str) {
|
||||
let t = {
|
||||
"noFile": { zh: "当前文件不存在!", ozh: "This file does not exist!", en: "File not found!" },
|
||||
"saveTime": { zh: "保存时间:", ozh: "保存時間:", en: "Saved on:" },
|
||||
"selectFile": { zh: "请选择文件", ozh: "請選擇文件", en: "Please select a file" },
|
||||
"comfirm": { zh: "确认", ozh: "確認", en: "Confirm" },
|
||||
"cancel": { zh: "取消", ozh: "取消", en: "Cancel" },
|
||||
"SDFile": { zh: "SoonDesign模板文件", ozh: "SoonDesign模闆文件", en: "SoonDesign Template File" },
|
||||
"vers": { zh: "版本:", ozh: "版本:", en: "Version:" },
|
||||
"copyright": { zh: "版权所有 © 2023", ozh: "版權所有 © 2023", en: "Copyright © 2023" },
|
||||
"about": { zh: "关于 SoonDesign", ozh: "關於 SoonDesign", en: "About SoonDesign" },
|
||||
"lost": { zh: "(文件已丢失)", ozh: "(文件已丟失)", en: "(File Lost)" },
|
||||
"clickToDelete": { zh: "文件已丢失,点击删除此记录", ozh: "文件已丟失,點擊刪除此記錄", en: "File lost, click to remove" },
|
||||
"deleted": { zh: "已删除", ozh: "已刪除", en: "Deleted" },
|
||||
"whetherSave": { zh: "是否保存当前文件?", ozh: "是否保存當前文件?", en: "Save current file?" }, // 补全可能用到的翻译
|
||||
"delTitle": { zh: "警告", ozh: "警告", en: "Warning" },
|
||||
"delContent": { zh: "文件已丢失,确认是否删除?", ozh: "文件已丟失,確認是否刪除?", en: "File lost, confirm delete?" }
|
||||
}
|
||||
return t[str][s_lan] || t[str]['zh'];
|
||||
}
|
||||
|
||||
layui.use(['layer', 'form', 'jquery'], function () {
|
||||
var $ = layui.$;
|
||||
var layer = layui.layer
|
||||
, form = layui.form;
|
||||
|
||||
if (window.sysAPI && window.sysAPI.getAppVersion) {
|
||||
window.sysAPI.getAppVersion().then(ver => {
|
||||
$("#app-version").text("v" + ver);
|
||||
}).catch(e => console.log(e));
|
||||
}
|
||||
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.send('get-sys-language');
|
||||
ipcRenderer.on('close', (event, message) => {
|
||||
ipcRenderer.send('run-close');
|
||||
});
|
||||
}
|
||||
|
||||
function langua_ge(lan = 'zh') {
|
||||
$("[language='m']").each(function (i) {
|
||||
$(this).html($(this).attr(lan));
|
||||
})
|
||||
$("[language='t']").each(function (i) {
|
||||
$(this).attr("title", $(this).attr(lan));
|
||||
})
|
||||
}
|
||||
|
||||
$('#language_select').on('change', function () {
|
||||
langua_ge($(this).find('option:selected').val());
|
||||
s_lan = $(this).find('option:selected').val();
|
||||
localStorage.setItem("lang", s_lan);
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
if (ipcRenderer && ipcRenderer.on) {
|
||||
ipcRenderer.on('sys-lan', (event, data) => {
|
||||
let lang = localStorage.getItem("lang");
|
||||
if (lang) {
|
||||
s_lan = lang;
|
||||
} else {
|
||||
if (data && data.startsWith('zh')) {
|
||||
s_lan = data === 'zh-TW' ? 'ozh' : 'zh';
|
||||
} else {
|
||||
s_lan = 'en';
|
||||
}
|
||||
}
|
||||
langua_ge(s_lan);
|
||||
$("#language_select").val(s_lan);
|
||||
localStorage.setItem("lang", s_lan);
|
||||
loadHistory();
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 加载列表
|
||||
// ==========================================
|
||||
async function loadHistory() {
|
||||
try {
|
||||
if (!window.sysAPI || typeof window.sysAPI.readHistory !== 'function') {
|
||||
$(".card-list").html("");
|
||||
return;
|
||||
}
|
||||
const j = await window.sysAPI.readHistory();
|
||||
if (!j || !Array.isArray(j.history)) {
|
||||
$(".card-list").html("");
|
||||
return;
|
||||
}
|
||||
let h = "";
|
||||
let needUpdate = false;
|
||||
|
||||
for (let item of j.history) {
|
||||
let src = "";
|
||||
let fileExists = true;
|
||||
let imgStyle = "";
|
||||
let realType = item.type;
|
||||
const diskPath = resolveDiskPath(item.path);
|
||||
if (diskPath !== item.path) {
|
||||
item.path = diskPath;
|
||||
needUpdate = true;
|
||||
}
|
||||
const soonData = await window.sysAPI.readJsonFile(diskPath);
|
||||
if (soonData) {
|
||||
src = soonData.frontDisplayPic;
|
||||
let fileType = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1);
|
||||
if (item.type != fileType) {
|
||||
item.type = fileType;
|
||||
realType = fileType;
|
||||
needUpdate = true;
|
||||
}
|
||||
} else {
|
||||
fileExists = false;
|
||||
if (realType && (realType == 2 || realType == "2")) {
|
||||
src = soonAsset("bg_2.png");
|
||||
} else {
|
||||
src = soonAsset("bg_1.png");
|
||||
}
|
||||
imgStyle = "opacity: 0.6; filter: grayscale(100%);";
|
||||
}
|
||||
|
||||
// 网页端历史 path 为 soondesign_session:文件名.soon,展示时去掉前缀
|
||||
const sessionPrefix = 'soondesign_session:';
|
||||
let displayName = (item.path && item.path.indexOf(sessionPrefix) === 0)
|
||||
? item.path.substring(sessionPrefix.length)
|
||||
: get_filename(item.path);
|
||||
let cardTitle = (item.path && item.path.indexOf(sessionPrefix) === 0)
|
||||
? displayName
|
||||
: item.path;
|
||||
let cardStyle = "";
|
||||
|
||||
if (!fileExists) {
|
||||
displayName += ` <span style='color:#ff5722;font-size:12px;'>${language_str("lost")}</span>`;
|
||||
cardTitle = language_str("clickToDelete");
|
||||
cardStyle = "border: 1px dashed #ff5722;";
|
||||
}
|
||||
|
||||
h += `<div class="card" data-file="${escapeAttr(item.path)}" title="${escapeAttr(cardTitle)}" style="${cardStyle}">
|
||||
<div class="rect ${realType == 2 ? 'rect1' : ''}">
|
||||
<img src="${src}" style="width:190px; ${imgStyle}">
|
||||
<div class="tip">${displayName}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
$(".card-list").html(h);
|
||||
|
||||
if (needUpdate) {
|
||||
await window.sysAPI.writeHistory({ history: j.history });
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("加载历史记录出错", e);
|
||||
}
|
||||
}
|
||||
|
||||
loadHistory();
|
||||
|
||||
// 按钮事件
|
||||
$("#openfile").click(function () { OpenDialog(); });
|
||||
$("#new1").click(function () {
|
||||
if (window.platformBridge && window.platformBridge.openDesignPage) window.platformBridge.openDesignPage("", 1);
|
||||
else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-design-page', "", 1);
|
||||
});
|
||||
$("#new2").click(function () {
|
||||
if (window.platformBridge && window.platformBridge.openDesignPage) window.platformBridge.openDesignPage("", 2);
|
||||
else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-design-page', "", 2);
|
||||
});
|
||||
|
||||
$("#about").click(async function () {
|
||||
let version = "3.0.0";
|
||||
try {
|
||||
if (window.sysAPI && window.sysAPI.getAppVersion) {
|
||||
const v = await window.sysAPI.getAppVersion();
|
||||
version = "v" + v;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
layer.open({
|
||||
type: 1
|
||||
, title: language_str("about")
|
||||
, area: '450px;'
|
||||
, id: 'LAY_layuipro'
|
||||
, moveType: 1
|
||||
, content: '<div style="padding-top:20px;padding-bottom:30px;background-color: #32363A; color: #fff; font-weight: 300;text-align:center;">' +
|
||||
'<div style="width: 120px;margin: 0 auto;"><img style="width: 120px;" src="' + soonAsset("IconA.png") + '"></div>' +
|
||||
'<div style="font-size:24px;">Soon Design</div>' +
|
||||
'<div style="font-size:14px;">' + language_str("vers") + ' ' + version + '</div>' +
|
||||
'<div style="font-size:14px;">' + language_str("copyright") + '</div>' +
|
||||
'</div>'
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 点击事件 (修改后:完全匹配“保存”弹窗样式)
|
||||
// ==========================================
|
||||
$(".card-list").on("click", '.card', async function () {
|
||||
let filePath = resolveDiskPath($(this).attr("data-file") || $(this).attr("data"));
|
||||
// 再次检查文件是否存在
|
||||
const soonData = await window.sysAPI.readJsonFile(filePath);
|
||||
if (soonData) {
|
||||
// 1. 文件存在 -> 正常打开
|
||||
let type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1);
|
||||
const currentData = await window.sysAPI.readHistory();
|
||||
let newHistory = currentData.history.filter(item => resolveDiskPath(item.path) !== filePath);
|
||||
newHistory.unshift({ path: filePath, time: Date.now(), type: type });
|
||||
await window.sysAPI.writeHistory({ history: newHistory });
|
||||
|
||||
if (window.platformBridge && window.platformBridge.openDesignPage) window.platformBridge.openDesignPage(filePath, type);
|
||||
else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-design-page', filePath, type);
|
||||
} else {
|
||||
// 2. 文件不存在 -> 警告确认
|
||||
|
||||
// 【关键修改】
|
||||
// - 纯文字,左对齐 (text-align: left)
|
||||
// - 适当的 padding
|
||||
// - 去除所有图标
|
||||
let contentHtml = `
|
||||
<div style="padding: 20px 20px; text-align: left; color: #333; font-size: 14px;">
|
||||
${language_str("delContent")}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 使用 layer.open
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: language_str("delTitle"), // 标题:警告
|
||||
content: contentHtml,
|
||||
btn: [language_str("comfirm"), language_str("cancel")],
|
||||
btnAlign: 'r', // 【关键修改】按钮右对齐
|
||||
area: ['300px', 'auto'], // 【关键修改】宽度设为 300px,标准小窗口
|
||||
resize: false,
|
||||
shadeClose: true,
|
||||
yes: async function(index){
|
||||
const currentData = await window.sysAPI.readHistory();
|
||||
const newHistory = currentData.history.filter(item => resolveDiskPath(item.path) !== filePath);
|
||||
|
||||
if (newHistory.length !== currentData.history.length) {
|
||||
await window.sysAPI.writeHistory({ history: newHistory });
|
||||
layer.msg(language_str("deleted"), { icon: 1, time: 1000 });
|
||||
loadHistory();
|
||||
}
|
||||
layer.close(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function OpenDialog() {
|
||||
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showOpenDialog ? { showOpenDialog: function(opts) { return window.platformBridge.showOpenDialog(opts); } } : null);
|
||||
if (!dialogApi) return;
|
||||
dialogApi.showOpenDialog({
|
||||
title: language_str("selectFile"),
|
||||
buttonLabel: language_str("comfirm"),
|
||||
filters: [
|
||||
{ name: language_str("SDFile"), extensions: ['soon'] },
|
||||
]
|
||||
}).then(async function(result) {
|
||||
if (result.canceled) return;
|
||||
var filePath = result.filePaths && result.filePaths[0];
|
||||
var file = (result.files && result.files[0]) || result.file;
|
||||
var soonData;
|
||||
if (file && file.text) {
|
||||
filePath = file.name || 'design.soon';
|
||||
soonData = await window.sysAPI.readJsonFile(file);
|
||||
// 网页端:将内容存到 sessionStorage,设计页通过 sessionKey 读取
|
||||
if (soonData && typeof sessionStorage !== 'undefined' && isWebPortal()) {
|
||||
try {
|
||||
sessionStorage.setItem('soondesign_session:' + filePath, JSON.stringify(soonData));
|
||||
filePath = 'soondesign_session:' + filePath;
|
||||
} catch (e) {}
|
||||
}
|
||||
} else if (filePath) {
|
||||
soonData = await window.sysAPI.readJsonFile(filePath);
|
||||
}
|
||||
if (!soonData) {
|
||||
if (!filePath && !file) return;
|
||||
layer.msg(language_str("noFile"));
|
||||
return;
|
||||
}
|
||||
var type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1);
|
||||
var currentData = await window.sysAPI.readHistory();
|
||||
var newHistory = currentData.history.filter(function(item) { return resolveDiskPath(item.path) !== filePath; });
|
||||
newHistory.unshift({ path: filePath, time: Date.now(), type: type });
|
||||
await window.sysAPI.writeHistory({ history: newHistory });
|
||||
if (window.platformBridge && window.platformBridge.openDesignPage) window.platformBridge.openDesignPage(filePath, type);
|
||||
else if (ipcRenderer && ipcRenderer.send) ipcRenderer.send('open-design-page', filePath, type);
|
||||
loadHistory();
|
||||
}).catch(function(err) { console.log(err); });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user