// ==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 = `

任务配置

请选择或新增一个任务进行配置
`; // 添加响应式类名 this.contentEl.querySelector('div').classList.add('schedule-content'); this.setupEventListeners(); this.updateTaskList(); } setupEventListeners() { const addBtn = this.contentEl.querySelector('#add-task-btn'); addBtn.addEventListener('click', () => this.addNewTask()); const importBtn = this.contentEl.querySelector('#import-tasks-btn'); importBtn.addEventListener('click', () => this.importTasks()); const exportBtn = this.contentEl.querySelector('#export-tasks-btn'); exportBtn.addEventListener('click', () => this.exportTasks()); } updateTaskList() { const listEl = this.contentEl.querySelector('#task-list'); this.tasks = ScheduledTaskStorage.getAll(); if (this.tasks.length === 0) { listEl.innerHTML = '
暂无定时任务
'; return; } listEl.innerHTML = this.tasks.map(task => { const command = this.commands.find(cmd => cmd.id === task.commandId); const commandName = command ? command.name : '未知指令'; const nextRun = task.nextRun ? new Date(task.nextRun).toLocaleString() : '未设置'; const isSelected = this.editingTask && this.editingTask.id === task.id; return `
${task.name}
指令: ${commandName}
时间: ${task.schedule}
下次执行: ${nextRun}
状态: ${task.enabled ? '启用' : '禁用'}
`; }).join(''); // 添加点击事件 listEl.querySelectorAll('.task-item').forEach(item => { item.addEventListener('click', () => { const taskId = item.dataset.id; const task = this.tasks.find(t => t.id === taskId); this.editTask(task); }); // 添加悬停效果 item.addEventListener('mouseenter', () => { if (!item.dataset.id || (this.editingTask && this.editingTask.id !== item.dataset.id)) { item.style.background = 'rgba(0,123,255,0.05)'; } }); item.addEventListener('mouseleave', () => { if (!item.dataset.id || (this.editingTask && this.editingTask.id !== item.dataset.id)) { item.style.background = 'var(--tmx-bg)'; } }); }); // 添加状态切换按钮事件 listEl.querySelectorAll('.toggle-status-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const taskId = btn.dataset.taskId; this.toggleTaskStatus(taskId); }); }); } addNewTask() { const newTask = { id: Date.now().toString(), name: '新任务', commandId: '', schedule: 'every-minute', enabled: true, createTime: Date.now(), nextRun: null }; this.editTask(newTask, true); } editTask(task, isNew = false) { this.editingTask = task; this.isNewTask = isNew; this.updateTaskList(); const formEl = this.contentEl.querySelector('#task-form'); formEl.innerHTML = `
格式: 分 时 日 月 周
例如: 0 8 * * * (每天8点), 0 8 * * 3 (每周三8点), 0 8 1,L * * (每月1号和最后一天8点)
${!isNew ? '' : ''}
`; this.setupFormEventListeners(); } setupFormEventListeners() { const formEl = this.contentEl.querySelector('#task-form'); // 移除之前的事件监听器(通过克隆节点) const newFormEl = formEl.cloneNode(true); formEl.parentNode.replaceChild(newFormEl, formEl); // 重新获取表单元素 const scheduleSelect = newFormEl.querySelector('#task-schedule'); const customDiv = newFormEl.querySelector('#custom-schedule'); const saveBtn = newFormEl.querySelector('#save-task-btn'); const deleteBtn = newFormEl.querySelector('#delete-task-btn'); const cancelBtn = newFormEl.querySelector('#cancel-task-btn'); // 自定义时间配置显示/隐藏 scheduleSelect.addEventListener('change', () => { customDiv.style.display = scheduleSelect.value === 'custom' ? 'block' : 'none'; }); // 保存按钮(添加防重复提交机制) saveBtn.addEventListener('click', () => { if (saveBtn.disabled) return; this.saveTask(); }); // 删除按钮 if (deleteBtn) { deleteBtn.addEventListener('click', () => this.deleteTask()); } // 取消按钮 cancelBtn.addEventListener('click', () => this.cancelEdit()); } saveTask() { const formEl = this.contentEl.querySelector('#task-form'); const saveBtn = formEl.querySelector('#save-task-btn'); // 防重复提交 if (saveBtn.disabled) { return; } // 禁用保存按钮 saveBtn.disabled = true; saveBtn.textContent = '保存中...'; saveBtn.style.opacity = '0.6'; try { const name = formEl.querySelector('#task-name').value.trim(); const commandId = formEl.querySelector('#task-command').value; const schedule = formEl.querySelector('#task-schedule').value; const customSchedule = formEl.querySelector('#custom-schedule-input').value.trim(); const enabled = formEl.querySelector('#task-enabled').checked; if (!name) { Toast.show('请输入任务名称', 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; saveBtn.style.opacity = '1'; return; } if (!commandId) { Toast.show('请选择执行指令', 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; saveBtn.style.opacity = '1'; return; } let finalSchedule = schedule; if (schedule === 'custom') { if (!customSchedule) { Toast.show('请输入自定义时间配置', 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; saveBtn.style.opacity = '1'; return; } finalSchedule = 'custom:' + customSchedule; } this.editingTask.name = name; this.editingTask.commandId = commandId; this.editingTask.schedule = finalSchedule; this.editingTask.enabled = enabled; if (this.isNewTask) { ScheduledTaskStorage.add(this.editingTask); Toast.show('任务创建成功', 'success'); } else { ScheduledTaskStorage.update(this.editingTask.id, { name: this.editingTask.name, commandId: this.editingTask.commandId, schedule: this.editingTask.schedule, enabled: this.editingTask.enabled }); Toast.show('任务更新成功', 'success'); } // 重新启动调度器以应用更改 if (window.scheduler) { window.scheduler.loadTasks(); } // 先更新任务列表再取消编辑 this.updateTaskList(); this.cancelEdit(); // 标记保存成功,避免在finally中重复恢复按钮状态 return true; } catch (error) { // 保存失败时恢复按钮状态 saveBtn.disabled = false; saveBtn.textContent = '保存'; saveBtn.style.opacity = '1'; console.error('保存任务失败:', error); } } deleteTask() { if (confirm('确定要删除这个定时任务吗?')) { const taskId = this.editingTask.id; ScheduledTaskStorage.remove(taskId); Toast.show('任务删除成功', 'success'); // 重新启动调度器以应用更改 if (window.scheduler) { window.scheduler.loadTasks(); } // 清除编辑状态 this.editingTask = null; this.isNewTask = false; // 立即更新任务列表 this.updateTaskList(); // 重置表单区域 const formEl = this.contentEl.querySelector('#task-form'); formEl.innerHTML = '
请选择或新增一个任务进行配置
'; } } toggleTaskStatus(taskId) { const task = this.tasks.find(t => t.id === taskId); if (!task) return; const newEnabled = !task.enabled; // 更新存储 ScheduledTaskStorage.update(taskId, { enabled: newEnabled }); // 更新本地任务数据 task.enabled = newEnabled; // 更新调度器 if (window.scheduler) { if (newEnabled) { window.scheduler.addScheduledTask(task); } else { window.scheduler.removeScheduledTask(taskId); } } // 重新获取任务数据并更新界面 this.tasks = ScheduledTaskStorage.getAll(); this.updateTaskList(); // 如果当前正在编辑这个任务,也要更新编辑表单 if (this.editingTask && this.editingTask.id === taskId) { this.editingTask.enabled = newEnabled; const enabledCheckbox = this.contentEl.querySelector('#task-enabled'); if (enabledCheckbox) { enabledCheckbox.checked = newEnabled; } } Toast.show(`任务已${newEnabled ? '启用' : '禁用'}`, 'success'); } cancelEdit() { this.editingTask = null; this.isNewTask = false; this.updateTaskList(); const formEl = this.contentEl.querySelector('#task-form'); formEl.innerHTML = '
请选择或新增一个任务进行配置
'; } show() { this.commands = CommandStorage.getAll(); this.updateTaskList(); this.overlay.style.display = 'block'; this.visible = true; } hide() { this.overlay.style.display = 'none'; this.visible = false; } toggle() { if (this.visible) { this.hide(); } else { this.show(); } } exportTasks() { try { const tasks = ScheduledTaskStorage.getAll(); if (tasks.length === 0) { Toast.show('暂无任务可导出', 'warning'); return; } // 创建导出数据,包含任务和相关指令信息 const exportData = { version: '1.0', exportTime: new Date().toISOString(), tasks: tasks, commands: this.commands.filter(cmd => tasks.some(task => task.commandId === cmd.id) ) }; const jsonStr = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `scheduled_tasks_${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Toast.show(`成功导出 ${tasks.length} 个任务`, 'success'); } catch (error) { console.error('导出任务失败:', error); Toast.show('导出失败: ' + error.message, 'error'); } } importTasks() { try { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.style.display = 'none'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const importData = JSON.parse(event.target.result); this.processImportData(importData); } catch (error) { console.error('解析JSON文件失败:', error); Toast.show('文件格式错误: ' + error.message, 'error'); } }; reader.readAsText(file); }; document.body.appendChild(input); input.click(); document.body.removeChild(input); } catch (error) { console.error('导入任务失败:', error); Toast.show('导入失败: ' + error.message, 'error'); } } processImportData(importData) { try { // 验证导入数据格式 if (!importData.tasks || !Array.isArray(importData.tasks)) { throw new Error('无效的任务数据格式'); } const existingTasks = ScheduledTaskStorage.getAll(); const existingCommands = CommandStorage.getAll(); let importedTaskCount = 0; let importedCommandCount = 0; let skippedCount = 0; // 导入指令(如果有) if (importData.commands && Array.isArray(importData.commands)) { for (const command of importData.commands) { const existingCommand = existingCommands.find(cmd => cmd.id === command.id); if (!existingCommand) { // 生成新的ID避免冲突 const newCommand = { ...command, id: Date.now().toString() + Math.random().toString(36).substr(2, 9) }; CommandStorage.add(newCommand.name, newCommand.code, newCommand.description || ''); importedCommandCount++; // 更新任务中的指令ID引用 importData.tasks.forEach(task => { if (task.commandId === command.id) { task.commandId = newCommand.id; } }); } } } // 导入任务 for (const task of importData.tasks) { // 检查是否已存在相同名称的任务 const existingTask = existingTasks.find(t => t.name === task.name); if (existingTask) { skippedCount++; continue; } // 生成新的任务ID const newTask = { ...task, id: Date.now().toString() + Math.random().toString(36).substr(2, 9), createTime: Date.now(), lastRun: null, nextRun: ScheduledTaskStorage.calculateNextRun(task.schedule) }; ScheduledTaskStorage.add(newTask); importedTaskCount++; } // 刷新界面 this.tasks = ScheduledTaskStorage.getAll(); this.commands = CommandStorage.getAll(); this.updateTaskList(); // 重新启动调度器 if (window.scheduler) { window.scheduler.loadTasks(); } // 显示导入结果 let message = `导入完成: ${importedTaskCount} 个任务`; if (importedCommandCount > 0) { message += `, ${importedCommandCount} 个指令`; } if (skippedCount > 0) { message += ` (跳过 ${skippedCount} 个重复任务)`; } Toast.show(message, 'success'); } catch (error) { console.error('处理导入数据失败:', error); Toast.show('导入失败: ' + error.message, 'error'); } } } // 创建全局指令选择器实例 let commandSelector = null; function toggleCommandSelector(btnEl) { if (!commandSelector) { commandSelector = new CommandSelector(); window.commandSelector = commandSelector; } commandSelector.toggle(); if (commandSelector.visible) { commandSelector.updateCommandButtons(); } if (btnEl) { btnEl.style.borderStyle = commandSelector.visible ? 'inset' : 'outset'; } } // 创建全局定时任务管理器实例 let scheduleManager = null; function toggleScheduleManager(btnEl) { if (!scheduleManager) { scheduleManager = new ScheduleManager(); } scheduleManager.toggle(); if (btnEl) { btnEl.style.borderStyle = scheduleManager.visible ? 'inset' : 'outset'; } } function toggleToast() { const flag = store.get('toast.enabled', 0) ? 0 : 1; store.set('toast.enabled', flag); const btn = buttonMap.get('toast'); if (btn) { btn.style.borderStyle = flag ? 'inset' : 'outset'; btn.textContent = flag ? '关闭弹窗' : '弹窗提示'; } if (flag) { // 开启弹窗提示时显示弹窗 Toast.show('提示', '你好'); } else { // 关闭弹窗提示时,如果右下角有弹窗则自动关闭 Toast.hide(); } // 通知调试窗口更新最小化容器位置,避免与弹窗重叠 if (window.DebugWindow && window.DebugWindow.updateMinimizedContainerPosition) { window.DebugWindow.updateMinimizedContainerPosition(); } } /** -------------------- 组控制 -------------------- */ function toggleGroup(name) { return (btnEl) => { const gp = groupMap.get(name); if (!gp) return; gp.toggle(); // 可选:当弹窗显示时,让主按钮有一个视觉效果 if (btnEl) btnEl.style.borderStyle = gp.visible ? 'inset' : 'outset'; }; } /** -------------------- makeToggle(返回 handler) -------------------- */ function makeToggle(id, openLabel, closeLabel, storeKey) { return (activeOrBtn, maybeBtn) => { // 兼容两种调用方式: // 1) group popup calls handler(active, btn) -> active is boolean, maybeBtn is btn // 2) column button calls handler(btn) -> activeOrBtn is btn if (typeof activeOrBtn === 'boolean') { const active = activeOrBtn; const btn = maybeBtn; // save to store store.set(storeKey, active ? 1 : 0); // update text if btn provided if (btn) btn.innerText = active ? closeLabel : openLabel; console.log(`[${openLabel}] 状态:${active ? '已开启' : '已关闭'}`); } else { // called as handler(btn) from column (rare for these toggles) - just toggle state const btn = activeOrBtn; const cur = store.get(storeKey, 0) === 1; const will = !cur; store.set(storeKey, will ? 1 : 0); if (btn) { btn.innerText = will ? closeLabel : openLabel; btn.style.borderStyle = will ? 'inset' : 'outset'; } console.log(`[${openLabel}] 状态:${will ? '已开启' : '已关闭'}`); } }; } function noop(name) { return () => console.log(`[${name}] 点击`); } /** -------------------- 配置集按钮行为 -------------------- */ async function configClipboardApi() { try { const key = 'clipboard_api'; const current = store.get(key, ''); console.log('Dialog: 准备显示prompt对话框'); const value = await Dialog.prompt('请输入剪切板API:', current || '', '配置剪切板API'); console.log('Dialog: prompt对话框返回值:', value); if (value !== null) { store.set(key, value); console.log('[配置集] 已保存剪切板API:', value); } } catch (err) { console.error('Dialog错误:', err); } } async function configRemoteCommandUrl() { try { const key = 'remote_command_url'; const current = store.get(key, ''); const value = await Dialog.prompt('请输入远程指令集URL:', current || '', '配置远程指令集'); if (value !== null) { store.set(key, value); console.log('[配置集] 已保存远程指令集URL:', value); // 如果启用了远程指令,立即尝试同步 if (store.get('remote_commands_enabled', 0) === 1) { await syncRemoteCommands(true); } } } catch (err) { console.error('配置远程指令URL错误:', err); } } /** -------------------- 调试功能 -------------------- */ function executeDebugCode() { // 创建新的调试代码窗口 const defaultCode = '// 示例代码\nconsole.log("Hello World!");\nalert("测试弹窗");\n\n// 获取页面元素\nconst elements = document.querySelectorAll("div");\nconsole.log("页面div元素数量:", elements.length);\n\n// 便捷点击函数示例\n// clickbtn("百度一下"); // 点击包含"百度一下"文本的按钮\n// clickhref("百度一下"); // 点击包含"百度一下"文本的链接\n// clickgo("#su"); // 点击id为su的元素\n// clickgo("input[type=\"submit\"]"); // 点击提交按钮'; const windowId = DebugWindowManager.createWindow(defaultCode); console.log(`[调试执行器] 创建调试窗口: ${windowId}`); Logger.append(`[调试执行器] 创建新调试窗口: ${windowId}`); } async function configSafeCode() { const key = 'clipboard_safecode'; const current = store.get(key, ''); const value = await Dialog.prompt('请输入剪切板安全码:', current || '', '配置安全码'); if (value !== null) { store.set(key, value); console.log('[配置集] 已保存剪切板安全码:', value); } } /** -------------------- 远程指令集管理 -------------------- */ // 远程指令集存储 const RemoteCommandStorage = { STORAGE_KEY: 'remote_commands_cache', LAST_SYNC_KEY: 'remote_commands_last_sync', // 获取缓存的远程指令 getCache() { try { return store.get(this.STORAGE_KEY, []); } catch (e) { console.error('获取远程指令缓存失败:', e); return []; } }, // 保存远程指令到缓存 saveCache(commands) { try { store.set(this.STORAGE_KEY, commands); store.set(this.LAST_SYNC_KEY, Date.now()); return true; } catch (e) { console.error('保存远程指令缓存失败:', e); return false; } }, // 获取上次同步时间 getLastSyncTime() { try { return store.get(this.LAST_SYNC_KEY, 0); } catch (e) { return 0; } }, // 清除缓存 clearCache() { try { store.remove(this.STORAGE_KEY); store.remove(this.LAST_SYNC_KEY); } catch (e) { console.error('清除远程指令缓存失败:', e); } } }; // 同步远程指令集 async function syncRemoteCommands(showToast = true) { const url = store.get('remote_command_url', ''); if (!url) { console.log('[远程指令] 未配置远程指令集URL'); if (showToast) { Toast.show('未配置远程指令集URL', 'warning'); } return false; } // 显示加载状态 if (showToast) { Toast.show('正在同步远程指令集...', 'info'); } try { console.log('[远程指令] 开始同步远程指令集:', url); // 使用GM_xmlhttpRequest避免跨域问题 const data = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache', 'User-Agent': 'ClipboardSender/1.0' }, timeout: 15000, // 15秒超时 onload: function(response) { try { if (response.status < 200 || response.status >= 300) { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); return; } const contentType = response.responseHeaders.toLowerCase(); if (!contentType.includes('application/json') && !contentType.includes('text/json')) { // 尝试解析,可能服务器没有设置正确的Content-Type console.warn('[远程指令] 响应头未包含JSON类型,尝试强制解析'); } const jsonData = JSON.parse(response.responseText); resolve(jsonData); } catch (parseError) { reject(new Error(`JSON解析失败: ${parseError.message}`)); } }, onerror: function(error) { reject(new Error('网络请求失败')); }, ontimeout: function() { reject(new Error('请求超时')); } }); }); // 验证数据格式 if (!Array.isArray(data)) { throw new Error('远程指令集格式错误:应为数组格式'); } if (data.length === 0) { console.log('[远程指令] 远程指令集为空'); if (showToast) { Toast.show('远程指令集为空', 'warning'); } RemoteCommandStorage.saveCache([]); return true; } // 处理远程指令数据 const remoteCommands = data.map((cmd, index) => { // 验证必要字段 if (!cmd.name && !cmd.code) { console.warn(`[远程指令] 跳过无效指令 (索引 ${index}):`, cmd); return null; } // 确保每个指令都有必要的字段 const processedCmd = { id: cmd.id || `remote_${Date.now()}_${index}`, name: cmd.name || `远程指令_${index + 1}`, description: cmd.description || '', code: cmd.code || '', createTime: cmd.createTime || new Date().toISOString(), isRemote: true, // 标记为远程指令 remoteUrl: url // 记录来源URL }; return processedCmd; }).filter(Boolean); // 过滤掉无效指令 // 保存到缓存 const saveSuccess = RemoteCommandStorage.saveCache(remoteCommands); if (!saveSuccess) { throw new Error('保存远程指令到本地缓存失败'); } console.log(`[远程指令] 同步成功,获取到 ${remoteCommands.length} 个远程指令`); if (showToast) { Toast.show(`远程指令同步成功,获取 ${remoteCommands.length} 个指令`, 'success'); } return true; } catch (error) { console.error('[远程指令] 同步失败:', error); let errorMessage = '远程指令同步失败'; if (error.message.includes('请求超时')) { errorMessage = '远程指令同步超时,请检查网络连接'; } else if (error.message.includes('网络请求失败')) { errorMessage = '网络连接失败,请检查URL或网络状态'; } else if (error.message.includes('JSON解析失败')) { errorMessage = '远程指令数据格式错误,请检查URL返回的内容'; } else { errorMessage = `远程指令同步失败:${error.message}`; } if (showToast) { Toast.show(errorMessage, 'error'); } return false; } } // 检查文本是否包含可疑模式 function containsSuspiciousPatterns(text) { const suspiciousPatterns = [ /\x4D\x5A/, // MZ (可执行文件头) /\x7F\x45\x4C\x46/, // ELF (Linux可执行文件) /\x23\x21/, // Shebang (#!) /<\?php/i, // PHP代码 /]*>/i, // Script标签 /eval\(/i, // eval函数 /javascript:/i, // javascript协议 /vbscript:/i, // vbscript协议 ]; return suspiciousPatterns.some(pattern => pattern.test(text)); } async function pushClipboardText() { const api = store.get('clipboard_api', ''); const token = store.get('clipboard_safecode', ''); if (!api || !token) { await Dialog.alert('请先配置剪切板API和安全码', '推送失败'); return; } try { const text = await navigator.clipboard.readText(); if (!text) { await Dialog.alert('剪切板为空,无法推送', '推送失败'); return; } // 检查是否包含可疑内容 if (containsSuspiciousPatterns(text)) { const confirmed = await Dialog.confirm( '检测到剪切板内容包含可疑模式(如脚本代码、可执行文件等),可能存在安全风险。\n\n是否仍要继续推送?', '安全警告' ); if (!confirmed) { Logger.append('[推送文本] 用户取消:内容包含可疑模式'); return; } } GM_xmlhttpRequest({ method: 'POST', url: api, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, data: JSON.stringify({ text }), onload: function (res) { console.log("✅ 请求成功!"); console.log("状态码:", res.status); console.log("响应文本:", res.responseText); console.log("响应头:", res.responseHeaders); if (res.status >= 200 && res.status < 300) { console.log('[推送文本] 成功:', text); Logger.append(`[推送文本] 成功:${text}`); } else { console.error('[推送文本] 失败:', res.status, res.responseText); Logger.append(`[推送文本] 失败: ${res.status} ${res.responseText}`); // 特殊处理可疑内容错误 if (res.status === 400 && res.responseText.includes('suspicious patterns')) { Dialog.alert('服务器检测到内容包含可疑模式,推送被拒绝。\n\n请检查剪切板内容是否包含脚本代码、可执行文件等敏感内容。', '推送失败'); } } }, onerror: function (err) { console.error('[推送文本] 网络错误:', err); Logger.append('[推送文本] 网络错误'); } }); } catch (err) { console.error('[推送文本] 剪切板读取失败:', err); Logger.append('[推送文本] 剪切板读取失败: ' + err.message); await Dialog.alert(`剪切板读取失败: ${err.message}`, '推送失败'); } } /** *************************** 自定义对话框 *********************************** */ const Dialog = (() => { let overlay, panel, titleEl, contentEl, inputEl, buttonArea; let resolvePromise = null; function ensure() { return new Promise((resolve) => { console.log('Dialog ensure: checking elements', { overlay: !!overlay, titleEl: !!titleEl, contentEl: !!contentEl, buttonArea: !!buttonArea }); // 检查所有必要的DOM元素是否都已创建 if (overlay && titleEl && contentEl && buttonArea) { console.log('Dialog ensure: all elements exist, resolving'); resolve(); return; } // 确保document.body已经存在 if (!document.body) { console.error('Dialog: document.body not ready'); setTimeout(() => ensure().then(resolve), 100); return; } console.log('Dialog ensure: initializing dialog'); initializeDialog(); console.log('Dialog ensure: after init', { overlay: !!overlay, titleEl: !!titleEl, contentEl: !!contentEl, buttonArea: !!buttonArea }); resolve(); }); } function initializeDialog() { // 创建遮罩层 overlay = h('div', { style: { position: 'fixed', inset: '0', zIndex: 2147483647, // 最高层级,确保在指令管理界面之上 display: 'none', background: 'rgba(0,0,0,0.5)', alignItems: 'center', justifyContent: 'center' } }); // 创建对话框面板 panel = h('div', { style: { width: '320px', maxWidth: '90vw', background: '#fff', borderRadius: '4px', boxShadow: '0 2px 10px rgba(0,0,0,0.2)', overflow: 'hidden', fontFamily: 'Arial, sans-serif' } }); // 标题栏 const header = h('div', { style: { padding: '12px 15px', borderBottom: '1px solid #eee', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', fontWeight: 'bold', fontSize: '14px', display: 'flex', justifyContent: 'flex-start', alignItems: 'flex-start', gap: '8px', lineHeight: '1.4', whiteSpace: 'normal', wordWrap: 'break-word', minHeight: '40px' } }); titleEl = h('div', { style: { flex: '1 1 auto', minWidth: '0', marginRight: '10px', lineHeight: '1.4', fontSize: '14px', whiteSpace: 'normal', wordWrap: 'break-word', wordBreak: 'break-word', overflowWrap: 'anywhere', overflow: 'visible' } }, '对话框'); // 右上角关闭按钮 const closeButton = h('button', { style: { background: 'none', border: 'none', color: 'var(--tmx-fg)', fontSize: '18px', cursor: 'pointer', padding: '0', width: '20px', height: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '2px', marginLeft: 'auto', flex: '0 0 auto' }, onclick: () => hide(null) }, '×'); // 鼠标悬停效果 closeButton.addEventListener('mouseenter', () => { closeButton.style.background = 'rgba(255,255,255,0.2)'; }); closeButton.addEventListener('mouseleave', () => { closeButton.style.background = 'none'; }); header.appendChild(titleEl); header.appendChild(closeButton); // 内容区域 contentEl = h('div', { style: { padding: '15px', minHeight: '50px', maxHeight: '300px', overflow: 'auto', whiteSpace: 'normal', wordWrap: 'break-word', lineHeight: '1.5' } }); // 输入框区域(用于prompt) inputEl = h('input', { type: 'text', style: { display: 'none', width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', marginTop: '10px', boxSizing: 'border-box' } }); contentEl.appendChild(inputEl); // 按钮区域 buttonArea = h('div', { style: { padding: '10px 15px', borderTop: '1px solid #eee', textAlign: 'right' } }); panel.append(header, contentEl, buttonArea); overlay.appendChild(panel); document.body.appendChild(overlay); // 添加调试日志 console.log('Dialog: DOM elements created and appended to body'); } function createButton(text, isPrimary = false, onClick) { return h('button', { style: { padding: '6px 12px', marginLeft: '8px', background: isPrimary ? 'var(--tmx-bg)' : '#f8f9fa', color: isPrimary ? 'var(--tmx-fg)' : '#333', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer' }, onclick: onClick }, text); } async function show(title, content, options = {}) { await ensure(); console.log('Dialog show: titleEl =', titleEl); if (!titleEl) { console.error('Dialog show: titleEl is still undefined after ensure()'); return; } titleEl.textContent = title || '提示'; // 清理旧内容 const oldContent = contentEl.querySelectorAll(':not(input)'); oldContent.forEach(el => el.remove()); // 设置内容 if (typeof content === 'string') { const contentNode = document.createElement('div'); contentNode.innerHTML = content; // 设置内容区域的文本换行样式 contentNode.style.whiteSpace = 'normal'; contentNode.style.wordWrap = 'break-word'; contentNode.style.wordBreak = 'break-word'; contentNode.style.overflowWrap = 'anywhere'; contentNode.style.lineHeight = '1.5'; contentEl.insertBefore(contentNode, inputEl); } else { contentEl.insertBefore(content, inputEl); } // 处理输入框 inputEl.style.display = options.showInput ? 'block' : 'none'; inputEl.value = options.defaultValue || ''; if (options.showInput) { setTimeout(() => inputEl.focus(), 100); } // 清空并添加按钮 buttonArea.innerHTML = ''; if (options.buttons) { options.buttons.forEach(btn => { buttonArea.appendChild(btn); }); } // 显示对话框 overlay.style.display = 'flex'; // 返回Promise return new Promise(resolve => { resolvePromise = resolve; }); } function hide(result) { if (overlay) { overlay.style.display = 'none'; // 清理内容 const oldContent = contentEl.querySelectorAll(':not(input)'); oldContent.forEach(el => el.remove()); inputEl.style.display = 'none'; inputEl.value = ''; buttonArea.innerHTML = ''; } if (resolvePromise) { resolvePromise(result); resolvePromise = null; } } function alert(message, title = '提示') { const okButton = createButton('确定', true, () => hide(true)); return show(title, message, { buttons: [okButton] }); } function confirm(message, title = '确认') { const cancelButton = createButton('取消', false, () => hide(false)); const okButton = createButton('确定', true, () => hide(true)); return show(title, message, { buttons: [cancelButton, okButton] }); } function prompt(message, defaultValue = '', title = '输入') { const cancelButton = createButton('取消', false, () => hide(null)); const okButton = createButton('确定', true, () => hide(inputEl.value)); return show(title, message, { showInput: true, defaultValue: defaultValue, buttons: [cancelButton, okButton] }); } async function multilinePrompt(message, defaultValue = '', title = '多行输入', options = {}) { await ensure(); titleEl.textContent = title || '多行输入'; // 清理旧内容 const oldContent = contentEl.querySelectorAll(':not(input)'); oldContent.forEach(el => el.remove()); // 设置内容 if (typeof message === 'string') { const contentNode = document.createElement('div'); contentNode.innerHTML = message; contentEl.insertBefore(contentNode, inputEl); } else { contentEl.insertBefore(message, inputEl); } // 创建多行文本输入框 const textareaEl = h('textarea', { style: { width: options.width || '100%', height: options.height || '200px', padding: '8px', border: '1px solid #ddd', borderRadius: '4px', marginTop: '10px', boxSizing: 'border-box', fontFamily: 'Consolas, Monaco, "Courier New", monospace', fontSize: '12px', lineHeight: '1.4', resize: 'both', minHeight: '100px', maxHeight: '400px' }, placeholder: options.placeholder || '请输入代码...' }); textareaEl.value = defaultValue || ''; contentEl.insertBefore(textareaEl, inputEl); // 隐藏原输入框 inputEl.style.display = 'none'; // 调整对话框大小 panel.style.width = options.dialogWidth || '600px'; panel.style.maxWidth = '90vw'; // 创建按钮 const cancelButton = createButton('取消', false, () => { panel.style.width = '320px'; // 恢复默认宽度 hide(null); }); const okButton = createButton('确定', true, () => { const value = textareaEl.value; panel.style.width = '320px'; // 恢复默认宽度 hide(value); }); // 清空并添加按钮 buttonArea.innerHTML = ''; buttonArea.appendChild(cancelButton); buttonArea.appendChild(okButton); // 显示对话框 overlay.style.display = 'flex'; // 聚焦到文本框 setTimeout(() => textareaEl.focus(), 100); // 返回Promise return new Promise(resolve => { resolvePromise = resolve; }); } function applyTheme() { if (!panel || !buttonArea) return; const header = panel.querySelector('div'); if (header) { header.style.background = 'var(--tmx-bg)'; header.style.color = 'var(--tmx-fg)'; } const primaryButtons = buttonArea.querySelectorAll('button'); primaryButtons.forEach((btn, index) => { if (index === primaryButtons.length - 1) { // 主按钮通常是最后一个 btn.style.background = 'var(--tmx-bg)'; btn.style.color = 'var(--tmx-fg)'; } }); } // 初始化函数,确保DOM元素已创建 function initialize() { // 确保DOM元素已创建 ensure(); console.log('Dialog: 初始化完成'); } return { alert, confirm, prompt, multilinePrompt, applyTheme, initialize }; })(); /** *************************** 调试代码窗口管理器 *********************************** */ const DebugWindowManager = (() => { let windowCounter = 0; const activeWindows = new Map(); const minimizedWindows = new Map(); let minimizedContainer = null; function createMinimizedContainer() { // 动态计算Toast弹窗的实际高度,为调试窗口留出空间 const toastElement = document.getElementById('tmx-toast'); let toastHeight = 0; if (toastElement && toastElement.style.display !== 'none') { // 弹窗显示时,获取其实际高度 toastHeight = toastElement.offsetHeight; } // 如果没有弹窗或获取不到高度,使用默认值 if (toastHeight === 0) { toastHeight = 50; // 默认高度 } const bottomOffset = 10 + toastHeight + 15; // 基础间距 + Toast实际高度 + 额外间距,避免遮挡弹窗按钮 if (minimizedContainer) { // 如果容器已存在,更新其位置 minimizedContainer.style.bottom = bottomOffset + 'px'; return; } minimizedContainer = h('div', { style: { position: 'fixed', bottom: bottomOffset + 'px', right: '10px', zIndex: 2147483646, display: 'flex', flexDirection: 'column', gap: '5px', maxWidth: '300px' } }); document.body.appendChild(minimizedContainer); } function createDebugWindow(defaultCode = '') { windowCounter++; const windowId = `debug-window-${windowCounter}`; // 检测是否为移动端设备 const isMobile = /Android|iPhone|SymbianOS|Windows Phone|iPad|iPod/i.test(navigator.userAgent); // 创建窗口遮罩 const overlay = h('div', { style: { position: 'fixed', inset: '0', zIndex: 2147483640, display: 'flex', background: 'rgba(0,0,0,0.3)', alignItems: isMobile ? 'flex-start' : 'center', justifyContent: 'center', paddingTop: isMobile ? (CONFIG.buttonTop + CONFIG.buttonHeight * 3 + 20) + 'px' : '0' } }); // 创建窗口面板 const panel = h('div', { style: { width: '700px', maxWidth: '90vw', background: '#fff', borderRadius: '6px', boxShadow: '0 4px 20px rgba(0,0,0,0.3)', overflow: 'hidden', fontFamily: 'Arial, sans-serif', display: 'flex', flexDirection: 'column', maxHeight: isMobile && window.innerHeight <= 667 ? '70vh' : '80vh' // iPhone SE等小屏设备优化 } }); // 标题栏 const header = h('div', { style: { padding: '10px 15px', borderBottom: '1px solid #eee', background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }); const titleEl = h('span', {}, `调试代码 #${windowCounter}`); // 窗口控制按钮容器 const controlButtons = h('div', { style: { display: 'flex', gap: '5px' } }); // 最小化按钮 const minimizeButton = h('button', { style: { background: 'none', border: 'none', color: 'var(--tmx-fg)', fontSize: '16px', cursor: 'pointer', padding: '2px 6px', borderRadius: '2px', lineHeight: '1' }, onclick: () => minimizeWindow(windowId) }, '−'); // 关闭按钮 const closeButton = h('button', { style: { background: 'none', border: 'none', color: 'var(--tmx-fg)', fontSize: '16px', cursor: 'pointer', padding: '2px 6px', borderRadius: '2px', lineHeight: '1' }, onclick: () => closeWindow(windowId) }, '×'); // 按钮悬停效果 [minimizeButton, closeButton].forEach(btn => { btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.2)'; }); btn.addEventListener('mouseleave', () => { btn.style.background = 'none'; }); }); controlButtons.appendChild(minimizeButton); controlButtons.appendChild(closeButton); header.appendChild(titleEl); header.appendChild(controlButtons); // 添加拖动功能 let isDragging = false; let dragOffset = { x: 0, y: 0 }; // 设置标题栏样式支持拖动 header.style.cursor = 'move'; header.style.userSelect = 'none'; header.addEventListener('mousedown', (e) => { // 只有点击标题区域才能拖动,避免点击按钮时触发拖动 if (e.target === header || e.target === titleEl) { isDragging = true; const rect = panel.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; // 防止文本选择 e.preventDefault(); // 添加全局鼠标事件 document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); } }); function handleMouseMove(e) { if (!isDragging) return; const newX = e.clientX - dragOffset.x; const newY = e.clientY - dragOffset.y; // 限制窗口不超出视窗边界 const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; const constrainedX = Math.max(0, Math.min(newX, maxX)); const constrainedY = Math.max(0, Math.min(newY, maxY)); panel.style.position = 'fixed'; panel.style.left = constrainedX + 'px'; panel.style.top = constrainedY + 'px'; panel.style.transform = 'none'; } function handleMouseUp() { isDragging = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); } // 内容区域 const contentEl = h('div', { style: { padding: '15px', flex: '1', display: 'flex', flexDirection: 'column', overflow: 'hidden' } }); // 多行文本输入框 const textareaEl = h('textarea', { style: { width: '100%', height: isMobile && window.innerHeight <= 667 ? '200px' : '300px', // 小屏设备减少高度 padding: '10px', border: '1px solid #ddd', borderRadius: '4px', boxSizing: 'border-box', fontFamily: 'Consolas, Monaco, "Courier New", monospace', fontSize: '13px', lineHeight: '1.4', resize: 'vertical', minHeight: isMobile && window.innerHeight <= 667 ? '150px' : '200px', // 小屏设备减少最小高度 maxHeight: isMobile && window.innerHeight <= 667 ? '300px' : '500px', // 小屏设备减少最大高度 flex: '1' }, placeholder: '请输入JavaScript代码...\n\n支持多行输入,例如:\nconsole.log("调试信息");\nalert("弹窗测试");\ndocument.querySelector("body").style.background = "red";' }); textareaEl.value = defaultCode; // 按钮区域 const buttonArea = h('div', { style: { padding: '15px', borderTop: '1px solid #eee', display: 'flex', justifyContent: 'flex-end', gap: '10px' } }); // 执行按钮 const executeButton = 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: () => executeCode(textareaEl.value, windowId) }, '执行代码'); // 添加到指令集按钮 const addToCommandButton = h('button', { style: { padding: '8px 16px', background: '#FF9800', color: '#ffffff', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold', marginRight: '5px' }, onclick: () => addToCommandSet(textareaEl.value) }, '添加到指令集'); // 从指令集选择按钮 const selectFromCommandButton = h('button', { style: { padding: '8px 16px', background: '#4CAF50', color: '#ffffff', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold', marginRight: '5px' }, onclick: () => selectFromCommandSet(textareaEl) }, '从指令集选择'); // 清空按钮 const clearButton = h('button', { style: { padding: '8px 16px', background: '#f8f9fa', color: '#333', border: '1px solid #ddd', borderRadius: '4px', cursor: 'pointer' }, onclick: () => { textareaEl.value = ''; textareaEl.focus(); } }, '清空'); buttonArea.appendChild(clearButton); buttonArea.appendChild(addToCommandButton); buttonArea.appendChild(selectFromCommandButton); buttonArea.appendChild(executeButton); contentEl.appendChild(textareaEl); panel.appendChild(header); panel.appendChild(contentEl); panel.appendChild(buttonArea); overlay.appendChild(panel); // 存储窗口信息 const windowInfo = { id: windowId, overlay, panel, textareaEl, titleEl }; activeWindows.set(windowId, windowInfo); document.body.appendChild(overlay); // 聚焦到文本框 setTimeout(() => textareaEl.focus(), 100); return windowId; } function minimizeWindow(windowId) { const windowInfo = activeWindows.get(windowId); if (!windowInfo) return; // 隐藏窗口 windowInfo.overlay.style.display = 'none'; // 移动到最小化列表 minimizedWindows.set(windowId, windowInfo); activeWindows.delete(windowId); // 创建最小化容器 createMinimizedContainer(); // 创建最小化项 const minimizedItem = h('div', { style: { background: 'var(--tmx-bg)', color: 'var(--tmx-fg)', width: '120px', // 设置固定宽度,与Toast弹窗一致 height: '32px', // 设置固定高度,与Toast弹窗一致 padding: '8px 12px', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', boxShadow: '0 2px 8px rgba(0,0,0,0.2)', boxSizing: 'border-box' // 确保padding包含在尺寸内 }, onclick: () => restoreWindow(windowId) }); const titleSpan = h('span', {}, windowInfo.titleEl.textContent); const closeBtn = h('span', { style: { cursor: 'pointer', fontSize: '14px', fontWeight: 'bold' }, onclick: (e) => { e.stopPropagation(); closeWindow(windowId); } }, '×'); minimizedItem.appendChild(titleSpan); minimizedItem.appendChild(closeBtn); minimizedContainer.appendChild(minimizedItem); // 存储最小化项引用 windowInfo.minimizedItem = minimizedItem; } function restoreWindow(windowId) { const windowInfo = minimizedWindows.get(windowId); if (!windowInfo) return; // 显示窗口 windowInfo.overlay.style.display = 'flex'; // 移回活动列表 activeWindows.set(windowId, windowInfo); minimizedWindows.delete(windowId); // 移除最小化项 if (windowInfo.minimizedItem) { windowInfo.minimizedItem.remove(); delete windowInfo.minimizedItem; } // 如果没有最小化窗口了,移除容器 if (minimizedWindows.size === 0 && minimizedContainer) { minimizedContainer.remove(); minimizedContainer = null; } // 聚焦到文本框 setTimeout(() => windowInfo.textareaEl.focus(), 100); } function closeWindow(windowId) { // 从活动窗口中移除 const activeWindow = activeWindows.get(windowId); if (activeWindow) { activeWindow.overlay.remove(); activeWindows.delete(windowId); } // 从最小化窗口中移除 const minimizedWindow = minimizedWindows.get(windowId); if (minimizedWindow) { minimizedWindow.overlay.remove(); if (minimizedWindow.minimizedItem) { minimizedWindow.minimizedItem.remove(); } minimizedWindows.delete(windowId); } // 如果没有最小化窗口了,移除容器 if (minimizedWindows.size === 0 && minimizedContainer) { minimizedContainer.remove(); minimizedContainer = null; } } // 便捷点击函数 - 通过按钮名称点击 function clickbtn(buttonText) { const buttons = Array.from(document.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]')); const targetButton = buttons.find(btn => { const text = btn.textContent || btn.value || btn.getAttribute('aria-label') || ''; return text.trim().includes(buttonText); }); if (targetButton) { targetButton.click(); console.log(`点击按钮: ${buttonText}`); return targetButton; } else { console.warn(`未找到包含文本 "${buttonText}" 的按钮`); return null; } } // 便捷点击函数 - 通过链接名称点击 function clickhref(linkText) { const links = Array.from(document.querySelectorAll('a')); const targetLink = links.find(link => { const text = link.textContent || link.getAttribute('title') || link.getAttribute('aria-label') || ''; return text.trim().includes(linkText); }); if (targetLink) { targetLink.click(); console.log(`点击链接: ${linkText}`); return targetLink; } else { console.warn(`未找到包含文本 "${linkText}" 的链接`); return null; } } // 便捷点击函数 - 通过CSS选择器点击 function clickgo(selector) { try { const element = document.querySelector(selector); if (element) { element.click(); console.log(`点击元素: ${selector}`); return element; } else { console.warn(`未找到选择器 "${selector}" 对应的元素`); return null; } } catch (error) { console.error(`选择器 "${selector}" 无效: ${error.message}`); return null; } } function copyWithGreasemonkey(text) { if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); console.log('内容已通过油猴脚本复制到剪贴板'); return true; } return false; } // 将便捷函数挂载到全局window对象,使其在F12控制台中也可用 window.clickbtn = clickbtn; window.clickhref = clickhref; window.clickgo = clickgo; window.copyWithGreasemonkey = copyWithGreasemonkey; console.log('[便捷函数] clickbtn、clickhref、clickgo、copyWithGreasemonkey 已挂载到全局,可在控制台直接使用'); async function executeCode(code, windowId) { if (!code || code.trim() === '') { await Dialog.alert('请输入要执行的代码', '提示'); return; } try { console.log(`[调试窗口 #${windowId}] 执行代码:`, code); const result = eval(code); console.log(`[调试窗口 #${windowId}] 执行结果:`, result); // 显示执行结果 if (result !== undefined) { const resultStr = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result); await Dialog.alert(`执行结果:\n${resultStr}`, '调试结果'); } else { await Dialog.alert('代码执行完成(无返回值)', '调试结果'); } Logger.append(`[调试窗口] 执行成功: ${code.split('\n')[0]}${code.split('\n').length > 1 ? '...' : ''}`); } catch (error) { console.error(`[调试窗口 #${windowId}] 执行错误:`, error); await Dialog.alert(`执行错误:\n${error.message}\n\n堆栈信息:\n${error.stack}`, '调试错误'); Logger.append(`[调试窗口] 执行错误: ${error.message}`); } } // 添加到指令集功能 async function addToCommandSet(code) { if (!code || code.trim() === '') { await Dialog.alert('请先输入要保存的代码', '提示'); return; } try { // 请求输入指令名称 const commandName = await Dialog.prompt('请输入指令名称:', '', '添加到指令集'); if (commandName === null) { // 用户取消了输入 return; } if (!commandName || commandName.trim() === '') { await Dialog.alert('指令名称不能为空', '错误'); return; } // 请求输入指令描述(可选) const commandDescription = await Dialog.prompt('请输入指令描述(可选):', '', '添加到指令集'); if (commandDescription === null) { // 用户取消了输入 return; } // 检查指令名称是否已存在 const existingCommands = CommandStorage.getAll(); const nameExists = existingCommands.some(cmd => cmd.name === commandName.trim()); if (nameExists) { const confirmed = await Dialog.confirm(`指令名称 "${commandName.trim()}" 已存在,是否覆盖?`, '确认覆盖'); if (!confirmed) { return; } // 删除同名指令 const existingCommand = existingCommands.find(cmd => cmd.name === commandName.trim()); if (existingCommand) { CommandStorage.remove(existingCommand.id); } } // 添加指令到存储 const success = CommandStorage.add(commandName.trim(), code.trim(), commandDescription ? commandDescription.trim() : ''); if (success) { Toast.show(`指令 "${commandName.trim()}" 已添加到指令集`); console.log(`[指令集] 添加指令成功: ${commandName.trim()}`); Logger.append(`[指令集] 添加指令: ${commandName.trim()}`); // 如果指令选择器已打开,更新按钮显示 if (commandSelector && commandSelector.visible) { commandSelector.updateCommandButtons(); } } else { await Dialog.alert('添加指令失败,请重试', '错误'); } } catch (error) { console.error('[指令集] 添加指令失败:', error); await Dialog.alert(`添加指令失败: ${error.message}`, '错误'); } } // 从指令集选择功能 function selectFromCommandSet(textareaEl) { const commands = CommandStorage.getAll(); if (commands.length === 0) { Toast.show('没有可选择的指令', 'warning'); return; } // 创建临时的指令选择弹窗 const commandSelectPopup = new GroupPopup('选择指令'); // 为每个指令添加按钮 commands.forEach(command => { commandSelectPopup.addButton(command.name, () => { // 将指令代码加载到调试代码区域 textareaEl.value = command.code; textareaEl.focus(); Toast.show(`已加载指令: ${command.name}`); console.log(`[调试执行器] 加载指令: ${command.name}`); Logger.append(`[调试执行器] 加载指令: ${command.name}`); // 关闭弹窗 commandSelectPopup.hide(); }); }); // 显示弹窗 commandSelectPopup.show(); } function applyTheme() { // 为所有活动窗口应用主题 activeWindows.forEach(windowInfo => { const header = windowInfo.panel.querySelector('div'); if (header) { header.style.background = 'var(--tmx-bg)'; header.style.color = 'var(--tmx-fg)'; } const executeButton = windowInfo.panel.querySelector('button[onclick*="executeCode"]'); if (executeButton) { executeButton.style.background = 'var(--tmx-bg)'; executeButton.style.color = 'var(--tmx-fg)'; } }); // 为最小化项应用主题 if (minimizedContainer) { const items = minimizedContainer.querySelectorAll('div'); items.forEach(item => { item.style.background = 'var(--tmx-bg)'; item.style.color = 'var(--tmx-fg)'; }); } } // 更新最小化容器位置的方法 function updateMinimizedContainerPosition() { if (minimizedContainer && minimizedWindows.size > 0) { // 重新计算位置 createMinimizedContainer(); } } // 获取最小化容器信息的方法 return { createWindow: createDebugWindow, closeWindow, minimizeWindow, restoreWindow, applyTheme, updateMinimizedContainerPosition }; })(); /** *************************** 初始化 *********************************** */ function init() { Theme.apply(); Logger.hook(); Logger.append(`${META.name}: v${META.version}`); Logger.append(`布局偏移:${getLayoutOffset()}`); // 显示存储模式信息 const storageInfo = store.getStorageInfo(); Logger.append(`存储模式:${storageInfo.mode}${storageInfo.crossDomain ? ' (支持跨域共享)' : ' (仅当前域名)'}`); console.log('脚本存储信息:', storageInfo); render(); Dialog.initialize(); Dialog.applyTheme(); // 将DebugWindowManager和Toast暴露到全局,供相互调用 window.DebugWindow = DebugWindowManager; window.Toast = Toast; // 初始:同步 toast 按钮状态(如果存在) const toastOn = store.get('toast.enabled', 0) === 1; const toastBtn = buttonMap.get('toast'); if (toastBtn) { toastBtn.textContent = toastOn ? '关闭弹窗' : '弹窗提示'; toastBtn.style.borderStyle = toastOn ? 'inset' : 'outset'; } if (toastOn) { Toast.show('提示', '你好'); } else { // 如果弹窗提示是关闭状态,确保关闭可能存在的弹窗 Toast.hide(); } // 初始:同步日志显示状态 const loggerHidden = store.get('logger.hidden', 0) === 1; if (loggerHidden) { Logger.hide(); const logBtn = buttonMap.get('toggle-log'); if (logBtn) { logBtn.textContent = '显示日志'; logBtn.style.borderStyle = 'outset'; } } else { Logger.show(); const logBtn = buttonMap.get('toggle-log'); if (logBtn) { logBtn.textContent = '隐藏日志'; logBtn.style.borderStyle = 'inset'; } } // 初始:同步按钮显示状态 const buttonsHidden = store.get('buttons.hidden', 0) === 1; if (buttonsHidden) { for (const [id, el] of buttonMap) { if (id === 'toggle-buttons') continue; el.style.visibility = 'hidden'; } const btn = buttonMap.get('toggle-buttons'); const btnBox = btn ? btn.parentElement : null; for (const [, box] of columns.columns) { 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 { const btnToggle = buttonMap.get('toggle-buttons'); if (btnToggle) { btnToggle.textContent = '隐按钮'; btnToggle.style.borderStyle = 'inset'; } for (const [, box] of columns.columns) { box.style.display = ''; } const btn = buttonMap.get('toggle-buttons'); const btnBox = btn ? btn.parentElement : null; if (btnBox) { btnBox.style.position = ''; btnBox.style.left = ''; btnBox.style.top = ''; btnBox.style.width = CONFIG.columnWidth + 'px'; btnBox.style.zIndex = ''; } } // 给组内的 toggle 按钮同步初始样式(因为 gp.addButton 已处理,这里确保若你需要同步主按钮的样式也可) const kgjBtn = buttonMap.get('kgj-open'); const gp = groupMap.get('开关集'); if (kgjBtn && gp) { kgjBtn.style.borderStyle = gp.visible ? 'inset' : 'outset'; } // 数据迁移函数:将localStorage数据迁移到GM全局存储 function migrateLocalStorageToGM() { try { // 迁移标记,避免重复迁移 const migrated = store.get('data_migrated', false); if (migrated) return; console.log('开始迁移localStorage数据到GM全局存储...'); // 迁移本地指令集 const localCommands = localStorage.getItem('custom_commands'); if (localCommands && !store.get('custom_commands', null)) { try { const commands = JSON.parse(localCommands); store.set('custom_commands', commands); console.log('已迁移本地指令集:', commands.length, '条'); } catch (e) { console.error('迁移本地指令集失败:', e); } } // 迁移定时任务 const scheduledTasks = localStorage.getItem('scheduled_tasks'); if (scheduledTasks && !store.get('scheduled_tasks', null)) { try { const tasks = JSON.parse(scheduledTasks); store.set('scheduled_tasks', tasks); console.log('已迁移定时任务:', tasks.length, '条'); } catch (e) { console.error('迁移定时任务失败:', e); } } // 迁移远程指令缓存 const remoteCommands = localStorage.getItem('remote_commands_cache'); if (remoteCommands && !store.get('remote_commands_cache', null)) { try { const commands = JSON.parse(remoteCommands); store.set('remote_commands_cache', commands); console.log('已迁移远程指令缓存:', commands.length, '条'); } catch (e) { console.error('迁移远程指令缓存失败:', e); } } // 迁移远程指令同步时间 const lastSyncTime = localStorage.getItem('remote_commands_last_sync'); if (lastSyncTime && !store.get('remote_commands_last_sync', null)) { try { const time = parseInt(lastSyncTime); store.set('remote_commands_last_sync', time); console.log('已迁移远程指令同步时间:', new Date(time).toLocaleString()); } catch (e) { console.error('迁移远程指令同步时间失败:', e); } } // 标记迁移完成 store.set('data_migrated', true); console.log('数据迁移完成!'); } catch (error) { console.error('数据迁移过程中发生错误:', error); } } // 启动调度器并加载定时任务 Scheduler.start(); // 创建全局调度器实例供管理界面使用 window.scheduler = Scheduler; // 数据迁移:将localStorage数据迁移到GM全局存储 migrateLocalStorageToGM(); // 页面加载时自动同步远程指令集 try { const remoteUrl = store.get('remote_command_url', ''); const remoteEnabled = store.get('remote_commands_enabled', 0) === 1; if (remoteEnabled && remoteUrl) { console.log('页面加载时自动同步远程指令集:', remoteUrl); syncRemoteCommands(false).catch(error => { console.error('自动同步远程指令集失败:', error); }); } } catch (error) { console.error('检查远程指令集配置失败:', error); } const now = new Date().toLocaleString(); console.log(`上次网页刷新时间:${now}`); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();