改用browserwindows实现systemdialog,和现有弹窗UI统一参数格式

This commit is contained in:
fofolee
2025-01-19 23:08:31 +08:00
parent 272cf488a3
commit 1b1c79deb5
7 changed files with 760 additions and 449 deletions

View File

@@ -0,0 +1,192 @@
const { ipcRenderer } = require("electron");
// 等待 DOM 加载完成
document.addEventListener("DOMContentLoaded", () => {
let parentId = null;
let dialogType = null;
// 监听父窗口发来的对话框配置
ipcRenderer.on("dialog-config", (event, config) => {
parentId = event.senderId;
dialogType = config.type;
// 设置主题
document.documentElement.setAttribute(
"data-theme",
config.isDark ? "dark" : "light"
);
// 设置对话框标题
document.getElementById("title-text").textContent = config.title || "提示";
// 设置对话框内容
if (config.content) {
document.getElementById("content").textContent = config.content;
}
// 根据类型设置不同的对话框内容
switch (config.type) {
case "message":
document.body.classList.add("dialog-message"); // 添加消息对话框的类
break;
case "input":
document.getElementById("input").style.display = "block";
document.body.classList.add("dialog-input");
// 创建输入框
const inputContainer = document.getElementById("input-container");
inputContainer.innerHTML = ""; // 清空现有内容
config.inputOptions.forEach((inputOption, index) => {
console.log(inputOption);
const div = document.createElement("div");
div.className = "input-group";
const label = document.createElement("label");
label.textContent =
typeof inputOption === "string" ? inputOption : inputOption.label;
const input = document.createElement("input");
input.type = "text";
input.id = `input-${index}`;
if (typeof inputOption !== "string") {
input.value = inputOption.value || "";
input.placeholder = inputOption.hint || "";
}
div.appendChild(label);
div.appendChild(input);
inputContainer.appendChild(div);
});
document.getElementById("input-0").focus();
break;
case "confirm":
document.getElementById("confirm").style.display = "block";
document.body.classList.add("dialog-confirm");
break;
case "buttons":
document.getElementById("buttons").style.display = "block";
document.body.classList.add("dialog-buttons");
// 创建按钮
const buttonContainer = document.getElementById("button-container");
buttonContainer.innerHTML = "";
config.buttons.forEach((btn, index) => {
const button = document.createElement("button");
button.textContent = btn;
button.onclick = () => {
ipcRenderer.sendTo(parentId, "dialog-result", {
id: index,
text: btn,
});
};
buttonContainer.appendChild(button);
});
break;
case "textarea":
document.getElementById("textarea").style.display = "block";
document.body.classList.add("dialog-textarea");
const textarea = document.getElementById("text-content");
if (config.placeholder) {
textarea.placeholder = config.placeholder;
}
if (config.defaultText) {
textarea.value = config.defaultText;
}
textarea.focus();
break;
}
ipcRenderer.sendTo(parentId, "dialog-ready", calculateHeight());
});
const calculateHeight = () => {
const titleBar = document.querySelector(".title-bar");
const buttonBar = document.querySelector(".button-bar");
const contentWrapper = document.querySelector(".content-wrapper");
// 计算总高度
const totalHeight =
titleBar.offsetHeight +
contentWrapper.scrollHeight +
(buttonBar.style.display !== "none" ? buttonBar.offsetHeight : 0);
// 确保高度在最小值和最大值之间
return Math.min(Math.max(totalHeight, 100), 520);
};
// 确定按钮点击事件
document.getElementById("ok-btn").onclick = () => {
let result;
switch (dialogType) {
case "message":
result = true;
break;
case "input":
const inputs = document.querySelectorAll("#input-container input");
result = Array.from(inputs).map((input) => input.value);
break;
case "confirm":
result = true;
break;
case "textarea":
result = document.getElementById("text-content").value;
break;
}
ipcRenderer.sendTo(parentId, "dialog-result", result);
};
const cancelDialog = () => {
let result;
switch (dialogType) {
case "input":
result = [];
break;
case "textarea":
result = "";
break;
case "confirm":
result = false;
break;
case "buttons":
result = {};
break;
default:
result = null;
}
ipcRenderer.sendTo(parentId, "dialog-result", result);
};
// 取消按钮点击事件
document.getElementById("cancel-btn").onclick = () => {
cancelDialog();
};
// ESC键关闭窗口
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
cancelDialog();
}
});
// 回车键确认
document.addEventListener("keydown", (e) => {
// 如果是文本区域且按下回车,不触发确认
if (e.key === "Enter") {
if (dialogType === "textarea" && !e.ctrlKey) {
return;
}
document.getElementById("ok-btn").click();
}
});
// 关闭按钮点击事件
document.querySelector(".close-btn").onclick = () => {
cancelDialog();
};
});

View File

@@ -0,0 +1,163 @@
const { ipcRenderer } = require("electron");
const { createBrowserWindow } = utools;
/**
* 创建对话框窗口
* @param {object} config - 对话框配置
* @returns {Promise} 返回对话框结果
*/
const createDialog = (config) => {
return new Promise((resolve) => {
const dialogPath = "lib/dialog/view.html";
const preloadPath = "lib/dialog/controller.js";
const dialogOptions = {
title: config.title || "对话框",
width: 470,
height: 80,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
skipTaskbar: true,
alwaysOnTop: true,
frame: false,
webPreferences: {
preload: preloadPath,
},
};
// 创建窗口
const UBrowser = createBrowserWindow(dialogPath, dialogOptions, () => {
const dialogResultHandler = (event, result) => {
resolve(result);
// 移除监听器
ipcRenderer.removeListener("dialog-result", dialogResultHandler);
UBrowser.destroy();
};
const dialogReadyHandler = (event, height) => {
// 获取当前窗口位置
const bounds = UBrowser.getBounds();
// 调整y坐标保持窗口中心点不变
const y = Math.round(bounds.y - (height - bounds.height) / 2);
// 确保坐标和尺寸都是有效的整数
const newBounds = {
x: Math.round(bounds.x),
y: Math.max(0, y), // 确保不会超出屏幕顶部
width: 470,
height: Math.round(height),
};
// 设置新的位置和大小
UBrowser.setBounds(newBounds);
ipcRenderer.removeListener("dialog-ready", dialogReadyHandler);
};
ipcRenderer.on("dialog-ready", dialogReadyHandler);
// 添加监听器
ipcRenderer.on("dialog-result", dialogResultHandler);
// 发送配置到子窗口
ipcRenderer.sendTo(UBrowser.webContents.id, "dialog-config", {
...config,
isDark: utools.isDarkColors(),
});
});
});
};
/**
* 显示一个系统级消息框
* @param {string} content - 消息内容
* @param {string} [title] - 标题,默认为空
* @returns {Promise<void>} Promise
*/
const showSystemMessageBox = async (content, title = "") => {
await createDialog({
type: "message",
title,
content,
});
};
/**
* 显示一个系统级输入框组对话框
* @param {string[]|{label:string,value:string,hint:string}[]} options - 输入框配置,可以是标签数组或者带属性的对象数组
* @param {string} [title] - 标题,默认为空
* @returns {Promise<string[]|null>} 输入的内容数组
*/
const showSystemInputBox = async (options, title = "") => {
// 确保 options 是数组
const optionsArray = Array.isArray(options) ? options : [options];
// 转换每个选项为正确的格式
const inputOptions = optionsArray.map((opt) => {
if (typeof opt === "string") {
return opt;
}
if (typeof opt === "object" && opt.label) {
return {
label: opt.label,
value: opt.value || "",
hint: opt.hint || "",
};
}
throw new Error("输入框配置格式错误");
});
return await createDialog({
type: "input",
title,
inputOptions,
});
};
/**
* 显示一个系统级确认框,返回是否点击了确认
* @param {string} content - 确认内容
* @param {string} [title] - 标题,默认为空
* @returns {Promise<boolean>} 是否确认
*/
const showSystemConfirmBox = async (content, title = "") => {
const result = await createDialog({
type: "confirm",
title,
content,
});
return !!result;
};
/**
* 显示一个系统级按钮组对话框,返回点击的按钮的索引和文本
* @param {string[]} buttons - 按钮文本数组
* @param {string} [title] - 标题,默认为空
* @returns {Promise<{id: number, text: string}|null>} 选择的按钮信息
*/
const showSystemButtonBox = async (buttons, title = "") => {
return await createDialog({
type: "buttons",
title,
buttons: Array.isArray(buttons) ? buttons : [buttons],
});
};
/**
* 显示一个系统级多行文本输入框
* @param {string} [placeholder] - 输入框的提示文本
* @param {string} [defaultText] - 输入框的默认文本,默认为空
* @returns {Promise<string|null>} 编辑后的文本
*/
const showSystemTextArea = async (placeholder = "请输入", defaultText = "") => {
return await createDialog({
type: "textarea",
placeholder,
defaultText,
});
};
module.exports = {
showSystemMessageBox,
showSystemInputBox,
showSystemConfirmBox,
showSystemButtonBox,
showSystemTextArea,
};

344
plugin/lib/dialog/view.html Normal file
View File

@@ -0,0 +1,344 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>对话框</title>
<style>
:root {
--bg-color: #fff;
--text-color: #333;
--border-color: #ddd;
--title-bg: #f5f5f5;
--input-bg: #fff;
--input-border: #ddd;
--input-focus: #0d6efd;
--button-bg: #0d6efd;
--button-hover: #0b5ed7;
--button-text: #fff;
--cancel-bg: #6c757d;
--cancel-border: #6c757d;
}
:root[data-theme="dark"] {
--bg-color: #282727;
--text-color: #e0e0e0;
--border-color: #404040;
--title-bg: #2d2d2d;
--input-bg: #2d2d2d;
--input-border: #404040;
--input-focus: #0d6efd;
--button-bg: #0d6efd;
--button-hover: #0b5ed7;
--button-text: #fff;
--cancel-bg: #4a4a4a;
--cancel-border: #4a4a4a;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background: var(--bg-color);
color: var(--text-color);
user-select: none;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 标题栏样式 */
.title-bar {
background: var(--title-bg);
border-bottom: 1px solid var(--border-color);
padding: 4px 12px;
-webkit-app-region: drag;
display: flex;
align-items: center;
flex-shrink: 0;
}
.title-left {
flex: 1;
display: flex;
align-items: center;
}
.logo {
width: 20px;
height: 20px;
margin-right: 6px;
}
.title-text {
font-size: 13px;
font-weight: 500;
color: var(--text-color);
margin: 0;
}
.close-btn {
-webkit-app-region: no-drag;
background-color: rgba(0, 0, 0, 0.05);
width: 16px;
height: 16px;
min-width: 16px;
min-height: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
cursor: pointer;
opacity: 0.8;
transition: all 0.2s;
margin-left: 8px;
position: relative;
}
:root[data-theme="dark"] .close-btn {
background-color: rgba(255, 255, 255, 0.1);
}
.close-btn:hover {
opacity: 1;
background-color: #ff4d4d;
}
.close-btn::before,
.close-btn::after {
content: "";
position: absolute;
width: 8px;
height: 1px;
background-color: var(--text-color);
transform-origin: center;
}
.close-btn::before {
transform: rotate(45deg);
}
.close-btn::after {
transform: rotate(-45deg);
}
.close-btn:hover::before,
.close-btn:hover::after {
background-color: #fff;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.content-wrapper {
padding: 16px;
min-height: 60px;
max-height: 449px;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
}
#content {
line-height: 1.4;
font-size: 13px;
user-select: text;
}
.button-bar {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 0 16px 16px;
flex-shrink: 0;
background: var(--bg-color);
}
button {
padding: 4px 12px;
border-radius: 4px;
border: 1px solid var(--button-bg);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.08) 0%,
rgba(0, 0, 0, 0.05) 100%
),
var(--button-bg);
color: var(--button-text);
cursor: pointer;
font-size: 13px;
min-width: 70px;
transition: all 0.2s ease;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
button:hover {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(0, 0, 0, 0.05) 100%
),
var(--button-hover);
}
button:active {
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.05) 0%,
rgba(255, 255, 255, 0.08) 100%
),
var(--button-bg);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
#ok-btn {
padding: 0 12px;
height: 20px;
}
#cancel-btn {
padding: 0 12px;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.08) 0%,
rgba(0, 0, 0, 0.05) 100%
),
var(--cancel-bg);
border: 1px solid var(--cancel-border);
height: 20px;
}
#cancel-btn:hover {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(0, 0, 0, 0.05) 100%
),
var(--cancel-bg);
}
#input-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
display: block;
padding: 0 0 4px 2px;
color: var(--text-color);
font-size: 13px;
}
.input-group input {
width: 100%;
padding: 6px 8px;
border: 1px solid var(--input-border);
border-radius: 4px;
font-size: 13px;
box-sizing: border-box;
background: var(--input-bg);
color: var(--text-color);
}
.input-group input:focus {
border-color: var(--input-focus);
outline: none;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
}
/* 文本区域样式 */
textarea {
width: 100%;
height: 400px;
padding: 6px 8px;
border: 1px solid var(--input-border);
border-radius: 4px;
font-size: 13px;
resize: none;
box-sizing: border-box;
background: var(--input-bg);
color: var(--text-color);
}
textarea:focus {
border-color: var(--input-focus);
outline: none;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
}
/* 按钮组样式 */
#button-container {
display: flex;
flex-direction: column;
gap: 10px;
padding-top: 4px;
}
#button-container button {
width: 100%;
text-align: center;
padding: 6px 12px;
}
/* 根据对话框类型显示/隐藏取消按钮 */
.dialog-message #cancel-btn {
display: none;
}
.dialog-buttons .button-bar {
display: none;
}
/* 隐藏所有对话框内容 */
#input,
#confirm,
#buttons,
#textarea {
display: none;
}
</style>
</head>
<body>
<div class="container">
<!-- 标题栏 -->
<div class="title-bar">
<div class="title-left">
<img src="../../logo.png" alt="logo" class="logo" />
<h1 class="title-text" id="title-text">对话框</h1>
</div>
<div class="close-btn"></div>
</div>
<div class="content-wrapper">
<div id="content"></div>
<!-- 输入对话框 -->
<div id="input">
<div id="input-container"></div>
</div>
<!-- 确认对话框 -->
<div id="confirm"></div>
<!-- 按钮选择对话框 -->
<div id="buttons">
<div id="button-container"></div>
</div>
<!-- 文本区域对话框 -->
<div id="textarea">
<textarea id="text-content"></textarea>
</div>
</div>
<div class="button-bar">
<button id="cancel-btn">取消</button>
<button id="ok-btn">确定</button>
</div>
</div>
</body>
</html>