您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一个由 AI 驱动的用户脚本(UserScript),支持 OpenAI 和 Gemini,可以快速将网页保存并智能分类到您的 Notion 数据库中。它拥有一个设计优雅、可拖动的悬浮 UI、一个功能全面的设置面板,并为兼容现代网站而精心设计。
当前为
// ==UserScript== // @name Notion AI 快速保存助手 // @namespace http://tampermonkey.net/ // @version 2.1 // @description 一个由 AI 驱动的用户脚本(UserScript),支持 OpenAI 和 Gemini,可以快速将网页保存并智能分类到您的 Notion 数据库中。它拥有一个设计优雅、可拖动的悬浮 UI、一个功能全面的设置面板,并为兼容现代网站而精心设计。 // @author tsdw // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license MIT // @connect * // ==/UserScript== (function() { 'use strict'; // =================================================================== // ===================== 初始化锁 (双重保险) ========================= // =================================================================== if (window.self !== window.top) { return; } if (window.NQS_SCRIPT_RUNNING) { return; } window.NQS_SCRIPT_RUNNING = true; // =================================================================== // ===================== 全局容器与样式隔离 ======================== // =================================================================== const globalContainer = document.createElement('div'); globalContainer.id = 'NQS_GLOBAL_CONTAINER'; document.body.appendChild(globalContainer); // =================================================================== // ===================== 默认配置与分类管理 ======================== // =================================================================== const DEFAULT_CATEGORIES = [ "前端开发", "后端开发", "人工智能", "运维安全", "生活服务", "美食烹饪", "旅行", "健康医疗", "影视", "音乐", "游戏", "动漫","综艺", "学术研究", "教育学习", "金融理财", "二手交易", "求职招聘", "办公工具", "职业规划", "热点与政策", "社区互动", "其他" ]; const SETTINGS_DEFAULTS = { theme: 'auto', notion_api_key: '', database_id: '', ai_provider: 'openai', ai_api_key: '', ai_api_url: 'https://api.openai.com/v1/chat/completions', ai_model: 'gpt-3.5-turbo', ai_enabled: true, ai_include_body: false, ai_timeout: 20000, ai_model_fetch_timeout: 10000, // 新增: 独立的模型列表获取超时 ai_retry_count: 2, // 新增: AI调用重试次数 ai_confidence_threshold: 0.7, // 新增: AI分类置信度阈值 ai_learning_enabled: true, // 新增: 启用AI分类学习 content_extraction_enabled: true, // 新增: 启用内容提取增强 auto_summary_enabled: false, // 新增: 自动生成摘要 auto_keywords_enabled: false, // 新增: 自动提取关键词 content_save_mode: 'link', // 新增: 内容保存模式 (link/summary/full) notification_enabled: true, // 新增: 启用浏览器通知 notification_success_enabled: true, // 新增: 成功通知 notification_error_enabled: true, // 新增: 错误通知 progress_indicator_enabled: true, // 新增: 显示进度指示器 proxy_enabled: false, proxy_url: '', user_categories: JSON.stringify(DEFAULT_CATEGORIES), prop_name_title: '名称', prop_name_url: '链接', prop_name_category: '类型', read_later_enabled: true, read_later_category: '稍后读', ai_prompt: `你是一个专精于网页内容分类的AI模型。你的唯一任务是遵循下述协议,分析网页信息,并从一个预设的分类列表中,选择唯一且最匹配的一个分类。 # 分析层级与协议 (Analysis Hierarchy & Protocol) 你必须严格按照以下优先级顺序获取和分析信息。高优先级方法成功后,低优先级信息仅用作辅助验证。 1. **最高优先级:实时网页分析 (Live Webpage Analysis)** * **行动指令:** 如果你的能力允许,首先尝试直接访问并完整分析 Page URL 指向的实时网页内容。 * **基本原理:** 实时网页是最权威、最准确的信息源。它的内容、结构和互动元素能最完整地反映页面的真实用途。 2. **次高优先级:静态元数据分析 (Static Metadata Analysis)** * **触发条件:** 当且仅当你无法访问实时网页(例如,技术限制、访问错误、防火墙)时,启用此层级。 * **行动指令:** 详细分析提供的网页标题 (Page Title) 和 元描述 (Meta Description)。这两个字段是网站所有者设定的核心摘要。 3. **最低优先级:辅助正文参考 (Supplementary Body Text)** * **触发条件:** 仅在前序层级(实时网页或静态元数据)分析后,分类结果依然高度模糊、难以抉择时,才可使用此信息。 * **行动指令:** 将提供的{page_body_text}作为最后的补充线索。 * **注意:** 此文本可能是不完整或过时的快照,其权重远低于实时网页内容和核心元数据。 # 分类决策原则 在获得信息后,运用以下原则进行最终决策: - **选择最具体的:** 如果页面符合一个宽泛分类(如"社区互动")和一个具体分类(如"前端开发"),优先选择更具体的那个。 - **识别核心用途:** 对于多主题页面(如门户网站首页),判断其最核心、最主要的功能或主题进行分类。 - **识别平台/服务核心价值:** 对于像 GitHub, YouTube, Amazon 这样的平台型网站,应根据其核心服务来分类(例如,GitHub -> "后端开发" 或 "办公工具",YouTube -> "影视",Amazon -> "生活服务"),而不是其首页上可能出现的具体内容。 - **选择最佳匹配:** 必须从下方列表中选择。即使没有完美的选项,也要选择最接近的一个。 # 预设分类列表 {categories} # 输出格式规定 - **【绝对必须】** 只输出最终的分类名称,且该名称必须完整存在于"预设分类列表"中。 - **【绝对禁止】** 禁止添加任何形式的解释、说明、标点符号(如 "", 「」)、Markdown标记(如 *, \`\`)或其他任何多余字符。 # 任务开始 根据下面提供的信息,并严格遵守上述所有协议、原则和规则,给出你的分类结果。 ## 网页信息: {page_metadata}{optional_body_section}` , ai_prompt_templates: JSON.stringify([{ id: 'default', title: '默认', content: '', builtIn: true }]), ai_active_template_id: 'default' }; const LOG_LIMIT = 100; // =================================================================== // ================= CSP TrustedHTML 处理器 ========================== // =================================================================== let nqsPolicy; if (window.trustedTypes && window.trustedTypes.createPolicy) { try { nqsPolicy = window.trustedTypes.createPolicy('nqs-policy', { createHTML: string => string }); } catch (e) { nqsPolicy = window.trustedTypes.defaultPolicy || window.trustedTypes.policies.get("nqs-policy"); } } function createSafeHTML(htmlString) { return nqsPolicy ? nqsPolicy.createHTML(htmlString) : htmlString; } function setSafeInnerHTML(element, htmlString) { element.innerHTML = createSafeHTML(htmlString); } // =================================================================== // ====================== 核心功能与辅助函数 ======================= // =================================================================== function getHighSignalPageData(doc) { const url = doc.location.href; const title = doc.title; const description = (doc.querySelector('meta[name="description"]') || {}).content || '无'; const keywords = (doc.querySelector('meta[name="keywords"]') || {}).content || '无'; return `Page URL: ${url}\nPage Title: ${title}\nMeta Description: ${description.trim()}\nMeta Keywords: ${keywords.trim()}`; } // =================================================================== // ====================== 增强内容提取功能 ============================ // =================================================================== function extractMainContent(doc) { try { const main = doc.querySelector('main'); if (main) return main.innerText; const articles = doc.querySelectorAll('article'); if (articles.length > 0) return Array.from(articles).map(el => el.innerText).join('\n\n'); const clonedBody = doc.body.cloneNode(true); clonedBody.querySelectorAll('nav, footer, header, aside, .sidebar, #sidebar, [role="navigation"], [role="banner"], [role="contentinfo"], .ad, #ad, .advertisement').forEach(el => el.remove()); const cleanedText = clonedBody.innerText; return (cleanedText && cleanedText.trim().length > 100) ? cleanedText : doc.body.innerText; } catch (error) { console.error("NQS - 智能内容提取失败:", error); return doc.body.innerText; } } function extractAdvancedContent(doc) { const content = { title: doc.title, url: doc.location.href, description: (doc.querySelector('meta[name="description"]') || {}).content || '', keywords: (doc.querySelector('meta[name="keywords"]') || {}).content || '', author: (doc.querySelector('meta[name="author"]') || {}).content || '', publishTime: '', readingTime: 0, mainContent: '', images: [], links: [] }; // 提取发布时间 const timeSelectors = [ 'time[datetime]', '[datetime]', '.publish-time', '.date', '.post-date' ]; for (const selector of timeSelectors) { const timeEl = doc.querySelector(selector); if (timeEl) { content.publishTime = timeEl.getAttribute('datetime') || timeEl.textContent.trim(); break; } } // 提取主要内容 content.mainContent = extractMainContent(doc); // 计算阅读时间(基于词数,中文按字符数) const textLength = content.mainContent.replace(/\s+/g, '').length; content.readingTime = Math.ceil(textLength / 500); // 假设每分钟500字 // 提取图片 const images = doc.querySelectorAll('img[src]'); content.images = Array.from(images).slice(0, 5).map(img => ({ src: img.src, alt: img.alt || '', title: img.title || '' })); // 提取重要链接 const links = doc.querySelectorAll('a[href]'); content.links = Array.from(links).filter(link => link.href.startsWith('http') && !link.href.includes(doc.location.hostname) ).slice(0, 10).map(link => ({ href: link.href, text: link.textContent.trim() })); return content; } async function generateContentSummary(content, settings) { if (!settings.ai_enabled || !settings.auto_summary_enabled) { return null; } try { const summaryPrompt = `请为以下网页内容生成一个简洁的摘要(100-200字): 标题:${content.title} 内容:${content.mainContent.substring(0, 2000)} 要求: 1. 提取核心要点 2. 语言简洁明了 3. 保持客观中性 4. 只输出摘要内容,无需其他说明`; const aiResult = await makeSummaryRequest(settings, summaryPrompt); return aiResult.summary; } catch (error) { console.warn('NQS - 自动摘要生成失败:', error.message); return null; } } async function extractKeywords(content, settings) { if (!settings.ai_enabled || !settings.auto_keywords_enabled) { return []; } try { const keywordPrompt = `请从以下网页内容中提取5-8个关键词: 标题:${content.title} 描述:${content.description} 内容:${content.mainContent.substring(0, 1500)} 要求: 1. 关键词应该是名词或名词短语 2. 体现内容的核心主题 3. 用逗号分隔 4. 只输出关键词,无需其他文字`; const aiResult = await makeSummaryRequest(settings, keywordPrompt); return aiResult.summary.split(',').map(k => k.trim()).filter(k => k.length > 0); } catch (error) { console.warn('NQS - 关键词提取失败:', error.message); return []; } } async function makeSummaryRequest(settings, prompt) { const { ai_provider, ai_api_key, ai_api_url, ai_model, ai_timeout, proxy_enabled, proxy_url } = settings; return new Promise((resolve, reject) => { let requestDetails = { method: 'POST', url: '', headers: { 'Content-Type': 'application/json' }, data: '', timeout: parseInt(ai_timeout, 10) || 20000, ontimeout: () => reject(new Error("摘要生成超时")), onload: null, onerror: () => reject(new Error('摘要生成网络请求失败')) }; if (ai_provider === 'gemini') { const geminiUrl = `https://generativelanguage.googleapis.com/v1/models/${ai_model}:generateContent?key=${ai_api_key}`; requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : geminiUrl; if (proxy_enabled && proxy_url) { requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`; } requestDetails.data = JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: 0.3, maxOutputTokens: 300 } }); requestDetails.onload = (response) => { if (response.status >= 200 && response.status < 300) { try { const result = JSON.parse(response.responseText); const summary = result.candidates[0]?.content?.parts[0]?.text || ""; resolve({ summary: summary.trim() }); } catch (e) { reject(new Error(`解析摘要响应失败: ${e.message}`)); } } else { reject(new Error(`摘要生成API错误: ${response.status}`)); } }; } else { // OpenAI requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : ai_api_url; requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`; requestDetails.data = JSON.stringify({ model: ai_model, messages: [{ role: 'user', content: prompt }], temperature: 0.3, max_tokens: 300 }); requestDetails.onload = (response) => { if (response.status >= 200 && response.status < 300) { try { const result = JSON.parse(response.responseText); const summary = result.choices[0].message.content || ""; resolve({ summary: summary.trim() }); } catch (e) { reject(new Error(`解析摘要响应失败: ${e.message}`)); } } else { reject(new Error(`摘要生成API错误: ${response.status}`)); } }; } GM_xmlhttpRequest(requestDetails); }); } async function addLog(level, message, details = {}) { let logs = []; try { logs = JSON.parse(await GM_getValue('nqs_logs', '[]') || '[]'); } catch (e) { console.error("NQS - 解析本地日志失败:", e); logs = []; } logs.unshift({ timestamp: Date.now(), level, message, details }); if (logs.length > LOG_LIMIT) logs.splice(LOG_LIMIT); await GM_setValue('nqs_logs', JSON.stringify(logs)); } async function loadAllSettings() { const settings = {}; for (const key of Object.keys(SETTINGS_DEFAULTS)) { settings[key] = await GM_getValue(key, SETTINGS_DEFAULTS[key]); } return settings; } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } async function applyTheme() { const theme = await GM_getValue('theme', SETTINGS_DEFAULTS.theme); const container = document.getElementById('NQS_GLOBAL_CONTAINER'); if(container) container.dataset.theme = theme; } // =================================================================== // ===================== UI创建与管理 (CSP-Ready) ===================== // =================================================================== function closeAllNQSPopups() { const container = document.getElementById('NQS_GLOBAL_CONTAINER'); if (container) container.querySelectorAll('.nqs-overlay').forEach(el => el.remove()); } function createBasePanel(title, subtitle = '', options = {}) { injectStyles(); const container = document.getElementById('NQS_GLOBAL_CONTAINER'); const overlay = document.createElement('div'); overlay.className = 'nqs-overlay'; if (options.panelId) overlay.id = options.panelId; if (options.isNested) { const existingOverlays = container.querySelectorAll('.nqs-overlay'); let topZ = 100000; if (existingOverlays.length > 0) { const zIndexes = Array.from(existingOverlays).map(el => parseInt(window.getComputedStyle(el).zIndex, 10)); const maxZ = Math.max(...zIndexes.filter(z => !isNaN(z))); if (isFinite(maxZ)) topZ = maxZ; } overlay.style.zIndex = topZ + 1; } const panel = document.createElement('div'); panel.className = `nqs-panel ${options.panelClass || ''}`; if (options.maxWidth) panel.style.maxWidth = options.maxWidth; const header = document.createElement('div'); header.className = 'nqs-header'; const h1 = document.createElement('h1'); h1.textContent = title; header.appendChild(h1); if (subtitle) { const p = document.createElement('p'); p.textContent = subtitle; header.appendChild(p); } const body = document.createElement('div'); body.className = 'nqs-body'; const footer = document.createElement('div'); footer.className = 'nqs-footer'; panel.appendChild(header); panel.appendChild(body); panel.appendChild(footer); overlay.appendChild(panel); container.appendChild(overlay); const close = () => { overlay.classList.remove('visible'); setTimeout(() => overlay.remove(), 300); }; panel.addEventListener('click', e => e.stopPropagation()); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); setTimeout(() => overlay.classList.add('visible'), 10); return { overlay, body, footer, close }; } function highlightJson(jsonString) { if (typeof jsonString !== 'string') jsonString = JSON.stringify(jsonString, undefined, 2); jsonString = jsonString.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return jsonString.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'json-number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'json-key'; } else { cls = 'json-string'; } } else if (/true|false/.test(match)) { cls = 'json-boolean'; } else if (/null/.test(match)) { cls = 'json-null'; } return '<span class="' + cls + '">' + match + '</span>'; });} function showAlertModal(title, message) { return new Promise((resolve) => { const { body, footer, close } = createBasePanel(title, '', { maxWidth: '480px', isNested: true }); setSafeInnerHTML(body, `<p class="nqs-p">${message}</p>`); setSafeInnerHTML(footer, `<div style="flex-grow: 1;"></div><button id="nqs-alert-ok" class="nqs-button nqs-button-primary">确定</button>`); footer.querySelector('#nqs-alert-ok').onclick = () => { close(); resolve(); }; });} function showConfirmationModal(title, message, options = {}) { return new Promise((resolve) => { const { body, footer, close } = createBasePanel(title, '', { maxWidth: '480px', isNested: true }); const { danger = false, confirmText = '确认', cancelText = '取消' } = options; setSafeInnerHTML(body, `<p class="nqs-p">${message}</p>`); const confirmClass = danger ? 'nqs-button-danger' : 'nqs-button-primary'; setSafeInnerHTML(footer, `<button id="nqs-confirm-cancel" class="nqs-button nqs-button-secondary">${cancelText}</button><div style="flex-grow: 1;"></div><button id="nqs-confirm-ok" class="nqs-button ${confirmClass}">${confirmText}</button>`); footer.querySelector('#nqs-confirm-ok').onclick = () => { close(); resolve(true); }; footer.querySelector('#nqs-confirm-cancel').onclick = () => { close(); resolve(false); }; });} function showDetailsModal(title, detailsObject) { const { body, footer, close } = createBasePanel(title, '上下文详细信息', { maxWidth: '800px', isNested: true }); body.classList.add('nqs-json-viewer'); const pre = document.createElement('pre'); pre.style.whiteSpace = 'pre-wrap'; pre.style.wordWrap = 'break-word'; pre.style.margin = '0'; setSafeInnerHTML(pre, highlightJson(detailsObject)); body.appendChild(pre); setSafeInnerHTML(footer, `<div style="flex-grow: 1;"></div><button class="nqs-button nqs-button-primary">关闭</button>`); footer.querySelector('button').addEventListener('click', close);} async function openSettingsPanel() { closeAllNQSPopups(); const { body, footer, close } = createBasePanel('⚙️ 脚本设置', '配置您的 Notion AI 快速保存助手,实现高效网页收藏', { maxWidth: '900px', panelClass: 'nqs-panel--settings' }); const elements = {}; // 创建设置分组的辅助函数 const createSettingsGroup = (title, description = '', icon = '⚙️') => { const group = document.createElement('div'); group.className = 'nqs-settings-group'; const header = document.createElement('div'); header.className = 'nqs-settings-group-header'; header.innerHTML = ` <div class="nqs-settings-group-icon">${icon}</div> <div class="nqs-settings-group-content"> <h3 class="nqs-settings-group-title">${title}</h3> ${description ? `<p class="nqs-settings-group-description">${description}</p>` : ''} </div> `; const content = document.createElement('div'); content.className = 'nqs-settings-group-content'; group.appendChild(header); group.appendChild(content); return { group, content }; }; // 创建设置字段的辅助函数 const createSettingField = (parent, id, labelText, descText, type = 'text', options = {}) => { const field = document.createElement('div'); field.className = 'nqs-setting-field'; // 用于后续显隐控制(例如代理URL) field.dataset.fieldId = id; // 支持全宽字段(无标签或明确要求时) if (!labelText || options.fullWidth) { field.classList.add('nqs-setting-field--full'); } const labelGroup = document.createElement('div'); labelGroup.className = 'nqs-setting-label-group'; const label = document.createElement('label'); label.htmlFor = 'nqs-' + id; label.textContent = labelText; labelGroup.appendChild(label); if (descText) { const desc = document.createElement('p'); desc.className = 'nqs-setting-description'; desc.innerHTML = createSafeHTML(descText); labelGroup.appendChild(desc); } field.appendChild(labelGroup); const inputGroup = document.createElement('div'); inputGroup.className = 'nqs-setting-input-group'; const creators = { toggle: () => createToggleSwitch(id), select: () => createSelectInput(id, options.choices || []), textarea: () => createTextareaInput(id, options.placeholder || ''), number: () => createNumberInput(id, options.min, options.max, options.step), multiselect: () => createMultiSelectInput(id, options.choices || []) }; const input = creators[type] ? creators[type]() : createTextInput(id, type, options.placeholder || ''); inputGroup.appendChild(input); field.appendChild(inputGroup); parent.appendChild(field); elements[id] = input; return field; }; // 创建开关组件 const createToggleSwitch = (id) => { const wrapper = document.createElement('div'); wrapper.className = 'nqs-toggle-switch'; const label = document.createElement('label'); label.className = 'nqs-switch'; const input = document.createElement('input'); input.type = 'checkbox'; input.id = 'nqs-' + id; const span = document.createElement('span'); span.className = 'nqs-slider'; label.appendChild(input); label.appendChild(span); wrapper.appendChild(label); return wrapper; }; // 创建选择器组件 const createSelectInput = (id, choices) => { const select = document.createElement('select'); select.id = 'nqs-' + id; select.className = 'nqs-select'; choices.forEach(choice => { const option = document.createElement('option'); option.value = choice.value; option.textContent = choice.label; select.appendChild(option); }); return select; }; // 创建多选选择器组件(统一风格) const createMultiSelectInput = (id, choices) => { const select = document.createElement('select'); select.id = 'nqs-' + id; select.className = 'nqs-select'; select.multiple = true; choices.forEach(choice => { const option = document.createElement('option'); option.value = choice.value; option.textContent = choice.label; select.appendChild(option); }); return select; }; // 创建文本输入组件 const createTextInput = (id, type, placeholder) => { const input = document.createElement('input'); input.id = 'nqs-' + id; input.className = 'nqs-input'; input.type = type; if (placeholder) input.placeholder = placeholder; return input; }; // 创建数字输入组件 const createNumberInput = (id, min, max, step) => { const input = document.createElement('input'); input.id = 'nqs-' + id; input.className = 'nqs-input'; input.type = 'number'; if (min !== undefined) input.min = min; if (max !== undefined) input.max = max; if (step !== undefined) input.step = step; return input; }; // 创建文本域组件 const createTextareaInput = (id, placeholder) => { const textarea = document.createElement('textarea'); textarea.id = 'nqs-' + id; textarea.className = 'nqs-textarea'; if (placeholder) textarea.placeholder = placeholder; return textarea; }; // 创建分类管理组件 const createCategoryManager = (parent) => { const categorySection = document.createElement('div'); categorySection.className = 'nqs-category-manager-section'; const header = document.createElement('div'); header.className = 'nqs-category-manager-header'; header.innerHTML = ` <h4>分类列表</h4> <p>管理您的自定义分类,这些分类将用于手动选择和AI自动分类</p> `; categorySection.appendChild(header); const inputGroup = document.createElement('div'); inputGroup.className = 'nqs-category-input-group'; const input = document.createElement('input'); input.type = 'text'; input.className = 'nqs-category-input'; input.placeholder = '输入新分类名称...'; input.id = 'nqs-new-category-input'; const addBtn = document.createElement('button'); addBtn.className = 'nqs-button nqs-button-primary nqs-category-add-btn'; addBtn.textContent = '添加分类'; addBtn.id = 'nqs-add-category-btn'; inputGroup.appendChild(input); inputGroup.appendChild(addBtn); categorySection.appendChild(inputGroup); const categoryList = document.createElement('div'); categoryList.className = 'nqs-category-list-container'; categorySection.appendChild(categoryList); parent.appendChild(categorySection); return { input, addBtn, categoryList }; }; // 创建AI模型选择器组件 const createModelSelector = (parent) => { const modelSection = document.createElement('div'); modelSection.className = 'nqs-model-selector-section'; const inputGroup = document.createElement('div'); inputGroup.className = 'nqs-model-input-group'; const input = document.createElement('input'); input.type = 'text'; input.className = 'nqs-input'; input.id = 'nqs-ai_model'; input.placeholder = '选择AI模型...'; const fetchBtn = document.createElement('button'); fetchBtn.className = 'nqs-icon-button nqs-fetch-models-btn'; fetchBtn.title = '获取可用模型列表'; fetchBtn.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"> <path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1.293-2.707a1 1 0 0 1-1.414-1.414l.001-.001 2.122-2.121a1 1 0 0 1 1.414 0l2.121 2.121a1 1 0 0 1-1.414 1.414L13 16.414V20a1 1 0 1 1-2 0v-3.586l-.293.293zM13 4a1 1 0 1 1 2 0v3.586l.293-.293a1 1 0 1 1 1.414 1.414l-2.121 2.121a1 1 0 0 1-1.414 0L10.05 8.707a1 1 0 0 1 1.414-1.414L11.76 7.586 11 7.586V4h2z"></path> </svg> `; inputGroup.appendChild(input); inputGroup.appendChild(fetchBtn); modelSection.appendChild(inputGroup); const dropdown = document.createElement('div'); dropdown.className = 'nqs-model-dropdown'; modelSection.appendChild(dropdown); parent.appendChild(modelSection); return { input, fetchBtn, dropdown }; }; // 创建设置内容容器(供右侧内容滚动) const settingsContainer = document.createElement('div'); settingsContainer.className = 'nqs-settings-container'; // 1. 外观设置组 const appearanceGroup = createSettingsGroup('🎨 外观设置', '自定义界面主题和显示效果', '🎨'); createSettingField(appearanceGroup.content, 'theme', '主题模式', '选择UI的显示主题,"自动"将跟随系统设置', 'select', { choices: [ { value: 'auto', label: '自动 (跟随系统)' }, { value: 'light', label: '明亮主题' }, { value: 'dark', label: '暗黑主题' } ] }); // 2. 核心功能组 const coreGroup = createSettingsGroup('🚀 核心功能', '配置脚本的基本功能和AI自动分类', '🚀'); createSettingField(coreGroup.content, 'ai_enabled', 'AI 自动分类', '开启后将自动判断分类,关闭则需要手动选择', 'toggle'); createSettingField(coreGroup.content, 'read_later_enabled', '"稍后读"功能', '开启后将显示一个独立的"稍后读"快捷按钮', 'toggle'); createSettingField(coreGroup.content, 'read_later_category', '"稍后读"分类名', '指定用于"稍后读"功能的分类名称', 'text', { placeholder: '例如:稍后读' }); // 3. 通知设置组 const notificationGroup = createSettingsGroup('🔔 通知设置', '配置各种通知和进度提示', '🔔'); createSettingField(notificationGroup.content, 'notification_enabled', '浏览器通知', '启用桌面通知功能', 'toggle'); createSettingField(notificationGroup.content, 'notification_success_enabled', '成功通知', '保存成功时显示通知', 'toggle'); createSettingField(notificationGroup.content, 'notification_error_enabled', '错误通知', '保存失败时显示通知', 'toggle'); createSettingField(notificationGroup.content, 'progress_indicator_enabled', '顶部进度条', '显示操作进度的顶部状态栏', 'toggle'); // 4. 分类管理组 const categoryGroup = createSettingsGroup('📂 分类管理', '管理您的自定义分类列表', '📂'); const categoryManager = createCategoryManager(categoryGroup.content); // 5. Notion 配置组 const notionGroup = createSettingsGroup('📝 Notion 配置', '配置Notion API和数据库连接(重要)', '📝'); createSettingField(notionGroup.content, 'notion_api_key', 'Notion API Key', '请填入您的Notion Internal Integration Token', 'password', { placeholder: 'sk_...' }); createSettingField(notionGroup.content, 'database_id', '数据库ID', '请填入您要保存到的Notion数据库ID', 'text', { placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }); const fieldMappingHeader = document.createElement('div'); fieldMappingHeader.className = 'nqs-subsection-header'; fieldMappingHeader.innerHTML = '<h4>数据库字段名称</h4><p>请确保以下名称与您Notion数据库中的属性列名完全一致</p>'; notionGroup.content.appendChild(fieldMappingHeader); createSettingField(notionGroup.content, 'prop_name_title', '标题属性名', '', 'text', { placeholder: '例如:名称' }); createSettingField(notionGroup.content, 'prop_name_url', '链接属性名', '', 'text', { placeholder: '例如:链接' }); createSettingField(notionGroup.content, 'prop_name_category', '分类属性名', '', 'text', { placeholder: '例如:类型' }); // 6. AI 配置组 const aiGroup = createSettingsGroup('🤖 AI 配置', '配置AI提供商和模型参数', '🤖'); createSettingField(aiGroup.content, 'ai_provider', 'AI 提供商', '选择用于内容分类的 AI 服务', 'select', { choices: [ { value: 'openai', label: 'OpenAI (GPT)' }, { value: 'gemini', label: 'Google Gemini' } ] }); createSettingField(aiGroup.content, 'ai_api_key', 'AI API Key', '填入所选提供商的 API Key', 'password', { placeholder: 'sk-... 或 AIza...' }); createSettingField(aiGroup.content, 'ai_api_url', 'AI API Endpoint', '兼容 OpenAI 格式的 API 地址,Gemini 可留空', 'text', { placeholder: 'https://api.openai.com/v1/chat/completions' }); const modelSelector = createModelSelector(aiGroup.content); elements['ai_model'] = modelSelector.input; createSettingField(aiGroup.content, 'ai_include_body', '附加网页正文', '开启后会提取并发送部分正文,可能增加成本但有助于分析复杂页面', 'toggle'); createSettingField(aiGroup.content, 'ai_timeout', 'AI分析超时(ms)', 'AI进行内容分析请求的等待上限', 'number', { min: 5000, max: 60000, step: 1000 }); createSettingField(aiGroup.content, 'ai_model_fetch_timeout', '获取模型超时(ms)', '获取可用模型列表的等待上限', 'number', { min: 5000, max: 30000, step: 1000 }); createSettingField(aiGroup.content, 'ai_retry_count', 'AI重试次数', 'AI请求失败时的重试次数', 'number', { min: 0, max: 5, step: 1 }); createSettingField(aiGroup.content, 'ai_confidence_threshold', '置信度阈值', '低于此阈值时将提示用户手动确认分类', 'number', { min: 0.1, max: 1.0, step: 0.1 }); createSettingField(aiGroup.content, 'ai_learning_enabled', 'AI分类学习', '记录用户的分类习惯,提高AI分类准确性', 'toggle'); // 7. 内容提取增强组 const contentGroup = createSettingsGroup('📊 内容提取增强', '配置页面内容提取和AI分析功能', '📊'); createSettingField(contentGroup.content, 'content_extraction_enabled', '启用内容提取增强', '提取页面的详细信息,如发布时间、阅读时间等', 'toggle'); createSettingField(contentGroup.content, 'auto_summary_enabled', '自动生成摘要', '使用AI自动为保存的页面生成内容摘要', 'toggle'); createSettingField(contentGroup.content, 'auto_keywords_enabled', '自动提取关键词', '使用AI自动提取页面关键词', 'toggle'); createSettingField(contentGroup.content, 'content_save_mode', '内容保存模式', '选择保存到Notion的内容详细程度', 'select', { choices: [ { value: 'link', label: '仅链接' }, { value: 'summary', label: '链接+摘要' }, { value: 'full', label: '完整内容' } ] }); // 8. AI 提示词组 const promptGroup = createSettingsGroup('💬 AI 提示词', '自定义AI分类指令模板(高级用户)', '💬'); const promptHeader = document.createElement('div'); promptHeader.className = 'nqs-prompt-header'; promptHeader.innerHTML = ` <div class="nqs-prompt-info"> <h4>系统提示词</h4> <p>自定义AI分类的指令模板,影响分类的准确性和风格</p> </div> <button id="nqs-reset-prompt" class="nqs-button nqs-button-text"> <span>🔄</span> 恢复默认 </button> `; promptGroup.content.appendChild(promptHeader); // 提示词模式切换(模板 / 自定义) const promptMode = document.createElement('div'); promptMode.className = 'nqs-segmented'; promptMode.innerHTML = ` <button class="nqs-seg-btn is-active" data-mode="template">模板</button> <button class="nqs-seg-btn" data-mode="custom">自定义</button> `; promptGroup.content.appendChild(promptMode); // 默认模板模式:隐藏文本域,仅展示卡片 promptGroup.content.setAttribute('data-prompt-mode', 'template'); createSettingField( promptGroup.content, 'ai_prompt', '', '', 'textarea', { placeholder: '输入自定义的 AI 系统提示词模板(支持 {categories}、{page_metadata}、{optional_body_section} 占位符)', fullWidth: true } ); // 提示词文本域增强:自适应高度 + 计数信息 const enhanceSystemPromptField = () => { const promptField = elements && elements['ai_prompt']; if (!promptField) return; promptField.classList.add('nqs-prompt-textarea'); const autosize = () => { promptField.style.height = 'auto'; const maxPx = Math.max(200, Math.floor(window.innerHeight * 0.5)); promptField.style.height = Math.min(promptField.scrollHeight + 2, maxPx) + 'px'; }; promptField.addEventListener('input', autosize); setTimeout(autosize, 0); const meta = document.createElement('div'); meta.className = 'nqs-prompt-meta'; const hint = document.createElement('span'); hint.textContent = '可使用占位符 {categories}、{page_metadata}、{optional_body_section}'; const counter = document.createElement('span'); counter.className = 'nqs-prompt-counter'; const updateCounter = () => { counter.textContent = (promptField.value || '').length + ' 字符'; }; promptField.addEventListener('input', updateCounter); setTimeout(updateCounter, 0); meta.appendChild(hint); meta.appendChild(counter); const inputGroup = promptField.parentElement; // .nqs-setting-input-group if (inputGroup) inputGroup.appendChild(meta); }; enhanceSystemPromptField(); // 覆盖旧的提示词输入方式:仅使用模板管理 (async () => { try { // 清空提示词分组内容,改为模板模式 if (promptGroup && promptGroup.content) { promptGroup.content.innerHTML = ''; } const checkPlaceholders = (text) => { const t = String(text || ''); const hasPageMeta = /\{page_metadata\}/.test(t); const hasCategories = /\{categories\}/.test(t); const hasOptional = /\{optional_body_section\}/.test(t); const missing = []; if (!hasPageMeta) missing.push('{page_metadata}'); if (!hasCategories) missing.push('{categories}'); if (!hasOptional) missing.push('{optional_body_section}'); return { hasPageMeta, hasCategories, hasOptional, missing }; }; let promptTemplates = []; let activeTemplateId = await GM_getValue('ai_active_template_id', SETTINGS_DEFAULTS.ai_active_template_id); try { promptTemplates = JSON.parse(await GM_getValue('ai_prompt_templates', SETTINGS_DEFAULTS.ai_prompt_templates) || '[]'); } catch { promptTemplates = []; } if (!promptTemplates.find(t => t.id === 'default')) { promptTemplates.unshift({ id: 'default', title: '默认', content: SETTINGS_DEFAULTS.ai_prompt, builtIn: true }); } else { const d = promptTemplates.find(t => t.id === 'default'); if (!d.content) d.content = SETTINGS_DEFAULTS.ai_prompt; } if (!activeTemplateId) activeTemplateId = 'default'; const saveTemplatesState = async () => { await GM_setValue('ai_prompt_templates', JSON.stringify(promptTemplates)); await GM_setValue('ai_active_template_id', activeTemplateId); }; const openTemplateEditor = async (tpl) => { const creating = !tpl; const data = tpl ? { ...tpl } : { id: `t_${Date.now()}`, title: '', content: '', builtIn: false }; const { body: mb, footer: mf, close } = createBasePanel(creating ? '新增模板' : '编辑模板', '必须包含 {page_metadata}、{categories}、{optional_body_section}', { maxWidth: '720px', isNested: true }); mb.innerHTML = ` <div class="nqs-field nqs-field-full"> <div class="nqs-setting-label-group"><label>模板名称</label></div> <div class="nqs-setting-input-group"><input type="text" id="nqs-tpl-title" class="nqs-input" placeholder="例如:精确分类"></div> </div> <div class="nqs-field nqs-field-full"> <div class="nqs-setting-label-group"><label>模板内容</label><p class="nqs-setting-description">必须包含 {page_metadata}、{categories}、{optional_body_section}</p></div> <div class="nqs-setting-input-group"><textarea id="nqs-tpl-content" class="nqs-textarea" rows="10" placeholder="输入模板内容..."></textarea></div> </div> `; mf.innerHTML = `<button class="nqs-button nqs-button-secondary" id="nqs-tpl-cancel">取消</button><div style="flex:1"></div><button class="nqs-button nqs-button-primary" id="nqs-tpl-ok">保存</button>`; const ti = mb.querySelector('#nqs-tpl-title'); const tc = mb.querySelector('#nqs-tpl-content'); ti.value = data.title || ''; tc.value = data.content || ''; const doSave = async () => { const title = ti.value.trim(); const content = tc.value; if (!title) { await showAlertModal('校验失败', '请填写模板名称'); return; } const chk = checkPlaceholders(content); if (chk.missing.length) { await showAlertModal('占位符缺失', `模板缺少:${chk.missing.join(', ')}`); return; } data.title = title; data.content = content; if (creating) promptTemplates.unshift(data); else { const i = promptTemplates.findIndex(t => t.id === data.id); if (i >= 0) promptTemplates[i] = data; } await saveTemplatesState(); renderPromptTemplates(); close(); }; mf.querySelector('#nqs-tpl-ok').addEventListener('click', doSave); mf.querySelector('#nqs-tpl-cancel').addEventListener('click', close); }; const renderPromptTemplates = () => { promptGroup.content.innerHTML = ''; const header = document.createElement('div'); header.className = 'nqs-prompt-header'; header.innerHTML = ` <div class="nqs-prompt-info"> <h4>模板列表</h4> <p>仅展示名称,点击可预览内容</p> </div> <div class="nqs-prompt-actions"> <button id="nqs-add-template" class="nqs-button nqs-button-primary"><span>+</span> 新增模板</button> </div>`; promptGroup.content.appendChild(header); const section = document.createElement('div'); section.className = 'nqs-prompt-templates'; const grid = document.createElement('div'); grid.className = 'nqs-template-grid'; section.appendChild(grid); promptGroup.content.appendChild(section); promptTemplates.forEach(tpl => { const card = document.createElement('div'); const isActive = tpl.id === activeTemplateId; card.className = 'nqs-template-card nqs-template-card--compact' + (isActive ? ' is-active' : ''); card.innerHTML = ` <div class="nqs-template-card-top"> <label class="nqs-template-select"><input type="radio" name="nqs-active-template" value="${tpl.id}" ${isActive ? 'checked' : ''}></label> <h5 class="nqs-template-title">${tpl.title}${tpl.builtIn ? '(默认)' : ''}</h5> </div> <div class="nqs-template-card-actions"> <button class="nqs-button nqs-button-secondary" data-act="edit" data-id="${tpl.id}">编辑</button> ${tpl.builtIn ? '' : `<button class="nqs-button nqs-button-danger" data-act="del" data-id="${tpl.id}">删除</button>`} </div>`; grid.appendChild(card); }); // 选择模板(单选) grid.addEventListener('change', async (e) => { const input = e.target; if (input && input.name === 'nqs-active-template') { activeTemplateId = input.value; // 更新选中样式 grid.querySelectorAll('.nqs-template-card').forEach(card => card.classList.remove('is-active')); const activeCard = input.closest('.nqs-template-card'); if (activeCard) activeCard.classList.add('is-active'); const at = promptTemplates.find(t => t.id === activeTemplateId); const chk = checkPlaceholders(at?.content || ''); if (chk.missing.length) await showAlertModal('占位符缺失', `当前模板缺少:${chk.missing.join(', ')}`); await saveTemplatesState(); } }); // 卡片点击快捷选择 + 操作按钮 grid.addEventListener('click', async (e) => { const actBtn = e.target.closest('button[data-act]'); if (actBtn) { const id = actBtn.getAttribute('data-id'); const act = actBtn.getAttribute('data-act'); const idx = promptTemplates.findIndex(t => t.id === id); if (idx < 0) return; if (act === 'del') { if (promptTemplates[idx].builtIn) return; const ok = await showConfirmationModal('删除模板', '确定要删除该模板吗?', { danger: true }); if (!ok) return; const removingActive = (activeTemplateId === id); promptTemplates.splice(idx, 1); if (removingActive) activeTemplateId = 'default'; await saveTemplatesState(); renderPromptTemplates(); } else if (act === 'edit') { await openTemplateEditor(promptTemplates[idx]); } return; } // 非操作区域点击则选中该卡片 const card = e.target.closest('.nqs-template-card'); if (card) { const radio = card.querySelector('input[type="radio"]'); if (radio) { radio.checked = true; radio.dispatchEvent(new Event('change', { bubbles: true })); } } }); header.querySelector('#nqs-add-template').addEventListener('click', async () => { await openTemplateEditor(); }); }; renderPromptTemplates(); } catch (e) { console.warn('Prompt templates init failed', e); } })(); // 提示词模板库(卡片列表 + 多选插入) const initPromptTemplates = () => { const promptTextarea = elements && elements['ai_prompt']; if (!promptTextarea) return; const templates = [ { id: 'precise-classify', title: '精确分类(严格匹配)', brief: '强调只从给定列表中选择,并说明优先级与权重', content: '【精确分类】\n- 仅从预设列表中选择,禁止输出列表外类别\n- 优先级:实时内容 > 元数据 > 正文快照\n- 若无法确定,选择最具体且覆盖度最高的一项\n- 输出:仅类别名,无其他内容' }, { id: 'strict-output', title: '严格输出(零其他字符)', brief: '输出只包含类别名称,严禁标点或解释', content: '【严格输出】\n- 仅输出最终分类名称\n- 禁止任何标点、引号、解释或多余字符\n- 如果分类不在列表中,选择“其他”' }, { id: 'learning-context', title: '含学习上下文偏好', brief: '结合域名历史修正进行偏好调整', content: '【学习上下文】\n- 结合历史修正示例调整判断偏好\n- 若历史记录与当前判断冲突,参考历史选择但以当前内容为准\n- 如无冲突,优先采纳更具体类别' } ]; const section = document.createElement('div'); section.className = 'nqs-prompt-templates'; section.innerHTML = ` <div class="nqs-prompt-templates-header"> <h4>模板库</h4> <div class="nqs-prompt-templates-actions"> <button class="nqs-button nqs-button-secondary" id="nqs-insert-selected" disabled>插入所选</button> <button class="nqs-button nqs-button-primary" id="nqs-apply-selected" disabled>应用所选</button> </div> </div> <div class="nqs-template-grid"></div> `; const grid = section.querySelector('.nqs-template-grid'); const selected = new Set(); const updateBulkBtn = () => { const btn1 = section.querySelector('#nqs-insert-selected'); const btn2 = section.querySelector('#nqs-apply-selected'); const disabled = selected.size === 0; btn1.disabled = disabled; btn2.disabled = disabled; }; const previewTemplate = (tpl) => { const { body: mBody, footer: mFooter, close } = createBasePanel(`模板预览 - ${tpl.title}`, '', { maxWidth: '800px', isNested: true }); const pre = document.createElement('pre'); pre.style.whiteSpace = 'pre-wrap'; pre.style.wordWrap = 'break-word'; pre.style.margin = '0'; pre.textContent = tpl.content; mBody.appendChild(pre); mFooter.innerHTML = '<div style="flex:1"></div><button class="nqs-button nqs-button-primary">关闭</button>'; mFooter.querySelector('button').addEventListener('click', close); }; templates.forEach(tpl => { const card = document.createElement('div'); card.className = 'nqs-template-card'; card.innerHTML = ` <div class="nqs-template-card-top"> <label class="nqs-template-select"> <input type="checkbox" data-id="${tpl.id}" /> </label> <h5 class="nqs-template-title">${tpl.title}</h5> <p class="nqs-template-brief">${tpl.brief}</p> </div> <div class="nqs-template-card-actions"> <button class="nqs-button nqs-button-text" data-action="preview">预览</button> <button class="nqs-button nqs-button-secondary" data-action="insert">插入</button> </div> `; card.querySelector('[data-action="preview"]').addEventListener('click', () => previewTemplate(tpl)); card.querySelector('[data-action="insert"]').addEventListener('click', () => { const joiner = (promptTextarea.value && !promptTextarea.value.endsWith('\n')) ? '\n\n' : ''; promptTextarea.value = promptTextarea.value + joiner + tpl.content; promptTextarea.dispatchEvent(new Event('input')); }); const checkbox = card.querySelector('input[type="checkbox"]'); checkbox.addEventListener('change', () => { if (checkbox.checked) selected.add(tpl.id); else selected.delete(tpl.id); updateBulkBtn(); }); grid.appendChild(card); }); section.querySelector('#nqs-insert-selected').addEventListener('click', () => { const picked = templates.filter(t => selected.has(t.id)).map(t => t.content); if (picked.length === 0) return; const addition = picked.join('\n\n'); const joiner = (promptTextarea.value && !promptTextarea.value.endsWith('\n')) ? '\n\n' : ''; promptTextarea.value = promptTextarea.value + joiner + addition; promptTextarea.dispatchEvent(new Event('input')); }); section.querySelector('#nqs-apply-selected').addEventListener('click', () => { const picked = templates.filter(t => selected.has(t.id)).map(t => t.content); if (picked.length === 0) return; promptTextarea.value = picked.join('\n\n'); promptTextarea.dispatchEvent(new Event('input')); // 切换到“自定义”模式以便查看与微调 setPromptMode('custom'); }); // 将模板区块追加到提示词输入区域之后 const inputGroup = promptTextarea.parentElement; if (inputGroup) inputGroup.appendChild(section); }; initPromptTemplates(); // 模式切换逻辑 const setPromptMode = (mode) => { if (!mode || (mode !== 'template' && mode !== 'custom')) return; promptGroup.content.setAttribute('data-prompt-mode', mode); const btns = promptMode.querySelectorAll('.nqs-seg-btn'); btns.forEach(b => b.classList.toggle('is-active', b.dataset.mode === mode)); }; promptMode.addEventListener('click', (e) => { const btn = e.target.closest('.nqs-seg-btn'); if (!btn) return; setPromptMode(btn.dataset.mode); }); // 9. 网络配置组 const networkGroup = createSettingsGroup('🌐 网络配置', '配置代理和自定义网络端点(高级)', '🌐'); createSettingField(networkGroup.content, 'proxy_enabled', '启用自定义端点', '当需要使用第三方中继服务器或反代地址来访问AI服务时,请开启此项', 'toggle'); createSettingField(networkGroup.content, 'proxy_url', '端点/代理服务器地址', '一个兼容目标AI提供商API格式的请求中继地址', 'text', { placeholder: 'https://your-proxy.com/api' }); // 添加所有设置组到容器 settingsContainer.appendChild(appearanceGroup.group); settingsContainer.appendChild(coreGroup.group); settingsContainer.appendChild(notificationGroup.group); settingsContainer.appendChild(categoryGroup.group); settingsContainer.appendChild(notionGroup.group); settingsContainer.appendChild(aiGroup.group); settingsContainer.appendChild(contentGroup.group); settingsContainer.appendChild(promptGroup.group); settingsContainer.appendChild(networkGroup.group); // --- 左侧侧边导航栏(快速跳转)--- // 组装分组元数据(用于构建导航和滚动联动) const groupsMeta = [ { key: 'appearance', title: '外观设置', icon: '🎨', el: appearanceGroup.group }, { key: 'core', title: '核心功能', icon: '🚀', el: coreGroup.group }, { key: 'notify', title: '通知设置', icon: '🔔', el: notificationGroup.group }, { key: 'category', title: '分类管理', icon: '📂', el: categoryGroup.group }, { key: 'notion', title: 'Notion 配置', icon: '📝', el: notionGroup.group }, { key: 'ai', title: 'AI 配置', icon: '🤖', el: aiGroup.group }, { key: 'content', title: '内容提取增强', icon: '📊', el: contentGroup.group }, { key: 'prompt', title: 'AI 提示词', icon: '💬', el: promptGroup.group }, { key: 'network', title: '网络配置', icon: '🌐', el: networkGroup.group } ]; groupsMeta.forEach(g => { if (g.el) g.el.id = `nqs-group-${g.key}`; }); // 构建布局:左侧导航 + 右侧内容 const layout = document.createElement('div'); layout.className = 'nqs-settings-layout'; const sidebar = document.createElement('aside'); sidebar.className = 'nqs-settings-sidebar'; const nav = document.createElement('nav'); nav.className = 'nqs-settings-nav'; sidebar.appendChild(nav); // 构建导航项 const navItems = {}; // 控制点击时的高亮与观察抑制,避免快速切换造成错乱 let suppressIO = false; let navClickTimer = null; const buildNavItem = (g) => { if (!g || !g.el) return null; const a = document.createElement('a'); a.href = `#${g.el.id}`; a.className = 'nqs-nav-item'; a.setAttribute('data-target', g.el.id); a.setAttribute('role', 'button'); a.setAttribute('tabindex', '0'); a.innerHTML = `<span class="nqs-nav-icon">${g.icon}</span><span class="nqs-nav-text">${g.title}</span>`; a.addEventListener('click', (e) => { e.preventDefault(); const target = document.getElementById(g.el.id); if (!target) return; // 精确滚动到分组顶部(相对于滚动容器) const offsetTop = target.getBoundingClientRect().top - contentWrap.getBoundingClientRect().top + contentWrap.scrollTop; // 若在快速连续点击期间,采用瞬间跳转,避免队列中的平滑动画叠加造成错乱 const isInstant = suppressIO === true; contentWrap.scrollTo({ top: offsetTop, behavior: isInstant ? 'auto' : 'smooth' }); // 立即同步侧边栏高亮,避免平滑滚动期间高亮不同步 setActiveNav(target.id); // 在抑制窗口内忽略 IntersectionObserver 更新,滚动后恢复 suppressIO = true; if (navClickTimer) clearTimeout(navClickTimer); navClickTimer = setTimeout(() => { suppressIO = false; }, 400); // 滚动完成后再确认一次(处理不同浏览器的动画时序) setTimeout(() => setActiveNav(target.id), 300); // 鼠标点击后移除焦点,避免与滚动同步产生“双高亮” if (e.detail && e.detail > 0) { setTimeout(() => a.blur(), 0); } }); a.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); a.click(); } }); navItems[g.key] = a; return a; }; groupsMeta.forEach(g => { const item = buildNavItem(g); if (item) nav.appendChild(item); }); const contentWrap = document.createElement('div'); contentWrap.className = 'nqs-settings-content'; contentWrap.appendChild(settingsContainer); layout.appendChild(sidebar); layout.appendChild(contentWrap); body.appendChild(layout); // 激活状态联动(滚动高亮) const setActiveNav = (id) => { const items = nav.querySelectorAll('.nqs-nav-item'); items.forEach(it => it.classList.toggle('active', it.getAttribute('data-target') === id)); // 仅当活动项不在可视区域时才滚动,避免频繁滚动造成“滑动错乱”的观感 const active = nav.querySelector('.nqs-nav-item.active'); if (active) { const navRect = nav.getBoundingClientRect(); const itemRect = active.getBoundingClientRect(); const outOfView = itemRect.top < navRect.top || itemRect.bottom > navRect.bottom; if (outOfView && typeof active.scrollIntoView === 'function') { active.scrollIntoView({ block: 'nearest' }); } } }; let io = null; const attachObservers = () => { if (!('IntersectionObserver' in window)) return; if (io) io.disconnect(); io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (!suppressIO && entry.isIntersecting) setActiveNav(entry.target.id); }); }, { root: contentWrap, threshold: [0.1, 0.3, 0.6], rootMargin: '-15% 0px -65% 0px' }); groupsMeta.forEach(g => { if (g.el && g.el.style.display !== 'none') io.observe(g.el); }); }; attachObservers(); // 补充:基于滚动位置的高亮同步(作为 IO 的稳定兜底) const syncActiveByScroll = () => { if (suppressIO) return; const wrapRect = contentWrap.getBoundingClientRect(); let bestId = null; let bestDelta = Infinity; groupsMeta.forEach(g => { if (!g.el || g.el.style.display === 'none') return; const rect = g.el.getBoundingClientRect(); const delta = Math.abs(rect.top - wrapRect.top - 12); // 与容器顶部的距离 if (delta < bestDelta) { bestDelta = delta; bestId = g.el.id; } }); if (bestId) setActiveNav(bestId); }; const debouncedScrollSync = debounce(syncActiveByScroll, 50); contentWrap.addEventListener('scroll', debouncedScrollSync, { passive: true }); // 初始高亮第一个可见分组 const firstVisible = groupsMeta.find(g => g.el && g.el.style.display !== 'none'); if (firstVisible && firstVisible.el) setActiveNav(firstVisible.el.id); // 底部按钮 const cancelButton = document.createElement('button'); cancelButton.id = 'nqs-close'; cancelButton.className = 'nqs-button nqs-button-secondary'; cancelButton.innerHTML = '<span>❌</span> 取消'; const spacer = document.createElement('div'); spacer.style.flexGrow = '1'; const saveButton = document.createElement('button'); saveButton.id = 'nqs-save'; saveButton.className = 'nqs-button nqs-button-primary'; saveButton.innerHTML = '<span>💾</span> 保存设置'; footer.append(cancelButton, spacer, saveButton); // --- 逻辑与事件绑定 --- let currentCategories = JSON.parse(await GM_getValue('user_categories', SETTINGS_DEFAULTS.user_categories)); // 切换AI相关设置组的可见性(同步侧边栏) const toggleAISectionVisibility = () => { if (!elements['ai_enabled'] || !elements['ai_enabled'].querySelector('input')) return; const aiEnabled = elements['ai_enabled'].querySelector('input').checked; if (aiGroup && aiGroup.group) aiGroup.group.style.display = aiEnabled ? 'block' : 'none'; if (contentGroup && contentGroup.group) contentGroup.group.style.display = aiEnabled ? 'block' : 'none'; if (promptGroup && promptGroup.group) promptGroup.group.style.display = aiEnabled ? 'block' : 'none'; if (networkGroup && networkGroup.group) networkGroup.group.style.display = aiEnabled ? 'block' : 'none'; // 同步侧边栏导航项显示状态 const safeToggle = (key, visible) => { const item = navItems[key]; if (item) item.style.display = visible ? '' : 'none'; }; safeToggle('ai', aiEnabled); safeToggle('content', aiEnabled); safeToggle('prompt', aiEnabled); safeToggle('network', aiEnabled); // 如果当前高亮项隐藏了,则选中第一个可见项 const activeItem = nav.querySelector('.nqs-nav-item.active'); if (activeItem && activeItem.style.display === 'none') { const first = Array.from(nav.querySelectorAll('.nqs-nav-item')).find(i => i.style.display !== 'none'); if (first) setActiveNav(first.getAttribute('data-target')); } // 重新挂载观察器,仅观察可见分组 attachObservers(); }; // 切换代理URL字段的可见性 const toggleProxyUrlVisibility = () => { if (!elements['proxy_enabled'] || !elements['proxy_enabled'].querySelector('input')) return; const proxyEnabled = elements['proxy_enabled'].querySelector('input').checked; if (networkGroup && networkGroup.content) { const proxyFieldWrapper = networkGroup.content.querySelector('[data-field-id="proxy_url"]'); if (proxyFieldWrapper) { proxyFieldWrapper.style.display = proxyEnabled ? 'grid' : 'none'; } } }; // 更新提供商特定的UI const updateProviderSpecificUI = () => { if (!elements['ai_provider'] || !elements['ai_api_url'] || !modelSelector || !modelSelector.input) return; const provider = elements['ai_provider'].value; const apiUrlInput = elements['ai_api_url']; const modelInput = modelSelector.input; if (provider === 'gemini') { apiUrlInput.placeholder = '通常留空,会自动使用谷歌官方地址'; modelInput.placeholder = '例如: gemini-1.5-flash-latest'; if (apiUrlInput.value.includes('openai.com')) { apiUrlInput.value = ''; } } else { // openai apiUrlInput.placeholder = '例如: https://api.openai.com/v1/chat/completions'; modelInput.placeholder = '例如: gpt-3.5-turbo'; if (!apiUrlInput.value) { apiUrlInput.value = SETTINGS_DEFAULTS.ai_api_url; } } }; // 渲染分类列表 const renderCategories = () => { if (!categoryManager || !categoryManager.categoryList) return; categoryManager.categoryList.innerHTML = ''; currentCategories.forEach(cat => { const categoryItem = document.createElement('div'); categoryItem.className = 'nqs-category-item'; categoryItem.innerHTML = ` <span class="nqs-category-name">${cat}</span> <button class="nqs-category-delete-btn" data-category="${cat}"> <span>×</span> </button> `; categoryManager.categoryList.appendChild(categoryItem); }); }; // 添加分类 const addCategoryAction = () => { if (!categoryManager || !categoryManager.input) return; const newCat = categoryManager.input.value.trim(); if (newCat && !currentCategories.includes(newCat)) { currentCategories.unshift(newCat); renderCategories(); categoryManager.input.value = ''; } }; // 删除分类 const deleteCategory = (category) => { currentCategories = currentCategories.filter(c => c !== category); renderCategories(); }; // 重置提示词 const resetPrompt = async () => { if (!elements['ai_prompt']) return; if (await showConfirmationModal('恢复默认提示词', '您当前输入的内容将被覆盖。确定要恢复吗?')) { elements['ai_prompt'].value = SETTINGS_DEFAULTS.ai_prompt; } }; // 获取可用模型列表 let availableModels = [], activeOptionIndex = -1; const renderModelDropdown = (filter = '') => { if (!modelSelector || !modelSelector.dropdown || !modelSelector.input) return; const filteredModels = availableModels.filter(model => model.toLowerCase().includes(filter.toLowerCase()) ); modelSelector.dropdown.innerHTML = ''; if (filteredModels.length === 0) { modelSelector.dropdown.classList.remove('is-visible'); return; } filteredModels.forEach((model, index) => { const item = document.createElement('div'); item.className = 'nqs-dropdown-item'; item.textContent = model; item.dataset.index = index; item.addEventListener('click', () => { modelSelector.input.value = model; modelSelector.dropdown.classList.remove('is-visible'); }); modelSelector.dropdown.appendChild(item); }); modelSelector.dropdown.classList.add('is-visible'); activeOptionIndex = -1; }; const updateActiveModelOption = (newIndex) => { if (!modelSelector || !modelSelector.dropdown) return; const items = modelSelector.dropdown.querySelectorAll('.nqs-dropdown-item'); if (activeOptionIndex >= 0 && items[activeOptionIndex]) { items[activeOptionIndex].classList.remove('is-active'); } if (newIndex >= 0 && items[newIndex]) { items[newIndex].classList.add('is-active'); items[newIndex].scrollIntoView({ block: 'nearest' }); activeOptionIndex = newIndex; } }; // 获取模型列表 const fetchModels = async () => { if (!elements['ai_provider'] || !elements['ai_api_url'] || !elements['ai_api_key'] || !elements['ai_model_fetch_timeout'] || !elements['proxy_enabled'] || !elements['proxy_url'] || !modelSelector || !modelSelector.fetchBtn || !modelSelector.input) { showAlertModal('操作失败', '必要的配置元素未找到。'); return; } const provider = elements['ai_provider'].value; const baseUrl = elements['ai_api_url'].value; const apiKey = elements['ai_api_key'].value; const fetchTimeout = elements['ai_model_fetch_timeout'].value; const proxySettings = { enabled: elements['proxy_enabled'].querySelector('input').checked, url: elements['proxy_url'].value }; if (!apiKey) { showAlertModal('操作失败', '请先填写 AI API Key。'); return; } modelSelector.fetchBtn.classList.add('is-loading'); modelSelector.fetchBtn.disabled = true; try { availableModels = await fetchAvailableModels(provider, baseUrl, apiKey, fetchTimeout, proxySettings); renderModelDropdown(modelSelector.input.value); await showAlertModal('操作成功', `成功获取 ${availableModels.length} 个可用模型!`); } catch (error) { await showAlertModal('操作失败', `无法获取模型列表,请检查您的网络、配置和 API Key 是否正确。\n\n错误详情: ${error.message}`); } finally { modelSelector.fetchBtn.classList.remove('is-loading'); modelSelector.fetchBtn.disabled = false; } }; // 加载当前设置值 for (const key of Object.keys(SETTINGS_DEFAULTS)) { const element = elements[key]; if (element) { const value = await GM_getValue(key, SETTINGS_DEFAULTS[key]); if (element.type === 'checkbox' || element.querySelector('input[type="checkbox"]')) { const checkbox = element.querySelector('input[type="checkbox"]') || element; checkbox.checked = value; } else if (element.tagName === 'SELECT') { element.value = value; } else { element.value = value; } } } // 初始化UI状态 renderCategories(); toggleAISectionVisibility(); toggleProxyUrlVisibility(); updateProviderSpecificUI(); // 绑定事件 - 确保所有元素都已创建 setTimeout(() => { // AI相关事件 if (elements['ai_enabled'] && elements['ai_enabled'].querySelector('input')) { elements['ai_enabled'].querySelector('input').addEventListener('change', toggleAISectionVisibility); } if (elements['proxy_enabled'] && elements['proxy_enabled'].querySelector('input')) { elements['proxy_enabled'].querySelector('input').addEventListener('change', toggleProxyUrlVisibility); } if (elements['ai_provider']) { elements['ai_provider'].addEventListener('change', updateProviderSpecificUI); } // 分类管理事件 if (categoryManager && categoryManager.addBtn) { categoryManager.addBtn.addEventListener('click', addCategoryAction); } if (categoryManager && categoryManager.input) { categoryManager.input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addCategoryAction(); } }); } // 分类删除事件委托 if (categoryManager && categoryManager.categoryList) { categoryManager.categoryList.addEventListener('click', (e) => { if (e.target.closest('.nqs-category-delete-btn')) { const btn = e.target.closest('.nqs-category-delete-btn'); const category = btn.dataset.category; deleteCategory(category); } }); } // 重置提示词事件 const resetPromptBtn = footer.querySelector('#nqs-reset-prompt'); if (resetPromptBtn) { resetPromptBtn.addEventListener('click', resetPrompt); } // 模型选择器事件 if (modelSelector && modelSelector.fetchBtn) { modelSelector.fetchBtn.addEventListener('click', fetchModels); } if (modelSelector && modelSelector.input) { modelSelector.input.addEventListener('focus', () => { if (availableModels.length > 0) renderModelDropdown(modelSelector.input.value); }); modelSelector.input.addEventListener('input', () => { renderModelDropdown(modelSelector.input.value); }); modelSelector.input.addEventListener('keydown', (e) => { const items = modelSelector.dropdown.querySelectorAll('.nqs-dropdown-item'); if (!modelSelector.dropdown.classList.contains('is-visible') || items.length === 0) return; switch(e.key) { case 'ArrowDown': e.preventDefault(); updateActiveModelOption(activeOptionIndex < items.length - 1 ? activeOptionIndex + 1 : 0); break; case 'ArrowUp': e.preventDefault(); updateActiveModelOption(activeOptionIndex > 0 ? activeOptionIndex - 1 : items.length - 1); break; case 'Enter': e.preventDefault(); if (activeOptionIndex >= 0 && items[activeOptionIndex]) { items[activeOptionIndex].click(); } break; case 'Escape': modelSelector.dropdown.classList.remove('is-visible'); break; } }); } // 点击外部关闭下拉 if (modelSelector && modelSelector.input) { document.addEventListener('click', (e) => { if (!modelSelector.input.parentNode.contains(e.target)) { modelSelector.dropdown.classList.remove('is-visible'); } }); } }, 100); // 保存设置 saveButton.addEventListener('click', async () => { // 使用当前选择的模板作为 ai_prompt try { const tpls = JSON.parse(await GM_getValue('ai_prompt_templates', SETTINGS_DEFAULTS.ai_prompt_templates) || '[]'); const activeId = await GM_getValue('ai_active_template_id', SETTINGS_DEFAULTS.ai_active_template_id); const activeTpl = (tpls.find(t => t.id === activeId) || tpls.find(t => t.id === 'default')); const content = (activeTpl && activeTpl.content) ? activeTpl.content : SETTINGS_DEFAULTS.ai_prompt; const missing = []; if (!/\{page_metadata\}/.test(content)) missing.push('{page_metadata}'); if (!/\{categories\}/.test(content)) missing.push('{categories}'); if (!/\{optional_body_section\}/.test(content)) missing.push('{optional_body_section}'); if (missing.length) { await showAlertModal('占位符缺失', `当前使用的模板缺少必需占位符:${missing.join(', ')}`); return; } if (!/\{page_metadata\}/.test(content)) { await showAlertModal('占位符缺失', '当前使用的模板缺少必需占位符 {page_metadata}'); return; } await GM_setValue('ai_prompt', content); } catch (e) { console.warn('Failed to apply active template, fallback to default.', e); await GM_setValue('ai_prompt', SETTINGS_DEFAULTS.ai_prompt); } for (const key of Object.keys(SETTINGS_DEFAULTS)) { const element = elements[key]; if (element) { let value; if (element.type === 'checkbox' || element.querySelector('input[type="checkbox"]')) { const checkbox = element.querySelector('input[type="checkbox"]') || element; value = checkbox.checked; } else { value = element.value; } // ai_prompt 已由模板系统统一保存 if (key !== 'user_categories' && key !== 'ai_prompt') { await GM_setValue(key, value); } } } await GM_setValue('user_categories', JSON.stringify(currentCategories)); await applyTheme(); notificationManager.showSuccessNotification('设置保存成功', '您的设置已成功保存并应用'); close(); initFloatingButtons(); }); cancelButton.addEventListener('click', close); } async function openLogViewerPanel() { closeAllNQSPopups(); const allLogs = JSON.parse(await GM_getValue('nqs_logs', '[]')); const { body, footer, close } = createBasePanel('📋 操作日志', '', { maxWidth: '1000px', panelClass: 'nqs-panel--log-viewer' }); // 创建日志统计卡片区域 const statsContainer = document.createElement('div'); statsContainer.className = 'nqs-log-stats'; // 统计不同类型的日志数量 const stats = allLogs.reduce((acc, log) => { const level = log.level || 'info'; acc[level] = (acc[level] || 0) + 1; acc.total++; return acc; }, { total: 0, info: 0, error: 0, debug: 0 }); statsContainer.innerHTML = ` <div class="nqs-stat-card nqs-stat-total"> <div class="nqs-stat-icon">📊</div> <div class="nqs-stat-content"> <div class="nqs-stat-number">${stats.total}</div> <div class="nqs-stat-label">总记录</div> </div> </div> <div class="nqs-stat-card nqs-stat-success"> <div class="nqs-stat-icon">✅</div> <div class="nqs-stat-content"> <div class="nqs-stat-number">${stats.info || 0}</div> <div class="nqs-stat-label">成功操作</div> </div> </div> <div class="nqs-stat-card nqs-stat-error"> <div class="nqs-stat-icon">❌</div> <div class="nqs-stat-content"> <div class="nqs-stat-number">${stats.error || 0}</div> <div class="nqs-stat-label">失败记录</div> </div> </div> <div class="nqs-stat-card nqs-stat-debug"> <div class="nqs-stat-icon">🔧</div> <div class="nqs-stat-content"> <div class="nqs-stat-number">${stats.debug || 0}</div> <div class="nqs-stat-label">调试信息</div> </div> </div> `; body.appendChild(statsContainer); // 创建过滤和搜索栏 const filterContainer = document.createElement('div'); filterContainer.className = 'nqs-log-filter-bar'; filterContainer.innerHTML = ` <div class="nqs-filter-left"> <div class="nqs-filter-group"> <button class="nqs-filter-btn active" data-filter="all"> <span class="nqs-filter-icon">📋</span> <span>全部</span> </button> <button class="nqs-filter-btn" data-filter="info"> <span class="nqs-filter-icon">✅</span> <span>成功</span> </button> <button class="nqs-filter-btn" data-filter="error"> <span class="nqs-filter-icon">❌</span> <span>错误</span> </button> </div> </div> <div class="nqs-filter-right"> <div class="nqs-search-box"> <input type="text" id="nqs-log-search" placeholder="搜索日志消息..." class="nqs-search-input"> <span class="nqs-search-icon">🔍</span> </div> <div class="nqs-toggle-group"> <label class="nqs-toggle-label"> <input type="checkbox" id="nqs-show-debug" class="nqs-toggle-input"> <span class="nqs-toggle-slider"></span> <span class="nqs-toggle-text">调试模式</span> </label> </div> </div> `; body.appendChild(filterContainer); // 创建表格容器 const tableContainer = document.createElement('div'); tableContainer.className = 'nqs-table-container'; body.appendChild(tableContainer); let currentFilter = 'all', showDebug = false, searchTerm = ''; const rerenderTable = () => { const filteredLogs = allLogs.filter(log => { const level = (log.level || 'info').toLowerCase(); const matchesFilter = (currentFilter === 'all' || level === currentFilter || (!log.level && currentFilter === 'info')); const matchesDebug = (showDebug || level !== 'debug'); const matchesSearch = (!searchTerm || log.message.toLowerCase().includes(searchTerm.toLowerCase())); return matchesFilter && matchesDebug && matchesSearch; }).slice(0, 100); if (filteredLogs.length === 0) { tableContainer.innerHTML = ` <div class="nqs-empty-state"> <div class="nqs-empty-icon">📝</div> <div class="nqs-empty-title">暂无日志记录</div> <div class="nqs-empty-message">当前筛选条件下没有找到相关的日志记录</div> </div> `; return; } const cardsHTML = ` <div class="nqs-log-cards"> ${filteredLogs.map((log, index) => { const originalIndex = allLogs.indexOf(log); const level = log.level || 'info'; const displayMessage = log.level ? log.message : `[Legacy] Saved '${log.title}' as '${log.result || 'N/A'}'`; const isLegacy = level === 'info' && !log.level; const levelIcons = { 'info': '✅', 'error': '❌', 'debug': '🔧', 'warn': '⚠️' }; const levelColors = { 'info': '#34C759', 'error': '#FF3B30', 'debug': '#8E8E93', 'warn': '#FF9500' }; return ` <div class="nqs-log-card nqs-log-card--${level}" data-log-index="${originalIndex}"> <div class="nqs-log-card-header"> <div class="nqs-log-level-badge" style="background: ${levelColors[level]}20; color: ${levelColors[level]};"> <span class="nqs-log-level-icon">${levelIcons[level] || '📋'}</span> <span class="nqs-log-level-text">${getLogLevelText(level, isLegacy)}</span> </div> <div class="nqs-log-time">${formatLogTime(log.timestamp)}</div> </div> <div class="nqs-log-card-body"> <div class="nqs-log-message">${displayMessage}</div> </div> <div class="nqs-log-card-footer"> <button class="nqs-log-detail-btn" data-log-index="${originalIndex}"> <span>🔍</span> 查看详情 </button> </div> </div> `; }).join('')} </div> `; setSafeInnerHTML(tableContainer, cardsHTML); }; // 格式化时间显示 window.formatLogTime = (timestamp) => { const date = new Date(timestamp); const now = new Date(); const diff = now - date; if (diff < 60000) return '刚刚'; if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`; if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前`; return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }; // 获取日志级别文本 window.getLogLevelText = (level, isLegacy = false) => { if (isLegacy) return 'LEGACY'; const levelMap = { 'info': 'INFO', 'error': 'ERROR', 'debug': 'DEBUG', 'warn': 'WARN' }; return levelMap[level] || level.toUpperCase(); }; // 创建日志详情弹窗函数 const showLogDetailModal = (log) => { const { body: detailBody, footer: detailFooter, close: closeDetail } = createBasePanel( '📋 日志详情', '', { maxWidth: '800px', panelClass: 'nqs-panel--log-detail', isNested: true } ); // 格式化日志数据 const logData = { 时间: new Date(log.timestamp).toLocaleString('zh-CN'), 级别: log.level ? log.level.toUpperCase() : 'LEGACY', 消息: log.message || (log.title ? `[Legacy] Saved '${log.title}' as '${log.result || 'N/A'}'` : '无消息'), ...(log.context && typeof log.context === 'object' ? { 上下文: log.context } : {}), ...(log.title && !log.level ? { 标题: log.title } : {}), ...(log.result && !log.level ? { 结果: log.result } : {}), ...(log.url && !log.level ? { 链接: log.url } : {}) }; detailBody.innerHTML = ` <div class="nqs-log-detail-content"> <div class="nqs-json-viewer"> <pre class="nqs-json-code">${JSON.stringify(logData, null, 2)}</pre> </div> </div> `; setSafeInnerHTML(detailFooter, ` <div class="nqs-log-detail-footer"> <button class="nqs-button nqs-button-secondary" id="copy-log-data"> <span>📋</span> 复制数据 </button> <button class="nqs-button nqs-button-primary" id="close-log-detail"> <span>✅</span> 关闭 </button> </div> `); // 绑定事件 detailFooter.querySelector('#copy-log-data').addEventListener('click', () => { navigator.clipboard.writeText(JSON.stringify(logData, null, 2)).then(() => { notificationManager.showSuccessNotification('复制成功', '日志数据已复制到剪贴板'); }).catch(() => { notificationManager.showErrorNotification('复制失败', '无法访问剪贴板'); }); }); detailFooter.querySelector('#close-log-detail').addEventListener('click', closeDetail); // 显示弹窗 setTimeout(() => { document.querySelector('.nqs-overlay:last-child').classList.add('visible'); }, 10); }; // 绑定卡片点击事件 tableContainer.addEventListener('click', (e) => { if (e.target.classList.contains('nqs-log-detail-btn') || e.target.closest('.nqs-log-detail-btn')) { const btn = e.target.classList.contains('nqs-log-detail-btn') ? e.target : e.target.closest('.nqs-log-detail-btn'); const logIndex = btn.dataset.logIndex; const log = allLogs[logIndex]; if (log) { showLogDetailModal(log); } } }); // 绑定过滤事件 filterContainer.querySelector('.nqs-filter-group').addEventListener('click', (e) => { if (!e.target.closest('.nqs-filter-btn')) return; const btn = e.target.closest('.nqs-filter-btn'); filterContainer.querySelectorAll('.nqs-filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentFilter = btn.dataset.filter; rerenderTable(); }); filterContainer.querySelector('#nqs-show-debug').addEventListener('change', (e) => { showDebug = e.target.checked; rerenderTable(); }); filterContainer.querySelector('#nqs-log-search').addEventListener('input', (e) => { searchTerm = e.target.value.trim(); rerenderTable(); }); // 底部操作按钮 setSafeInnerHTML(footer, ` <div class="nqs-log-footer-actions"> <div class="nqs-log-footer-left"> <button class="nqs-button nqs-button-secondary" id="export-logs"> <span>📤</span> 导出日志 </button> <button class="nqs-button nqs-button-danger" id="clear-logs"> <span>🗑️</span> 清空日志 </button> </div> <div class="nqs-log-footer-right"> <button class="nqs-button nqs-button-primary" id="close-logs"> <span>✅</span> 关闭 </button> </div> </div> `); footer.querySelector('#clear-logs').addEventListener('click', async () => { const confirmed = await showConfirmationModal('确认清空日志', '此操作不可撤销,您确定要删除所有日志记录吗?', { danger: true, confirmText: '确认清空' }); if (confirmed) { await GM_setValue('nqs_logs', '[]'); notificationManager.showSuccessNotification('日志已清空', '所有日志记录已成功清除'); close(); } }); footer.querySelector('#export-logs').addEventListener('click', () => { const dataStr = JSON.stringify(allLogs, null, 2); const dataBlob = new Blob([dataStr], {type: 'application/json'}); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = `notion-ai-logs-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); notificationManager.showSuccessNotification('日志已导出', '日志文件已成功下载到本地'); }); footer.querySelector('#close-logs').addEventListener('click', close); rerenderTable(); } async function openCategorySelectorPanel(pageTitle, pageUrl, uiContext) { closeAllNQSPopups(); const settings = await loadAllSettings(); let categories = JSON.parse(settings.user_categories); if (settings.read_later_enabled && settings.read_later_category) { categories = categories.filter(c => c !== settings.read_later_category); } const { body, close } = createBasePanel('选择或创建分类', '', { maxWidth: '560px' }); setSafeInnerHTML(body, `<div id="nqs-selector-list">${categories.map(cat => `<button class="nqs-button nqs-button-secondary" data-cat="${cat}">${cat}</button>`).join('')}</div><div class="nqs-category-manager"><input type="text" class="nqs-input" id="nqs-new-cat-input" placeholder="或输入新分类..."><button id="nqs-save-new-cat" class="nqs-button nqs-button-primary">添加并保存</button></div>`); const doSave = async (category) => { if (!category) return; close(); const saveButton = document.getElementById('nqs-save-button'); const originalText = '➤ Notion'; if (uiContext.source === 'fab' && saveButton) showNotification(saveButton, `🚀 保存为: ${category}`, true, originalText); else console.log(`NQS: [Menu] 🚀 即将保存为: ${category}`); try { const notionResponse = await saveToNotion(settings.notion_api_key, settings.database_id, pageTitle, pageUrl, category, settings); // 记录手动分类学习数据(如果之前有AI建议的话) const domain = new URL(pageUrl).hostname; const pageMetadata = getHighSignalPageData(document); await recordCategoryLearning(domain, pageMetadata, category, null); await addLog('info', `页面已手动保存 (${uiContext.source})`, { title: pageTitle, url: pageUrl, result: category }); if (uiContext.source === 'fab' && saveButton) showNotification(saveButton, notionResponse, false, originalText); else console.log(`NQS: [Menu] ${notionResponse}`); } catch (error) { if (uiContext.source === 'fab' && saveButton) showNotification(saveButton, error.message, false, originalText); else console.error(`NQS: [Menu] 保存失败!错误信息: ${error.message}`); await addLog('error', '手动保存至Notion失败', { title: pageTitle, url: pageUrl, category: category, error: error.message, stack: error.stack }); } }; body.querySelector('#nqs-selector-list').addEventListener('click', e => { if (e.target.tagName === 'BUTTON' && e.target.dataset.cat) doSave(e.target.dataset.cat); }); const newCatInputAction = async () => { const newCat = body.querySelector('#nqs-new-cat-input').value.trim(); if (newCat && !categories.includes(newCat)) { const allCategories = JSON.parse(await GM_getValue('user_categories', SETTINGS_DEFAULTS.user_categories)); allCategories.unshift(newCat); await GM_setValue('user_categories', JSON.stringify(allCategories)); } doSave(newCat); }; body.querySelector('#nqs-save-new-cat').addEventListener('click', newCatInputAction); body.querySelector('#nqs-new-cat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); newCatInputAction(); } }); } // =================================================================== // ====================== API 调用与主逻辑 ========================= // =================================================================== // =================================================================== // ====================== AI分类学习功能 ============================= // =================================================================== async function recordCategoryLearning(domain, pageMetadata, userChoice, aiSuggestion) { if (!await GM_getValue('ai_learning_enabled', SETTINGS_DEFAULTS.ai_learning_enabled)) return; let learningData = []; try { learningData = JSON.parse(await GM_getValue('nqs_learning_data', '[]') || '[]'); } catch (e) { console.error("NQS - 解析学习数据失败:", e); learningData = []; } learningData.unshift({ timestamp: Date.now(), domain: domain, metadata: pageMetadata, userChoice: userChoice, aiSuggestion: aiSuggestion }); // 保持最近1000条记录 if (learningData.length > 1000) learningData.splice(1000); await GM_setValue('nqs_learning_data', JSON.stringify(learningData)); } async function getDomainLearningContext(domain) { try { const learningData = JSON.parse(await GM_getValue('nqs_learning_data', '[]') || '[]'); const domainData = learningData.filter(item => item.domain === domain).slice(0, 10); if (domainData.length === 0) return ''; const corrections = domainData.filter(item => item.userChoice !== item.aiSuggestion); if (corrections.length === 0) return ''; return `\n\n# 历史分类学习 (${domain})\n用户在此域名下的历史修正:\n` + corrections.map(c => `- "${c.metadata.split('\n')[1]}" → 用户选择: ${c.userChoice} (AI建议: ${c.aiSuggestion})`).join('\n'); } catch (e) { console.error("NQS - 获取学习上下文失败:", e); return ''; } } // =================================================================== // ====================== 增强的AI分类功能 ============================ // =================================================================== function determineCategoryAI(settings, pageMetadata, pageBodyText) { return new Promise(async (resolve, reject) => { const { ai_provider, ai_api_key, ai_api_url, ai_model, ai_prompt, user_categories, ai_include_body, ai_timeout, ai_retry_count, read_later_enabled, read_later_category, proxy_enabled, proxy_url } = settings; if (!ai_model) return reject(new Error("AI Model 未在设置中指定")); if (!ai_api_key) return reject(new Error("AI API Key 未在设置中指定")); let categories = JSON.parse(user_categories); if (read_later_enabled && read_later_category) { categories = categories.filter(c => c !== read_later_category); } // 添加学习上下文 const domain = new URL(pageMetadata.split('\n')[0].replace('Page URL: ', '')).hostname; const learningContext = await getDomainLearningContext(domain); const bodySectionTemplate = `\n\n## (可选) 辅助正文快照:\n"""\n{page_body_text}\n"""`; const optionalBodySection = ai_include_body ? bodySectionTemplate.replace('{page_body_text}', pageBodyText) : "N/A"; const finalPrompt = ai_prompt.replace('{categories}', JSON.stringify(categories)).replace('{page_metadata}', pageMetadata).replace('{optional_body_section}', optionalBodySection) + learningContext; // 重试逻辑 const makeAIRequest = (attempt = 1) => { return new Promise((resolveRequest, rejectRequest) => { addLog('debug', `发送请求至AI (第${attempt}次尝试)`, { provider: ai_provider, model: ai_model, attempt }); let requestDetails = { method: 'POST', url: '', headers: { 'Content-Type': 'application/json' }, data: '', timeout: parseInt(ai_timeout, 10) || 20000, ontimeout: () => rejectRequest(new Error("AI分析超时")), onload: null, onerror: () => rejectRequest(new Error('AI网络请求失败')) }; if (ai_provider === 'gemini') { const geminiUrl = `https://generativelanguage.googleapis.com/v1/models/${ai_model}:generateContent?key=${ai_api_key}`; requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : geminiUrl; if (proxy_enabled && proxy_url) { requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`; } requestDetails.data = JSON.stringify({ contents: [{ parts: [{ text: finalPrompt }] }], generationConfig: { temperature: 0.0, maxOutputTokens: 100 }, safetySettings: [ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }, { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" }, { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" }, { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" } ] }); requestDetails.onload = (response) => { if (response.status >= 200 && response.status < 300) { try { const result = JSON.parse(response.responseText); if (!result.candidates || result.candidates.length === 0) { if (result.promptFeedback && result.promptFeedback.blockReason) { throw new Error(`AI请求被拒绝: ${result.promptFeedback.blockReason}`); } throw new Error("AI响应中缺少 'candidates' 字段"); } const rawResponse = (result.candidates[0]?.content?.parts[0]?.text || "").trim().replace(/["'「」`*]/g, ''); if (!rawResponse) throw new Error("AI返回了空响应。"); // 计算置信度(简单实现) const confidence = categories.includes(rawResponse) ? 0.9 : 0.3; const finalCategory = categories.includes(rawResponse) ? rawResponse : "其他"; if (finalCategory === "其他") console.warn("NQS - AI返回意外分类,回退至'其他'. Raw:", rawResponse); resolveRequest({ category: finalCategory, rawResponse, confidence, fullApiResponse: result, domain }); } catch (e) { rejectRequest(new Error(`解析AI响应失败: ${e.message}`)); } } else { rejectRequest(new Error(`AI接口错误: ${response.status} ${response.statusText}. 响应: ${response.responseText}`)); } }; } else { // OpenAI requestDetails.url = (proxy_enabled && proxy_url) ? proxy_url : ai_api_url; requestDetails.headers['Authorization'] = `Bearer ${ai_api_key}`; requestDetails.data = JSON.stringify({ model: ai_model, messages: [{ role: 'user', content: finalPrompt }], temperature: 0.0, max_tokens: 50 }); requestDetails.onload = (response) => { if (response.status >= 200 && response.status < 300) { try { const result = JSON.parse(response.responseText); if (!result.choices || result.choices.length === 0) throw new Error("AI响应中缺少 'choices' 字段"); const rawResponse = (result.choices[0].message.content || "").trim().replace(/["'「」]/g, ''); if (!rawResponse) throw new Error("AI返回了空响应。"); // 计算置信度(基于简单匹配,因为logprobs可能不被所有API支持) const confidence = categories.includes(rawResponse) ? 0.9 : 0.3; const finalCategory = categories.includes(rawResponse) ? rawResponse : "其他"; if (finalCategory === "其他") console.warn("NQS - AI返回意外分类,回退至'其他'. Raw:", rawResponse); resolveRequest({ category: finalCategory, rawResponse, confidence, fullApiResponse: result, domain }); } catch (e) { rejectRequest(new Error(`解析AI响应失败: ${e.message}`)); } } else { rejectRequest(new Error(`AI接口错误: ${response.status} ${response.statusText}. 响应: ${response.responseText}`)); } }; } GM_xmlhttpRequest(requestDetails); }); }; // 执行带重试的AI请求 const maxRetries = parseInt(ai_retry_count, 10) || 2; for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { try { const result = await makeAIRequest(attempt); resolve(result); return; } catch (error) { if (attempt === maxRetries + 1) { reject(error); return; } addLog('debug', `AI请求失败,准备重试`, { attempt, error: error.message }); // 等待一段时间后重试 await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); } } }); } function saveToNotion(notionKey, dbId, title, url, category, settings, content = null) { return new Promise((resolve, reject) => { if (!notionKey || !dbId) return reject(new Error("Notion Key或DB ID未在设置中指定")); const properties = { [settings.prop_name_title]: { title: [{ text: { content: title } }] }, [settings.prop_name_url]: { url: url }, [settings.prop_name_category]: { select: { name: category } } }; const requestBody = { parent: { database_id: dbId }, properties }; // 如果有内容,添加到页面body中 if (content) { requestBody.children = [{ object: "block", type: "paragraph", paragraph: { rich_text: [{ type: "text", text: { content: content.substring(0, 2000) // Notion限制单个文本块长度 } }] } }]; } GM_xmlhttpRequest({ method: 'POST', url: 'https://api.notion.com/v1/pages', headers: { 'Authorization': `Bearer ${notionKey}`, 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' }, data: JSON.stringify(requestBody), onload: (response) => { if (response.status >= 200 && response.status < 300) { resolve('✅ 成功保存到 Notion!'); } else { try { const error = JSON.parse(response.responseText); console.error('NQS - Notion API Error:', error); reject(new Error(`Notion保存失败: ${error.message}`)); } catch (e) { reject(new Error(`Notion保存失败: ${response.status} ${response.statusText}. 响应: ${response.responseText}`)); } } }, onerror: () => reject(new Error('❌ Notion网络请求失败!')) }); }); } function fetchAvailableModels(provider, baseUrl, apiKey, timeout, proxySettings) { return new Promise((resolve, reject) => { let finalUrl, headers = { 'Content-Type': 'application/json' }; if (proxySettings.enabled && proxySettings.url) { if (provider === 'gemini') { finalUrl = `${proxySettings.url}`; if (!finalUrl.endsWith('/models')) finalUrl += (finalUrl.endsWith('/') ? 'v1beta/models' : '/v1beta/models'); } else { // openai finalUrl = `${proxySettings.url}`; if (!finalUrl.endsWith('/models')) finalUrl += (finalUrl.endsWith('/') ? 'models' : '/models'); } headers['Authorization'] = `Bearer ${apiKey}`; } else { if (provider === 'gemini') { finalUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`; } else { // openai if (!baseUrl) return reject(new Error("未提供 OpenAI API Endpoint")); try { const tempUrl = new URL(baseUrl); const pathParts = tempUrl.pathname.split('/').filter(Boolean); const v1Index = pathParts.indexOf('v1'); if (v1Index === -1) { finalUrl = `${tempUrl.origin}/v1/models`; } else { const basePath = pathParts.slice(0, v1Index + 1).join('/'); finalUrl = `${tempUrl.origin}/${basePath}/models`; } } catch (e) { return reject(new Error(`无效的 Endpoint URL: ${e.message}`)); } headers['Authorization'] = `Bearer ${apiKey}`; } } GM_xmlhttpRequest({ method: 'GET', url: finalUrl, headers: headers, timeout: parseInt(timeout, 10) || 10000, ontimeout: () => reject(new Error('获取模型列表超时')), onload: (response) => { if (response.status >= 200 && response.status < 300) { try { const result = JSON.parse(response.responseText); let modelIds; if (provider === 'gemini') { if (!result.models || !Array.isArray(result.models)) throw new Error("API响应格式不正确,缺少 'models' 数组。"); modelIds = result.models.filter(m => m.supportedGenerationMethods.includes('generateContent')).map(m => m.name.replace('models/', '')).sort(); } else { if (!result.data || !Array.isArray(result.data)) throw new Error("API响应格式不正确,缺少 'data' 数组。"); modelIds = result.data.map(model => model.id).sort(); } resolve(modelIds); } catch (e) { reject(new Error(`解析模型列表响应失败: ${e.message}`)); } } else { reject(new Error(`API错误: ${response.status} ${response.statusText}. 响应: ${response.responseText}`)); } }, onerror: (e) => reject(new Error('网络请求失败,请检查网络或浏览器控制台。')) }); }); } // =================================================================== // ====================== 增强通知系统 =============================== // =================================================================== class NotificationManager { constructor() { this.permission = null; this.toastContainer = null; this.fabNotifications = new Map(); // 存储FAB按钮通知状态 this.checkPermission(); this.createToastContainer(); } async checkPermission() { if ('Notification' in window) { this.permission = Notification.permission; if (this.permission === 'default') { this.permission = await Notification.requestPermission(); } } } createToastContainer() { if (this.toastContainer) return; this.toastContainer = document.createElement('div'); this.toastContainer.className = 'nqs-toast-container'; const globalContainer = document.getElementById('NQS_GLOBAL_CONTAINER'); globalContainer.appendChild(this.toastContainer); } async showToast(title, message, type = 'info', options = {}) { const settings = await loadAllSettings(); // 检查通知类型设置 if (type === 'success' && !settings.notification_success_enabled) return; if (type === 'error' && !settings.notification_error_enabled) return; const toast = document.createElement('div'); toast.className = `nqs-toast ${type}`; const iconMap = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; toast.innerHTML = ` <div class="nqs-toast-content"> <div class="nqs-toast-icon">${iconMap[type] || iconMap.info}</div> <div class="nqs-toast-text"> <div class="nqs-toast-title">${title}</div> ${message ? `<div class="nqs-toast-message">${message}</div>` : ''} </div> </div> <button class="nqs-toast-close" aria-label="关闭">×</button> `; this.toastContainer.appendChild(toast); // 添加关闭事件 const closeBtn = toast.querySelector('.nqs-toast-close'); closeBtn.addEventListener('click', () => this.hideToast(toast)); // 显示动画 setTimeout(() => { toast.classList.add('visible'); }, 10); // 自动隐藏 const duration = options.duration || (type === 'error' ? 6000 : 4000); setTimeout(() => { this.hideToast(toast); }, duration); // 同时显示浏览器通知(如果启用) if (settings.notification_enabled && this.permission === 'granted') { this.showBrowserNotification(title, message, type); } } hideToast(toast) { toast.classList.remove('visible'); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); } async showBrowserNotification(title, message, type) { const iconMap = { success: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzEwYjk4MSI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTQgOGwtNiA2LTMtMy0xLjQxIDEuNDFMMTAgMTQgMTcuNTkgNi40MSAxNiA1IDEwIDExWiIvPjwvc3ZnPg==', error: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2VmNDQ0NCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJaIi8+PC9zdmc+', warning: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2Y1OWUwYiI+PHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxWm0xMi0zaC0ydi0yaDJWMThabS0yLTRoMlY5aC0yVjE0WiIvPjwvc3ZnPg==', info: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzNiODJmNiI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJaIi8+PC9zdmc+' }; new Notification(title, { body: message, icon: iconMap[type] || iconMap.info, tag: 'nqs-notification', requireInteraction: false, silent: false }); } showSuccessNotification(title, message = '') { return this.showToast(title, message, 'success'); } showErrorNotification(title, message = '') { return this.showToast(title, message, 'error'); } showWarningNotification(title, message = '') { return this.showToast(title, message, 'warning'); } showInfoNotification(title, message = '') { return this.showToast(title, message, 'info'); } // 统一的FAB按钮通知方法 showFabNotification(button, message, isLoading = false, originalText = '') { if (!button) return; const buttonId = button.id || 'unknown'; // 清除之前的定时器 if (this.fabNotifications.has(buttonId)) { clearTimeout(this.fabNotifications.get(buttonId)); } // 更新按钮文本和状态 button.textContent = message; if (isLoading) { button.classList.add('loading'); button.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; // 如果不是加载状态,3秒后恢复原始文本 if (originalText) { const timeoutId = setTimeout(() => { button.textContent = originalText; this.fabNotifications.delete(buttonId); }, 3000); this.fabNotifications.set(buttonId, timeoutId); } } } // 统一的通知方法 - 自动选择Toast或FAB showUnifiedNotification(title, message = '', type = 'info', fabButton = null, originalText = '') { // 显示Toast通知 this.showToast(title, message, type); // 如果有FAB按钮,也更新按钮状态 if (fabButton) { const isLoading = type === 'loading'; this.showFabNotification(fabButton, title, isLoading, originalText); } } } class ProgressIndicator { constructor() { this.container = null; this.progressBar = null; this.isShowing = false; this.createProgressBar(); } createProgressBar() { // 创建顶部进度条 this.container = document.createElement('div'); this.container.className = 'nqs-top-progress'; this.container.innerHTML = ` <div class="nqs-progress-bar"> <div class="nqs-progress-fill"></div> </div> <div class="nqs-progress-status"> <span class="nqs-progress-text">准备就绪</span> </div> `; const globalContainer = document.getElementById('NQS_GLOBAL_CONTAINER'); globalContainer.appendChild(this.container); this.progressBar = this.container.querySelector('.nqs-progress-fill'); } async show(message = '处理中...') { const settings = await loadAllSettings(); if (!settings.progress_indicator_enabled) return; if (this.isShowing) return; this.isShowing = true; this.updateMessage(message); this.container.classList.add('visible'); // 开始进度条动画 this.progressBar.style.width = '0%'; this.animateProgress(); } animateProgress() { let progress = 0; const interval = setInterval(() => { progress += Math.random() * 15; if (progress > 90) progress = 90; this.progressBar.style.width = progress + '%'; if (!this.isShowing) { clearInterval(interval); } }, 200); } updateMessage(message) { if (this.container) { const textEl = this.container.querySelector('.nqs-progress-text'); if (textEl) textEl.textContent = message; } } hide() { if (!this.isShowing) return; // 完成进度条 this.progressBar.style.width = '100%'; setTimeout(() => { this.container.classList.remove('visible'); this.isShowing = false; // 重置进度条 setTimeout(() => { this.progressBar.style.width = '0%'; this.updateMessage('准备就绪'); }, 300); }, 200); } } const notificationManager = new NotificationManager(); const progressIndicator = new ProgressIndicator(); // =================================================================== // ====================== 批量保存功能 =============================== // =================================================================== // =================================================================== // ====================== 智能收藏夹功能 ============================= // =================================================================== async function openSmartBookmarkManager() { closeAllNQSPopups(); const { body, footer, close } = createBasePanel('🚀 智能收藏夹管理', '快速保存和整理网页资源', { maxWidth: '900px', panelClass: 'nqs-panel--bookmark-manager' }); const content = ` <div class="nqs-bookmark-manager"> <!-- 主要功能区域 --> <div class="nqs-feature-grid"> <div class="nqs-feature-card nqs-feature-primary" data-action="save-session"> <div class="nqs-feature-icon">🖥️</div> <div class="nqs-feature-content"> <h4>网站会话保存</h4> <p>智能识别并保存当前网站的相关页面,自动归类整理</p> <div class="nqs-feature-badge">推荐</div> </div> <div class="nqs-feature-arrow">→</div> </div> <div class="nqs-feature-card" data-action="save-links"> <div class="nqs-feature-icon">🔗</div> <div class="nqs-feature-content"> <h4>页面链接提取</h4> <p>提取页面中的有价值链接,批量保存到Notion</p> </div> <div class="nqs-feature-arrow">→</div> </div> <div class="nqs-feature-card" data-action="create-reading-list"> <div class="nqs-feature-icon">📚</div> <div class="nqs-feature-content"> <h4>主题阅读清单</h4> <p>基于当前内容创建个性化的学习资源清单</p> </div> <div class="nqs-feature-arrow">→</div> </div> </div> <!-- 快速分类保存 --> <div class="nqs-quick-save-section"> <div class="nqs-section-header"> <h3>⚡ 快速分类保存</h3> <p>一键保存到预设分类</p> </div> <div class="nqs-quick-categories"> <button class="nqs-category-btn" data-category="重要参考"> <span class="nqs-category-icon">📌</span> <span class="nqs-category-name">重要参考</span> </button> <button class="nqs-category-btn" data-category="学习教程"> <span class="nqs-category-icon">🎯</span> <span class="nqs-category-name">学习教程</span> </button> <button class="nqs-category-btn" data-category="灵感素材"> <span class="nqs-category-icon">💡</span> <span class="nqs-category-name">灵感素材</span> </button> <button class="nqs-category-btn" data-category="工具资源"> <span class="nqs-category-icon">🛠️</span> <span class="nqs-category-name">工具资源</span> </button> <button class="nqs-category-btn" data-category="技术文档"> <span class="nqs-category-icon">📖</span> <span class="nqs-category-name">技术文档</span> </button> <button class="nqs-category-btn" data-category="设计案例"> <span class="nqs-category-icon">🎨</span> <span class="nqs-category-name">设计案例</span> </button> </div> </div> <!-- 页面信息预览 --> <div class="nqs-page-preview"> <div class="nqs-section-header"> <h3>📄 当前页面信息</h3> </div> <div class="nqs-page-info"> <div class="nqs-page-title">${document.title}</div> <div class="nqs-page-url">${window.location.href}</div> <div class="nqs-page-meta"> <span class="nqs-meta-item"> <span class="nqs-meta-label">域名:</span> <span class="nqs-meta-value">${window.location.hostname}</span> </span> <span class="nqs-meta-item"> <span class="nqs-meta-label">类型:</span> <span class="nqs-meta-value">${getPageType()}</span> </span> </div> </div> </div> </div> `; setSafeInnerHTML(body, content); setSafeInnerHTML(footer, ` <div class="nqs-bookmark-footer"> <div class="nqs-footer-left"> <button class="nqs-button nqs-button-secondary" id="cancel-bookmark"> <span>❌</span> 取消 </button> </div> <div class="nqs-footer-center"> <button class="nqs-button nqs-button-text" id="open-settings"> <span>⚙️</span> 设置收藏规则 </button> </div> <div class="nqs-footer-right"> <button class="nqs-button nqs-button-primary" id="save-current-page"> <span>💾</span> 保存当前页面 </button> </div> </div> `); // 获取页面类型的辅助函数 function getPageType() { const url = window.location.href.toLowerCase(); const title = document.title.toLowerCase(); if (url.includes('github.com')) return 'GitHub项目'; if (url.includes('stackoverflow.com')) return '技术问答'; if (url.includes('medium.com') || url.includes('dev.to')) return '技术博客'; if (url.includes('youtube.com')) return '视频教程'; if (url.includes('docs.') || title.includes('documentation')) return '技术文档'; if (url.includes('tutorial') || title.includes('tutorial')) return '教程指南'; if (url.includes('news') || url.includes('blog')) return '新闻博客'; return '普通网页'; } // 绑定主要功能事件 body.querySelector('[data-action="save-session"]').addEventListener('click', () => { close(); saveCurrentSession(); }); body.querySelector('[data-action="save-links"]').addEventListener('click', () => { close(); saveDomainLinks(); }); body.querySelector('[data-action="create-reading-list"]').addEventListener('click', () => { close(); createReadingList(); }); // 绑定快速分类按钮事件 body.querySelectorAll('.nqs-category-btn').forEach(btn => { btn.addEventListener('click', () => { const category = btn.dataset.category; close(); quickSaveWithCategory(category); }); }); // 绑定底部按钮事件 footer.querySelector('#cancel-bookmark').addEventListener('click', close); footer.querySelector('#open-settings').addEventListener('click', () => { close(); openSettingsPanel(); }); footer.querySelector('#save-current-page').addEventListener('click', () => { close(); startSaveProcess({ source: 'bookmark-manager' }); }); // 添加卡片悬浮效果 body.querySelectorAll('.nqs-feature-card').forEach(card => { card.addEventListener('mouseenter', () => { card.style.transform = 'translateY(-4px)'; }); card.addEventListener('mouseleave', () => { card.style.transform = 'translateY(0)'; }); }); } async function saveCurrentSession() { try { const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!'); } await progressIndicator.show('📚 分析当前会话...'); const currentUrl = window.location.href; const currentTitle = document.title; const domain = new URL(currentUrl).hostname; // 创建会话标题 const sessionTitle = `【会话】${domain} - ${new Date().toLocaleDateString()}`; // 分析页面内容,提取相关信息 const content = extractAdvancedContent(document); progressIndicator.updateMessage('🤖 生成会话摘要...'); let sessionSummary = ''; if (settings.ai_enabled && settings.auto_summary_enabled) { try { const summaryPrompt = `基于以下网页信息,生成一个会话摘要,说明这个网站/页面的主要价值和学习要点: 网站:${domain} 页面标题:${currentTitle} 页面描述:${content.description} 主要内容:${content.mainContent.substring(0, 1000)} 请生成: 1. 网站/页面的主要价值(1-2句话) 2. 关键学习要点(3-5个要点) 3. 推荐的后续行动(如继续阅读的建议) 格式:简洁的markdown格式`; const result = await makeSummaryRequest(settings, summaryPrompt); sessionSummary = result.summary; } catch (error) { console.warn('AI摘要生成失败'); } } const sessionContent = `# 📚 网站会话记录 **访问时间:** ${new Date().toLocaleString('zh-CN')} **网站域名:** ${domain} **当前页面:** [${currentTitle}](${currentUrl}) ## 📋 会话摘要 ${sessionSummary || '本次会话的主要页面和资源记录'} ## 🎯 页面信息 - **阅读时长:** 约 ${content.readingTime} 分钟 - **页面类型:** ${content.author ? '文章页面' : '信息页面'} ${content.publishTime ? `- **发布时间:** ${content.publishTime}` : ''} ## 🔗 相关链接 ${content.links.slice(0, 5).map(link => `- [${link.text}](${link.href})`).join('\n')} --- *记录时间:${new Date().toLocaleString('zh-CN')}*`; progressIndicator.updateMessage('💾 保存会话记录...'); // 保存会话记录 await saveToNotion(settings.notion_api_key, settings.database_id, sessionTitle, currentUrl, '学习记录', settings, sessionContent); progressIndicator.hide(); notificationManager.showSuccessNotification('会话已保存', '当前网站会话记录已保存到Notion'); await addLog('info', '网站会话已保存', { domain: domain, title: currentTitle, linksCount: content.links.length }); } catch (error) { progressIndicator.hide(); notificationManager.showErrorNotification('会话保存失败', error.message); console.error('保存会话失败:', error); } } async function saveDomainLinks() { try { const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!'); } await progressIndicator.show('🔍 提取页面链接...'); const currentUrl = window.location.href; const currentTitle = document.title; const domain = new URL(currentUrl).hostname; // 提取所有外部链接 const links = Array.from(document.querySelectorAll('a[href]')) .filter(link => { const href = link.href; return href.startsWith('http') && !href.includes(domain) && link.textContent.trim().length > 0; }) .map(link => ({ text: link.textContent.trim(), href: link.href, context: link.closest('h1, h2, h3, h4, h5, h6, p, li')?.textContent.trim() || '' })) .filter((link, index, array) => // 去重 array.findIndex(l => l.href === link.href) === index ) .slice(0, 20); // 限制数量 if (links.length === 0) { throw new Error('❌ 未找到有效的外部链接'); } progressIndicator.updateMessage('📝 整理链接资源...'); const linksTitle = `【链接收集】${currentTitle}`; const linksContent = `# 🔗 链接资源收集 **来源页面:** [${currentTitle}](${currentUrl}) **收集时间:** ${new Date().toLocaleString('zh-CN')} **链接数量:** ${links.length} ## 📋 链接列表 ${links.map((link, index) => `### ${index + 1}. [${link.text}](${link.href}) ${link.context ? `> 上下文:${link.context.substring(0, 100)}${link.context.length > 100 ? '...' : ''}` : ''} `).join('\n')} --- *由 Notion AI 助手自动收集整理*`; progressIndicator.updateMessage('💾 保存链接收集...'); await saveToNotion(settings.notion_api_key, settings.database_id, linksTitle, currentUrl, '资源收集', settings, linksContent); progressIndicator.hide(); notificationManager.showSuccessNotification('链接已收集', `已收集 ${links.length} 个有价值的链接`); await addLog('info', '页面链接已收集', { sourceTitle: currentTitle, linksCount: links.length }); } catch (error) { progressIndicator.hide(); notificationManager.showErrorNotification('链接收集失败', error.message); console.error('链接收集失败:', error); } } async function createReadingList() { try { const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!'); } await progressIndicator.show('📖 创建阅读清单...'); const currentUrl = window.location.href; const currentTitle = document.title; const content = extractAdvancedContent(document); progressIndicator.updateMessage('🤖 AI生成推荐...'); let recommendations = ''; if (settings.ai_enabled) { try { const recommendPrompt = `基于以下页面信息,推荐5-8个相关的学习主题和关键词,用于进一步深入学习: 页面标题:${currentTitle} 页面描述:${content.description} 关键词:${content.keywords} 主要内容概要:${content.mainContent.substring(0, 800)} 请提供: 1. 核心学习主题(3-4个) 2. 相关技术栈/概念(4-6个) 3. 推荐的学习路径(简要说明) 格式:markdown列表格式,简洁明了`; const result = await makeSummaryRequest(settings, recommendPrompt); recommendations = result.summary; } catch (error) { console.warn('AI推荐生成失败'); } } const listTitle = `【学习清单】${currentTitle}`; const listContent = `# 📚 主题学习清单 **起始页面:** [${currentTitle}](${currentUrl}) **创建时间:** ${new Date().toLocaleString('zh-CN')} ## 🎯 当前页面要点 - **阅读时长:** ${content.readingTime} 分钟 - **主要类型:** ${content.description || '知识学习'} - **关键信息:** ${content.keywords || '待补充'} ## 🚀 推荐学习方向 ${recommendations || `基于"${currentTitle}"的内容,建议深入学习以下方向: ### 核心概念深化 - [ ] 相关基础理论 - [ ] 实践应用案例 - [ ] 最佳实践总结 ### 扩展学习 - [ ] 相关技术栈 - [ ] 进阶应用 - [ ] 行业应用案例`} ## ✅ 学习计划 - [ ] 完成当前页面学习 - [ ] 查找相关补充资料 - [ ] 实践练习/项目应用 - [ ] 总结学习心得 ## 📖 待读资源 ${content.links.slice(0, 3).map(link => `- [ ] [${link.text}](${link.href})`).join('\n')} --- *学习清单由 AI 助手生成,可根据个人需要调整*`; progressIndicator.updateMessage('💾 保存学习清单...'); await saveToNotion(settings.notion_api_key, settings.database_id, listTitle, currentUrl, '学习计划', settings, listContent); progressIndicator.hide(); notificationManager.showSuccessNotification('学习清单已创建', '个人化学习计划已保存'); await addLog('info', '学习清单已创建', { sourceTitle: currentTitle, hasAIRecommendations: !!recommendations }); } catch (error) { progressIndicator.hide(); notificationManager.showErrorNotification('清单创建失败', error.message); console.error('学习清单创建失败:', error); } } async function quickSaveWithCategory(customCategory) { try { const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!'); } await progressIndicator.show(`💾 保存为${customCategory}...`); const currentUrl = window.location.href; const currentTitle = document.title; const quickTitle = `【${customCategory}】${currentTitle}`; await saveToNotion(settings.notion_api_key, settings.database_id, quickTitle, currentUrl, customCategory, settings); progressIndicator.hide(); notificationManager.showSuccessNotification('快速保存成功', `已保存为"${customCategory}"`); await addLog('info', '快速分类保存', { title: currentTitle, category: customCategory }); } catch (error) { progressIndicator.hide(); notificationManager.showErrorNotification('快速保存失败', error.message); console.error('快速保存失败:', error); } } // =================================================================== // ====================== 智能摘录功能 =============================== // =================================================================== async function saveSelectedTextAsNote() { try { const selectedText = window.getSelection().toString().trim(); if (!selectedText) { throw new Error('❌ 请先选择要保存的文本内容。'); } if (selectedText.length < 20) { throw new Error('❌ 选中的文本太短,建议选择至少20个字符的内容。'); } const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!请在"设置"中填写 Notion API Key 和数据库ID。'); } await progressIndicator.show('📝 处理选中文本...'); // 获取选中文本的上下文 const selection = window.getSelection(); const range = selection.getRangeAt(0); const container = range.commonAncestorContainer.nodeType === Node.TEXT_NODE ? range.commonAncestorContainer.parentNode : range.commonAncestorContainer; // 尝试获取更多上下文信息 let contextInfo = ''; const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); let nearestHeading = ''; // 找到距离选中文本最近的标题 for (const heading of headings) { if (container.compareDocumentPosition && container.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_PRECEDING) { nearestHeading = heading.textContent.trim(); } } if (nearestHeading) { contextInfo = `\n\n**所在章节:** ${nearestHeading}`; } // 创建富文本摘录标题 const originalTitle = document.title; const sourceInfo = `【摘录】${nearestHeading || originalTitle}`; // 构建摘录内容 let noteContent = ''; if (settings.content_save_mode === 'full' || settings.auto_summary_enabled) { progressIndicator.updateMessage('🤖 AI分析中...'); // 使用AI生成摘录的背景和要点 try { const analysisPrompt = `请分析以下文本摘录,并提供: 1. 这段文字的核心观点(1-2句话) 2. 为什么这段话值得摘录(价值分析) 3. 相关的关键词标签(3-5个) 原文标题:${originalTitle} ${nearestHeading ? `章节:${nearestHeading}` : ''} 摘录内容: ${selectedText} 请用以下格式回答: 核心观点:[观点内容] 价值分析:[为什么重要] 关键词:[词1, 词2, 词3]`; const analysisResult = await makeSummaryRequest(settings, analysisPrompt); const analysis = analysisResult.summary; noteContent = `## 📖 文本摘录 **原文链接:** [${originalTitle}](${window.location.href})${contextInfo} **摘录内容:** > ${selectedText} ## 🎯 AI 分析 ${analysis} --- *摘录时间:${new Date().toLocaleString('zh-CN')}*`; } catch (aiError) { console.warn('AI分析失败,使用基础格式:', aiError); noteContent = `## 📖 文本摘录 **原文链接:** [${originalTitle}](${window.location.href})${contextInfo} **摘录内容:** > ${selectedText} --- *摘录时间:${new Date().toLocaleString('zh-CN')}*`; } } else { noteContent = selectedText; } progressIndicator.updateMessage('🚀 保存到 Notion...'); // 智能分类 let category = "学习笔记"; if (settings.ai_enabled) { try { const pageMetadata = `Page URL: ${window.location.href}\nPage Title: ${originalTitle}\nContext: ${nearestHeading}\nSelected Text: ${selectedText.substring(0, 500)}`; const aiResult = await determineCategoryAI(settings, pageMetadata, selectedText); category = aiResult.category; } catch (error) { console.warn('AI分类失败,使用默认分类'); } } // 保存摘录(这里需要扩展saveToNotion函数支持富文本内容) await saveToNotion(settings.notion_api_key, settings.database_id, sourceInfo, window.location.href, category, settings, noteContent); progressIndicator.hide(); await addLog('info', '文本摘录已保存', { originalTitle: originalTitle, textLength: selectedText.length, category: category, hasAnalysis: noteContent.includes('AI 分析') }); notificationManager.showSuccessNotification('摘录保存成功', `已保存到分类:"${category}"`); console.log(`NQS: [Menu] ✅ 文本摘录已保存为: ${category}`); } catch (error) { progressIndicator.hide(); notificationManager.showErrorNotification('摘录保存失败', error.message); console.error('NQS: [Menu] 保存文本摘录失败:', error.message); await addLog('error', '保存文本摘录失败', { error: error.message }); } } // =================================================================== // ====================== 统一的保存逻辑 (Refactored) =============== // =================================================================== async function runAiSave(settings, pageTitle, pageUrl, uiContext) { const saveButton = uiContext.buttonElement; const originalText = '➤ Notion'; try { // 显示进度条和状态 await progressIndicator.show('🧠 AI 分类中...'); // 只更新FAB按钮状态,不显示Toast通知 if (uiContext.source === 'fab' && saveButton) { notificationManager.showFabNotification(saveButton, '🧠 AI 分析中...', true, originalText); } const pageMetadata = getHighSignalPageData(document); const pageBodyText = settings.ai_include_body ? extractMainContent(document).substring(0, 4000) : "N/A"; const aiResult = await determineCategoryAI(settings, pageMetadata, pageBodyText); const { category, confidence, domain } = aiResult; // 更新进度 progressIndicator.updateMessage('🚀 保存到 Notion...'); // 显示置信度信息 const confidenceText = confidence ? ` (${Math.round(confidence * 100)}%)` : ''; // 更新FAB按钮状态 if (uiContext.source === 'fab' && saveButton) { notificationManager.showFabNotification(saveButton, `🚀 保存中...`, true, originalText); } const notionResponse = await saveToNotion(settings.notion_api_key, settings.database_id, pageTitle, pageUrl, category, settings); // 隐藏进度条 progressIndicator.hide(); await addLog('info', `页面已通过AI保存 (${uiContext.source})`, { title: pageTitle, url: pageUrl, result: category, confidence: confidence, provider: settings.ai_provider, domain: domain }); // 显示成功通知和恢复FAB按钮状态 const confidenceInfo = confidence ? ` (AI置信度: ${Math.round(confidence * 100)}%)` : ''; notificationManager.showSuccessNotification('保存成功', `已保存为"${category}"${confidenceInfo}`); if (uiContext.source === 'fab' && saveButton) { notificationManager.showFabNotification(saveButton, '✅ 已保存', false, originalText); setTimeout(() => { notificationManager.showFabNotification(saveButton, originalText, false, originalText); }, 2000); } } catch(error) { progressIndicator.hide(); // 只更新FAB按钮状态,显示错误Toast通知 if (uiContext.source === 'fab' && saveButton) { notificationManager.showFabNotification(saveButton, '❌ 失败', false, originalText); setTimeout(() => { notificationManager.showFabNotification(saveButton, originalText, false, originalText); }, 3000); } // 显示错误Toast通知 notificationManager.showErrorNotification('保存失败', error.message); throw error; } } async function startSaveProcess(uiContext) { const pageTitle = document.title; const pageUrl = window.location.href; const saveButton = uiContext.buttonElement; try { if (uiContext.source === 'fab') showNotification(saveButton, '⚙️ 读取中...', true, '➤ Notion'); else console.log('NQS: [Menu] 开始处理 "保存"...'); const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!请在"设置"中填写 Notion API Key 和数据库ID。'); } if (settings.ai_enabled) { await runAiSave(settings, pageTitle, pageUrl, uiContext); } else { if (uiContext.source === 'fab') showNotification(saveButton, '📂 手动选择', false, '➤ Notion'); else console.log('NQS: [Menu] AI已关闭,正在打开分类选择器...'); await openCategorySelectorPanel(pageTitle, pageUrl, uiContext); } } catch (error) { if (uiContext.source === 'fab') showNotification(saveButton, error.message, false, '➤ Notion'); else console.error(`NQS: [Menu] 保存失败!错误信息: ${error.message}`); await addLog('error', `保存操作失败 (${uiContext.source}): ${error.message}`, { title: pageTitle, url: pageUrl, error: error.message, stack: error.stack }); if (error.message.includes('配置')) setTimeout(openSettingsPanel, 1000); } } async function startReadLaterSave(uiContext) { const pageTitle = document.title; const pageUrl = window.location.href; const readLaterButton = uiContext.buttonElement; const originalText = '◷ 稍后读'; try { // 显示进度条 await progressIndicator.show('⚙️ 读取配置中...'); // 只更新FAB按钮状态 if (uiContext.source === 'fab' && readLaterButton) { notificationManager.showFabNotification(readLaterButton, '⚙️ 准备中...', true, originalText); } const settings = await loadAllSettings(); if (!settings.notion_api_key || !settings.database_id) { throw new Error('❌ 配置不完整!请在"设置"中填写 Notion API Key 和数据库ID。'); } if (!settings.read_later_enabled) { throw new Error('💡 "稍后读"功能未开启。'); } if (!settings.read_later_category) { throw new Error('❌ 未设置"稍后读"分类名称。'); } const category = settings.read_later_category; // 更新保存状态 progressIndicator.updateMessage('🚀 保存到 Notion...'); // 只更新FAB按钮状态 if (uiContext.source === 'fab' && readLaterButton) { notificationManager.showFabNotification(readLaterButton, '🚀 保存中...', true, originalText); } const notionResponse = await saveToNotion(settings.notion_api_key, settings.database_id, pageTitle, pageUrl, category, settings); // 隐藏进度条 progressIndicator.hide(); await addLog('info', `页面已存为稍后读 (${uiContext.source})`, { title: pageTitle, url: pageUrl, result: category }); // 显示成功通知和恢复FAB按钮状态 notificationManager.showSuccessNotification('保存成功', `已添加到"${category}"`); if (uiContext.source === 'fab' && readLaterButton) { notificationManager.showFabNotification(readLaterButton, '✅ 已保存', false, originalText); setTimeout(() => { notificationManager.showFabNotification(readLaterButton, originalText, false, originalText); }, 2000); } } catch (error) { progressIndicator.hide(); // 只更新FAB按钮状态,显示错误Toast通知 if (uiContext.source === 'fab' && readLaterButton) { notificationManager.showFabNotification(readLaterButton, '❌ 失败', false, originalText); setTimeout(() => { notificationManager.showFabNotification(readLaterButton, originalText, false, originalText); }, 3000); } // 显示错误Toast通知 notificationManager.showErrorNotification('保存失败', error.message); await addLog('error', `"稍后读"失败 (${uiContext.source}): ${error.message}`, { title: pageTitle, url: pageUrl, error: error.message, stack: error.stack }); if (error.message.includes('配置')) { setTimeout(openSettingsPanel, 1000); } } } // =================================================================== // ====================== 脚本初始化与执行 ======================= // =================================================================== class FabPositionManager { constructor() { this.position = null; this.storageKey = 'nqs_fab_pos'; } async loadPosition() { try { const rawPos = await GM_getValue(this.storageKey, null); if (typeof rawPos === 'string') { this.position = JSON.parse(rawPos); } else if (rawPos && typeof rawPos === 'object') { this.position = rawPos; } return this.position; } catch (error) { console.warn('Failed to load fab position:', error); return null; } } async savePosition(position) { this.position = position; try { await GM_setValue(this.storageKey, JSON.stringify(position)); } catch (error) { console.error('Failed to save fab position:', error); } } updatePosition(fabElement) { if (!fabElement) return; const rect = fabElement.getBoundingClientRect(); this.position = { topPercent: rect.top / window.innerHeight, leftPercent: rect.left / window.innerWidth, snapped: false }; } } class FabDragHandler { constructor(fabElement, positionManager) { this.fabElement = fabElement; this.positionManager = positionManager; this.isDragging = false; this.wasDragged = false; this.offset = { x: 0, y: 0 }; this.bindEvents(); } bindEvents() { const trigger = this.fabElement.querySelector('#nqs-fab-trigger'); if (!trigger) return; trigger.addEventListener('mousedown', this.handleDragStart.bind(this)); document.addEventListener('mousemove', this.handleDragMove.bind(this)); document.addEventListener('mouseup', this.handleDragEnd.bind(this)); trigger.addEventListener('click', (e) => { if (this.wasDragged) { e.preventDefault(); e.stopPropagation(); } }); } handleDragStart(e) { this.fabElement.classList.remove('is-expanded'); this.isDragging = true; this.wasDragged = false; this.fabElement.classList.add('is-dragging'); const rect = this.fabElement.getBoundingClientRect(); this.offset.x = e.clientX - rect.left; this.offset.y = e.clientY - rect.top; document.body.style.userSelect = 'none'; } handleDragMove(e) { if (!this.isDragging) return; e.preventDefault(); this.wasDragged = true; const newPosition = this.calculateNewPosition(e); this.applyPosition(newPosition); this.removeSnapState(); fabInstance.updateAlignmentClass(); } calculateNewPosition(e) { let newX = e.clientX - this.offset.x; let newY = e.clientY - this.offset.y; const fabRect = this.fabElement.getBoundingClientRect(); const maxX = window.innerWidth - fabRect.width; const maxY = window.innerHeight - fabRect.height; return { x: Math.max(0, Math.min(newX, maxX)), y: Math.max(0, Math.min(newY, maxY)) }; } applyPosition({ x, y }) { Object.assign(this.fabElement.style, { left: `${x}px`, top: `${y}px`, right: 'auto', bottom: 'auto' }); } removeSnapState() { this.fabElement.classList.remove('snapped-right', 'snapped-left'); } async handleDragEnd() { if (!this.isDragging) return; this.isDragging = false; this.fabElement.classList.remove('is-dragging'); document.body.style.userSelect = 'auto'; setTimeout(() => { this.wasDragged = false; }, 0); const position = this.calculateFinalPosition(); fabInstance.applyPosition(position); await this.positionManager.savePosition(position); } calculateFinalPosition() { const rect = this.fabElement.getBoundingClientRect(); const snapThresholdRight = window.innerWidth * 0.98; const snapThresholdLeft = window.innerWidth * 0.02; if (rect.right > snapThresholdRight) { return { topPercent: rect.top / window.innerHeight, snapped: 'right' }; } else if (rect.left < snapThresholdLeft) { return { topPercent: rect.top / window.innerHeight, snapped: 'left' }; } return { topPercent: rect.top / window.innerHeight, leftPercent: rect.left / window.innerWidth, snapped: false }; } } class FloatingActionButton { constructor() { this.positionManager = new FabPositionManager(); this.dragHandler = null; this.container = null; this.resizeHandler = debounce(this.handleResize.bind(this), 150); this.leaveTimeoutId = null; } async init() { await this.cleanup(); injectStyles(); await this.createContainer(); await this.setupPosition(); this.setupInteractions(); this.bindResizeHandler(); } async cleanup() { const existingContainer = document.querySelector('#nqs-fab-container'); if (existingContainer) existingContainer.remove(); } async createContainer() { const globalContainer = document.getElementById('NQS_GLOBAL_CONTAINER'); this.container = document.createElement('div'); this.container.id = 'nqs-fab-container'; await this.createButtons(); globalContainer.appendChild(this.container); } async createButtons() { const settings = await loadAllSettings(); const optionsWrapper = document.createElement('div'); optionsWrapper.className = 'nqs-fab-options'; const saveButton = this.createActionButton('nqs-save-button', '➤ Notion'); saveButton.addEventListener('click', (e) => { e.stopPropagation(); this.handleButtonClick(e, 'save', saveButton); }); optionsWrapper.appendChild(saveButton); if (settings.read_later_enabled) { const readLaterButton = this.createActionButton('nqs-read-later-button', '◷ 稍后读'); readLaterButton.addEventListener('click', (e) => { e.stopPropagation(); this.handleButtonClick(e, 'readLater', readLaterButton); }); optionsWrapper.appendChild(readLaterButton); } const triggerButton = document.createElement('div'); triggerButton.id = 'nqs-fab-trigger'; setSafeInnerHTML(triggerButton, '➤'); this.container.appendChild(triggerButton); this.container.appendChild(optionsWrapper); } createActionButton(id, text) { const button = document.createElement('div'); button.id = id; button.className = 'nqs-fab-action-btn'; button.textContent = text; return button; } handleButtonClick(e, action, buttonElement) { (action === 'save' ? startSaveProcess : startReadLaterSave)({ source: 'fab', buttonElement }); } async setupPosition() { const savedPos = await this.positionManager.loadPosition(); if (savedPos) this.applyPosition(savedPos); else this.setDefaultPosition(); } applyPosition(position) { this.container.classList.remove('snapped-right', 'snapped-left'); if (position.snapped === 'right') { Object.assign(this.container.style, { top: `${position.topPercent * window.innerHeight}px`, right: '0px', left: 'auto', bottom: 'auto' }); this.container.classList.add('snapped-right'); } else if (position.snapped === 'left') { Object.assign(this.container.style, { top: `${position.topPercent * window.innerHeight}px`, left: '0px', right: 'auto', bottom: 'auto' }); this.container.classList.add('snapped-left'); } else { Object.assign(this.container.style, { top: `${position.topPercent * window.innerHeight}px`, left: `${position.leftPercent * window.innerWidth}px`, right: 'auto', bottom: 'auto' }); } this.updateAlignmentClass(); } setDefaultPosition() { Object.assign(this.container.style, { right: '30px', bottom: '30px' }); setTimeout(() => { this.positionManager.updatePosition(this.container); this.updateAlignmentClass(); }, 0); } updateAlignmentClass() { if (!this.container) return; const rect = this.container.getBoundingClientRect(); const viewportCenterX = window.innerWidth / 2; const fabCenterX = rect.left + rect.width / 2; if (fabCenterX < viewportCenterX) this.container.classList.add('align-left'); else this.container.classList.remove('align-left'); } setupInteractions() { this.dragHandler = new FabDragHandler(this.container, this.positionManager); this.container.addEventListener('mouseenter', (event) => { if (this.leaveTimeoutId) { clearTimeout(this.leaveTimeoutId); this.leaveTimeoutId = null; } if (this.dragHandler.isDragging || event.buttons === 1) return; this.container.classList.add('is-expanded'); }); this.container.addEventListener('mouseleave', () => { this.leaveTimeoutId = setTimeout(() => { this.container.classList.remove('is-expanded'); }, 300); }); } handleResize() { const position = this.positionManager.position; if (!this.container || !position || this.dragHandler?.isDragging) return; if (!position.snapped) { const newX = position.leftPercent * window.innerWidth; this.container.style.left = `${newX}px`; } const newY = position.topPercent * window.innerHeight; this.container.style.top = `${newY}px`; this.updateAlignmentClass(); } bindResizeHandler() { window.addEventListener('resize', this.resizeHandler); } destroy() { window.removeEventListener('resize', this.resizeHandler); this.container?.remove(); } } let fabInstance = null; function injectStyles() { const styleId = 'nqs-custom-ui-styles'; const container = document.getElementById('NQS_GLOBAL_CONTAINER'); if (container.querySelector(`#${styleId}`)) return; const css = ` /* === 统一的主题变量系统 === */ #NQS_GLOBAL_CONTAINER { all: initial; /* 明亮主题变量 */ --nqs-bg: #ffffff; --nqs-bg-subtle: #f8fafc; --nqs-bg-hover: #f1f5f9; --nqs-bg-active: #e2e8f0; --nqs-border: #e2e8f0; --nqs-border-hover: #cbd5e1; --nqs-text-primary: #0f172a; --nqs-text-secondary: #64748b; --nqs-text-tertiary: #94a3b8; --nqs-accent: #3b82f6; --nqs-accent-hover: #2563eb; --nqs-accent-light: #dbeafe; --nqs-success: #10b981; --nqs-success-light: #d1fae5; --nqs-warning: #f59e0b; --nqs-warning-light: #fef3c7; --nqs-danger: #ef4444; --nqs-danger-light: #fecaca; --nqs-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --nqs-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); --nqs-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); --nqs-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --nqs-radius: 8px; --nqs-radius-lg: 12px; --nqs-radius-xl: 16px; } /* 暗黑主题变量 */ #NQS_GLOBAL_CONTAINER[data-theme='dark'] { --nqs-bg: #0f172a; --nqs-bg-subtle: #1e293b; --nqs-bg-hover: #334155; --nqs-bg-active: #475569; --nqs-border: #334155; --nqs-border-hover: #475569; --nqs-text-primary: #f8fafc; --nqs-text-secondary: #cbd5e1; --nqs-text-tertiary: #94a3b8; --nqs-accent: #3b82f6; --nqs-accent-hover: #60a5fa; --nqs-accent-light: #1e3a8a; --nqs-success: #10b981; --nqs-success-light: #064e3b; --nqs-warning: #f59e0b; --nqs-warning-light: #78350f; --nqs-danger: #ef4444; --nqs-danger-light: #7f1d1d; --nqs-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); --nqs-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); --nqs-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.3); } /* 自动主题跟随系统 */ @media (prefers-color-scheme: dark) { #NQS_GLOBAL_CONTAINER[data-theme='auto'] { --nqs-bg: #0f172a; --nqs-bg-subtle: #1e293b; --nqs-bg-hover: #334155; --nqs-bg-active: #475569; --nqs-border: #334155; --nqs-border-hover: #475569; --nqs-text-primary: #f8fafc; --nqs-text-secondary: #cbd5e1; --nqs-text-tertiary: #94a3b8; --nqs-accent: #3b82f6; --nqs-accent-hover: #60a5fa; --nqs-accent-light: #1e3a8a; --nqs-success: #10b981; --nqs-success-light: #064e3b; --nqs-warning: #f59e0b; --nqs-warning-light: #78350f; --nqs-danger: #ef4444; --nqs-danger-light: #7f1d1d; --nqs-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); --nqs-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); --nqs-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.3); } } /* === 基础样式和动画 === */ #NQS_GLOBAL_CONTAINER * { box-sizing: border-box; font-family: var(--nqs-font-family); } @keyframes nqs-spin { from { transform: translateY(-50%) rotate(0deg); } to { transform: translateY(-50%) rotate(360deg); } } @keyframes nqs-fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes nqs-slideIn { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } /* === 模态弹窗样式 === */ #NQS_GLOBAL_CONTAINER .nqs-overlay{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); display: flex; justify-content: center; align-items: center; opacity: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 100000; } #NQS_GLOBAL_CONTAINER .nqs-overlay.visible{ opacity: 1; } #NQS_GLOBAL_CONTAINER .nqs-panel{ width: 100%; max-width: 720px; background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-xl); box-shadow: var(--nqs-shadow-xl); display: flex; flex-direction: column; max-height: 90vh; margin: 1rem; transform: scale(0.95) translateY(20px); transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-overlay.visible .nqs-panel{ transform: scale(1) translateY(0); } /* === 面板内容样式 === */ #NQS_GLOBAL_CONTAINER .nqs-header{ padding: 1.5rem 2rem; border-bottom: 1px solid var(--nqs-border); flex-shrink: 0; background: var(--nqs-bg-subtle); border-top-left-radius: var(--nqs-radius-xl); border-top-right-radius: var(--nqs-radius-xl); } #NQS_GLOBAL_CONTAINER .nqs-header h1{ font-size: 1.5rem; margin: 0; color: var(--nqs-text-primary); font-weight: 700; letter-spacing: -0.025em; } #NQS_GLOBAL_CONTAINER .nqs-header p{ font-size: 0.9rem; margin: 0.5rem 0 0 0; color: var(--nqs-text-secondary); line-height: 1.5; } #NQS_GLOBAL_CONTAINER .nqs-body{ padding: 2rem; overflow-y: auto; flex-grow: 1; min-height: 0; background: var(--nqs-bg); } /* === 日志查看器布局样式 === */ #NQS_GLOBAL_CONTAINER .nqs-panel--log-viewer .nqs-body { padding: 0; display: flex; flex-direction: column; height: 75vh; min-height: 600px; max-height: 800px; background: var(--nqs-bg-subtle); width: 100%; overflow: hidden; } /* === 日志统计卡片样式 === */ #NQS_GLOBAL_CONTAINER .nqs-log-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; padding: 20px; background: var(--nqs-bg); border-bottom: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-stat-card { background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); padding: 20px; display: flex; align-items: center; gap: 16px; transition: all 0.3s ease; position: relative; overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-stat-card.nqs-stat-success::before { background: linear-gradient(90deg, #34C759, #30D158); } #NQS_GLOBAL_CONTAINER .nqs-stat-card.nqs-stat-error::before { background: linear-gradient(90deg, #FF3B30, #FF453A); } #NQS_GLOBAL_CONTAINER .nqs-stat-card.nqs-stat-debug::before { background: linear-gradient(90deg, #8E8E93, #AEAEB2); } #NQS_GLOBAL_CONTAINER .nqs-stat-card:hover { transform: translateY(-2px); box-shadow: var(--nqs-shadow-lg); } #NQS_GLOBAL_CONTAINER .nqs-stat-icon { font-size: 28px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; background: var(--nqs-bg-subtle); border-radius: 12px; flex-shrink: 0; } #NQS_GLOBAL_CONTAINER .nqs-stat-content { flex: 1; } #NQS_GLOBAL_CONTAINER .nqs-stat-number { font-size: 24px; font-weight: 700; color: var(--nqs-text-primary); line-height: 1.2; } #NQS_GLOBAL_CONTAINER .nqs-stat-label { font-size: 14px; color: var(--nqs-text-secondary); margin-top: 4px; } /* === 过滤栏样式 === */ #NQS_GLOBAL_CONTAINER .nqs-log-filter-bar { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; flex-shrink: 0; background: var(--nqs-bg); border-bottom: 1px solid var(--nqs-border); gap: 20px; } #NQS_GLOBAL_CONTAINER .nqs-filter-left { display: flex; align-items: center; gap: 16px; } #NQS_GLOBAL_CONTAINER .nqs-filter-right { display: flex; align-items: center; gap: 16px; } #NQS_GLOBAL_CONTAINER .nqs-search-box { position: relative; display: flex; align-items: center; } #NQS_GLOBAL_CONTAINER .nqs-search-input { padding: 8px 12px 8px 36px; border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); background: var(--nqs-bg-subtle); color: var(--nqs-text-primary); font-size: 14px; width: 200px; transition: all 0.2s ease; } #NQS_GLOBAL_CONTAINER .nqs-search-input:focus { outline: none; border-color: var(--nqs-accent); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } #NQS_GLOBAL_CONTAINER .nqs-search-icon { position: absolute; left: 12px; font-size: 14px; color: var(--nqs-text-secondary); } #NQS_GLOBAL_CONTAINER .nqs-toggle-group { display: flex; align-items: center; } #NQS_GLOBAL_CONTAINER .nqs-toggle-label { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-toggle-input { display: none; } #NQS_GLOBAL_CONTAINER .nqs-toggle-slider { width: 40px; height: 20px; background: var(--nqs-border); border-radius: 10px; position: relative; transition: all 0.3s ease; } #NQS_GLOBAL_CONTAINER .nqs-toggle-slider::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: white; border-radius: 50%; transition: all 0.3s ease; } #NQS_GLOBAL_CONTAINER .nqs-toggle-input:checked + .nqs-toggle-slider { background: var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-toggle-input:checked + .nqs-toggle-slider::after { transform: translateX(20px); } #NQS_GLOBAL_CONTAINER .nqs-toggle-text { font-weight: 500; } /* === 表格容器样式 === */ #NQS_GLOBAL_CONTAINER .nqs-table-container { overflow-y: auto; flex-grow: 1; padding: 0; width: 100%; background: var(--nqs-bg); min-height: 0; } /* === 日志卡片样式 === */ #NQS_GLOBAL_CONTAINER .nqs-log-cards { display: flex; flex-direction: column; gap: 12px; padding: 16px; } #NQS_GLOBAL_CONTAINER .nqs-log-card { background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); padding: 16px; transition: all 0.3s ease; position: relative; overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-log-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-log-card--info::before { background: linear-gradient(90deg, #34C759, #30D158); } #NQS_GLOBAL_CONTAINER .nqs-log-card--error::before { background: linear-gradient(90deg, #FF3B30, #FF453A); } #NQS_GLOBAL_CONTAINER .nqs-log-card--debug::before { background: linear-gradient(90deg, #8E8E93, #AEAEB2); } #NQS_GLOBAL_CONTAINER .nqs-log-card--warn::before { background: linear-gradient(90deg, #FF9500, #FF9F0A); } #NQS_GLOBAL_CONTAINER .nqs-log-card:hover { transform: translateY(-2px); box-shadow: var(--nqs-shadow-lg); border-color: var(--nqs-accent-light); } #NQS_GLOBAL_CONTAINER .nqs-log-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } #NQS_GLOBAL_CONTAINER .nqs-log-level-badge { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: var(--nqs-radius); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } #NQS_GLOBAL_CONTAINER .nqs-log-level-icon { font-size: 14px; } #NQS_GLOBAL_CONTAINER .nqs-log-time { font-size: 12px; color: var(--nqs-text-secondary); font-weight: 500; } #NQS_GLOBAL_CONTAINER .nqs-log-card-body { margin-bottom: 12px; } #NQS_GLOBAL_CONTAINER .nqs-log-message { color: var(--nqs-text-primary); line-height: 1.5; font-size: 14px; word-break: break-word; } #NQS_GLOBAL_CONTAINER .nqs-log-card-footer { display: flex; justify-content: flex-end; align-items: center; padding-top: 8px; border-top: 1px solid var(--nqs-border-light); } #NQS_GLOBAL_CONTAINER .nqs-log-detail-btn { background: var(--nqs-accent-light); color: var(--nqs-accent); border: 1px solid var(--nqs-accent); padding: 6px 12px; border-radius: var(--nqs-radius); cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.2s ease; display: flex; align-items: center; gap: 4px; } #NQS_GLOBAL_CONTAINER .nqs-log-detail-btn:hover { background: var(--nqs-accent); color: white; transform: scale(1.05); } #NQS_GLOBAL_CONTAINER .nqs-no-details { color: var(--nqs-text-tertiary); font-size: 12px; font-style: italic; } /* === 空状态样式 === */ #NQS_GLOBAL_CONTAINER .nqs-empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; text-align: center; } #NQS_GLOBAL_CONTAINER .nqs-empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; } #NQS_GLOBAL_CONTAINER .nqs-empty-title { font-size: 18px; font-weight: 600; color: var(--nqs-text-primary); margin-bottom: 8px; } #NQS_GLOBAL_CONTAINER .nqs-empty-message { font-size: 14px; color: var(--nqs-text-secondary); line-height: 1.5; } /* === 底部操作栏样式 === */ #NQS_GLOBAL_CONTAINER .nqs-log-footer-actions { display: flex; justify-content: space-between; align-items: center; width: 100%; } #NQS_GLOBAL_CONTAINER .nqs-log-footer-left { display: flex; gap: 12px; align-items: center; } #NQS_GLOBAL_CONTAINER .nqs-log-footer-right { display: flex; align-items: center; } #NQS_GLOBAL_CONTAINER .nqs-log-footer-actions .nqs-button { display: flex; align-items: center; gap: 8px; } #NQS_GLOBAL_CONTAINER .nqs-log-footer-actions .nqs-button span:first-child { font-size: 16px; } /* === 日志详情弹窗样式 === */ #NQS_GLOBAL_CONTAINER .nqs-panel--log-detail .nqs-body { padding: 0; max-height: 70vh; overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-log-detail-content { height: 100%; display: flex; flex-direction: column; } #NQS_GLOBAL_CONTAINER .nqs-json-viewer { flex: 1; overflow: auto; background: var(--nqs-bg-subtle); border-radius: var(--nqs-radius); border: 1px solid var(--nqs-border); margin: 16px; } #NQS_GLOBAL_CONTAINER .nqs-json-code { margin: 0; padding: 20px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; font-size: 13px; line-height: 1.6; color: var(--nqs-text-primary); background: transparent; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; } #NQS_GLOBAL_CONTAINER .nqs-log-detail-footer { display: flex; justify-content: space-between; align-items: center; width: 100%; } #NQS_GLOBAL_CONTAINER .nqs-log-detail-footer .nqs-button { display: flex; align-items: center; gap: 8px; } #NQS_GLOBAL_CONTAINER .nqs-log-detail-footer .nqs-button span:first-child { font-size: 16px; } #NQS_GLOBAL_CONTAINER .nqs-filter-group { display: flex; gap: 8px; align-items: center; } /* === 现代化过滤按钮样式 === */ #NQS_GLOBAL_CONTAINER .nqs-filter-btn { background: var(--nqs-bg-subtle); border: 1px solid var(--nqs-border); color: var(--nqs-text-primary); padding: 10px 16px; border-radius: var(--nqs-radius-lg); cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.3s ease; position: relative; overflow: hidden; display: flex; align-items: center; gap: 8px; } #NQS_GLOBAL_CONTAINER .nqs-filter-icon { font-size: 16px; } #NQS_GLOBAL_CONTAINER .nqs-filter-btn:hover { background: var(--nqs-bg-hover); border-color: var(--nqs-accent); transform: translateY(-1px); box-shadow: var(--nqs-shadow); } #NQS_GLOBAL_CONTAINER .nqs-filter-btn.active { background: var(--nqs-accent); color: white; border-color: var(--nqs-accent); box-shadow: var(--nqs-shadow-lg); } #NQS_GLOBAL_CONTAINER .nqs-filter-btn.active:hover { background: var(--nqs-accent-hover); transform: translateY(-1px); } /* === 详情按钮样式 === */ #NQS_GLOBAL_CONTAINER .nqs-detail-btn { background: var(--nqs-accent-light); color: var(--nqs-accent); border: 1px solid var(--nqs-accent); padding: 6px 12px; border-radius: var(--nqs-radius); cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.2s ease; } #NQS_GLOBAL_CONTAINER .nqs-detail-btn:hover { background: var(--nqs-accent); color: white; transform: scale(1.05); } #NQS_GLOBAL_CONTAINER .nqs-no-detail { color: var(--nqs-text-tertiary); font-size: 12px; } /* === 底部操作栏样式 === */ #NQS_GLOBAL_CONTAINER .nqs-footer{ padding: 1.5rem 2rem; border-top: 1px solid var(--nqs-border); display: flex; align-items: center; gap: 1rem; background: var(--nqs-bg-subtle); border-bottom-left-radius: var(--nqs-radius-xl); border-bottom-right-radius: var(--nqs-radius-xl); flex-shrink: 0; } /* === 统一按钮样式 === */ #NQS_GLOBAL_CONTAINER .nqs-button{ display: inline-flex; align-items: center; justify-content: center; text-align: center; border-radius: var(--nqs-radius); padding: 0.75rem 1.5rem; font-size: 0.95rem; font-weight: 600; cursor: pointer; border: 1px solid transparent; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); text-decoration: none; white-space: nowrap; position: relative; overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-button:hover{ transform: translateY(-2px); box-shadow: var(--nqs-shadow-lg); } #NQS_GLOBAL_CONTAINER .nqs-button:active{ transform: translateY(0); box-shadow: var(--nqs-shadow); } #NQS_GLOBAL_CONTAINER .nqs-button-primary{ background: var(--nqs-accent); color: white; border-color: var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-button-primary:hover{ background: var(--nqs-accent-hover); border-color: var(--nqs-accent-hover); } #NQS_GLOBAL_CONTAINER .nqs-button-secondary{ background: var(--nqs-bg-subtle); border-color: var(--nqs-border); color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-button-secondary:hover{ background: var(--nqs-bg-hover); border-color: var(--nqs-border-hover); } #NQS_GLOBAL_CONTAINER .nqs-button-danger{ background: var(--nqs-danger); color: white; border-color: var(--nqs-danger); } #NQS_GLOBAL_CONTAINER .nqs-button-danger:hover{ background: #dc2626; border-color: #dc2626; } #NQS_GLOBAL_CONTAINER .nqs-button-text{ background: none; border: none; padding: 0.5rem 1rem; color: var(--nqs-accent); font-weight: 500; cursor: pointer; text-align: left; line-height: 1.5; font-size: 0.9rem; border-radius: var(--nqs-radius); transition: all 0.2s ease; } #NQS_GLOBAL_CONTAINER .nqs-button-text:hover{ background: var(--nqs-accent-light); color: var(--nqs-accent-hover); transform: none; box-shadow: none; } /* === iOS风格日志表格样式 === */ #NQS_GLOBAL_CONTAINER .nqs-log-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 15px; background: rgba(255, 255, 255, 0.95); margin: 0; letter-spacing: -0.24px; } /* 暗色主题适配 */ #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table { background: rgba(28, 28, 30, 0.95); } #NQS_GLOBAL_CONTAINER .nqs-log-table th { text-align: left; padding: 16px 20px; font-weight: 600; color: var(--nqs-text-secondary); background: rgba(242, 242, 247, 0.95); position: sticky; top: 0; z-index: 5; font-size: 13px; text-transform: uppercase; letter-spacing: 0.6px; border-bottom: 1px solid rgba(0, 0, 0, 0.08); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); } /* 暗色主题适配 */ #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table th { background: rgba(44, 44, 46, 0.95); border-bottom-color: rgba(255, 255, 255, 0.12); } #NQS_GLOBAL_CONTAINER .nqs-log-table td { padding: 16px 20px; vertical-align: middle; color: var(--nqs-text-primary); background: rgba(255, 255, 255, 0.95); border-bottom: 1px solid rgba(0, 0, 0, 0.06); position: relative; font-size: 15px; letter-spacing: -0.24px; } /* 暗色主题适配 */ #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table td { background: rgba(28, 28, 30, 0.95); border-bottom-color: rgba(255, 255, 255, 0.08); } /* 基础行样式 */ #NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } #NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 0; background: linear-gradient(90deg, var(--nqs-accent), transparent); transition: width 0.3s ease; z-index: 1; } #NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:nth-child(even) td { background: rgba(242, 242, 247, 0.5); } #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table tbody tr:nth-child(even) td { background: rgba(44, 44, 46, 0.5); } #NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:hover { box-shadow: 0 4px 20px rgba(0, 122, 255, 0.08), 0 1px 3px rgba(0, 0, 0, 0.1); transform: scale(1.005); } #NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:hover::before { width: 3px; } #NQS_GLOBAL_CONTAINER .nqs-log-table tbody tr:hover td { background: rgba(0, 122, 255, 0.04) !important; position: relative; z-index: 2; } #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-table tbody tr:hover td { background: rgba(0, 122, 255, 0.08) !important; } /* iOS风格日志行状态样式 */ #NQS_GLOBAL_CONTAINER .nqs-log-row--info td { border-left: 3px solid #007AFF; background: rgba(0, 122, 255, 0.04); } #NQS_GLOBAL_CONTAINER .nqs-log-row--info:nth-child(even) td { background: rgba(0, 122, 255, 0.06); } #NQS_GLOBAL_CONTAINER .nqs-log-row--info:hover td { background: rgba(0, 122, 255, 0.1) !important; } #NQS_GLOBAL_CONTAINER .nqs-log-row--error td { border-left: 3px solid #FF3B30; background: rgba(255, 59, 48, 0.04); } #NQS_GLOBAL_CONTAINER .nqs-log-row--error:nth-child(even) td { background: rgba(255, 59, 48, 0.06); } #NQS_GLOBAL_CONTAINER .nqs-log-row--error:hover td { background: rgba(255, 59, 48, 0.1) !important; } #NQS_GLOBAL_CONTAINER .nqs-log-row--debug td { border-left: 3px solid #8E8E93; background: rgba(142, 142, 147, 0.04); } #NQS_GLOBAL_CONTAINER .nqs-log-row--debug:nth-child(even) td { background: rgba(142, 142, 147, 0.06); } #NQS_GLOBAL_CONTAINER .nqs-log-row--debug:hover td { background: rgba(142, 142, 147, 0.1) !important; } /* iOS风格单元格内容样式 */ #NQS_GLOBAL_CONTAINER .nqs-log-cell-time { white-space: nowrap; color: var(--nqs-text-secondary); font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; font-size: 13px; font-weight: 500; min-width: 140px; letter-spacing: -0.08px; opacity: 0.8; } #NQS_GLOBAL_CONTAINER .nqs-log-cell-level { min-width: 80px; } #NQS_GLOBAL_CONTAINER .nqs-log-cell-message { word-break: break-word; white-space: normal; line-height: 1.5; max-width: 400px; letter-spacing: -0.24px; } #NQS_GLOBAL_CONTAINER .nqs-log-cell-actions { min-width: 80px; text-align: center; } #NQS_GLOBAL_CONTAINER .nqs-log-cell-actions a { color: #007AFF; text-decoration: none; font-weight: 500; padding: 6px 12px; border-radius: 16px; transition: all 0.2s ease; font-size: 13px; letter-spacing: -0.08px; background: rgba(0, 122, 255, 0.1); } #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-cell-actions a { color: #0A84FF; background: rgba(10, 132, 255, 0.15); } #NQS_GLOBAL_CONTAINER .nqs-log-cell-actions a:hover { background: rgba(0, 122, 255, 0.2); color: #005BB5; transform: scale(1.05); } #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-log-cell-actions a:hover { background: rgba(10, 132, 255, 0.25); color: #409CFF; } /* === 日志标签样式 === */ #NQS_GLOBAL_CONTAINER .nqs-log-tag { display: inline-flex; align-items: center; justify-content: center; padding: 0.4rem 0.8rem; border-radius: var(--nqs-radius); font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; line-height: 1; border: none; min-width: 60px; text-align: center; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: all 0.2s ease; } #NQS_GLOBAL_CONTAINER .nqs-log-tag.info { background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; } #NQS_GLOBAL_CONTAINER .nqs-log-tag.error { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; } #NQS_GLOBAL_CONTAINER .nqs-log-tag.debug { background: linear-gradient(135deg, #6b7280, #4b5563); color: white; } #NQS_GLOBAL_CONTAINER .nqs-log-tag.legacy { background: linear-gradient(135deg, #f59e0b, #d97706); color: white; } #NQS_GLOBAL_CONTAINER .nqs-json-viewer { background-color: var(--nqs-bg-subtle); color: var(--nqs-text-primary); font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 14px; padding: 1.5rem; } #NQS_GLOBAL_CONTAINER .json-key { color: #9cdcfe; } #NQS_GLOBAL_CONTAINER .json-string { color: #ce9178; } #NQS_GLOBAL_CONTAINER .json-number { color: #b5cea8; } #NQS_GLOBAL_CONTAINER .json-boolean { color: #569cd6; } #NQS_GLOBAL_CONTAINER .json-null { color: #c586c0; } /* 保留旧的标签组样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER .nqs-label-group{padding-top:.5rem;display:flex;flex-direction:column;} #NQS_GLOBAL_CONTAINER .nqs-label-group label{display:block;font-weight:500;color:var(--nqs-text-primary); margin: 0; padding: 0;} #NQS_GLOBAL_CONTAINER p.nqs-p, #NQS_GLOBAL_CONTAINER .nqs-label-group p {font-size:.85rem;color:var(--nqs-text-secondary);margin:.25rem 0 0 0; line-height: 1.6;} /* 新的设置标签组样式 */ #NQS_GLOBAL_CONTAINER .nqs-toggle-switch{display:flex;align-items:center;justify-content:flex-end} #NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch{position:relative;display:inline-block;width:50px;height:28px} #NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch input{opacity:0;width:0;height:0} #NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#ccc;transition:.4s;border-radius:28px} #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-toggle-switch .nqs-slider { background-color: var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-slider:before{position:absolute;content:"";height:20px;width:20px;left:4px;bottom:4px;background-color:#fff;transition:.4s;border-radius:50%} #NQS_GLOBAL_CONTAINER .nqs-toggle-switch input:checked+.nqs-slider{background-color:var(--nqs-accent)} #NQS_GLOBAL_CONTAINER .nqs-toggle-switch input:checked+.nqs-slider:before{transform:translateX(22px)} /* === 表单元素样式 === */ #NQS_GLOBAL_CONTAINER .nqs-input, #NQS_GLOBAL_CONTAINER .nqs-textarea{ width: 100%; padding: 0.875rem 1rem; font-size: 0.95rem; font-family: inherit; color: var(--nqs-text-primary); background-color: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); line-height: 1.5; } #NQS_GLOBAL_CONTAINER .nqs-input:hover, #NQS_GLOBAL_CONTAINER .nqs-textarea:hover{ border-color: var(--nqs-border-hover); background-color: var(--nqs-bg-hover); } #NQS_GLOBAL_CONTAINER .nqs-input:focus, #NQS_GLOBAL_CONTAINER .nqs-textarea:focus{ outline: 0; border-color: var(--nqs-accent); background-color: var(--nqs-bg); box-shadow: 0 0 0 3px var(--nqs-accent-light); transform: translateY(-1px); } #NQS_GLOBAL_CONTAINER .nqs-input::placeholder, #NQS_GLOBAL_CONTAINER .nqs-textarea::placeholder{ color: var(--nqs-text-tertiary); } #NQS_GLOBAL_CONTAINER .nqs-section{margin-bottom:2.5rem}.nqs-section:last-child{margin-bottom:0} #NQS_GLOBAL_CONTAINER .nqs-section-title{font-size:1.25rem;font-weight:600;color:var(--nqs-text-primary);border-bottom:1px solid var(--nqs-border);padding-bottom:.75rem;margin-bottom:1.5rem} #NQS_GLOBAL_CONTAINER .nqs-subsection-title{font-size:1rem;font-weight:600;color:var(--nqs-text-primary);margin-top:1.5rem;margin-bottom:0.5rem;} #NQS_GLOBAL_CONTAINER .nqs-subsection-p{font-size:.85rem;color:var(--nqs-text-secondary);margin-top:0;margin-bottom:1rem;} /* 保留旧的字段样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER .nqs-field{display:grid;grid-template-columns:1fr 1.5fr;gap:1rem 2rem;align-items:flex-start;margin-bottom:1.5rem} #NQS_GLOBAL_CONTAINER .nqs-field-full{grid-template-columns:1fr} /* 新的设置字段样式 */ /* 保留旧的分类管理样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER .nqs-category-manager{display: flex; margin-top: 0.5rem;} #NQS_GLOBAL_CONTAINER .nqs-category-manager input{border-top-right-radius:0;border-bottom-right-radius:0} #NQS_GLOBAL_CONTAINER .nqs-category-manager button{border-top-left-radius:0;border-bottom-left-radius:0} /* 新的分类管理样式 */ /* 保留旧的模型选择器样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER .nqs-model-selector-wrapper { position: relative; } #NQS_GLOBAL_CONTAINER .nqs-model-selector-wrapper .nqs-input { padding-right: 44px !important; } /* 新的模型选择器样式 */ /* 保留旧的图标按钮样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER .nqs-icon-button { position: absolute; top: 50%; right: 5px; transform: translateY(-50%); width: 34px; height: 34px; border: none; background: transparent; color: var(--nqs-text-secondary); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: color 0.2s, background-color 0.2s; } #NQS_GLOBAL_CONTAINER .nqs-icon-button:hover { background-color: var(--nqs-bg-subtle); color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-icon-button:disabled { cursor: not-allowed; color: var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-icon-button.is-loading svg { animation: nqs-spin 1s linear infinite; } /* 新的获取模型按钮样式 */ /* 保留旧的自定义下拉框样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER .nqs-custom-dropdown { position: absolute; top: 100%; left: 0; right: 0; z-index: 10; background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin-top: 0.5rem; max-height: 200px; overflow-y: auto; opacity: 0; transform: translateY(-10px); transition: opacity .2s ease, transform .2s ease; pointer-events: none; } #NQS_GLOBAL_CONTAINER .nqs-custom-dropdown.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; } /* 新的模型下拉框样式 */ /* 保留旧的下拉框项目样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER .nqs-dropdown-item { padding: 0.75rem 1rem; cursor: pointer; transition: background-color .2s; color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-dropdown-item:hover, #NQS_GLOBAL_CONTAINER .nqs-dropdown-item.is-active { background-color: var(--nqs-bg-subtle); } /* 新的下拉框项目样式 */ #NQS_GLOBAL_CONTAINER .nqs-textarea{min-height:150px;resize:vertical} /* 保留旧的分类列表样式以兼容其他功能 */ #NQS_GLOBAL_CONTAINER #nqs-category-list{list-style:none;padding:0;margin-top:1rem;display:flex;flex-wrap:wrap;gap:.75rem} #NQS_GLOBAL_CONTAINER #nqs-category-list li{display:flex;align-items:center;background:var(--nqs-bg-subtle);color:var(--nqs-text-primary);padding:.5rem 1rem;border-radius:8px;font-size:.9rem;font-weight:500; line-height:1; border: 1px solid var(--nqs-border);} #NQS_GLOBAL_CONTAINER #nqs-category-list .delete-cat{margin-left:.75rem;cursor:pointer;color:var(--nqs-text-secondary);font-weight:700;font-size:1.3em;line-height:1;transition:color .2s ease} #NQS_GLOBAL_CONTAINER #nqs-category-list .delete-cat:hover{color:var(--nqs-danger)} /* 新的分类列表样式 */ #NQS_GLOBAL_CONTAINER .nqs-button-text{background:none;border:none;padding:4px 8px;color:var(--nqs-accent);font-weight:500;cursor:pointer;text-align:left;line-height:1.5; font-size: 0.8rem;} #NQS_GLOBAL_CONTAINER .nqs-button-text:hover{text-decoration:underline} #NQS_GLOBAL_CONTAINER #nqs-selector-list {display: flex; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1.5rem;} #NQS_GLOBAL_CONTAINER #nqs-selector-list .nqs-button-secondary { background: var(--nqs-bg-subtle); color: var(--nqs-text-primary); border-color: var(--nqs-border); } /* === FAB (悬浮球) 样式 === */ #NQS_GLOBAL_CONTAINER #nqs-fab-container { position: fixed; z-index: 99999; width: 48px; height: 48px; transition: transform 0.3s ease-out; } #NQS_GLOBAL_CONTAINER #nqs-fab-trigger { width: 100%; height: 100%; background: var(--nqs-accent); color: white; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 22px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); cursor: grab; transition: transform 0.2s ease, box-shadow 0.2s ease; position: relative; z-index: 2; } #NQS_GLOBAL_CONTAINER #nqs-fab-trigger:active { cursor: grabbing; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); } #NQS_GLOBAL_CONTAINER #nqs-fab-container.is-dragging #nqs-fab-trigger { transform: scale(1.1); } #NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded #nqs-fab-trigger { transform: rotate(90deg); } #NQS_GLOBAL_CONTAINER .nqs-fab-options { position: absolute; bottom: calc(100% + 15px); right: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; transition: opacity 0.2s ease, transform 0.2s ease; opacity: 0; transform: translateY(10px); pointer-events: none; z-index: 1; } #NQS_GLOBAL_CONTAINER #nqs-fab-container.align-left .nqs-fab-options { right: auto; left: 0; align-items: flex-start; } #NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded .nqs-fab-options { opacity: 1; transform: translateY(0); pointer-events: auto; } #NQS_GLOBAL_CONTAINER .nqs-fab-action-btn { padding: 8px 16px; color: white; border-radius: 20px; font-size: 14px; font-weight: 600; box-shadow: 0 6px 15px rgba(0,0,0,0.15); transition: all .2s ease-out; white-space: nowrap; cursor: pointer; } #NQS_GLOBAL_CONTAINER #nqs-save-button { background-color: rgba(0, 122, 255, 0.95); } #NQS_GLOBAL_CONTAINER #nqs-read-later-button { background-color: rgba(90, 90, 90, 0.95); } #NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded .nqs-fab-options #nqs-read-later-button { transition-delay: 0.05s; } #NQS_GLOBAL_CONTAINER #nqs-fab-container.is-expanded .nqs-fab-options #nqs-save-button { transition-delay: 0.1s; } #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-right { transform: translateX(30%); } #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-right:hover, #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-right.is-expanded { transform: translateX(0); } #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-left { transform: translateX(-30%); } #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-left:hover, #NQS_GLOBAL_CONTAINER #nqs-fab-container.snapped-left.is-expanded { transform: translateX(0); } /* === 顶部进度条样式 === */ #NQS_GLOBAL_CONTAINER .nqs-top-progress { position: fixed; top: 0; left: 0; right: 0; z-index: 100001; background: var(--nqs-bg); border-bottom: 1px solid var(--nqs-border); transform: translateY(-100%); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); } #NQS_GLOBAL_CONTAINER .nqs-top-progress.visible { transform: translateY(0); } #NQS_GLOBAL_CONTAINER .nqs-progress-bar { height: 3px; background: var(--nqs-border); overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-progress-fill { height: 100%; background: linear-gradient(90deg, var(--nqs-accent), var(--nqs-accent-hover)); width: 0%; transition: width 0.3s ease; position: relative; } #NQS_GLOBAL_CONTAINER .nqs-progress-fill::after { content: ''; position: absolute; top: 0; right: 0; bottom: 0; width: 20px; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3)); animation: nqs-shimmer 1.5s infinite; } @keyframes nqs-shimmer { 0% { transform: translateX(-20px); } 100% { transform: translateX(20px); } } #NQS_GLOBAL_CONTAINER .nqs-progress-status { padding: 0.75rem 1.5rem; display: flex; align-items: center; gap: 0.75rem; } #NQS_GLOBAL_CONTAINER .nqs-progress-text { color: var(--nqs-text-secondary); font-size: 0.85rem; font-weight: 500; letter-spacing: -0.025em; } #NQS_GLOBAL_CONTAINER .nqs-progress-status::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--nqs-accent); animation: nqs-pulse 2s infinite; } @keyframes nqs-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.2); } } /* === 浮动通知样式 === */ #NQS_GLOBAL_CONTAINER .nqs-toast-container { position: fixed; top: 20px; right: 20px; z-index: 100002; pointer-events: none; display: flex; flex-direction: column; gap: 12px; } #NQS_GLOBAL_CONTAINER .nqs-toast { background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); box-shadow: var(--nqs-shadow-lg); padding: 1rem 1.5rem; min-width: 320px; max-width: 400px; pointer-events: auto; transform: translateX(100%); opacity: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-left: 4px solid var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-toast.visible { transform: translateX(0); opacity: 1; } #NQS_GLOBAL_CONTAINER .nqs-toast.success { border-left-color: var(--nqs-success); } #NQS_GLOBAL_CONTAINER .nqs-toast.error { border-left-color: var(--nqs-danger); } #NQS_GLOBAL_CONTAINER .nqs-toast.warning { border-left-color: var(--nqs-warning); } #NQS_GLOBAL_CONTAINER .nqs-toast-content { display: flex; align-items: flex-start; gap: 12px; } #NQS_GLOBAL_CONTAINER .nqs-toast-icon { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; color: white; flex-shrink: 0; margin-top: 2px; } #NQS_GLOBAL_CONTAINER .nqs-toast.success .nqs-toast-icon { background: var(--nqs-success); } #NQS_GLOBAL_CONTAINER .nqs-toast.error .nqs-toast-icon { background: var(--nqs-danger); } #NQS_GLOBAL_CONTAINER .nqs-toast.warning .nqs-toast-icon { background: var(--nqs-warning); } #NQS_GLOBAL_CONTAINER .nqs-toast.info .nqs-toast-icon { background: var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-toast-text { flex: 1; min-width: 0; } #NQS_GLOBAL_CONTAINER .nqs-toast-title { font-weight: 600; color: var(--nqs-text-primary); margin: 0 0 4px 0; font-size: 0.95rem; line-height: 1.4; } #NQS_GLOBAL_CONTAINER .nqs-toast-message { color: var(--nqs-text-secondary); margin: 0; font-size: 0.85rem; line-height: 1.5; } #NQS_GLOBAL_CONTAINER .nqs-toast-close { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; border: none; background: none; color: var(--nqs-text-tertiary); cursor: pointer; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; font-size: 14px; } #NQS_GLOBAL_CONTAINER .nqs-toast-close:hover { background: var(--nqs-bg-hover); color: var(--nqs-text-primary); } /* iOS风格的加载状态按钮 */ #NQS_GLOBAL_CONTAINER .nqs-button.loading { position: relative; color: transparent !important; } #NQS_GLOBAL_CONTAINER .nqs-button.loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 16px; height: 16px; margin: -8px 0 0 -8px; border: 2px solid rgba(255, 255, 255, 0.3); border-top-color: white; border-radius: 50%; animation: nqs-spin 1s linear infinite; } /* === 现代化设置界面样式 === */ #NQS_GLOBAL_CONTAINER .nqs-panel--settings .nqs-body { padding: 0; background: var(--nqs-bg-subtle); } #NQS_GLOBAL_CONTAINER .nqs-settings-container { padding: 24px; max-height: 70vh; overflow-y: auto; } /* === 设置侧边导航布局与样式 === */ #NQS_GLOBAL_CONTAINER .nqs-settings-layout { display: grid; grid-template-columns: 220px 1fr; gap: 16px; padding: 24px; height: 70vh; box-sizing: border-box; } #NQS_GLOBAL_CONTAINER .nqs-settings-sidebar { align-self: start; position: sticky; top: 0; background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-settings-nav { display: flex; flex-direction: column; } #NQS_GLOBAL_CONTAINER .nqs-nav-item { display: flex; align-items: center; gap: 10px; padding: 12px 14px; text-decoration: none; color: var(--nqs-text-secondary); border-left: 3px solid transparent; transition: background .2s ease, color .2s ease, border-color .2s ease; min-height: 40px; /* 保持各项高度一致,避免“AI 配置”显得突兀 */ line-height: 1.2; } #NQS_GLOBAL_CONTAINER .nqs-nav-item:hover { background: var(--nqs-bg-subtle); color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-nav-item.active { background: var(--nqs-bg-subtle); color: var(--nqs-accent); border-left-color: var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-nav-icon { width: 20px; height: 20px; /* 统一高度,避免表情/图标导致的垂直错位 */ display: inline-flex; justify-content: center; } /* 导航文字单行省略,避免“AI 配置”等出现换行导致的错位感 */ #NQS_GLOBAL_CONTAINER .nqs-nav-text { display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px; /* 为侧栏保留足够留白,防止过长文本折行 */ } #NQS_GLOBAL_CONTAINER .nqs-settings-content { overflow: auto; } /* 避免内层再产生滚动条,由外层 content 控制滚动 */ #NQS_GLOBAL_CONTAINER .nqs-settings-content .nqs-settings-container { padding: 0; max-height: none; overflow: visible; } @media (max-width: 880px) { #NQS_GLOBAL_CONTAINER .nqs-settings-layout { grid-template-columns: 1fr; height: auto; } #NQS_GLOBAL_CONTAINER .nqs-settings-sidebar { position: static; overflow: auto; } #NQS_GLOBAL_CONTAINER .nqs-settings-nav { flex-direction: row; gap: 8px; padding-bottom: 8px; } #NQS_GLOBAL_CONTAINER .nqs-nav-item { border-left: none; border-bottom: 2px solid transparent; padding: 10px 12px; white-space: nowrap; } #NQS_GLOBAL_CONTAINER .nqs-nav-item.active { border-bottom-color: var(--nqs-accent); } } #NQS_GLOBAL_CONTAINER .nqs-settings-group { background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); margin-bottom: 20px; overflow: hidden; transition: all 0.3s ease; } #NQS_GLOBAL_CONTAINER .nqs-settings-group:hover { border-color: var(--nqs-accent-light); box-shadow: var(--nqs-shadow); } #NQS_GLOBAL_CONTAINER .nqs-settings-group-header { display: flex; align-items: center; gap: 16px; padding: 20px 24px; background: var(--nqs-bg-subtle); border-bottom: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-settings-group-icon { font-size: 24px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; background: var(--nqs-accent-light); border-radius: var(--nqs-radius-lg); color: var(--nqs-accent); flex-shrink: 0; } #NQS_GLOBAL_CONTAINER .nqs-settings-group-content { flex: 1; padding: 24px; } #NQS_GLOBAL_CONTAINER .nqs-settings-group-title { margin: 0 0 8px 0; font-size: 18px; font-weight: 600; color: var(--nqs-text-primary); line-height: 1.3; } #NQS_GLOBAL_CONTAINER .nqs-settings-group-description { margin: 0; font-size: 14px; color: var(--nqs-text-secondary); line-height: 1.5; } /* === 设置字段样式 === */ #NQS_GLOBAL_CONTAINER .nqs-setting-field { display: grid; grid-template-columns: 1fr 1.5fr; gap: 24px; align-items: flex-start; margin-bottom: 24px; padding: 16px 0; border-bottom: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-setting-field:last-child { border-bottom: none; margin-bottom: 0; } #NQS_GLOBAL_CONTAINER .nqs-setting-label-group { display: flex; flex-direction: column; gap: 8px; } #NQS_GLOBAL_CONTAINER .nqs-setting-label-group label { font-weight: 600; color: var(--nqs-text-primary); font-size: 15px; line-height: 1.4; } #NQS_GLOBAL_CONTAINER .nqs-setting-description { font-size: 13px; color: var(--nqs-text-secondary); line-height: 1.5; margin: 0; } #NQS_GLOBAL_CONTAINER .nqs-setting-input-group { display: flex; align-items: center; gap: 12px; } /* === 分类管理样式 === */ #NQS_GLOBAL_CONTAINER .nqs-category-manager-section { margin-top: 16px; } #NQS_GLOBAL_CONTAINER .nqs-category-manager-header { margin-bottom: 16px; } #NQS_GLOBAL_CONTAINER .nqs-category-manager-header h4 { margin: 0 0 8px 0; font-size: 16px; font-weight: 600; color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-category-manager-header p { margin: 0; font-size: 13px; color: var(--nqs-text-secondary); line-height: 1.5; } #NQS_GLOBAL_CONTAINER .nqs-category-input-group { display: flex; gap: 12px; margin-bottom: 16px; } #NQS_GLOBAL_CONTAINER .nqs-category-input { flex: 1; padding: 12px 16px; border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius); font-size: 14px; background: var(--nqs-bg); color: var(--nqs-text-primary); transition: all 0.2s ease; } #NQS_GLOBAL_CONTAINER .nqs-category-input:focus { outline: none; border-color: var(--nqs-accent); box-shadow: 0 0 0 3px var(--nqs-accent-light); } #NQS_GLOBAL_CONTAINER .nqs-category-add-btn { padding: 12px 20px; white-space: nowrap; } #NQS_GLOBAL_CONTAINER .nqs-category-list-container { display: flex; flex-wrap: wrap; gap: 12px; } #NQS_GLOBAL_CONTAINER .nqs-category-item { display: flex; align-items: center; gap: 12px; background: var(--nqs-bg-subtle); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius); padding: 8px 16px; transition: all 0.2s ease; } #NQS_GLOBAL_CONTAINER .nqs-category-item:hover { border-color: var(--nqs-accent); background: var(--nqs-accent-light); } #NQS_GLOBAL_CONTAINER .nqs-category-name { font-size: 14px; font-weight: 500; color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-category-delete-btn { background: none; border: none; color: var(--nqs-text-tertiary); cursor: pointer; padding: 4px; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; font-size: 16px; font-weight: bold; } #NQS_GLOBAL_CONTAINER .nqs-category-delete-btn:hover { background: var(--nqs-danger-light); color: var(--nqs-danger); } /* === AI模型选择器样式 === */ #NQS_GLOBAL_CONTAINER .nqs-model-selector-section { position: relative; } #NQS_GLOBAL_CONTAINER .nqs-model-input-group { position: relative; display: flex; align-items: center; gap: 12px; } #NQS_GLOBAL_CONTAINER .nqs-model-input-group .nqs-input { padding-right: 50px; } #NQS_GLOBAL_CONTAINER .nqs-fetch-models-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 36px; height: 36px; border: none; background: var(--nqs-accent-light); color: var(--nqs-accent); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; } #NQS_GLOBAL_CONTAINER .nqs-fetch-models-btn:hover { background: var(--nqs-accent); color: white; transform: translateY(-50%) scale(1.1); } #NQS_GLOBAL_CONTAINER .nqs-fetch-models-btn.is-loading svg { animation: nqs-spin 1s linear infinite; } #NQS_GLOBAL_CONTAINER .nqs-model-dropdown { position: absolute; top: 100%; left: 0; right: 0; z-index: 10; background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius); box-shadow: var(--nqs-shadow-lg); margin-top: 4px; max-height: 200px; overflow-y: auto; opacity: 0; transform: translateY(-10px); transition: all 0.2s ease; pointer-events: none; } #NQS_GLOBAL_CONTAINER .nqs-model-dropdown.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; } #NQS_GLOBAL_CONTAINER .nqs-dropdown-item { padding: 12px 16px; cursor: pointer; transition: background-color 0.2s; color: var(--nqs-text-primary); border-bottom: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-dropdown-item:last-child { border-bottom: none; } #NQS_GLOBAL_CONTAINER .nqs-dropdown-item:hover, #NQS_GLOBAL_CONTAINER .nqs-dropdown-item.is-active { background: var(--nqs-accent-light); color: var(--nqs-accent); } /* === 提示词头部样式 === */ #NQS_GLOBAL_CONTAINER .nqs-prompt-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; padding: 16px 0; border-bottom: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-prompt-info h4 { margin: 0 0 8px 0; font-size: 16px; font-weight: 600; color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-prompt-info p { margin: 0; font-size: 13px; color: var(--nqs-text-secondary); line-height: 1.5; } /* === 子分组标题样式 === */ #NQS_GLOBAL_CONTAINER .nqs-subsection-header { margin: 24px 0 16px 0; padding: 16px 0; border-bottom: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-subsection-header h4 { margin: 0 0 8px 0; font-size: 16px; font-weight: 600; color: var(--nqs-text-primary); } #NQS_GLOBAL_CONTAINER .nqs-subsection-header p { margin: 0; font-size: 13px; color: var(--nqs-text-secondary); line-height: 1.5; } /* === 选择器样式 === */ #NQS_GLOBAL_CONTAINER .nqs-select { width: 100%; padding: 12px 16px; border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); background: linear-gradient(0deg, rgba(0,0,0,0.02), rgba(255,255,255,0.02)), var(--nqs-bg); color: var(--nqs-text-primary); font-size: 14px; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease, transform .15s ease; min-height: 40px; line-height: 1.2; box-shadow: inset 0 1px 0 rgba(0,0,0,0.03); } #NQS_GLOBAL_CONTAINER .nqs-select:hover { border-color: var(--nqs-border-hover); background-color: var(--nqs-bg-hover); } #NQS_GLOBAL_CONTAINER .nqs-select:focus { outline: none; border-color: var(--nqs-accent); box-shadow: 0 0 0 3px var(--nqs-accent-light); } #NQS_GLOBAL_CONTAINER .nqs-select:focus-visible { outline: none; border-color: var(--nqs-accent); box-shadow: 0 0 0 3px var(--nqs-accent-light); } /* === 现代化智能收藏夹管理样式 === */ #NQS_GLOBAL_CONTAINER .nqs-bookmark-manager { padding: 24px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; background: var(--nqs-bg-subtle); border-radius: var(--nqs-radius-lg); } /* === 主要功能卡片网格 === */ #NQS_GLOBAL_CONTAINER .nqs-feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 32px; } #NQS_GLOBAL_CONTAINER .nqs-feature-card { background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); padding: 24px; display: flex; align-items: center; gap: 20px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-feature-card::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent); transition: left 0.6s ease; } #NQS_GLOBAL_CONTAINER .nqs-feature-card:hover::before { left: 100%; } #NQS_GLOBAL_CONTAINER .nqs-feature-card:hover { border-color: var(--nqs-accent); box-shadow: var(--nqs-shadow-lg); } #NQS_GLOBAL_CONTAINER .nqs-feature-card.nqs-feature-primary { border-color: var(--nqs-accent); background: linear-gradient(135deg, var(--nqs-bg) 0%, rgba(59, 130, 246, 0.02) 100%); } #NQS_GLOBAL_CONTAINER .nqs-feature-icon { font-size: 32px; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; background: var(--nqs-bg-subtle); border-radius: var(--nqs-radius-lg); flex-shrink: 0; } #NQS_GLOBAL_CONTAINER .nqs-feature-primary .nqs-feature-icon { background: linear-gradient(135deg, var(--nqs-accent-light), var(--nqs-accent)); color: white; } #NQS_GLOBAL_CONTAINER .nqs-feature-content { flex: 1; } #NQS_GLOBAL_CONTAINER .nqs-feature-content h4 { margin: 0 0 8px 0; font-size: 18px; font-weight: 600; color: var(--nqs-text-primary); line-height: 1.3; } #NQS_GLOBAL_CONTAINER .nqs-feature-content p { margin: 0; font-size: 14px; color: var(--nqs-text-secondary); line-height: 1.5; } #NQS_GLOBAL_CONTAINER .nqs-feature-badge { position: absolute; top: 12px; right: 12px; background: linear-gradient(135deg, #FF6B6B, #FF8E53); color: white; padding: 4px 12px; border-radius: var(--nqs-radius); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } #NQS_GLOBAL_CONTAINER .nqs-feature-arrow { font-size: 20px; color: var(--nqs-text-tertiary); transition: all 0.3s ease; } #NQS_GLOBAL_CONTAINER .nqs-feature-card:hover .nqs-feature-arrow { color: var(--nqs-accent); transform: translateX(4px); } /* === 快速分类保存区域 === */ #NQS_GLOBAL_CONTAINER .nqs-quick-save-section { margin-bottom: 32px; } #NQS_GLOBAL_CONTAINER .nqs-section-header { margin-bottom: 20px; } #NQS_GLOBAL_CONTAINER .nqs-section-header h3 { margin: 0 0 8px 0; font-size: 20px; font-weight: 600; color: var(--nqs-text-primary); display: flex; align-items: center; gap: 12px; } #NQS_GLOBAL_CONTAINER .nqs-section-header p { margin: 0; font-size: 14px; color: var(--nqs-text-secondary); line-height: 1.5; } #NQS_GLOBAL_CONTAINER .nqs-quick-categories { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; } #NQS_GLOBAL_CONTAINER .nqs-category-btn { background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); padding: 16px 20px; display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; transition: all 0.3s ease; text-align: center; } #NQS_GLOBAL_CONTAINER .nqs-category-btn:hover { border-color: var(--nqs-accent); background: var(--nqs-accent-light); transform: translateY(-2px); box-shadow: var(--nqs-shadow); } #NQS_GLOBAL_CONTAINER .nqs-category-icon { font-size: 24px; display: block; } #NQS_GLOBAL_CONTAINER .nqs-category-name { font-size: 13px; font-weight: 500; color: var(--nqs-text-primary); line-height: 1.2; } /* === 页面信息预览 === */ #NQS_GLOBAL_CONTAINER .nqs-page-preview { margin-bottom: 24px; } #NQS_GLOBAL_CONTAINER .nqs-page-info { background: var(--nqs-bg); border: 1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); padding: 20px; } #NQS_GLOBAL_CONTAINER .nqs-page-title { font-size: 16px; font-weight: 600; color: var(--nqs-text-primary); margin-bottom: 8px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-page-url { font-size: 13px; color: var(--nqs-accent); margin-bottom: 12px; word-break: break-all; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } #NQS_GLOBAL_CONTAINER .nqs-page-meta { display: flex; gap: 20px; flex-wrap: wrap; } #NQS_GLOBAL_CONTAINER .nqs-meta-item { display: flex; align-items: center; gap: 6px; } #NQS_GLOBAL_CONTAINER .nqs-meta-label { font-size: 12px; color: var(--nqs-text-tertiary); font-weight: 500; } #NQS_GLOBAL_CONTAINER .nqs-meta-value { font-size: 12px; color: var(--nqs-text-secondary); background: var(--nqs-bg-subtle); padding: 2px 8px; border-radius: var(--nqs-radius); } /* === 底部操作栏 === */ #NQS_GLOBAL_CONTAINER .nqs-bookmark-footer { display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 20px; } #NQS_GLOBAL_CONTAINER .nqs-footer-left, #NQS_GLOBAL_CONTAINER .nqs-footer-center, #NQS_GLOBAL_CONTAINER .nqs-footer-right { display: flex; align-items: center; } #NQS_GLOBAL_CONTAINER .nqs-footer-center { flex: 1; justify-content: center; } #NQS_GLOBAL_CONTAINER .nqs-bookmark-footer .nqs-button { display: flex; align-items: center; gap: 8px; } #NQS_GLOBAL_CONTAINER .nqs-bookmark-footer .nqs-button span:first-child { font-size: 16px; } /* ===== 追加的设置界面优化样式 ===== */ /* 小屏幕:设置字段改为单列,间距更紧凑 */ @media (max-width: 720px) { #NQS_GLOBAL_CONTAINER .nqs-setting-field { grid-template-columns: 1fr !important; gap: 12px !important; padding: 12px 0 !important; } } /* 设置内容滚动体验优化 */ #NQS_GLOBAL_CONTAINER .nqs-settings-content { overscroll-behavior: contain; } /* 统一滚动条样式(WebKit) */ #NQS_GLOBAL_CONTAINER .nqs-settings-content::-webkit-scrollbar, #NQS_GLOBAL_CONTAINER .nqs-settings-container::-webkit-scrollbar, #NQS_GLOBAL_CONTAINER .nqs-json-viewer::-webkit-scrollbar, #NQS_GLOBAL_CONTAINER .nqs-model-dropdown::-webkit-scrollbar { width: 10px; height: 10px; } #NQS_GLOBAL_CONTAINER .nqs-settings-content::-webkit-scrollbar-thumb, #NQS_GLOBAL_CONTAINER .nqs-settings-container::-webkit-scrollbar-thumb, #NQS_GLOBAL_CONTAINER .nqs-json-viewer::-webkit-scrollbar-thumb, #NQS_GLOBAL_CONTAINER .nqs-model-dropdown::-webkit-scrollbar-thumb { background: var(--nqs-border); border-radius: 8px; border: 2px solid transparent; background-clip: content-box; } #NQS_GLOBAL_CONTAINER .nqs-settings-content::-webkit-scrollbar-thumb:hover, #NQS_GLOBAL_CONTAINER .nqs-settings-container::-webkit-scrollbar-thumb:hover, #NQS_GLOBAL_CONTAINER .nqs-json-viewer::-webkit-scrollbar-thumb:hover, #NQS_GLOBAL_CONTAINER .nqs-model-dropdown::-webkit-scrollbar-thumb:hover { background: var(--nqs-border-hover); } /* 导航项键盘焦点样式(可达性) */ /* 仅键盘导航时显示焦点态,避免鼠标点击后残留视觉高亮 */ #NQS_GLOBAL_CONTAINER .nqs-nav-item:focus-visible { outline: none; color: var(--nqs-accent); box-shadow: 0 0 0 3px var(--nqs-accent-light) inset; } /* 输入/选择器交互反馈更细腻 */ #NQS_GLOBAL_CONTAINER .nqs-input, #NQS_GLOBAL_CONTAINER .nqs-textarea, #NQS_GLOBAL_CONTAINER .nqs-select { transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease, transform .15s ease; } /* 模型下拉样式优化 */ #NQS_GLOBAL_CONTAINER .nqs-model-dropdown { box-shadow: 0 8px 24px rgba(0,0,0,0.12); border-color: var(--nqs-border-hover); } /* 暗色模式下边框对比度提升 */ #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-panel, #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-settings-group, #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-header, #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-footer { border-color: rgba(148, 163, 184, 0.22); } /* 移动端 Toast 布局优化 */ @media (max-width: 520px) { #NQS_GLOBAL_CONTAINER .nqs-toast-container { top: auto; bottom: 12px; right: 12px; left: 12px; } #NQS_GLOBAL_CONTAINER .nqs-toast { width: 100%; min-width: auto; max-width: none; } } /* 尊重“减少动态效果”设置 */ @media (prefers-reduced-motion: reduce) { #NQS_GLOBAL_CONTAINER * { animation: none !important; transition: none !important; } } /* ===== 追加样式(本次优化) ===== */ /* 设置面板:仅保留内部滚动(去掉外层面板滚动条) */ #NQS_GLOBAL_CONTAINER .nqs-panel--settings .nqs-body { overflow: hidden; } /* 提示词文本域专属优化 */ #NQS_GLOBAL_CONTAINER #nqs-ai_prompt, #NQS_GLOBAL_CONTAINER .nqs-prompt-textarea { min-height: 240px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace; font-size: 13.5px; line-height: 1.6; white-space: pre-wrap; tab-size: 2; } #NQS_GLOBAL_CONTAINER .nqs-prompt-meta { width: 100%; display: flex; align-items: center; justify-content: space-between; margin-top: 6px; color: var(--nqs-text-tertiary); font-size: 12px; } /* 左侧导航视觉优化:更柔和的卡片与高亮 */ #NQS_GLOBAL_CONTAINER .nqs-settings-sidebar { padding: 8px; } #NQS_GLOBAL_CONTAINER .nqs-settings-nav { gap: 6px; padding: 4px; } #NQS_GLOBAL_CONTAINER .nqs-nav-item { border-radius: 10px; margin: 2px 4px; } #NQS_GLOBAL_CONTAINER .nqs-nav-item.active { background: var(--nqs-accent-light); color: var(--nqs-accent); } #NQS_GLOBAL_CONTAINER .nqs-nav-text { font-weight: 600; letter-spacing: 0.2px; } /* 右侧内容区域滚动优化 */ #NQS_GLOBAL_CONTAINER .nqs-settings-content { overflow: auto; overscroll-behavior: contain; } /* 全宽字段布局:用于无标签或大块输入(如系统提示词) */ #NQS_GLOBAL_CONTAINER .nqs-setting-field--full { grid-template-columns: 1fr !important; } #NQS_GLOBAL_CONTAINER .nqs-setting-field--full .nqs-setting-label-group { display: none; } /* 统一 Select 下拉样式(与输入框一致) */ #NQS_GLOBAL_CONTAINER .nqs-select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 7l5 5 5-5' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 12px 12px; padding-right: 40px; /* 预留箭头空间 */ font-family: inherit; } #NQS_GLOBAL_CONTAINER[data-theme='dark'] .nqs-select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 7l5 5 5-5' stroke='%23cbd5e1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } #NQS_GLOBAL_CONTAINER .nqs-select:disabled { background-color: var(--nqs-bg-subtle); color: var(--nqs-text-tertiary); cursor: not-allowed; } /* 多选下拉的尺寸与滚动控制 */ #NQS_GLOBAL_CONTAINER .nqs-select[multiple] { min-height: 140px; background-image: none; padding-right: 16px; overflow: auto; } #NQS_GLOBAL_CONTAINER .nqs-select[multiple] option { padding: 6px 8px; } /* 模板占位符提示徽章与预览 */ #NQS_GLOBAL_CONTAINER .nqs-template-badges { display:flex; gap:6px; flex-wrap: wrap; margin-bottom: 6px; } #NQS_GLOBAL_CONTAINER .nqs-badge { display:inline-flex; align-items:center; padding: 2px 6px; border-radius: 999px; font-size: 12px; border: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-badge.ok { background: var(--nqs-success-light); color: var(--nqs-success); border-color: var(--nqs-success); } #NQS_GLOBAL_CONTAINER .nqs-badge.warn { background: var(--nqs-warning-light); color: var(--nqs-warning); border-color: var(--nqs-warning); } #NQS_GLOBAL_CONTAINER .nqs-template-preview { max-height: 120px; overflow: hidden; white-space: pre-wrap; color: var(--nqs-text-secondary); font-size: 12px; border-top: 1px dashed var(--nqs-border); padding-top: 6px; } /* Toggle 开关统一 hover/focus/disabled 细节 */ #NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch:hover .nqs-slider { filter: brightness(0.98); } #NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch:focus-within .nqs-slider { box-shadow: 0 0 0 3px var(--nqs-accent-light); } #NQS_GLOBAL_CONTAINER .nqs-toggle-switch .nqs-switch input:disabled + .nqs-slider { background: var(--nqs-border); cursor: not-allowed; opacity: 0.7; } /* 提示词模板库样式 */ #NQS_GLOBAL_CONTAINER .nqs-prompt-templates { margin-top: 12px; } #NQS_GLOBAL_CONTAINER .nqs-prompt-templates-header { display:flex; justify-content: space-between; align-items:center; margin: 8px 0 10px; } #NQS_GLOBAL_CONTAINER .nqs-template-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; } #NQS_GLOBAL_CONTAINER .nqs-template-card { background: var(--nqs-bg); border:1px solid var(--nqs-border); border-radius: var(--nqs-radius-lg); padding: 12px; display:flex; flex-direction: column; gap:10px; transition: box-shadow .2s, border-color .2s; } #NQS_GLOBAL_CONTAINER .nqs-template-card:hover { border-color: var(--nqs-accent); box-shadow: var(--nqs-shadow); } #NQS_GLOBAL_CONTAINER .nqs-template-card.is-active { border-color: var(--nqs-accent); box-shadow: var(--nqs-shadow-lg); background: linear-gradient(0deg, var(--nqs-accent-light), transparent); } #NQS_GLOBAL_CONTAINER .nqs-template-card--compact .nqs-template-preview { display: none; } #NQS_GLOBAL_CONTAINER .nqs-template-select input { appearance: none; -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--nqs-border-hover); background: var(--nqs-bg); display: inline-block; transition: all .15s ease; box-shadow: inset 0 0 0 0 var(--nqs-success); } #NQS_GLOBAL_CONTAINER .nqs-template-select input:checked { border-color: var(--nqs-success); box-shadow: inset 0 0 0 4px var(--nqs-success); } #NQS_GLOBAL_CONTAINER .nqs-template-card.is-active .nqs-template-select input { border-color: var(--nqs-success); box-shadow: inset 0 0 0 4px var(--nqs-success); } #NQS_GLOBAL_CONTAINER .nqs-template-card-top { position: relative; padding-right: 8px; } #NQS_GLOBAL_CONTAINER .nqs-template-select { position: absolute; top: 0; right: 0; } #NQS_GLOBAL_CONTAINER .nqs-template-title { margin: 0 24px 0 0; font-size: 14px; color: var(--nqs-text-primary); font-weight: 600; } #NQS_GLOBAL_CONTAINER .nqs-template-brief { display:none; } #NQS_GLOBAL_CONTAINER .nqs-template-card-actions { display:flex; gap:8px; justify-content:flex-end; } /* 提示词模式分段控件 */ #NQS_GLOBAL_CONTAINER .nqs-segmented { display:inline-flex; border:1px solid var(--nqs-border); border-radius: 8px; overflow:hidden; background: var(--nqs-bg); margin: 8px 0 12px; } #NQS_GLOBAL_CONTAINER .nqs-seg-btn { padding: 6px 12px; font-size: 13px; color: var(--nqs-text-secondary); background: transparent; border: none; cursor: pointer; } #NQS_GLOBAL_CONTAINER .nqs-seg-btn.is-active { color: var(--nqs-accent); background: var(--nqs-accent-light); } /* 模板/自定义模式切换显示控制 */ #NQS_GLOBAL_CONTAINER .nqs-settings-group-content[data-prompt-mode="template"] [data-field-id="ai_prompt"] { display:none; } #NQS_GLOBAL_CONTAINER .nqs-settings-group-content[data-prompt-mode="custom"] .nqs-prompt-templates { display:none; } /* ===== 追加样式结束 ===== */ /* ===== 追加样式 v2(导航与提示词卡片美化) ===== */ /* 左侧导航:更清晰的激活态与指示条 */ #NQS_GLOBAL_CONTAINER .nqs-settings-sidebar { border-radius: var(--nqs-radius-lg); background: linear-gradient(0deg, rgba(0,0,0,0.02), rgba(255,255,255,0.02)), var(--nqs-bg); } #NQS_GLOBAL_CONTAINER .nqs-nav-item { position: relative; border-left: 3px solid transparent; } #NQS_GLOBAL_CONTAINER .nqs-nav-item::after { content: '›'; position: absolute; right: 10px; font-size: 14px; color: var(--nqs-text-tertiary); transition: color .2s ease, transform .2s ease; } #NQS_GLOBAL_CONTAINER .nqs-nav-item:hover::after { color: var(--nqs-accent); transform: translateX(2px); } #NQS_GLOBAL_CONTAINER .nqs-nav-item.active { border-left-color: var(--nqs-accent); background: linear-gradient(0deg, var(--nqs-accent-light), transparent); } #NQS_GLOBAL_CONTAINER .nqs-nav-item.active::after { background: var(--nqs-accent); } /* 分组:滚动校准,避免粘到顶部时视觉拥挤 */ #NQS_GLOBAL_CONTAINER .nqs-settings-group { scroll-margin-top: 12px; } /* 提示词模板卡片:层次更分明、交互更细腻 */ #NQS_GLOBAL_CONTAINER .nqs-template-card { border: 1px solid var(--nqs-border); background: linear-gradient(180deg, rgba(0,0,0,0.00), rgba(0,0,0,0.02)) , var(--nqs-bg); transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; } #NQS_GLOBAL_CONTAINER .nqs-template-card:hover { transform: translateY(-2px); border-color: var(--nqs-accent); box-shadow: var(--nqs-shadow-lg); } #NQS_GLOBAL_CONTAINER .nqs-template-card.is-active { border-color: var(--nqs-accent); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15), var(--nqs-shadow); background: linear-gradient(0deg, var(--nqs-accent-light), transparent); } #NQS_GLOBAL_CONTAINER .nqs-template-card-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } #NQS_GLOBAL_CONTAINER .nqs-template-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #NQS_GLOBAL_CONTAINER .nqs-template-card-actions .nqs-button { padding: 6px 10px; } /* Prompt 模式切换控件:更显著的激活态 */ #NQS_GLOBAL_CONTAINER .nqs-seg-btn { border-right: 1px solid var(--nqs-border); } #NQS_GLOBAL_CONTAINER .nqs-seg-btn:last-child { border-right: none; } #NQS_GLOBAL_CONTAINER .nqs-seg-btn.is-active { color: var(--nqs-accent); background: linear-gradient(0deg, var(--nqs-accent-light), transparent); } /* 模型下拉:活动项更明显 */ #NQS_GLOBAL_CONTAINER .nqs-model-dropdown .nqs-dropdown-item.is-active { background: var(--nqs-accent-light); color: var(--nqs-accent); } /* ===== 追加样式 v2 结束 ===== */ `; const style = document.createElement('style'); style.id = styleId; style.textContent = css; container.appendChild(style); } const showNotification = (button, message, stay = false, originalText) => { if (!button) return; button.textContent = message; if (!stay) { setTimeout(() => { button.textContent = originalText; }, 3000); } }; async function initFloatingButtons() { if (fabInstance) fabInstance.destroy(); fabInstance = new FloatingActionButton(); await fabInstance.init(); } function savePageViaMenu() { startSaveProcess({ source: 'menu' }); } function savePageForLaterViaMenu() { startReadLaterSave({ source: 'menu' }); } async function runScript() { await applyTheme(); GM_registerMenuCommand('⚙️ 设置 (Settings)', openSettingsPanel); GM_registerMenuCommand('📋 查看日志 (View Logs)', openLogViewerPanel); GM_registerMenuCommand('─'.repeat(20), () => {}); GM_registerMenuCommand('➤ 保存到 Notion', savePageViaMenu); GM_registerMenuCommand('◷ 保存为稍后读', savePageForLaterViaMenu); GM_registerMenuCommand('📝 智能文本摘录', () => saveSelectedTextAsNote()); GM_registerMenuCommand('📚 智能收藏夹管理', () => openSmartBookmarkManager()); GM_registerMenuCommand('─'.repeat(20), () => {}); try { initFloatingButtons(); } catch (e) { console.error("NQS: 无法初始化悬浮按钮UI。这可能是由于网站的安全策略(CSP)限制。请放心使用油猴菜单中的备用按钮,功能完全相同。", e); addLog('error', '悬浮按钮UI初始化失败', { error: e.message, stack: e.stack }); } } runScript(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址