// ==UserScript== // @name 通用网页脚本框架(重构版) // @namespace https://github.com/hjdhnx/drpy-node // @description 日志、右下角弹窗、按钮皮肤、可配置布局、按钮集合弹窗、按钮开关、定时任务等;结构化、可扩展。 // @version 2.0.2 // @author taoist (refactor by chatgpt) // @match https://*.baidu.com/* // @match https://www.baidu.com/* // @match https://connect.huaweicloud.com/* // @match https://*.huaweicloud.com/* // @match https://*.iconfont.cn/* // @match https://*.ziwierp.cn/* // @run-at document-end // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // ==/UserScript== /** * 本脚本对原有“智能剪切板推送”脚本进行结构化重构,目标: * 1) 更易读:模块化、注释、类型提示(JSDoc)。 * 2) 易扩展:统一的 Action 注册与按钮渲染;主题与布局可配置;开关与定时任务可复用。 * 3) 零依赖:不再注入 jQuery。 * * ——— 如何扩展(最重要)——— * 1. 在 ACTIONS 中新增一个 action: * { * id: 'hello', // 唯一 ID * label: '打个招呼', // 按钮文字 * column: 1, // 放在第几列(1 ~ 5) * handler() { alert('hi'); } * } * * 2. 若想把多个按钮放到“按钮集合弹窗”,把它们的 group 指定为同一个名字: * { id:'tf', label:'开逃犯', group:'开关集', handler(){ ... } } * 点击“开关集”主按钮时会弹出集合弹窗,里面包含同组按钮。 * * 3. 定时任务:Scheduler.registerDaily('08:30', () => { ... }, 'taskKey'); * 每天 08:30 触发;同一分钟只触发一次。 * * 4. 主题:Theme.next() 可切换皮肤;Theme.apply() 应用主题到组件。 * * 5. 日志:使用 Logger.log(...);点击“隐藏日志”可隐藏/显示。 */ /* 变更说明(修复): - GroupPopup.addButton 支持 isToggle(会显示 inset/outset)并把状态保存在 store。 - 主按钮 openGroup(name) 改为 toggleGroup(name),点击可切换显示/隐藏。 - 点击 overlay(弹窗外部)也会关闭弹窗。 - 点击组内按钮后(无论 toggle 还是普通)会收起弹窗(如需改为不收起可调整)。 */ (function () { 'use strict'; /** *************************** 基础配置 ******************************** */ const META = Object.freeze({ version: '2.0.2', name: '通用网页脚本框架(重构版)' }); const CONFIG = { buttonTop: 280, popTop: 150, baseLeft: 0, columnWidth: 70, columnGap: 70, buttonHeight: 24, layoutMode: 'fixed', // 'fixed' or 'auto' layoutOffset: 10, themes: [ { name: '紫色起源', fg: '#E0EEEE', bg: '#9370DB' }, { name: '淡绿生机', fg: '#BFEFFF', bg: '#BDB76B' }, { name: '丰收时节', fg: '#E0EEE0', bg: '#CD661D' }, { name: '粉色佳人', fg: '#FFFAFA', bg: '#FFB6C1' }, { name: '黑白优雅', fg: '#111', bg: '#eee' }, // 新增渐变色皮肤 { name: '清新蓝绿', fg: '#ffffff', bg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, { name: '热情夕阳', fg: '#4a2f2f', bg: 'linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%)' }, { name: '高级紫罗兰', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)' }, { name: '极光青绿', fg: '#083b2e', bg: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }, { name: '科技未来蓝紫', fg: '#ffffff', bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, // 新增更多渐变色皮肤 { name: '日落金橙', fg: '#ffffff', bg: 'linear-gradient(135deg, #f6d365 0%, #fda085 100%)' }, { name: '薄荷清凉', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%)' }, { name: '浪漫粉紫', fg: '#ffffff', bg: 'linear-gradient(135deg, #ff758c 0%, #ff7eb3 100%)' }, { name: '深海蓝', fg: '#ffffff', bg: 'linear-gradient(135deg, #0c2b5b 0%, #204584 100%)' }, { name: '森林绿意', fg: '#ffffff', bg: 'linear-gradient(135deg, #134e5e 0%, #71b280 100%)' }, { name: '莓果甜心', fg: '#ffffff', bg: 'linear-gradient(135deg, #c71d6f 0%, #d09693 100%)' }, { name: '柠檬青柚', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #96fbc4 0%, #f9f586 100%)' }, { name: '星空紫', fg: '#ffffff', bg: 'linear-gradient(135deg, #231557 0%, #44107a 29%, #ff1361 67%, #fff800 100%)' }, { name: '珊瑚橙红', fg: '#ffffff', bg: 'linear-gradient(135deg, #ff9966 0%, #ff5e62 100%)' }, { name: '冰川蓝白', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%)' }, // 新增现代感皮肤 { name: '赛博朋克', fg: '#00ffff', bg: 'linear-gradient(135deg, #0f0f23 0%, #2d1b69 50%, #ff006e 100%)' }, { name: '霓虹夜色', fg: '#ffffff', bg: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #e94560 100%)' }, { name: '极简黑金', fg: '#ffd700', bg: 'linear-gradient(135deg, #000000 0%, #434343 100%)' }, { name: '银河星尘', fg: '#ffffff', bg: 'linear-gradient(135deg, #2c3e50 0%, #3498db 50%, #9b59b6 100%)' }, { name: '电光蓝紫', fg: '#ffffff', bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)' }, { name: '炫彩极光', fg: '#ffffff', bg: 'linear-gradient(135deg, #ff0844 0%, #ffb199 25%, #00d4ff 50%, #90e0ef 75%, #a8dadc 100%)' }, { name: '暗黑科技', fg: '#00ff41', bg: 'linear-gradient(135deg, #0d1421 0%, #1a252f 50%, #2a3441 100%)' }, { name: '彩虹渐变', fg: '#ffffff', bg: 'linear-gradient(135deg, #ff0000 0%, #ff8000 16.66%, #ffff00 33.33%, #80ff00 50%, #00ff80 66.66%, #0080ff 83.33%, #8000ff 100%)' }, // 新增自然风皮肤 { name: '樱花飞舞', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 50%, #ff9a9e 100%)' }, { name: '秋叶满山', fg: '#ffffff', bg: 'linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff6b6b 100%)' }, { name: '海洋深处', fg: '#ffffff', bg: 'linear-gradient(135deg, #667db6 0%, #0082c8 50%, #0052d4 100%)' }, { name: '翡翠森林', fg: '#ffffff', bg: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)' }, { name: '薰衣草田', fg: '#ffffff', bg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }, // 新增艺术感皮肤 { name: '油画印象', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 30%, #ff9a9e 60%, #fecfef 100%)' }, { name: '水彩渲染', fg: '#ffffff', bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #f5576c 75%, #4facfe 100%)' }, { name: '抽象几何', fg: '#ffffff', bg: 'linear-gradient(45deg, #ff6b6b 0%, #4ecdc4 25%, #45b7d1 50%, #96ceb4 75%, #ffeaa7 100%)' }, { name: '梦幻极光', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 25%, #d299c2 50%, #fef9d7 75%, #dae2f8 100%)' }, { name: '水墨丹青', fg: '#ffffff', bg: 'linear-gradient(135deg, #2c3e50 0%, #34495e 30%, #7f8c8d 60%, #95a5a6 100%)' }, { name: '火焰燃烧', fg: '#ffffff', bg: 'linear-gradient(135deg, #ff4e50 0%, #f9ca24 50%, #ff6348 100%)' }, { name: '冰雪奇缘', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #e3f2fd 0%, #bbdefb 25%, #90caf9 50%, #64b5f6 75%, #42a5f5 100%)' }, { name: '紫罗兰梦', fg: '#ffffff', bg: 'linear-gradient(135deg, #8e44ad 0%, #9b59b6 25%, #af7ac5 50%, #c39bd3 75%, #d7bde2 100%)' }, // 新增经典配色 { name: '复古胶片', fg: '#f4f4f4', bg: 'linear-gradient(135deg, #8b5a3c 0%, #d4a574 50%, #f4e4bc 100%)' }, { name: '工业风格', fg: '#ffffff', bg: 'linear-gradient(135deg, #2c3e50 0%, #34495e 50%, #95a5a6 100%)' }, { name: '马卡龙色', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #ffeaa7 0%, #fab1a0 25%, #fd79a8 50%, #a29bfe 75%, #74b9ff 100%)' }, { name: '暗夜精灵', fg: '#00d4aa', bg: 'linear-gradient(135deg, #0c0c0c 0%, #1a1a1a 50%, #2d2d2d 100%)' }, // 新增时尚潮流皮肤 { name: '玫瑰金辉', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #f8cdda 0%, #1d2b64 100%)' }, { name: '翡翠绿洲', fg: '#ffffff', bg: 'linear-gradient(135deg, #134e5e 0%, #71b280 100%)' }, { name: '琥珀夕照', fg: '#ffffff', bg: 'linear-gradient(135deg, #fc4a1a 0%, #f7b733 100%)' }, { name: '深邃蓝海', fg: '#ffffff', bg: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 100%)' }, { name: '紫晶魅惑', fg: '#ffffff', bg: 'linear-gradient(135deg, #8360c3 0%, #2ebf91 100%)' }, { name: '橙红烈焰', fg: '#ffffff', bg: 'linear-gradient(135deg, #ff512f 0%, #dd2476 100%)' }, { name: '青春活力', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%)' }, { name: '梦幻粉紫', fg: '#ffffff', bg: 'linear-gradient(135deg, #cc2b5e 0%, #753a88 100%)' }, { name: '金属质感', fg: '#ffffff', bg: 'linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%)' }, { name: '炫酷黑红', fg: '#ffffff', bg: 'linear-gradient(135deg, #000000 0%, #e74c3c 100%)' }, // 新增自然风光皮肤 { name: '晨曦微光', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' }, { name: '暮色苍茫', fg: '#ffffff', bg: 'linear-gradient(135deg, #2c3e50 0%, #fd746c 100%)' }, { name: '春意盎然', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%)' }, { name: '秋韵浓浓', fg: '#ffffff', bg: 'linear-gradient(135deg, #f77062 0%, #fe5196 100%)' }, { name: '冬雪皑皑', fg: '#2d2d2d', bg: 'linear-gradient(135deg, #e6ddd4 0%, #d5def5 100%)' }, { name: '夏日清凉', fg: '#ffffff', bg: 'linear-gradient(135deg, #00b4db 0%, #0083b0 100%)' }, // 新增科幻未来皮肤 { name: '星际穿越', fg: '#ffffff', bg: 'linear-gradient(135deg, #0f0f23 0%, #8e44ad 50%, #3498db 100%)' }, { name: '量子空间', fg: '#00ffff', bg: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)' }, { name: '机械战警', fg: '#ffffff', bg: 'linear-gradient(135deg, #434343 0%, #000000 50%, #ff6b6b 100%)' }, { name: '虚拟现实', fg: '#ffffff', bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)' }, { name: '时空隧道', fg: '#ffffff', bg: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #43e97b 100%)' }, // 新增奢华典雅皮肤 { name: '皇室紫金', fg: '#ffd700', bg: 'linear-gradient(135deg, #2c1810 0%, #8e44ad 50%, #f39c12 100%)' }, { name: '贵族蓝银', fg: '#ffffff', bg: 'linear-gradient(135deg, #2c3e50 0%, #3498db 50%, #ecf0f1 100%)' }, { name: '典雅黑白', fg: '#ffffff', bg: 'linear-gradient(135deg, #000000 0%, #434343 50%, #ffffff 100%)' }, { name: '奢华红金', fg: '#ffd700', bg: 'linear-gradient(135deg, #8b0000 0%, #dc143c 50%, #ffd700 100%)' }, { name: '翡翠宝石', fg: '#ffffff', bg: 'linear-gradient(135deg, #134e5e 0%, #71b280 50%, #a8e6cf 100%)' }, ], defaultThemeIndex: 0, storagePrefix: 'tmx.framework.' }; function getLayoutOffset(defaultOffset = 10) { if (CONFIG.layoutMode === 'auto') { const isMobile = /Android|iPhone|SymbianOS|Windows Phone|iPad|iPod/i.test(navigator.userAgent); if (isMobile) return 10; const h = window.screen.height; if (h === 1080) return 300; if (h === 768) return 100; if (h === 720) return 50; if (h < 720) return 0; if (h > 1080) return 500; return defaultOffset; } return Number(CONFIG.layoutOffset) || defaultOffset; } /** *************************** 存储小工具 ******************************* */ class Store { constructor(prefix) { this.prefix = prefix; this.ls = window.localStorage; this.ss = window.sessionStorage; // 检测是否支持GM存储API this.hasGMStorage = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined'; } key(k) { return `${this.prefix}${k}`; } get(k, d = null) { try { if (this.hasGMStorage) { // 使用GM全局存储 const v = GM_getValue(this.key(k), null); return v == null ? d : JSON.parse(v); } else { // 降级到localStorage const v = this.ls.getItem(this.key(k)); return v == null ? d : JSON.parse(v); } } catch (e) { console.warn('存储读取失败:', e); return d; } } set(k, v) { try { if (this.hasGMStorage) { // 使用GM全局存储 GM_setValue(this.key(k), JSON.stringify(v)); } else { // 降级到localStorage this.ls.setItem(this.key(k), JSON.stringify(v)); } } catch (e) { console.warn('存储写入失败:', e); } } remove(k) { try { if (this.hasGMStorage) { // 使用GM全局存储 if (typeof GM_deleteValue !== 'undefined') { GM_deleteValue(this.key(k)); } else { GM_setValue(this.key(k), null); } } else { // 降级到localStorage this.ls.removeItem(this.key(k)); } } catch (e) { console.warn('存储删除失败:', e); } } // Session存储仍使用sessionStorage(因为GM不支持session级别存储) sget(k, d = null) { try { const v = this.ss.getItem(this.key(k)); return v == null ? d : JSON.parse(v); } catch (e) { console.warn('Session存储读取失败:', e); return d; } } sset(k, v) { try { this.ss.setItem(this.key(k), JSON.stringify(v)); } catch (e) { console.warn('Session存储写入失败:', e); } } sremove(k) { try { this.ss.removeItem(this.key(k)); } catch (e) { console.warn('Session存储删除失败:', e); } } // 获取所有存储的键(仅GM模式支持) getAllKeys() { if (this.hasGMStorage && typeof GM_listValues !== 'undefined') { try { return GM_listValues().filter(key => key.startsWith(this.prefix)); } catch (e) { console.warn('获取存储键列表失败:', e); return []; } } return []; } // 获取存储模式信息 getStorageInfo() { return { mode: this.hasGMStorage ? 'GM全局存储' : 'localStorage', crossDomain: this.hasGMStorage, prefix: this.prefix }; } } const store = new Store(CONFIG.storagePrefix); /** *************************** DOM 创建助手 ****************************** */ const h = (tag, attrs = {}, children = []) => { const el = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) { if (k === 'style' && typeof v === 'object') Object.assign(el.style, v); else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v); else if (v != null) el.setAttribute(k, String(v)); } for (const child of [].concat(children)) { if (child == null) continue; el.appendChild(typeof child === 'string' ? document.createTextNode(child) : child); } return el; }; /** *************************** 主题 ************************************* */ const Theme = { index: store.get('theme.index', CONFIG.defaultThemeIndex), get current() { return CONFIG.themes[this.index % CONFIG.themes.length]; }, next() { this.setIndex((this.index + 1) % CONFIG.themes.length); }, setIndex(i) { this.index = i; store.set('theme.index', i); this.apply(); }, apply() { document.documentElement.style.setProperty('--tmx-fg', this.current.fg); document.documentElement.style.setProperty('--tmx-bg', this.current.bg); document.documentElement.style.setProperty('--tmx-btn-h', CONFIG.buttonHeight + 'px'); } }; /** *************************** 日志(简化) ***************************** */ const Logger = (() => { let el, hooked = false, orig = { log: console.log, clear: console.clear }; function ensure() { if (el) return; // 检测是否为移动端设备 const isMobile = /Android|iPhone|SymbianOS|Windows Phone|iPad|iPod/i.test(navigator.userAgent); // 计算日志窗口的最大宽度:按钮宽度 * 总列数(5) const loggerMaxWidth = CONFIG.columnWidth * 5; // 70 * 5 = 350px let loggerStyle; if (isMobile) { // 移动端:日志窗体在隐藏日志按钮上方 const hideLogBtn = buttonMap.get('toggle-log'); let left = CONFIG.baseLeft + getLayoutOffset(); // 计算合适的日志窗口高度,确保不超出屏幕顶部 const viewportHeight = window.innerHeight; let maxLoggerHeight = Math.min(285, viewportHeight * 0.4); // 最大不超过视窗高度的40% let top = CONFIG.buttonTop - maxLoggerHeight - 10; // 日志窗体高度 + 10px间距 // 如果隐藏日志按钮已存在,根据其位置动态调整 if (hideLogBtn) { const btnRect = hideLogBtn.getBoundingClientRect(); left = btnRect.left; top = btnRect.top - maxLoggerHeight - 10; } // 确保不超出视窗顶部,留出至少10px边距 if (top < 10) { top = 10; // 如果顶部空间不足,重新计算高度 const availableHeight = (hideLogBtn ? hideLogBtn.getBoundingClientRect().top : CONFIG.buttonTop) - 20; if (availableHeight > 100) { maxLoggerHeight = Math.min(maxLoggerHeight, availableHeight); } } loggerStyle = { position: 'fixed', left: left + 'px', top: top + 'px', minWidth: '220px', maxWidth: Math.min(loggerMaxWidth, window.innerWidth - 10) + 'px', // 移动端:5个按钮宽度或屏幕宽度-10px maxHeight: maxLoggerHeight + 'px', overflow: 'auto', fontFamily: 'Helvetica,Arial,sans-serif', fontSize: '12px', fontWeight: 'bold', padding: '6px', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', border: '1px solid #aaa', zIndex: 2147483640, // 降低层级,确保GroupPopup在上方 opacity: 0.9, wordWrap: 'break-word', whiteSpace: 'pre-wrap' }; } else { // PC端:保持原有位置(最后一列按钮右边) loggerStyle = { position: 'fixed', left: (CONFIG.baseLeft + getLayoutOffset() + loggerMaxWidth) + 'px', top: (CONFIG.buttonTop + 3) + 'px', minWidth: '220px', maxWidth: loggerMaxWidth + 'px', // PC端使用计算出的最大宽度 maxHeight: '285px', overflow: 'auto', fontFamily: 'Helvetica,Arial,sans-serif', fontSize: '12px', fontWeight: 'bold', padding: '6px', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', border: '1px solid #aaa', zIndex: 2147483646, opacity: 0.9, wordWrap: 'break-word', whiteSpace: 'pre-wrap' }; } el = h('div', { id: 'tmx-logger', style: loggerStyle }); document.body.appendChild(el); } function hook() { if (hooked) return; ensure(); console.log = (...args) => { append(args.join(' ')); orig.log.apply(console, args); }; console.clear = () => { clear(); orig.clear.apply(console); }; hooked = true; } function append(text) { ensure(); const row = h('div', { style: { lineHeight: '18px', background: el.children.length % 2 ? 'rgba(255,255,255,0.2)' : '' } }, text); el.appendChild(row); el.scrollTop = el.scrollHeight - el.clientHeight; } function clear() { if (el) el.innerHTML = ''; } function hide() { if (el) el.style.display = 'none'; } function show() { ensure(); el.style.display = ''; } function applyTheme() { if (el) { el.style.background = 'var(--tmx-bg)'; el.style.color = 'var(--tmx-fg)'; } } return { hook, append, clear, hide, show, applyTheme }; })(); /** *************************** 右下角弹窗 ******************************** */ const Toast = (() => { let root, content, titleEl, minBtn; function ensure() { if (root) return; const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); root = h('div', { id: 'tmx-toast', style: { position: 'fixed', right: '10px', bottom: '10px', minWidth: isMobile ? '200px' : '250px', maxWidth: isMobile ? '90vw' : '400px', width: 'auto', border: '1px solid #aaa', background: '#fff', zIndex: 2147483645, display: 'none' } }); const header = h('div', { style: { height: '36px', lineHeight: '36px', padding: '0 8px', color: 'var(--tmx-fg)', background: 'var(--tmx-bg)', borderBottom: '1px solid #aaa', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }); titleEl = h('span', {}, '通知'); const btns = h('div', { style: { display: 'flex', gap: '5px' } }); minBtn = h('button', { style: { background: 'none', border: 'none', color: 'var(--tmx-fg)', fontSize: '16px', cursor: 'pointer', padding: '2px 6px', borderRadius: '2px', lineHeight: '1' } }, '−'); const closeBtn = h('button', { style: { background: 'none', border: 'none', color: 'var(--tmx-fg)', fontSize: '16px', cursor: 'pointer', padding: '2px 6px', borderRadius: '2px', lineHeight: '1' } }, '×'); btns.append(minBtn, closeBtn); header.append(titleEl, btns); content = h('div', { style: { minHeight: '60px', maxHeight: isMobile ? '40vh' : '300px', width: '100%', overflow: 'auto', fontSize: '13px', fontWeight: 'normal', // 确保文字不加粗 padding: '8px', textAlign: 'left', background: '#fff', // 设置内容区域背景色 borderTop: '1px solid #eee', // 添加顶部边框分隔线 borderRight: '1px solid #eee' // 添加右边框线条 } }); root.append(header, content); document.body.appendChild(root); let expanded = true; minBtn.addEventListener('click', () => { expanded = !expanded; if (expanded) { // 展开状态:恢复到右下角 content.style.display = ''; header.style.display = ''; // 重置header的flex布局样式 header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; root.style.right = '10px'; root.style.bottom = '10px'; root.style.minWidth = isMobile ? '200px' : '250px'; root.style.maxWidth = isMobile ? '90vw' : '400px'; root.style.width = 'auto'; root.style.height = ''; root.style.padding = ''; root.style.borderRadius = ''; root.style.boxShadow = ''; root.style.fontSize = ''; root.style.display = ''; // 恢复默认display root.style.justifyContent = ''; // 清除flex属性 root.style.alignItems = ''; // 清除flex属性 root.style.boxSizing = ''; root.style.background = ''; // 清除背景色 root.style.color = ''; // 清除文字颜色 root.style.cursor = ''; // 清除鼠标样式 content.style.background = '#fff'; // 重置内容区域背景色 content.style.borderTop = '1px solid #eee'; // 重置顶部边框 content.style.borderRight = '1px solid #eee'; // 重置右边框 content.style.fontWeight = 'normal'; // 重置字体粗细 // 清空最小化内容 - 移除所有直接添加到root的子元素(除了header和content) const childrenToRemove = []; for (let child of root.children) { if (child !== header && child !== content) { childrenToRemove.push(child); } } childrenToRemove.forEach(child => child.remove()); // 弹窗还原后重新计算调试代码容器位置 if (window.DebugWindow && window.DebugWindow.updateMinimizedContainerPosition) { window.DebugWindow.updateMinimizedContainerPosition(); } } else { // 最小化状态:固定在最右下角,样式与调试窗口一致 content.style.display = 'none'; header.style.display = 'none'; // 弹窗提示固定在底部 root.style.right = '10px'; root.style.bottom = '10px'; root.style.width = '120px'; // 设置固定宽度 root.style.minWidth = ''; // 清除最小宽度限制 root.style.maxWidth = ''; // 清除最大宽度限制 root.style.height = '32px'; // 设置固定高度 root.style.padding = '8px 12px'; // 与调试窗口最小化项一致 root.style.borderRadius = '4px'; // 与调试窗口最小化项一致 root.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)'; // 与调试窗口最小化项一致 root.style.fontSize = '12px'; // 与调试窗口最小化项一致 root.style.background = 'var(--tmx-bg)'; // 添加背景颜色,与全局皮肤色保持一致 root.style.color = 'var(--tmx-fg)'; // 添加文字颜色 root.style.display = 'flex'; // 使用flex布局,与调试窗口一致 root.style.justifyContent = 'space-between'; // 与调试窗口布局一致 root.style.alignItems = 'center'; // 垂直居中 root.style.boxSizing = 'border-box'; // 确保padding包含在尺寸内 root.style.cursor = 'pointer'; // 创建最小化内容 const minimizedTitle = h('span', { style: { fontWeight: 'normal' // 确保最小化标题文字不加粗 } }, titleEl.textContent); const minimizedCloseBtn = h('span', { style: { cursor: 'pointer', fontSize: '14px', fontWeight: 'bold' }, onclick: (e) => { e.stopPropagation(); root.remove(); } }, '×'); // 先清理可能存在的旧的最小化元素 const childrenToRemove = []; for (let child of root.children) { if (child !== header && child !== content) { childrenToRemove.push(child); } } childrenToRemove.forEach(child => child.remove()); root.appendChild(minimizedTitle); root.appendChild(minimizedCloseBtn); // 弹窗最小化后重新计算调试代码容器位置 if (window.DebugWindow && window.DebugWindow.updateMinimizedContainerPosition) { window.DebugWindow.updateMinimizedContainerPosition(); } } }); // 点击最小化状态时展开 root.addEventListener('click', (e) => { if (!expanded && (e.target === root || e.target === root.minimizedContent || e.target.tagName === 'SPAN' && e.target.textContent !== '×')) { minBtn.click(); } }); closeBtn.addEventListener('click', () => root.remove()); } function show(title, html) { ensure(); titleEl.textContent = title || '通知'; if (typeof html === 'string') content.innerHTML = html; else { content.innerHTML = ''; content.append(html); } root.style.display = ''; } function resize(hh, ww) { ensure(); content.style.height = Math.max(60, hh) + 'px'; root.style.width = Math.max(220, ww) + 'px'; } function hide() { if (root) { root.remove(); root = null; content = null; titleEl = null; minBtn = null; } } function applyTheme() { ensure(); } return { show, resize, hide, applyTheme }; })(); /** *************************** 按钮列 *********************************** */ class Columns { constructor() { this.columns = new Map(); for (let i = 1; i <= 5; i++) this.ensure(i); } ensure(index) { if (this.columns.has(index)) return this.columns.get(index); const offset = getLayoutOffset(); const left = CONFIG.baseLeft + offset + (index - 1) * CONFIG.columnGap; const box = h('div', { style: { position: 'fixed', top: CONFIG.buttonTop + 'px', left: left + 'px', width: CONFIG.columnWidth + 'px', zIndex: 2147483646 } }); document.body.appendChild(box); this.columns.set(index, box); return box; } addButton(index, label, onClick) { const box = this.ensure(index); const btn = h('button', { style: btnStyle(), title: label }, label); btn.addEventListener('click', onClick); box.appendChild(btn); return btn; } } function btnStyle() { return { display: 'block', width: '100%', height: 'var(--tmx-btn-h)', marginTop: '6px', color: 'var(--tmx-fg)', background: 'var(--tmx-bg)', border: '1px solid #999', cursor: 'pointer' }; } /** *************************** Z-Index管理器 ******************************* */ const ZIndexManager = { baseZIndex: 2147483647, // 最高基础层级 currentZIndex: 2147483647, getNextZIndex() { return ++this.currentZIndex; }, // 确保元素在最上层 bringToTop(element) { element.style.zIndex = this.getNextZIndex(); } }; /** *************************** 组弹窗(支持 toggle 按钮) **************** */ class GroupPopup { constructor(title) { this.title = title; // overlay covers full screen to allow click-outside-to-close this.overlay = h('div', { style: { position: 'fixed', inset: '0', zIndex: 2147483645, display: 'none', background: 'rgba(0,0,0,0)', pointerEvents: 'none' // 允许点击穿透到下层 } }); // 添加关闭按钮到panel const closeBtn = h('button', { style: { position: 'absolute', top: '5px', right: '5px', width: '20px', height: '20px', border: 'none', background: '#ff6b6b', color: 'white', borderRadius: '50%', cursor: 'pointer', fontSize: '12px', lineHeight: '1' } }, '×'); closeBtn.addEventListener('click', () => this.hide()); // 为panel单独设置pointer-events this.panelClickHandler = (e) => { e.stopPropagation(); }; // 创建固定定位的wrapper this.panelWrapper = h('div', { style: { position: 'fixed', top: CONFIG.popTop + 'px', left: getLayoutOffset() + 'px', pointerEvents: 'auto' } }); this.panel = h('div', { style: { position: 'relative', width: 'min(480px, calc(100vw - 20px))', // 5列按钮宽度,移动端不超出 padding: '10px 8px', background: '#B2DFEE', color: 'green', textAlign: 'center', border: '2px solid #ccc', boxSizing: 'border-box' } }); this.panel.addEventListener('click', this.panelClickHandler); // 添加关闭按钮到panel this.panel.appendChild(closeBtn); const titleBar = h('div', { style: { marginBottom: '6px', fontWeight: 'bold' } }, title); this.btnWrap = h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px', justifyContent: 'flex-start', alignItems: 'flex-start', minHeight: '40px' } }); this.panel.append(titleBar, this.btnWrap); this.panelWrapper.appendChild(this.panel); this.overlay.append(this.panelWrapper); document.body.appendChild(this.overlay); this.visible = false; } /** * 添加按钮 * @param {string} label * @param {Function} handler // will be called either as handler(btn) for toggles or handler() for normal * @param {Object} options { isToggle:boolean, storeKey:string } */ addButton(label, handler, options = {}) { const btn = h('button', { style: Object.assign({}, btnStyle(), { width: 'calc(20% - 3.2px)', // 每行5列,减去gap间距 minWidth: '60px', maxWidth: '80px', flex: '0 0 auto', padding: '3px 4px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: '12px' }), title: label }, label); // apply current theme colors btn.style.color = getComputedStyle(document.documentElement).getPropertyValue('--tmx-fg') || CONFIG.themes[0].fg; btn.style.background = getComputedStyle(document.documentElement).getPropertyValue('--tmx-bg') || CONFIG.themes[0].bg; if (options.isToggle && options.storeKey) { // read initial state from store let active = store.get(options.storeKey, 0) === 1; btn.style.borderStyle = active ? 'inset' : 'outset'; // click toggles state, calls handler with (active, btn) btn.addEventListener('click', (e) => { e.stopPropagation(); active = !active; btn.style.borderStyle = active ? 'inset' : 'outset'; try { handler(active, btn); } catch (err) { console.error(err); } this.hide(); // collapse after click (保持原版体验) }); } else { btn.addEventListener('click', (e) => { e.stopPropagation(); try { handler(btn); } catch (err) { console.error(err); } this.hide(); }); } this.btnWrap.appendChild(btn); return btn; } show() { this.overlay.style.display = ''; // 确保当前弹窗在最上层 ZIndexManager.bringToTop(this.overlay); this.visible = true; } hide() { this.overlay.style.display = 'none'; this.visible = false; } toggle() { this.visible ? this.hide() : this.show(); } } /** *************************** 定时任务存储系统 ************************** */ const ScheduledTaskStorage = { STORAGE_KEY: 'scheduled_tasks', // 获取所有定时任务 getAll() { try { return store.get(this.STORAGE_KEY, []); } catch (e) { console.error('获取定时任务失败:', e); return []; } }, // 保存定时任务 save(tasks) { try { store.set(this.STORAGE_KEY, tasks); return true; } catch (e) { console.error('保存定时任务失败:', e); return false; } }, // 添加定时任务 add(taskData) { const tasks = this.getAll(); if (typeof taskData === 'string') { // 兼容旧的调用方式 const [name, commandId, schedule] = arguments; taskData = { id: Date.now().toString(), name: name, commandId: commandId, schedule: schedule, enabled: true, createTime: Date.now(), lastRun: null, nextRun: this.calculateNextRun(schedule) }; } else { // 新的调用方式,传入完整的任务对象 if (!taskData.nextRun) { taskData.nextRun = this.calculateNextRun(taskData.schedule); } } tasks.push(taskData); return this.save(tasks) ? taskData : null; }, // 删除定时任务 remove(id) { const tasks = this.getAll(); const filtered = tasks.filter(task => task.id !== id); return this.save(filtered); }, // 更新定时任务 update(id, updates) { const tasks = this.getAll(); const taskIndex = tasks.findIndex(task => task.id === id); if (taskIndex !== -1) { tasks[taskIndex] = { ...tasks[taskIndex], ...updates }; if (updates.schedule) { tasks[taskIndex].nextRun = this.calculateNextRun(updates.schedule); } return this.save(tasks); } return false; }, // 计算下次执行时间 calculateNextRun(schedule) { const now = new Date(); const next = new Date(now); switch (schedule.type) { case 'interval': next.setMinutes(next.getMinutes() + schedule.minutes); break; case 'daily': const [hours, minutes] = schedule.time.split(':').map(Number); next.setHours(hours, minutes, 0, 0); if (next <= now) { next.setDate(next.getDate() + 1); } break; case 'weekly': const [weekHours, weekMinutes] = schedule.time.split(':').map(Number); next.setHours(weekHours, weekMinutes, 0, 0); const targetDay = schedule.dayOfWeek; // 0=Sunday, 1=Monday, ... const currentDay = next.getDay(); let daysToAdd = targetDay - currentDay; if (daysToAdd < 0 || (daysToAdd === 0 && next <= now)) { daysToAdd += 7; } next.setDate(next.getDate() + daysToAdd); break; case 'monthly': const [monthHours, monthMinutes] = schedule.time.split(':').map(Number); next.setHours(monthHours, monthMinutes, 0, 0); if (schedule.dayOfMonth === 'last') { // 每月最后一天 next.setMonth(next.getMonth() + 1, 0); if (next <= now) { next.setMonth(next.getMonth() + 1, 0); } } else { // 指定日期 next.setDate(schedule.dayOfMonth); if (next <= now) { next.setMonth(next.getMonth() + 1); } } break; default: next.setMinutes(next.getMinutes() + 1); } return next.toISOString(); } }; /** *************************** 增强定时任务调度器 ************************** */ const Scheduler = (() => { const dailyTasks = new Map(); const scheduledTasks = new Map(); let isRunning = false; function start() { if (isRunning) return; isRunning = true; // 加载已保存的定时任务 loadScheduledTasks(); setInterval(() => { const now = new Date(); const hh = String(now.getHours()).padStart(2, '0'); const mm = String(now.getMinutes()).padStart(2, '0'); const timeKey = `${hh}:${mm}`; const tag = `tick.${timeKey}`; if (store.sget(tag)) return; store.sset(tag, 1); setTimeout(() => store.sremove(tag), 65 * 1000); // 执行原有的每日任务 for (const [, t] of dailyTasks) { if (t.time === timeKey) { try { t.fn(); } catch (err) { console.error('[Scheduler]', err); } } } // 执行新的定时任务 checkScheduledTasks(now); }, 10 * 1000); } function loadScheduledTasks() { const tasks = ScheduledTaskStorage.getAll(); tasks.forEach(task => { if (task.enabled) { scheduledTasks.set(task.id, task); } }); console.log(`[Scheduler] 加载了 ${tasks.length} 个定时任务`); } function checkScheduledTasks(now) { for (const [taskId, task] of scheduledTasks) { if (!task.enabled) continue; const nextRun = new Date(task.nextRun); if (now >= nextRun) { executeScheduledTask(task); } } } function executeScheduledTask(task) { try { console.log(`[Scheduler] 执行定时任务: ${task.name}`); // 查找对应的指令 const commands = CommandStorage.getAll(); const command = commands.find(cmd => cmd.id === task.commandId); if (!command) { console.error(`[Scheduler] 找不到指令 ID: ${task.commandId}`); return; } // 执行指令代码 const result = eval(command.code); if (result !== undefined) { console.log(`[Scheduler] 任务执行结果:`, result); } // 更新任务状态 const now = new Date(); task.lastRun = now.toISOString(); task.nextRun = ScheduledTaskStorage.calculateNextRun(task.schedule); // 保存到存储 ScheduledTaskStorage.update(task.id, { lastRun: task.lastRun, nextRun: task.nextRun }); // 更新内存中的任务 scheduledTasks.set(task.id, task); Toast.show(`定时任务 "${task.name}" 执行完成`); } catch (error) { console.error(`[Scheduler] 任务执行失败: ${task.name}`, error); Toast.show(`定时任务执行失败: ${error.message}`, 'error'); } } function registerDaily(hhmm, fn, key) { dailyTasks.set(key || hhmm, { time: hhmm, fn }); } function unregister(key) { dailyTasks.delete(key); } function addScheduledTask(task) { if (task.enabled) { scheduledTasks.set(task.id, task); } } function removeScheduledTask(taskId) { scheduledTasks.delete(taskId); } function updateScheduledTask(taskId, updates) { const task = scheduledTasks.get(taskId); if (task) { Object.assign(task, updates); if (!task.enabled) { scheduledTasks.delete(taskId); } } } return { start, registerDaily, unregister, addScheduledTask, removeScheduledTask, updateScheduledTask, loadScheduledTasks, loadTasks: loadScheduledTasks // 为管理界面提供重新加载任务的方法 }; })(); /** *************************** Action 注册 ******************************** */ /** * 我这里把 group 内的开关(如 tf)加上额外字段 isToggle + storeKey, * 这样 GroupPopup.addButton 能自动读取和切换状态并显示凹陷效果。 */ const ACTIONS = [ // 第1列:隐藏日志、显按钮 { id: 'toggle-log', label: '隐藏日志', column: 1, handler: toggleLog }, { id: 'toggle-buttons', label: '显按钮', column: 1, handler: toggleButtons }, // 第2列:皮肤集、换皮肤 { id: 'skin-open', label: '皮肤集', column: 2, handler: toggleSkinSelector }, { id: 'theme', label: '换皮肤', column: 2, handler: switchTheme }, // 第3列:弹出提示、调试执行 { id: 'toast', label: '弹出提示', column: 3, handler: toggleToast }, { id: 'debug', label: '调试执行', column: 3, handler: executeDebugCode }, // 第4列:定时任务、推送文本 { id: 'schedule-open', label: '定时任务', column: 4, handler: toggleScheduleManager }, // 第5列:配置集、开关集、指令集 { id: 'cfg-open', label: '配置集', column: 5, handler: toggleGroup('配置集') }, { id: 'kgj-open', label: '开关集', column: 5, handler: toggleGroup('开关集') }, { id: 'command-open', label: '指令集', column: 5, handler: toggleCommandSelector }, // 组内按钮,带 isToggle + storeKey 的会显示凹陷效果 { id: 'tf', label: '开逃犯', group: '开关集', isToggle: true, storeKey: 'tf_killset', handler: makeToggle('tf', '开逃犯', '关逃犯', 'tf_killset') }, { id: 'tj', label: '开天剑', group: '开关集', handler: noop('开天剑') }, { id: 'bc', label: '开镖车', group: '开关集', handler: noop('开镖车') }, { id: 'bz', label: '开帮战', group: '开关集', handler: noop('开帮战') }, { id: 'hb', label: '开红包', group: '开关集', handler: noop('开红包') }, { id: 'qc', label: '开抢菜', group: '开关集', handler: noop('开抢菜') }, { id: 'dm', label: '开灯谜', group: '开关集', handler: noop('开灯谜') }, { id: 'js', label: '开救赎', group: '开关集', handler: noop('开救赎') }, { id: 'zx', label: '开智悬', group: '开关集', handler: noop('开智悬') }, { id: 'zxs', label: '设智悬', group: '开关集', handler: noop('设智悬') }, // 分组:配置集 { id: 'cfg-api', label: '剪切板API', group: '配置集', handler: configClipboardApi }, { id: 'cfg-code', label: '安全码', group: '配置集', handler: configSafeCode }, { id: 'cfg-remote-url', label: '远程指令URL', group: '配置集', handler: configRemoteCommandUrl }, { id: 'cfg-remote-enable', label: '启用远程指令', group: '配置集', isToggle: true, storeKey: 'remote_commands_enabled', handler: makeToggle('cfg-remote-enable', '启用远程指令', '禁用远程指令', 'remote_commands_enabled') }, // 组内按钮:推送文本 { id: 'cfg-push', label: '推送文本', column: 4, handler: pushClipboardText }, ]; /** *************************** 渲染与状态 ******************************** */ const columns = new Columns(); const groupMap = new Map(); const buttonMap = new Map(); function render() { // 按列渲染(独立按钮) for (const act of ACTIONS.filter(a => a.column)) { const btn = columns.addButton(act.column, act.label, () => act.handler()); buttonMap.set(act.id, btn); } // 分组渲染:先采集组 const groups = ACTIONS.filter(a => a.group).reduce((acc, a) => { (acc[a.group] = acc[a.group] || []).push(a); return acc; }, {}); for (const [name, acts] of Object.entries(groups)) { const gp = new GroupPopup(name); groupMap.set(name, gp); for (const a of acts) { // 把 isToggle / storeKey 转交给 gp.addButton gp.addButton(a.label, a.handler, { isToggle: !!a.isToggle, storeKey: a.storeKey }); } } } /** -------------------- 独立按钮行为 -------------------- */ function toggleLog() { // 使用store中的状态作为主要判断依据,DOM状态作为备用 const storedHidden = store.get('logger.hidden', 0) === 1; const domHidden = document.getElementById('tmx-logger')?.style.display === 'none'; const isCurrentlyHidden = storedHidden || domHidden; if (isCurrentlyHidden) { Logger.show(); store.set('logger.hidden', 0); const btn = buttonMap.get('toggle-log'); if (btn) { btn.textContent = '隐藏日志'; btn.style.borderStyle = 'inset'; } } else { Logger.hide(); store.set('logger.hidden', 1); const btn = buttonMap.get('toggle-log'); if (btn) { btn.textContent = '显示日志'; btn.style.borderStyle = 'outset'; } } } function toggleButtons() { const btn = buttonMap.get('toggle-buttons'); const nowHidden = Array.from(buttonMap.values()).some(b => b.style.visibility === 'hidden'); for (const [id, el] of buttonMap) { if (id === 'toggle-buttons') continue; el.style.visibility = nowHidden ? 'visible' : 'hidden'; } store.set('buttons.hidden', nowHidden ? 0 : 1); const hidden = store.get('buttons.hidden', 0) === 1; const boxes = columns.columns; const btnBox = btn ? btn.parentElement : null; if (hidden) { for (const [, box] of boxes) { if (box !== btnBox) { box.style.display = 'none'; } } if (btnBox) { btnBox.style.position = 'fixed'; btnBox.style.top = CONFIG.buttonTop + 'px'; btnBox.style.left = '0px'; btnBox.style.width = '18px'; btnBox.style.zIndex = ZIndexManager.getNextZIndex(); } if (btn) { btn.textContent = '<<'; btn.style.borderStyle = 'inset'; btn.style.width = '18px'; btn.style.minWidth = '18px'; btn.style.padding = '0'; btn.style.visibility = 'visible'; } } else { for (const [, box] of boxes) { box.style.display = ''; } if (btnBox) { let colIndex = 1; for (const [idx, b] of boxes) { if (b === btnBox) { colIndex = idx; break; } } const offset = getLayoutOffset(); const left = CONFIG.baseLeft + offset + (colIndex - 1) * CONFIG.columnGap; btnBox.style.position = 'fixed'; btnBox.style.left = left + 'px'; btnBox.style.top = CONFIG.buttonTop + 'px'; btnBox.style.width = CONFIG.columnWidth + 'px'; btnBox.style.zIndex = 2147483646; } if (btn) { btn.textContent = '隐按钮'; btn.style.borderStyle = 'inset'; btn.style.width = '100%'; btn.style.minWidth = ''; btn.style.padding = ''; } } } function switchTheme() { Theme.next(); Theme.apply(); Logger.applyTheme(); Toast.applyTheme(); Dialog.applyTheme(); // 同步按钮颜色 for (const [, el] of buttonMap) { el.style.color = 'var(--tmx-fg)'; el.style.background = 'var(--tmx-bg)'; } for (const [, gp] of groupMap) { Array.from(gp.btnWrap.children).forEach(b => { b.style.color = 'var(--tmx-fg)'; b.style.background = 'var(--tmx-bg)'; }); } console.log(`当前皮肤为 -- ${Theme.current.name}`); } /** *************************** 皮肤选择器 ******************************** */ class SkinSelector { constructor() { this.overlay = h('div', { style: { position: 'fixed', inset: '0', zIndex: 2147483646, display: 'none', background: 'rgba(0,0,0,0.3)' } }); this.overlay.addEventListener('click', (e) => { if (e.target === this.overlay) this.hide(); }); this.panel = h('div', { style: { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 'min(480px, calc(100vw - 20px))', maxHeight: '70vh', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', border: '2px solid #ccc', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0,0,0,0.3)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box' } }); // 固定的标题栏容器 const titleContainer = h('div', { style: { position: 'relative', padding: '15px 15px 0 15px', flexShrink: '0' } }); const titleBar = h('div', { style: { marginBottom: '15px', fontWeight: 'bold', fontSize: '16px', textAlign: 'center', borderBottom: '1px solid #ccc', paddingBottom: '10px' } }, `选择皮肤主题 (共${CONFIG.themes.length}套)`); this.closeBtn = h('button', { style: { position: 'absolute', top: '10px', right: '15px', background: 'transparent', border: 'none', fontSize: '20px', cursor: 'pointer', color: 'var(--tmx-fg)', fontWeight: 'bold' } }, '×'); this.closeBtn.addEventListener('click', () => this.hide()); titleContainer.append(titleBar, this.closeBtn); // 可滚动的皮肤网格容器 const skinContainer = h('div', { style: { flex: '1', overflow: 'auto', padding: '0 15px 15px 15px' } }); this.skinGrid = h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' } }); skinContainer.appendChild(this.skinGrid); this.panel.append(titleContainer, skinContainer); this.overlay.append(this.panel); document.body.appendChild(this.overlay); this.createSkinButtons(); this.visible = false; } createSkinButtons() { CONFIG.themes.forEach((theme, index) => { const skinBtn = h('div', { style: { padding: '8px 4px', border: '2px solid #ccc', borderRadius: '4px', cursor: 'pointer', textAlign: 'center', fontSize: '11px', fontWeight: 'bold', background: theme.bg, color: theme.fg, transition: 'all 0.2s ease', position: 'relative', minHeight: '45px', display: 'flex', alignItems: 'center', justifyContent: 'center', wordBreak: 'break-all', lineHeight: '1.2' }, title: theme.name }, theme.name); // 当前选中的皮肤添加特殊标识 if (index === Theme.index) { skinBtn.style.borderColor = '#007bff'; skinBtn.style.borderWidth = '3px'; skinBtn.style.boxShadow = '0 0 10px rgba(0,123,255,0.5)'; } skinBtn.addEventListener('mouseenter', () => { if (index !== Theme.index) { skinBtn.style.transform = 'scale(1.05)'; skinBtn.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; } }); skinBtn.addEventListener('mouseleave', () => { if (index !== Theme.index) { skinBtn.style.transform = 'scale(1)'; skinBtn.style.boxShadow = 'none'; } }); skinBtn.addEventListener('click', () => { // 移除之前选中的样式 this.skinGrid.querySelectorAll('div').forEach(btn => { btn.style.borderColor = '#ccc'; btn.style.borderWidth = '2px'; btn.style.boxShadow = 'none'; }); // 应用新皮肤 Theme.index = index; Theme.apply(); Logger.applyTheme(); Toast.applyTheme(); Dialog.applyTheme(); // 同步按钮颜色 for (const [, el] of buttonMap) { el.style.color = 'var(--tmx-fg)'; el.style.background = 'var(--tmx-bg)'; } for (const [, gp] of groupMap) { Array.from(gp.btnWrap.children).forEach(b => { b.style.color = 'var(--tmx-fg)'; b.style.background = 'var(--tmx-bg)'; }); } // 更新弹窗样式 this.panel.style.background = 'var(--tmx-bg)'; this.panel.style.color = 'var(--tmx-fg)'; this.closeBtn.style.color = 'var(--tmx-fg)'; // 标记当前选中 skinBtn.style.borderColor = '#007bff'; skinBtn.style.borderWidth = '3px'; skinBtn.style.boxShadow = '0 0 10px rgba(0,123,255,0.5)'; console.log(`已切换到皮肤: ${theme.name}`); }); this.skinGrid.appendChild(skinBtn); }); } show() { this.overlay.style.display = ''; this.visible = true; } hide() { this.overlay.style.display = 'none'; this.visible = false; } toggle() { this.visible ? this.hide() : this.show(); } } // 创建全局皮肤选择器实例 let skinSelector = null; function toggleSkinSelector(btnEl) { if (!skinSelector) { skinSelector = new SkinSelector(); } skinSelector.toggle(); if (btnEl) { btnEl.style.borderStyle = skinSelector.visible ? 'inset' : 'outset'; } } /** *************************** 指令存储系统 ******************************** */ const CommandStorage = { STORAGE_KEY: 'custom_commands', // 获取所有指令(包括本地和远程) getAll() { try { // 获取本地指令(使用全局存储) let localCommands = store.get(this.STORAGE_KEY, []); // 数据迁移:确保所有本地指令都有必要的字段 let needsSave = false; localCommands = localCommands.map(cmd => { if (!cmd.id) { cmd.id = Date.now().toString() + Math.random().toString(36).substr(2, 9); needsSave = true; console.warn('为指令添加缺失的ID:', cmd.name, cmd.id); } if (cmd.code === undefined || cmd.code === null) { cmd.code = ''; needsSave = true; console.warn('为指令添加缺失的代码字段:', cmd.name); } if (!cmd.name) { cmd.name = '未命名指令_' + cmd.id; needsSave = true; console.warn('为指令添加缺失的名称字段:', cmd.id); } // 标记为本地指令 cmd.isRemote = false; return cmd; }); // 如果有数据需要迁移,保存回存储 if (needsSave) { this.save(localCommands); console.log('指令数据迁移完成'); } // 获取远程指令(如果启用) let remoteCommands = []; if (store.get('remote_commands_enabled', 0) === 1) { remoteCommands = RemoteCommandStorage.getCache(); } // 合并指令:远程指令在前,本地指令在后 const allCommands = [...remoteCommands, ...localCommands]; return allCommands; } catch (e) { console.error('获取指令失败:', e); return []; } }, // 获取仅本地指令 getLocalOnly() { try { let commands = store.get(this.STORAGE_KEY, []); // 确保本地指令都有必要的字段 commands = commands.map(cmd => { if (!cmd.id) { cmd.id = Date.now().toString() + Math.random().toString(36).substr(2, 9); } if (cmd.code === undefined || cmd.code === null) { cmd.code = ''; } if (!cmd.name) { cmd.name = '未命名指令_' + cmd.id; } cmd.isRemote = false; return cmd; }); return commands; } catch (e) { console.error('获取本地指令失败:', e); return []; } }, // 保存指令(仅保存本地指令) save(commands) { try { // 过滤出本地指令 const localCommands = commands.filter(cmd => !cmd.isRemote); store.set(this.STORAGE_KEY, localCommands); return true; } catch (e) { console.error('保存指令失败:', e); return false; } }, // 添加指令(仅添加到本地) add(name, code, description = '') { const localCommands = this.getLocalOnly(); const newCommand = { id: Date.now().toString(), name: name, description: description, code: code, createTime: new Date().toISOString(), isRemote: false }; localCommands.push(newCommand); return this.save(localCommands); }, // 删除指令(仅删除本地指令) remove(id) { const localCommands = this.getLocalOnly(); const filtered = localCommands.filter(cmd => cmd.id !== id); return this.save(filtered); }, // 导入指令(仅导入到本地) import(commandsData) { try { if (Array.isArray(commandsData)) { const localCommands = this.getLocalOnly(); commandsData.forEach(cmd => { if (cmd.name && cmd.code) { localCommands.push({ id: Date.now().toString() + Math.random().toString(36).substr(2, 9), name: cmd.name, description: cmd.description || '', code: cmd.code, createTime: new Date().toISOString(), isRemote: false }); } }); return this.save(localCommands); } return false; } catch (e) { console.error('导入指令失败:', e); return false; } }, // 导出指令(仅导出本地指令) export() { const localCommands = this.getLocalOnly(); return localCommands.map(cmd => ({ name: cmd.name, description: cmd.description || '', code: cmd.code })); } }; /** *************************** 指令选择器 ******************************** */ class CommandSelector extends GroupPopup { constructor() { super('指令集'); this.updateCommandButtons(); } updateCommandButtons() { // 清空现有按钮 this.btnWrap.innerHTML = ''; const commands = CommandStorage.getAll(); // 创建导入按钮 this.addButton('导入指令', () => this.importCommands()); // 创建导出按钮 this.addButton('导出指令', () => this.exportCommands()); // 创建指令管理按钮 this.addButton('指令管理', () => this.manageCommands()); // 创建自定义指令按钮 commands.forEach(command => { const btn = this.addButton(command.name, () => this.executeCommand(command)); // 为自定义指令按钮添加右键删除功能 btn.addEventListener('contextmenu', (e) => { e.preventDefault(); if (confirm(`确定要删除指令 "${command.name}" 吗?`)) { CommandStorage.remove(command.id); this.updateCommandButtons(); console.log(`已删除指令: ${command.name}`); } }); btn.title = `${command.name}\n\n右键删除指令`; }); } executeCommand(command) { try { console.log(`执行指令: ${command.name}`); const result = eval(command.code); if (result !== undefined) { console.log('执行结果:', result); } Toast.show(`指令 "${command.name}" 执行完成`); } catch (error) { console.error('指令执行失败:', error); Toast.show(`指令执行失败: ${error.message}`, 'error'); } } importCommands() { const input = h('input', { type: 'file', accept: '.json', style: { display: 'none' } }); input.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { try { const data = JSON.parse(event.target.result); if (CommandStorage.import(data)) { this.updateCommandButtons(); Toast.show(`成功导入 ${data.length} 个指令`); } else { Toast.show('导入失败,请检查文件格式', 'error'); } } catch (error) { console.error('导入失败:', error); Toast.show('导入失败,文件格式错误', 'error'); } }; reader.readAsText(file); } }); document.body.appendChild(input); input.click(); document.body.removeChild(input); } exportCommands() { const commands = CommandStorage.export(); if (commands.length === 0) { Toast.show('没有可导出的指令', 'warning'); return; } const dataStr = JSON.stringify(commands, null, 2); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = h('a', { href: url, download: `custom_commands_${new Date().toISOString().slice(0, 10)}.json`, style: { display: 'none' } }); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Toast.show(`成功导出 ${commands.length} 个指令`); } manageCommands() { const commands = CommandStorage.getAll(); if (commands.length === 0) { Toast.show('没有可管理的指令', 'warning'); return; } // 创建指令管理弹窗 this.createManageDialog(commands); } createManageDialog(commands) { // 创建指令管理弹窗遮罩 const sortOverlay = h('div', { className: 'tmx-command-manage-dialog', style: { position: 'fixed', inset: '0', zIndex: 2147483646, display: 'flex', background: 'rgba(0,0,0,0.5)', alignItems: 'center', justifyContent: 'center' } }); // 创建排序弹窗面板 const sortPanel = h('div', { style: { width: '500px', maxWidth: '90vw', maxHeight: '80vh', background: '#fff', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0,0,0,0.3)', overflow: 'hidden', fontFamily: 'Arial, sans-serif', display: 'flex', flexDirection: 'column' } }); // 标题栏 const header = h('div', { style: { padding: '15px 20px', borderBottom: '1px solid #eee', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', fontWeight: 'bold', fontSize: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }); const title = h('span', {}, '指令管理'); const closeBtn = h('button', { style: { background: 'none', border: 'none', color: 'var(--tmx-fg)', fontSize: '18px', cursor: 'pointer', padding: '0', width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onclick: () => { document.body.removeChild(sortOverlay); } }, '×'); header.appendChild(title); header.appendChild(closeBtn); // 说明文字 const instruction = h('div', { style: { padding: '15px 20px 10px', color: '#666', fontSize: '14px', borderBottom: '1px solid #f0f0f0', lineHeight: '1.5', whiteSpace: 'normal', wordWrap: 'break-word' } }, '拖拽下方指令项目可调整执行顺序,点击红色删除按钮可删除指令,操作后点击"保存排序"生效'); // 可排序列表容器 const listContainer = h('div', { className: 'tmx-command-list-container', style: { flex: '1', overflow: 'auto', padding: '10px' } }); // 创建可拖拽的指令列表 const sortableList = this.createSortableList(commands.slice()); listContainer.appendChild(sortableList); // 按钮区域 const buttonArea = h('div', { style: { padding: '15px 20px', borderTop: '1px solid #eee', display: 'flex', justifyContent: 'flex-end', gap: '10px' } }); const cancelBtn = h('button', { style: { padding: '8px 16px', background: '#f8f9fa', color: '#333', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer' }, onclick: () => { document.body.removeChild(sortOverlay); } }, '取消'); const saveBtn = h('button', { style: { padding: '8px 16px', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }, onclick: () => { this.saveSortedCommands(sortableList); document.body.removeChild(sortOverlay); } }, '保存排序'); buttonArea.appendChild(cancelBtn); buttonArea.appendChild(saveBtn); // 组装弹窗 sortPanel.appendChild(header); sortPanel.appendChild(instruction); sortPanel.appendChild(listContainer); sortPanel.appendChild(buttonArea); sortOverlay.appendChild(sortPanel); // 点击遮罩关闭 sortOverlay.addEventListener('click', (e) => { if (e.target === sortOverlay) { document.body.removeChild(sortOverlay); } }); document.body.appendChild(sortOverlay); } refreshManageDialog() { // FIXME: 编辑指令保存后仍会创建重复的指令管理弹窗,需要进一步调试弹窗查找逻辑 // 查找现有的指令管理弹窗 const existingOverlay = document.querySelector('.tmx-command-manage-dialog'); if (!existingOverlay) { // 如果没有现有弹窗,创建新的 this.createManageDialog(CommandStorage.getAll()); return; } // 找到列表容器并更新内容 const listContainer = existingOverlay.querySelector('.tmx-command-list-container'); if (listContainer) { // 清空现有内容 listContainer.innerHTML = ''; // 重新创建指令列表 const newList = this.createSortableList(CommandStorage.getAll()); listContainer.appendChild(newList); } } createSortableList(commands) { const list = h('div', { className: 'sortable-list', style: { display: 'flex', flexDirection: 'column', gap: '8px' } }); commands.forEach((command, index) => { const item = this.createSortableItem(command, index); list.appendChild(item); }); // 添加拖拽功能 this.makeSortable(list); return list; } createSortableItem(command, index) { const isRemote = command.isRemote; const item = h('div', { draggable: !isRemote, // 远程指令不可拖拽 'data-command-id': command.id, 'data-index': index, 'data-is-remote': isRemote, style: { padding: '12px 15px', background: isRemote ? '#e8f4fd' : '#f8f9fa', // 远程指令使用不同背景色 border: isRemote ? '1px solid #bee5eb' : '1px solid #e9ecef', borderRadius: '6px', cursor: isRemote ? 'default' : 'move', // 远程指令不显示移动光标 transition: 'all 0.2s ease', display: 'flex', alignItems: 'center', gap: '10px', userSelect: 'none', opacity: isRemote ? '0.8' : '1' // 远程指令稍微透明 } }); // 远程标识或拖拽图标 const iconElement = h('span', { style: { color: isRemote ? '#0066cc' : '#6c757d', fontSize: '14px', fontFamily: 'monospace', fontWeight: isRemote ? 'bold' : 'normal' } }, isRemote ? '🌐' : '⋮⋮'); // 序号 const orderNumber = h('span', { style: { minWidth: '24px', height: '24px', background: isRemote ? '#0066cc' : 'var(--tmx-bg)', color: isRemote ? '#fff' : 'var(--tmx-fg)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '12px', fontWeight: 'bold' } }, (index + 1).toString()); // 指令名称 const commandName = h('span', { style: { flex: '1', fontWeight: '500', color: isRemote ? '#0066cc' : '#333' } }, isRemote ? `${command.name} (远程)` : command.name); // 指令描述(如果有) const commandDesc = h('span', { style: { color: '#6c757d', fontSize: '12px', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, command.description || '无描述'); // 编辑按钮(远程指令禁用) const editBtn = h('button', { title: isRemote ? '远程指令不可编辑' : '编辑指令', style: { background: isRemote ? '#6c757d' : '#007bff', color: 'white', border: 'none', borderRadius: '4px', width: '24px', height: '24px', cursor: isRemote ? 'not-allowed' : 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s ease', marginRight: '5px', opacity: isRemote ? '0.5' : '1' }, onclick: async (e) => { console.log('编辑按钮被点击', command.name, 'isRemote:', isRemote); e.stopPropagation(); if (isRemote) { Toast.show('远程指令不可编辑', 'warning'); return; } const commandSelector = window.commandSelector || this; console.log('commandSelector:', commandSelector); try { await commandSelector.editCommand(command); } catch (error) { console.error('编辑指令失败:', error); Toast.show('编辑指令失败: ' + error.message, 'error'); } } }, '✎'); // 删除按钮(远程指令禁用) const deleteBtn = h('button', { title: isRemote ? '远程指令不可删除' : '删除指令', style: { background: isRemote ? '#6c757d' : '#dc3545', color: 'white', border: 'none', borderRadius: '4px', width: '24px', height: '24px', cursor: isRemote ? 'not-allowed' : 'pointer', fontSize: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s ease', opacity: isRemote ? '0.5' : '1' }, onclick: async (e) => { console.log('删除按钮被点击', command.name, 'isRemote:', isRemote); e.stopPropagation(); if (isRemote) { Toast.show('远程指令不可删除', 'warning'); return; } const commandSelector = window.commandSelector || this; console.log('commandSelector:', commandSelector); try { await commandSelector.deleteCommand(command, item); } catch (error) { console.error('删除指令失败:', error); Toast.show('删除指令失败: ' + error.message, 'error'); } } }, '×'); // 编辑按钮悬停效果(仅本地指令) if (!isRemote) { editBtn.addEventListener('mouseenter', () => { editBtn.style.background = '#0056b3'; editBtn.style.transform = 'scale(1.1)'; }); editBtn.addEventListener('mouseleave', () => { editBtn.style.background = '#007bff'; editBtn.style.transform = 'scale(1)'; }); // 删除按钮悬停效果(仅本地指令) deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = '#c82333'; deleteBtn.style.transform = 'scale(1.1)'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = '#dc3545'; deleteBtn.style.transform = 'scale(1)'; }); } item.appendChild(iconElement); item.appendChild(orderNumber); item.appendChild(commandName); item.appendChild(commandDesc); item.appendChild(editBtn); item.appendChild(deleteBtn); // 添加悬停效果(远程指令使用不同样式) item.addEventListener('mouseenter', () => { const hoverBg = isRemote ? '#d1ecf1' : '#e9ecef'; item.style.background = hoverBg; if (!isRemote) { item.style.transform = 'translateY(-1px)'; item.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; } }); item.addEventListener('mouseleave', () => { if (!item.classList.contains('dragging')) { const normalBg = isRemote ? '#e8f4fd' : '#f8f9fa'; item.style.background = normalBg; if (!isRemote) { item.style.transform = 'translateY(0)'; item.style.boxShadow = 'none'; } } }); return item; } async editCommand(command) { this.createEditDialog(command); } createEditDialog(command) { // 确保指令对象有必要的字段 if (!command.id) { command.id = Date.now().toString(); console.warn('指令缺少ID,已自动生成:', command.id); } if (!command.code) { command.code = ''; console.warn('指令缺少代码字段,已初始化为空字符串'); } if (!command.name) { command.name = '未命名指令'; console.warn('指令缺少名称字段,已设置默认名称'); } // 创建编辑弹窗遮罩 const editOverlay = h('div', { className: 'tmx-command-edit-dialog', style: { position: 'fixed', inset: '0', zIndex: 2147483647, // 最高层级,确保在指令管理界面之上 display: 'flex', background: 'rgba(0,0,0,0.5)', alignItems: 'center', justifyContent: 'center' } }); // 创建编辑弹窗面板 const editPanel = h('div', { style: { width: '600px', maxWidth: '90vw', maxHeight: '80vh', background: '#fff', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0,0,0,0.3)', overflow: 'hidden', fontFamily: 'Arial, sans-serif', display: 'flex', flexDirection: 'column' } }); // 标题栏 const header = h('div', { style: { padding: '15px 20px', borderBottom: '1px solid #eee', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', fontWeight: 'bold', fontSize: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }); const title = h('span', {}, '编辑指令'); const closeBtn = h('button', { style: { background: 'none', border: 'none', color: 'var(--tmx-fg)', fontSize: '18px', cursor: 'pointer', padding: '0', width: '24px', height: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onclick: () => { document.body.removeChild(editOverlay); } }, '×'); header.appendChild(title); header.appendChild(closeBtn); // 内容区域 const content = h('div', { style: { flex: '1', padding: '20px', overflow: 'auto' } }); // 创建表单容器 const formContainer = h('div', { style: { display: 'flex', flexDirection: 'column', gap: '20px' } }); // 指令名称字段 const nameField = h('div', { style: { display: 'flex', flexDirection: 'column' } }); const nameLabel = h('label', { style: { fontSize: '14px', fontWeight: '600', color: '#333', marginBottom: '8px' } }, '指令名称'); const nameInput = h('input', { type: 'text', value: command.name || '', style: { width: '100%', padding: '12px', border: '2px solid #e1e5e9', borderRadius: '6px', fontSize: '14px', boxSizing: 'border-box', transition: 'border-color 0.2s ease', outline: 'none' }, placeholder: '输入指令名称', onfocus: function() { this.style.borderColor = '#007bff'; }, onblur: function() { this.style.borderColor = '#e1e5e9'; } }); nameField.appendChild(nameLabel); nameField.appendChild(nameInput); // 指令描述字段 const descField = h('div', { style: { display: 'flex', flexDirection: 'column' } }); const descLabel = h('label', { style: { fontSize: '14px', fontWeight: '600', color: '#333', marginBottom: '8px' } }, '指令描述'); const descInput = h('input', { type: 'text', value: command.description || '', style: { width: '100%', padding: '12px', border: '2px solid #e1e5e9', borderRadius: '6px', fontSize: '14px', boxSizing: 'border-box', transition: 'border-color 0.2s ease', outline: 'none' }, placeholder: '输入指令描述(可选)', onfocus: function() { this.style.borderColor = '#007bff'; }, onblur: function() { this.style.borderColor = '#e1e5e9'; } }); descField.appendChild(descLabel); descField.appendChild(descInput); // 指令代码字段 const codeField = h('div', { style: { display: 'flex', flexDirection: 'column', flex: '1' } }); const codeLabel = h('label', { style: { fontSize: '14px', fontWeight: '600', color: '#333', marginBottom: '8px' } }, '指令代码'); const codeTextarea = h('textarea', { style: { width: '100%', minHeight: '200px', padding: '12px', border: '2px solid #e1e5e9', borderRadius: '6px', fontSize: '13px', fontFamily: 'Consolas, Monaco, "Courier New", monospace', resize: 'vertical', boxSizing: 'border-box', transition: 'border-color 0.2s ease', outline: 'none', lineHeight: '1.5' }, placeholder: '输入指令代码', onfocus: function() { this.style.borderColor = '#007bff'; }, onblur: function() { this.style.borderColor = '#e1e5e9'; } }); // 设置textarea的值 codeTextarea.value = command.code || ''; codeTextarea.textContent = command.code || ''; codeField.appendChild(codeLabel); codeField.appendChild(codeTextarea); formContainer.appendChild(nameField); formContainer.appendChild(descField); formContainer.appendChild(codeField); content.appendChild(formContainer); // 按钮区域 const buttonArea = h('div', { style: { padding: '15px 20px', borderTop: '1px solid #eee', display: 'flex', justifyContent: 'flex-end', gap: '10px' } }); const cancelBtn = h('button', { style: { padding: '8px 16px', background: '#f8f9fa', color: '#333', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer' }, onclick: () => { document.body.removeChild(editOverlay); } }, '取消'); const saveBtn = h('button', { style: { padding: '8px 16px', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }, onclick: async () => { await this.saveEditedCommand(command, nameInput.value.trim(), descInput.value.trim(), codeTextarea.value.trim(), editOverlay); } }, '保存'); buttonArea.appendChild(cancelBtn); buttonArea.appendChild(saveBtn); editPanel.appendChild(header); editPanel.appendChild(content); editPanel.appendChild(buttonArea); editOverlay.appendChild(editPanel); document.body.appendChild(editOverlay); // 聚焦到名称输入框 setTimeout(() => { nameInput.focus(); nameInput.select(); }, 100); } async saveEditedCommand(originalCommand, newName, newDescription, newCode, overlay) { // 验证输入 if (!newName) { Toast.show('指令名称不能为空', 'error'); return; } if (!newCode) { Toast.show('指令代码不能为空', 'error'); return; } try { // 检查名称是否与其他指令冲突 const commands = CommandStorage.getAll(); const nameConflict = commands.find(cmd => cmd.id !== originalCommand.id && cmd.name === newName); if (nameConflict) { const confirmed = await Dialog.confirm( `指令名称 "${newName}" 已存在,是否覆盖现有指令?`, '名称冲突' ); if (!confirmed) { return; } // 删除冲突的指令 CommandStorage.remove(nameConflict.id); } // 更新指令 let updatedCommands = commands.map(cmd => { if (cmd.id === originalCommand.id) { return { ...cmd, name: newName, description: newDescription || '', code: newCode, updateTime: new Date().toISOString() }; } return cmd; }); // 如果有名称冲突,过滤掉冲突的指令 if (nameConflict) { updatedCommands = updatedCommands.filter(cmd => cmd.id !== nameConflict.id); } // 保存到存储 CommandStorage.save(updatedCommands); // 关闭编辑弹窗 document.body.removeChild(overlay); // 刷新界面 this.updateCommandButtons(); // 刷新现有的指令管理弹窗内容,而不是重新创建 this.refreshManageDialog(); Toast.show(`指令 "${newName}" 已更新`, 'success'); } catch (error) { console.error('保存指令失败:', error); Toast.show('保存指令失败', 'error'); } } async deleteCommand(command, itemElement) { // 显示确认对话框 const confirmed = await Dialog.confirm( `确定要删除指令"${command.name}"吗?\n\n此操作不可撤销。`, '确认删除' ); if (confirmed) { try { // 从全局存储中删除指令 const commands = CommandStorage.getAll(); const updatedCommands = commands.filter(cmd => cmd.id !== command.id); CommandStorage.save(updatedCommands); // 从界面中移除元素 itemElement.style.transition = 'all 0.3s ease'; itemElement.style.opacity = '0'; itemElement.style.transform = 'translateX(-100%)'; setTimeout(() => { itemElement.remove(); // 更新序号 this.updateItemNumbers(); // 刷新指令按钮显示 this.updateCommandButtons(); }, 300); Toast.show(`指令"${command.name}"已删除`, 'success'); } catch (error) { console.error('删除指令失败:', error); Toast.show('删除指令失败', 'error'); } } } makeSortable(list) { let draggedElement = null; let placeholder = null; list.addEventListener('dragstart', (e) => { // 检查是否为远程指令,如果是则阻止拖拽 if (e.target.getAttribute('data-is-remote') === 'true') { e.preventDefault(); Toast.show('远程指令不可排序', 'warning'); return; } draggedElement = e.target; draggedElement.classList.add('dragging'); draggedElement.style.opacity = '0.5'; // 创建占位符 placeholder = h('div', { style: { height: draggedElement.offsetHeight + 'px', background: 'linear-gradient(90deg, #007bff, #0056b3)', borderRadius: '6px', margin: '4px 0', opacity: '0.3', border: '2px dashed #007bff' } }); }); list.addEventListener('dragend', (e) => { if (draggedElement) { draggedElement.classList.remove('dragging'); draggedElement.style.opacity = '1'; draggedElement.style.background = '#f8f9fa'; draggedElement.style.transform = 'translateY(0)'; draggedElement.style.boxShadow = 'none'; } if (placeholder && placeholder.parentNode) { placeholder.parentNode.removeChild(placeholder); } draggedElement = null; placeholder = null; // 更新序号 this.updateItemNumbers(list); }); list.addEventListener('dragover', (e) => { e.preventDefault(); if (!draggedElement || !placeholder) return; const afterElement = this.getDragAfterElement(list, e.clientY); if (afterElement == null) { list.appendChild(placeholder); } else { list.insertBefore(placeholder, afterElement); } }); list.addEventListener('drop', (e) => { e.preventDefault(); if (!draggedElement || !placeholder) return; // 将拖拽元素插入到占位符位置 list.insertBefore(draggedElement, placeholder); }); } getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll('[draggable="true"]:not(.dragging)')]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } updateItemNumbers(list) { const items = list.querySelectorAll('[data-command-id]'); items.forEach((item, index) => { const numberSpan = item.children[1]; // 序号元素是第二个子元素 if (numberSpan) { numberSpan.textContent = (index + 1).toString(); } item.setAttribute('data-index', index); }); } saveSortedCommands(sortableList) { // 只获取本地指令的ID(排除远程指令) const items = sortableList.querySelectorAll('[data-command-id]:not([data-is-remote="true"])'); const sortedIds = Array.from(items).map(item => item.getAttribute('data-command-id')); // 获取本地指令 const localCommands = CommandStorage.getLocalOnly(); // 创建ID到指令的映射 const commandMap = new Map(); localCommands.forEach(command => { commandMap.set(command.id, command); }); // 按新顺序重新排列本地指令 const sortedLocalCommands = sortedIds.map(id => commandMap.get(id)).filter(Boolean); // 保存到localStorage(只保存本地指令) try { CommandStorage.save(sortedLocalCommands); Toast.show('指令排序已保存', 'success'); // 刷新指令按钮显示 this.updateCommandButtons(); } catch (error) { console.error('保存指令管理失败:', error); Toast.show('保存失败,请重试', 'error'); } } show() { this.updateCommandButtons(); super.show(); } } // 定时任务管理器类 class ScheduleManager { constructor() { this.title = '定时任务管理'; this.tasks = ScheduledTaskStorage.getAll(); this.commands = CommandStorage.getAll(); this.editingTask = null; this.visible = false; this.createDialog(); this.setupContent(); } createDialog() { // 创建遮罩层 this.overlay = h('div', { style: { position: 'fixed', inset: '0', zIndex: 2147483645, display: 'none', background: 'rgba(0,0,0,0.5)' } }); // 点击遮罩层关闭 this.overlay.addEventListener('click', (e) => { if (e.target === this.overlay) this.hide(); }); // 创建对话框面板 this.panel = h('div', { style: { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 'min(550px, 92vw)', height: 'min(500px, 85vh)', maxWidth: '92vw', maxHeight: '85vh', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', border: '1px solid #ccc', borderRadius: '6px', boxShadow: '0 2px 15px rgba(0,0,0,0.2)', display: 'flex', flexDirection: 'column', fontFamily: 'Arial, sans-serif', overflow: 'hidden' } }); // 创建标题栏 const titleBar = h('div', { style: { padding: '10px 15px', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', borderBottom: '1px solid #ddd', borderRadius: '6px 6px 0 0', fontWeight: 'bold', fontSize: '14px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, this.title); // 创建关闭按钮 const closeBtn = h('button', { style: { background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', border: '1px solid #ddd', borderRadius: '3px', fontSize: '14px', cursor: 'pointer', padding: '2px 6px', width: '20px', height: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center' } }, '×'); closeBtn.addEventListener('click', () => this.hide()); titleBar.appendChild(closeBtn); // 创建内容区域 this.contentEl = h('div', { style: { flex: '1', padding: '12px', overflow: 'hidden', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)' } }); this.panel.append(titleBar, this.contentEl); this.overlay.appendChild(this.panel); document.body.appendChild(this.overlay); } setupContent() { this.contentEl.innerHTML = `