新增quickcommand.showSystemSelectList

This commit is contained in:
fofolee 2025-01-20 00:58:08 +08:00
parent 1b1c79deb5
commit 5b13f5c2a1
4 changed files with 449 additions and 5 deletions

View File

@ -1,4 +1,5 @@
const { ipcRenderer } = require("electron");
const pinyinMatch = require("pinyin-match");
// 等待 DOM 加载完成
document.addEventListener("DOMContentLoaded", () => {
@ -96,6 +97,218 @@ document.addEventListener("DOMContentLoaded", () => {
}
textarea.focus();
break;
case "select":
document.getElementById("select").style.display = "block";
document.body.classList.add("dialog-select");
const selectContainer = document.getElementById("select-container");
const filterInput = document.getElementById("filter-input");
const selectList = document.querySelector(".select-list");
selectContainer.innerHTML = "";
let currentSelected = null;
let allItems = [];
let filteredItems = [];
let hoverTimeout = null;
let isKeyboardNavigation = false;
// 创建选项
const createSelectItem = (item, index) => {
const div = document.createElement("div");
div.className = "select-item";
// 点击事件
div.onclick = () => {
const originalIndex = allItems.indexOf(item);
const result =
typeof item === "string"
? {
id: originalIndex,
text: item,
}
: item;
ipcRenderer.sendTo(parentId, "dialog-result", result);
};
// 鼠标移入事件(带防抖)
div.onmouseenter = () => {
if (isKeyboardNavigation) return;
if (hoverTimeout) {
clearTimeout(hoverTimeout);
}
hoverTimeout = setTimeout(() => {
if (currentSelected) {
currentSelected.classList.remove("selected");
}
div.classList.add("selected");
currentSelected = div;
}, 50);
};
// 鼠标移动事件
div.onmousemove = () => {
if (isKeyboardNavigation) {
isKeyboardNavigation = false;
selectList.classList.remove("keyboard-nav");
}
};
// 高亮文本
const highlightText = (text, filterText) => {
if (!filterText) return text;
const matchResult = pinyinMatch.match(text, filterText);
if (!matchResult) return text;
const [start, end] = matchResult;
return (
text.slice(0, start) +
`<span class="highlight">${text.slice(start, end + 1)}</span>` +
text.slice(end + 1)
);
};
if (typeof item === "string" || typeof item === "number") {
const highlightedText = highlightText(
String(item),
filterInput.value
);
div.innerHTML = `
<div class="select-item-content">
<p class="select-item-title">${highlightedText}</p>
</div>
`;
} else {
const highlightedTitle = highlightText(
item.title,
filterInput.value
);
const highlightedDesc = item.description
? highlightText(item.description, filterInput.value)
: "";
div.innerHTML = `
${
item.icon
? `
<div class="select-item-icon">
<img src="${item.icon}" alt="">
</div>
`
: ""
}
<div class="select-item-content">
<p class="select-item-title">${highlightedTitle}</p>
${
item.description
? `
<p class="select-item-description">${highlightedDesc}</p>
`
: ""
}
</div>
`;
}
return div;
};
// 过滤并更新列表
const updateList = (filterText = "") => {
selectContainer.innerHTML = "";
filteredItems = allItems.filter((item) => {
if (typeof item === "string" || typeof item === "number") {
return (
filterText === "" || pinyinMatch.match(String(item), filterText)
);
} else {
const titleMatch = pinyinMatch.match(item.title, filterText);
const descMatch = item.description
? pinyinMatch.match(item.description, filterText)
: false;
return filterText === "" || titleMatch || descMatch;
}
});
filteredItems.forEach((item, index) => {
const div = createSelectItem(item, index);
selectContainer.appendChild(div);
});
// 默认选中第一项
if (selectContainer.firstChild) {
selectContainer.firstChild.classList.add("selected");
currentSelected = selectContainer.firstChild;
}
};
// 初始化列表
allItems = config.items;
updateList();
// 添加筛选功能
let filterTimeout = null;
filterInput.addEventListener("input", (e) => {
if (filterTimeout) {
clearTimeout(filterTimeout);
}
filterTimeout = setTimeout(() => {
updateList(e.target.value);
}, 100);
});
// 添加键盘导航
const keydownHandler = (e) => {
const items = selectContainer.children;
if (!items.length) return;
if (
!isKeyboardNavigation &&
(e.key === "ArrowUp" || e.key === "ArrowDown")
) {
isKeyboardNavigation = true;
selectList.classList.add("keyboard-nav");
}
const currentIndex = Array.from(items).indexOf(currentSelected);
let newIndex = currentIndex;
switch (e.key) {
case "ArrowUp":
e.preventDefault();
if (currentIndex > 0) {
newIndex = currentIndex - 1;
}
break;
case "ArrowDown":
e.preventDefault();
if (currentIndex < items.length - 1) {
newIndex = currentIndex + 1;
}
break;
case "Enter":
e.preventDefault();
if (currentSelected) {
currentSelected.click();
}
break;
case "Escape":
e.preventDefault();
cancelDialog();
break;
}
if (newIndex !== currentIndex) {
if (currentSelected) {
currentSelected.classList.remove("selected");
}
items[newIndex].classList.add("selected");
currentSelected = items[newIndex];
currentSelected.scrollIntoView({ block: "nearest" });
}
};
document.addEventListener("keydown", keydownHandler);
// 聚焦筛选框
filterInput.focus();
break;
}
ipcRenderer.sendTo(parentId, "dialog-ready", calculateHeight());
});
@ -111,8 +324,10 @@ document.addEventListener("DOMContentLoaded", () => {
contentWrapper.scrollHeight +
(buttonBar.style.display !== "none" ? buttonBar.offsetHeight : 0);
const maxHeight = dialogType === "select" ? 620 : 520;
// 确保高度在最小值和最大值之间
return Math.min(Math.max(totalHeight, 100), 520);
return Math.min(Math.max(totalHeight, 100), maxHeight);
};
// 确定按钮点击事件
@ -156,6 +371,9 @@ document.addEventListener("DOMContentLoaded", () => {
case "buttons":
result = {};
break;
case "select":
result = {};
break;
default:
result = null;
}

View File

@ -83,7 +83,7 @@ const showSystemMessageBox = async (content, title = "") => {
* 显示一个系统级输入框组对话框
* @param {string[]|{label:string,value:string,hint:string}[]} options - 输入框配置可以是标签数组或者带属性的对象数组
* @param {string} [title] - 标题默认为空
* @returns {Promise<string[]|null>} 输入的内容数组
* @returns {Promise<string[]>} 输入的内容数组
*/
const showSystemInputBox = async (options, title = "") => {
// 确保 options 是数组
@ -130,7 +130,7 @@ const showSystemConfirmBox = async (content, title = "") => {
* 显示一个系统级按钮组对话框返回点击的按钮的索引和文本
* @param {string[]} buttons - 按钮文本数组
* @param {string} [title] - 标题默认为空
* @returns {Promise<{id: number, text: string}|null>} 选择的按钮信息
* @returns {Promise<{id: number, text: string}>} 选择的按钮信息
*/
const showSystemButtonBox = async (buttons, title = "") => {
return await createDialog({
@ -144,7 +144,7 @@ const showSystemButtonBox = async (buttons, title = "") => {
* 显示一个系统级多行文本输入框
* @param {string} [placeholder] - 输入框的提示文本
* @param {string} [defaultText] - 输入框的默认文本默认为空
* @returns {Promise<string|null>} 编辑后的文本
* @returns {Promise<string>} 编辑后的文本
*/
const showSystemTextArea = async (placeholder = "请输入", defaultText = "") => {
return await createDialog({
@ -154,10 +154,31 @@ const showSystemTextArea = async (placeholder = "请输入", defaultText = "") =
});
};
/**
* 显示一个系统级选择列表对话框
* @param {Array<string|{title: string, description?: string, icon?: string}>} items - 选项列表
* @param {object} [options] - 配置选项
* @param {string} [options.title] - 对话框标题
* @returns {Promise<{id: number, text: string, data: any}>} 选择的结果
*/
const showSystemSelectList = async (items, options = {}) => {
const defaultOptions = {
title: "",
};
const finalOptions = { ...defaultOptions, ...options };
return await createDialog({
type: "select",
title: finalOptions.title,
items: items,
});
};
module.exports = {
showSystemMessageBox,
showSystemInputBox,
showSystemConfirmBox,
showSystemButtonBox,
showSystemTextArea,
showSystemSelectList,
};

View File

@ -143,6 +143,12 @@
flex: 1;
}
/* 选择列表对话框的内容区域padding和高度 */
.dialog-select .content-wrapper {
padding: 16px 5px;
max-height: 600px;
}
#content {
line-height: 1.4;
font-size: 13px;
@ -298,7 +304,150 @@
#input,
#confirm,
#buttons,
#textarea {
#textarea,
#select {
display: none;
}
/* 选择列表样式 */
.select-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 505px;
overflow-y: auto;
}
.filter-input {
margin-bottom: 8px;
padding: 0 2px;
}
.filter-input 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);
}
.filter-input input:focus {
border-color: var(--input-focus);
outline: none;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
}
.select-list-container {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 360px;
overflow-y: auto;
}
.select-item {
display: flex;
align-items: center;
padding: 6px 8px;
border-radius: 8px;
cursor: pointer;
position: relative;
transform: translateY(0) scale(1);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.select-item.selected {
background-color: rgba(13, 110, 253, 0.1);
position: relative;
transform: translateY(-1px) scale(0.995);
will-change: transform;
}
.select-item-icon {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.select-item.selected .select-item-icon {
transform: scale(1.05);
filter: brightness(1.05);
}
:root[data-theme="dark"]
.select-list:not(.keyboard-nav)
.select-item:hover {
background-color: rgba(13, 110, 253, 0.2);
}
:root[data-theme="dark"] .select-item.selected {
background-color: rgba(13, 110, 253, 0.2);
}
/* 添加选择时的轻微阴影效果 */
.select-item.selected,
.select-list:not(.keyboard-nav) .select-item:hover {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
:root[data-theme="dark"] .select-item.selected,
:root[data-theme="dark"]
.select-list:not(.keyboard-nav)
.select-item:hover {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.select-item-icon {
width: 34px;
height: 34px;
margin-right: 8px;
border-radius: 4px;
overflow: hidden;
}
.select-item-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.select-item-content {
flex: 1;
min-width: 0;
}
.select-item-title {
font-size: 13px;
line-height: 1.4;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select-item-description {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:root[data-theme="dark"] .select-item-description {
color: rgba(255, 255, 255, 0.6);
}
/* 搜索结果高亮样式 */
.highlight {
color: #ec3535;
}
/* 隐藏确定和取消按钮 */
.dialog-select .button-bar {
display: none;
}
</style>
@ -330,6 +479,18 @@
<div id="button-container"></div>
</div>
<!-- 选择列表对话框 -->
<div id="select">
<div class="filter-input">
<input
type="text"
id="filter-input"
placeholder="输入关键字进行筛选"
/>
</div>
<div id="select-container" class="select-list"></div>
</div>
<!-- 文本区域对话框 -->
<div id="textarea">
<textarea id="text-content"></textarea>

View File

@ -574,6 +574,50 @@ interface quickcommandApi {
defaultText?: string
): Promise<string | null>;
/**
*
*
* ```js
* // plaintext
* var opt = []
* for (var i = 0; i < 15; i++) {
* // 每一个选项为文本格式
* opt.push(`选项` + i)
* }
* quickcommand.showSystemSelectList(opt).then(choise => {
* console.log(`选择的选项为${choise.text}`)
* })
*
* // json
* var opt = []
* for (var i = 0; i < 15; i++) {
* // 每一个选项为 json 格式, 使用clickFn注册选项单击事件时id属性是必需的
* opt.push({
* id: i,
* title: `选项${i}`,
* description: `选项${i}的描述`,
* icon: `http://www.u.tools/favicon.ico`,
* abcd: `选项${i}的自定义属性`,
* clickFn:function(e){console.log(e)}
* })
* }
* quickcommand.showSystemSelectList(opt, {optionType: 'json'}).then(choise => {
* console.log(`选择的选项为${choise.title}`)
* })
*
* ```
*
* @param selects
* @param options placeholder: 搜索框占位符optionType: 选项的格式plaintext
*/
showSystemSelectList(
selects: string[] | object[],
options?: {
placeholder: string;
optionType: "plaintext" | "json";
}
): Promise<{ id: number; text: string | object }>;
/**
*
* @param code