diff --git a/plugin/lib/dialog/controller.js b/plugin/lib/dialog/controller.js index 5d172f3..d834cfa 100644 --- a/plugin/lib/dialog/controller.js +++ b/plugin/lib/dialog/controller.js @@ -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) + + `${text.slice(start, end + 1)}` + + text.slice(end + 1) + ); + }; + + if (typeof item === "string" || typeof item === "number") { + const highlightedText = highlightText( + String(item), + filterInput.value + ); + div.innerHTML = ` +
+

${highlightedText}

+
+ `; + } else { + const highlightedTitle = highlightText( + item.title, + filterInput.value + ); + const highlightedDesc = item.description + ? highlightText(item.description, filterInput.value) + : ""; + div.innerHTML = ` + ${ + item.icon + ? ` +
+ +
+ ` + : "" + } +
+

${highlightedTitle}

+ ${ + item.description + ? ` +

${highlightedDesc}

+ ` + : "" + } +
+ `; + } + 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; } diff --git a/plugin/lib/dialog/service.js b/plugin/lib/dialog/service.js index 5e98bbc..d45a8db 100644 --- a/plugin/lib/dialog/service.js +++ b/plugin/lib/dialog/service.js @@ -83,7 +83,7 @@ const showSystemMessageBox = async (content, title = "") => { * 显示一个系统级输入框组对话框 * @param {string[]|{label:string,value:string,hint:string}[]} options - 输入框配置,可以是标签数组或者带属性的对象数组 * @param {string} [title] - 标题,默认为空 - * @returns {Promise} 输入的内容数组 + * @returns {Promise} 输入的内容数组 */ 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} 编辑后的文本 + * @returns {Promise} 编辑后的文本 */ const showSystemTextArea = async (placeholder = "请输入", defaultText = "") => { return await createDialog({ @@ -154,10 +154,31 @@ const showSystemTextArea = async (placeholder = "请输入", defaultText = "") = }); }; +/** + * 显示一个系统级选择列表对话框 + * @param {Array} 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, }; diff --git a/plugin/lib/dialog/view.html b/plugin/lib/dialog/view.html index 83e99cb..396039b 100644 --- a/plugin/lib/dialog/view.html +++ b/plugin/lib/dialog/view.html @@ -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; } @@ -330,6 +479,18 @@
+ +
+
+ +
+
+
+
diff --git a/src/plugins/monaco/types/quickcommand.api.d.ts b/src/plugins/monaco/types/quickcommand.api.d.ts index e6475d9..c32c442 100644 --- a/src/plugins/monaco/types/quickcommand.api.d.ts +++ b/src/plugins/monaco/types/quickcommand.api.d.ts @@ -574,6 +574,50 @@ interface quickcommandApi { defaultText?: string ): Promise; + /** + * 显示一个支持搜索的系统级选项列表,选项类型为文本时,返回选择的索引和文本,为对象时,返回选择的对象 + * + * ```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 代码