// ==UserScript==
// @name VVQuest - VV表情包助手
// @namespace https://zvv.quest/
// @version 0.1.3
// @description 基于 VVQuest 项目的表情包检索工具,适用于百度和知乎
// @author xy0v0
// @match *://*.baidu.com/*
// @match *://*.zhihu.com/*
// @icon https://cn-sy1.rains3.com/pic/pic/2025/03/e0607ef1dfd70ae54612c795de0c4de5.png
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_notification
// @grant GM_addStyle
// @connect api.zvv.quest
// @license CC BY-NC-SA 4.0
// ==/UserScript==
(function() {
'use strict';
// 脚本配置
const config = {
apiUrl: 'https://api.zvv.quest/search',
enhancedApiUrl: 'https://api.zvv.quest/enhancedsearch',
cooldownTime: 3000, // 无联网搜索时冷却时间,单位毫秒
enhancedCooldownTime: 10000, // 联网搜索时冷却时间,单位毫秒
lastRequestTime: 0,
enableNetworkSearch: false, // 是否启用联网搜索功能
isSearching: false, // 是否正在搜索
};
// 添加样式
GM_addStyle(`
/* 统一的按钮样式,基于知乎样式 */
.vvquest-btn {
background: linear-gradient(135deg, #0084ff, #0066cc);
color: white;
border: none;
border-radius: 8px;
padding: 8px 15px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
margin: 0 8px;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 132, 255, 0.4);
}
/* 搜索区域样式 */
.vvquest-search-section {
margin-bottom: 15px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.vvquest-section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
}
.vvquest-section-title {
font-weight: bold;
color: #333;
}
.vvquest-toggle-btn {
background: none;
border: none;
color: #666;
font-size: 16px;
cursor: pointer;
transition: transform 0.3s ease;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.vvquest-toggle-btn.collapsed {
transform: rotate(-90deg);
}
.vvquest-toggle-btn.collapsed {
transform: rotate(-90deg);
}
.vvquest-search-content {
padding: 15px;
transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease;
max-height: 1000px;
opacity: 1;
overflow: hidden;
}
.vvquest-search-content.collapsed {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
}
.vvquest-btn:hover {
background: linear-gradient(135deg, #0066cc, #004c99);
box-shadow: 0 4px 12px rgba(0, 132, 255, 0.6);
transform: translateY(-2px);
}
.vvquest-btn:active {
transform: translateY(1px);
box-shadow: 0 1px 4px rgba(0, 132, 255, 0.3);
}
/* 其他样式保持不变 */
.vvquest-floating-btn {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 9999;
width: auto;
height: auto;
padding: 10px 16px;
font-size: 16px;
}
/* 知乎左下角固定按钮样式 - 继承统一样式 */
.vvquest-zhihu-btn {
position: fixed;
left: 20px;
bottom: 20px;
z-index: 9999;
width: auto;
height: auto;
padding: 10px 16px;
font-size: 16px;
}
/* 删除表情图标前缀 */
.vvquest-zhihu-btn::before {
content: "";
margin-right: 0;
}
/* 模态框样式优化 */
.vvquest-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.7);
backdrop-filter: blur(3px);
overflow-y: auto;
padding: 10px 0;
}
.vvquest-modal-content {
background: #ffffff;
margin: 2% auto;
padding: 20px;
border: none;
width: 480px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0,0,0,0.25);
animation: vvquestFadeIn 0.3s ease;
padding-bottom: 30px;
display: flex;
flex-direction: column;
}
@keyframes vvquestFadeIn {
from {opacity: 0; transform: translateY(-20px);}
to {opacity: 1; transform: translateY(0);}
}
.vvquest-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
position: relative;
}
.vvquest-modal-title {
font-size: 20px;
font-weight: bold;
color: #333;
position: relative;
}
.vvquest-modal-title::after {
content: '';
position: absolute;
bottom: -15px;
left: 0;
width: 140px;
height: 3px;
background: #3498db;
}
.vvquest-close {
color: #aaa;
font-size: 28px;
font-weight: normal;
cursor: pointer;
transition: all 0.2s;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.vvquest-close:hover {
color: #777;
}
/* 调试内容区样式 */
.vvquest-content-preview {
background-color: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 10px 12px;
margin: 12px 0 15px 0;
max-height: 100px;
overflow-y: auto;
font-size: 14px;
color: #333;
line-height: 1.5;
}
.vvquest-content-preview-title {
font-weight: bold;
margin-bottom: 6px;
color: #0066cc;
font-size: 15px;
}
.vvquest-content-preview-text {
word-break: break-word;
white-space: pre-wrap;
}
/* 复选框容器样式 */
.vvquest-checkbox-container {
display: flex !important;
align-items: center !important;
background-color: #f0f8ff !important;
padding: 10px 12px !important;
border-radius: 8px !important;
margin: 15px 0 !important;
border: 1px solid #d5e9fb !important;
}
.vvquest-checkbox {
margin-right: 10px !important;
width: 20px !important;
height: 20px !important;
position: relative !important;
cursor: pointer !important;
accent-color: #3498db !important;
}
.vvquest-checkbox + label {
font-size: 15px !important;
color: #333 !important;
cursor: pointer !important;
}
/* 搜索按钮样式 */
.vvquest-search-btn {
background: #3498db !important;
color: white !important;
border: none !important;
border-radius: 30px !important;
padding: 12px 0 !important;
font-size: 16px !important;
font-weight: bold !important;
cursor: pointer !important;
width: 100% !important;
margin-top: 15px !important;
margin-bottom: 20px !important;
box-shadow: 0 2px 10px rgba(52, 152, 219, 0.3) !important;
text-transform: none !important;
letter-spacing: normal !important;
}
.vvquest-search-btn:hover {
background: #2980b9 !important;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.5) !important;
}
/* 禁用按钮样式 */
.vvquest-btn-disabled {
opacity: 0.6 !important;
cursor: not-allowed !important;
transform: none !important;
box-shadow: none !important;
}
/* 搜索结果区域样式 */
.vvquest-results {
margin-top: 20px;
display: none;
flex-direction: column;
gap: 15px;
max-height: 40vh;
overflow-y: auto;
padding-right: 5px;
}
/* 调整滚动条样式 */
.vvquest-results::-webkit-scrollbar {
width: 6px;
}
.vvquest-results::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.vvquest-results::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.vvquest-results::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* AI解析结果样式 */
.vvquest-ai-explanation {
background-color: #f0f8ff;
border: 1px solid #d1e5f5;
border-radius: 8px;
padding: 12px;
margin-top: 10px;
margin-bottom: 15px;
display: none;
font-size: 14px;
line-height: 1.5;
color: #333;
max-height: 120px;
overflow-y: auto;
}
/* AI解析结果滚动条样式 */
.vvquest-ai-explanation::-webkit-scrollbar {
width: 6px;
}
.vvquest-ai-explanation::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.vvquest-ai-explanation::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.vvquest-ai-explanation::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* 表情包项目样式 */
.vvquest-meme-item {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
}
/* 表情包图片样式 */
.vvquest-meme-img {
max-width: 100%;
max-height: 200px;
margin-bottom: 10px;
border-radius: 4px;
}
/* 表情包按钮组样式 */
.vvquest-meme-actions {
display: flex;
gap: 10px;
width: 100%;
justify-content: center;
}
/* 表情包操作按钮样式 */
.vvquest-meme-btn {
background: #f2f2f2;
color: #333;
border: none;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.vvquest-meme-btn:hover {
background: #e0e0e0;
}
/* 加载指示器样式 */
.vvquest-loading {
display: none;
justify-content: center;
margin: 20px 0;
}
.vvquest-spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(0,0,0,0.1);
border-radius: 50%;
border-top-color: #3498db;
animation: vvquest-spin 1s linear infinite;
}
@keyframes vvquest-spin {
to { transform: rotate(360deg); }
}
/* 冷却倒计时样式 */
.vvquest-cooldown {
text-align: center;
color: #e74c3c;
font-size: 14px;
margin-top: 10px;
font-weight: bold;
display: none;
}
/* 滑块容器样式 */
.vvquest-slider-container {
margin: 15px 0;
padding: 10px 12px;
background-color: #f0f8ff;
border: 1px solid #d5e9fb;
border-radius: 8px;
}
.vvquest-slider-container label {
display: block;
margin-bottom: 8px;
font-size: 15px;
color: #333;
}
.vvquest-slider {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #ddd;
outline: none;
border-radius: 3px;
margin: 10px 0;
}
.vvquest-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #3498db;
cursor: pointer;
transition: all 0.2s;
}
.vvquest-slider::-webkit-slider-thumb:hover {
background: #2980b9;
transform: scale(1.2);
}
#vvquest-image-count-value {
font-weight: bold;
color: #3498db;
}
/* 关于按钮样式 */
.vvquest-about-btn {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #3498db;
background-color: transparent;
position: absolute;
right: 50px;
top: 0;
transition: all 0.2s;
font-size: 14px;
font-weight: bold;
height: 36px;
padding: 0 10px;
}
.vvquest-about-btn:hover {
color: #2980b9;
text-decoration: underline;
}
/* 关于界面样式 */
.vvquest-about {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 450px;
max-height: 90vh;
overflow-y: auto;
background-color: #fff;
z-index: 11000;
padding: 20px;
box-sizing: border-box;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
animation: vvquestAboutFadeIn 0.3s ease;
}
.vvquest-about-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.vvquest-about-title {
font-size: 20px;
font-weight: bold;
color: #333;
}
.vvquest-about-close {
color: #aaa;
font-size: 28px;
font-weight: normal;
cursor: pointer;
transition: all 0.2s;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.vvquest-about-close:hover {
color: #777;
}
.vvquest-about-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
}
.vvquest-about-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #3498db;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
margin-bottom: 15px;
}
.vvquest-about-name {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.vvquest-about-description {
font-size: 14px;
color: #666;
text-align: center;
margin-bottom: 20px;
line-height: 1.6;
}
.vvquest-about-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin-top: 10px;
width: 100%;
}
.vvquest-about-link {
display: flex;
align-items: center;
background-color: #f8f9fa;
padding: 10px 15px;
border-radius: 30px;
text-decoration: none;
color: #333;
font-size: 14px;
transition: all 0.2s;
border: 1px solid #eee;
}
.vvquest-about-link:hover {
background-color: #f0f0f0;
transform: translateY(-2px);
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
}
.vvquest-about-link i {
margin-right: 8px;
font-size: 18px;
}
.vvquest-icon-bilibili {
color: #00a1d6;
}
.vvquest-icon-home {
color: #3498db;
}
.vvquest-icon-blog {
color: #2ecc71;
}
.vvquest-icon-afdian {
color: #946ce6;
}
/* 关于界面专用动画 */
@keyframes vvquestAboutFadeIn {
from {opacity: 0; transform: translate(-50%, -55%);}
to {opacity: 1; transform: translate(-50%, -50%);}
}
`);
// 网站集成基类
class SiteIntegration {
constructor() {
this.initUI();
this.addEventListeners();
}
// 初始化UI
initUI() {
this.createButton();
this.createVVQuestModal();
}
// 创建按钮 - 由子类实现
createButton() {
throw new Error('createButton must be implemented by subclass');
}
// 获取内容 - 由子类实现
getContent() {
throw new Error('getContent must be implemented by subclass');
}
// 创建VVQuest模态框
createVVQuestModal() {
const modal = document.createElement('div');
modal.className = 'vvquest-modal';
modal.id = 'vvquest-modal';
modal.innerHTML = `
<div class="vvquest-modal-content">
<div class="vvquest-modal-header">
<div class="vvquest-modal-title">VVQuest 表情包助手</div>
<div class="vvquest-about-btn" title="关于作者">
关于
</div>
<span class="vvquest-close">×</span>
</div>
<!-- 搜索区域(可折叠) -->
<div class="vvquest-search-section" id="vvquest-search-section">
<div class="vvquest-section-header">
<span class="vvquest-section-title">搜索区域</span>
<button class="vvquest-toggle-btn" id="vvquest-toggle-search" title="折叠/展开搜索区域">▼</button>
</div>
<div class="vvquest-search-content" id="vvquest-search-content">
<!-- 添加调试内容预览区 -->
<div class="vvquest-content-preview">
<div class="vvquest-content-preview-title">当前检索内容:</div>
<div id="vvquest-content-text" class="vvquest-content-preview-text">
未获取到内容...
</div>
</div>
<div class="vvquest-checkbox-container">
<input type="checkbox" id="vvquest-network-search" class="vvquest-checkbox" ${config.enableNetworkSearch ? 'checked' : ''}>
<label for="vvquest-network-search">启用联网搜索 ( beta,不稳定,遇到错误请关闭 )</label>
</div>
<!-- 图片数量滑块控件 -->
<div class="vvquest-slider-container">
<label for="vvquest-image-count">显示图片数量: <span id="vvquest-image-count-value">5</span></label>
<input type="range" id="vvquest-image-count" class="vvquest-slider" min="1" max="25" value="5">
</div>
<!-- 加载指示器 -->
<div id="vvquest-loading" class="vvquest-loading">
<div class="vvquest-spinner"></div>
</div>
<!-- 冷却倒计时 -->
<div id="vvquest-cooldown" class="vvquest-cooldown">
冷却中,请等待 <span id="vvquest-cooldown-time">0</span> 秒
</div>
<button id="vvquest-search-btn" class="vvquest-search-btn">搜索表情包</button>
</div>
</div>
<!-- AI解析结果区域 -->
<div id="vvquest-ai-explanation" class="vvquest-ai-explanation"></div>
<!-- 搜索结果区域 -->
<div id="vvquest-results" class="vvquest-results"></div>
<!-- 关于界面 -->
<div id="vvquest-about" class="vvquest-about">
<div class="vvquest-about-header">
<div class="vvquest-about-title">关于作者</div>
<span class="vvquest-about-close">×</span>
</div>
<div class="vvquest-about-content">
<img src="https://cn-sy1.rains3.com/pic/pic/2025/02/dac95bccccaacbdec4bf801a9f309527.png" alt="xy0v0" class="vvquest-about-avatar">
<div class="vvquest-about-name">xy0v0</div>
<div class="vvquest-about-description">
VVQuest 开发者<br>
感谢使用我的作品,欢迎关注我的其他平台!
</div>
<div class="vvquest-about-links">
<a href="https://space.bilibili.com/165404794" target="_blank" class="vvquest-about-link">
<span class="vvquest-icon-bilibili">📺</span> Bilibili
</a>
<a href="https://zvv.quest/" target="_blank" class="vvquest-about-link">
<span class="vvquest-icon-home">🏠</span> 项目主站
</a>
<a href="https://www.xy0v0.top/" target="_blank" class="vvquest-about-link">
<span class="vvquest-icon-blog">📝</span> 博客
</a>
<a href="https://afdian.com/a/xy0v0" target="_blank" class="vvquest-about-link">
<span class="vvquest-icon-afdian">💰</span> 爱发电
</a>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
}
// 添加事件监听器
addEventListeners() {
// 等待按钮和模态框加载完成
this.waitForElement('#vvquest-btn', (btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
// 获取内容并显示在预览区
const content = this.getContent();
document.getElementById('vvquest-content-text').textContent = content || '未获取到内容...';
// 清除之前的搜索结果
this.clearResults();
// 显示模态框
document.getElementById('vvquest-modal').style.display = 'block';
});
});
this.waitForElement('.vvquest-close', (closeBtn) => {
closeBtn.addEventListener('click', () => {
document.getElementById('vvquest-modal').style.display = 'none';
});
});
this.waitForElement('#vvquest-network-search', (checkbox) => {
checkbox.addEventListener('change', () => {
config.enableNetworkSearch = checkbox.checked;
});
});
// 添加滑块值变化事件监听器
this.waitForElement('#vvquest-image-count', (slider) => {
slider.addEventListener('input', () => {
document.getElementById('vvquest-image-count-value').textContent = slider.value;
});
});
// 折叠/展开搜索区域按钮事件
this.waitForElement('#vvquest-toggle-search', (toggleBtn) => {
toggleBtn.addEventListener('click', () => {
const content = document.getElementById('vvquest-search-content');
// 切换折叠状态
content.classList.toggle('collapsed');
toggleBtn.classList.toggle('collapsed');
});
});
this.waitForElement('#vvquest-search-btn', (searchBtn) => {
searchBtn.addEventListener('click', () => {
// 获取预览区的内容作为搜索内容
const questionTitle = document.getElementById('vvquest-content-text').textContent;
// 处理请求 - 不再关闭模态框,以便显示结果
if (questionTitle.trim() !== '' && questionTitle !== '未获取到内容...') {
if (config.enableNetworkSearch) {
this.handleUserRequest(questionTitle);
} else {
// 本地搜索功能
this.handleUserRequest(questionTitle);
}
} else {
this.showNotification('未获取到内容,请重试');
}
});
});
// 点击模态框外部关闭
window.addEventListener('click', (event) => {
const modal = document.getElementById('vvquest-modal');
if (event.target === modal) {
modal.style.display = 'none';
}
});
// 关于按钮点击事件
this.waitForElement('.vvquest-about-btn', (aboutBtn) => {
aboutBtn.addEventListener('click', () => {
document.getElementById('vvquest-about').style.display = 'block';
});
});
// 关于界面关闭按钮点击事件
this.waitForElement('.vvquest-about-close', (closeBtn) => {
closeBtn.addEventListener('click', () => {
document.getElementById('vvquest-about').style.display = 'none';
});
});
}
// 工具方法
waitForElement(selector, callback, maxAttempts = 20, interval = 500) {
let attempts = 0;
const checkElement = function() {
const element = document.querySelector(selector);
if (element) {
callback(element);
return;
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(checkElement, interval);
} else {
console.log(`未能找到元素: ${selector}`);
callback(null);
}
};
checkElement();
}
clearResults() {
const resultsEl = document.getElementById('vvquest-results');
const explanationEl = document.getElementById('vvquest-ai-explanation');
if (resultsEl) {
resultsEl.innerHTML = '';
resultsEl.style.display = 'none';
}
if (explanationEl) {
explanationEl.innerHTML = '';
explanationEl.style.display = 'none';
}
}
showNotification(message) {
GM_notification({
text: message,
title: 'VVQuest 表情包助手',
timeout: 3000
});
}
handleUserRequest(query) {
// 清除之前的结果
this.clearResults();
// 检查冷却时间
const now = Date.now();
const currentCooldown = config.enableNetworkSearch ? config.enhancedCooldownTime : config.cooldownTime;
if (now - config.lastRequestTime < currentCooldown) {
const remainingTime = Math.ceil((config.lastRequestTime + currentCooldown - now) / 1000);
this.showCooldown(remainingTime);
return;
}
// 开始搜索
config.isSearching = true;
this.showLoading(true);
// 更新最后请求时间
config.lastRequestTime = now;
// 发送API请求
this.sendAPIRequest(query);
}
showCooldown(seconds) {
const cooldownEl = document.getElementById('vvquest-cooldown');
const timeEl = document.getElementById('vvquest-cooldown-time');
if (cooldownEl && timeEl) {
timeEl.textContent = seconds;
cooldownEl.style.display = 'block';
// 禁用搜索按钮
const searchBtn = document.getElementById('vvquest-search-btn');
if (searchBtn) {
searchBtn.classList.add('vvquest-btn-disabled');
searchBtn.disabled = true;
}
// 倒计时
let countdown = seconds;
const timer = setInterval(() => {
countdown--;
if (countdown <= 0) {
clearInterval(timer);
cooldownEl.style.display = 'none';
// 恢复搜索按钮
if (searchBtn) {
searchBtn.classList.remove('vvquest-btn-disabled');
searchBtn.disabled = false;
}
} else {
timeEl.textContent = countdown;
}
}, 1000);
}
}
showLoading(show) {
const loadingEl = document.getElementById('vvquest-loading');
if (loadingEl) {
loadingEl.style.display = show ? 'flex' : 'none';
}
// 禁用或启用搜索按钮
const searchBtn = document.getElementById('vvquest-search-btn');
if (searchBtn) {
if (show) {
searchBtn.classList.add('vvquest-btn-disabled');
searchBtn.disabled = true;
} else {
searchBtn.classList.remove('vvquest-btn-disabled');
searchBtn.disabled = false;
}
}
}
sendAPIRequest(query) {
// 获取图片数量滑块的值
const imageCount = document.getElementById('vvquest-image-count').value;
const url = config.enableNetworkSearch
? `${config.enhancedApiUrl}?q=${encodeURIComponent(query)}&n=${imageCount}`
: `${config.apiUrl}?q=${encodeURIComponent(query)}&n=${imageCount}`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Accept': 'application/json'
},
onload: function(response) {
this.showLoading(false);
config.isSearching = false;
this.handleAPIResponse(response, config.enableNetworkSearch);
}.bind(this),
onerror: function(error) {
this.showLoading(false);
config.isSearching = false;
this.handleAPIError(error);
}.bind(this)
});
}
handleAPIResponse(response, isEnhanced) {
try {
const data = JSON.parse(response.responseText);
if (data.code === 200) {
if (isEnhanced) {
// 处理联网搜索结果 - 修改判断逻辑
if (data.data && (data.data.memes || data.data.images)) {
this.displayEnhancedResults(data.data);
} else {
this.handleAPIError({ message: '解析联网搜索响应失败' });
}
} else if (Array.isArray(data.data)) {
// 处理普通搜索结果
this.displayResults(data.data);
} else {
this.handleAPIError({ message: '解析API响应失败' });
}
} else {
// 处理API返回的错误
this.handleAPIError({ message: data.msg || '未找到匹配的表情包' });
}
} catch (error) {
this.handleAPIError({ message: '解析API响应失败: ' + error.message });
}
}
handleAPIError(error) {
this.showNotification(`出错了: ${error.message || '未知错误'}`);
}
displayResults(memeUrls) {
const resultsEl = document.getElementById('vvquest-results');
if (resultsEl && memeUrls && memeUrls.length > 0) {
resultsEl.innerHTML = '';
memeUrls.forEach((memeUrl) => {
resultsEl.appendChild(this.createMemeItem(memeUrl));
});
resultsEl.style.display = 'flex';
// 搜索完成后自动折叠搜索区域
const content = document.getElementById('vvquest-search-content');
const toggleBtn = document.getElementById('vvquest-toggle-search');
content.classList.add('collapsed');
toggleBtn.classList.add('collapsed');
} else {
this.showNotification('未找到匹配的表情包');
}
}
displayEnhancedResults(data) {
const resultsEl = document.getElementById('vvquest-results');
const explanationEl = document.getElementById('vvquest-ai-explanation');
// 显示AI解析结果
if (explanationEl && data.explanation) {
explanationEl.textContent = data.explanation;
explanationEl.style.display = 'block';
}
// 显示表情包结果 - 兼容两种可能的返回格式
const memeUrls = data.memes || data.images || [];
if (resultsEl && memeUrls.length > 0) {
resultsEl.innerHTML = '';
memeUrls.forEach((memeUrl) => {
resultsEl.appendChild(this.createMemeItem(memeUrl));
});
resultsEl.style.display = 'flex';
// 搜索完成后自动折叠搜索区域
const content = document.getElementById('vvquest-search-content');
const toggleBtn = document.getElementById('vvquest-toggle-search');
content.classList.add('collapsed');
toggleBtn.classList.add('collapsed');
} else {
this.showNotification('未找到匹配的表情包');
}
}
createMemeItem(memeUrl) {
const item = document.createElement('div');
item.className = 'vvquest-meme-item';
// 创建图片元素
const img = document.createElement('img');
img.className = 'vvquest-meme-img';
img.src = memeUrl;
img.alt = '表情包';
img.loading = 'lazy';
// 创建按钮组
const actions = document.createElement('div');
actions.className = 'vvquest-meme-actions';
// 复制图片按钮
const copyImgBtn = document.createElement('button');
copyImgBtn.className = 'vvquest-meme-btn';
copyImgBtn.textContent = '复制图片';
copyImgBtn.addEventListener('click', () => this.copyImageToClipboard(memeUrl, true));
// 复制链接按钮
const copyLinkBtn = document.createElement('button');
copyLinkBtn.className = 'vvquest-meme-btn';
copyLinkBtn.textContent = '复制链接';
copyLinkBtn.addEventListener('click', () => this.copyImageToClipboard(memeUrl, false));
// 下载图片按钮
const downloadBtn = document.createElement('button');
downloadBtn.className = 'vvquest-meme-btn';
downloadBtn.textContent = '下载图片';
downloadBtn.addEventListener('click', () => this.downloadImage(memeUrl));
// 组装元素
actions.appendChild(copyImgBtn);
actions.appendChild(copyLinkBtn);
actions.appendChild(downloadBtn);
item.appendChild(img);
item.appendChild(actions);
return item;
}
downloadImage(imageUrl) {
try {
const link = document.createElement('a');
link.href = imageUrl;
link.download = imageUrl.split('/').pop() || 'meme.png';
link.target = '_blank';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showNotification('正在下载表情包');
} catch (error) {
this.showNotification('下载表情包失败');
console.error('下载表情包失败:', error);
}
}
copyImageToClipboard(imageUrl, isImage) {
if (isImage) {
// 复制图片到剪贴板
// 创建临时画布来获取图片数据
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
try {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// 尝试用新API复制图片
canvas.toBlob(function(blob) {
try {
// 尝试使用 ClipboardItem API
if (navigator.clipboard && navigator.clipboard.write) {
const clipboardItem = new ClipboardItem({ 'image/png': blob });
navigator.clipboard.write([clipboardItem])
.then(() => this.showNotification('表情包已复制到剪贴板'))
.catch(err => {
console.error('复制图片失败:', err);
// 如果新API失败,回退到复制链接
GM_setClipboard(imageUrl);
this.showNotification('复制图片失败,已复制图片链接到剪贴板');
});
} else {
// 不支持 ClipboardItem API,复制链接
GM_setClipboard(imageUrl);
this.showNotification('您的浏览器不支持复制图片,已复制图片链接到剪贴板');
}
} catch (e) {
// 处理所有其他错误
console.error('复制过程中出错:', e);
GM_setClipboard(imageUrl);
this.showNotification('复制图片时出错,已复制图片链接到剪贴板');
}
}.bind(this));
} catch (e) {
// 画布操作错误
console.error('创建画布失败:', e);
GM_setClipboard(imageUrl);
this.showNotification('复制图片时出错,已复制图片链接到剪贴板');
}
};
img.onerror = function() {
// 图片加载失败
console.error('图片加载失败');
GM_setClipboard(imageUrl);
this.showNotification('图片加载失败,已复制图片链接到剪贴板');
}.bind(this);
img.src = imageUrl;
} else {
// 直接复制链接
GM_setClipboard(imageUrl);
this.showNotification('表情包链接已复制到剪贴板');
}
}
}
// 百度贴吧集成
class TiebaIntegration extends SiteIntegration {
createButton() {
// 尝试定位到发表按钮旁边的位置
this.waitForElement('.j_floating', (floatingElement) => {
if (floatingElement) {
// 尝试找到发表按钮区域
const postBtnArea = document.querySelector('.j_floating');
if (postBtnArea) {
const btn = this.createVVQuestBtn();
postBtnArea.parentNode.insertBefore(btn, postBtnArea.nextSibling);
console.log('成功将VVQuest按钮插入到发表按钮旁边');
return;
}
}
// 如果找不到发表按钮,则尝试其他常见位置
const selectors = [
'.tb-exbtn-wrapper',
'.poster_head',
'.edui-btn-toolbar',
'.j_media_box',
'.tb-editor-toolbar',
'ul[class^="tbui_aside_float_bar"]',
'.btn_default',
'.btn_sub',
'.j_choo',
'.j_submit'
];
this.waitForElements(selectors, (elements) => {
if (elements && elements.length > 0) {
const targetElement = elements[0];
if (targetElement.tagName === 'UL') {
const li = document.createElement('li');
li.appendChild(this.createVVQuestBtn());
targetElement.appendChild(li);
} else {
const btn = this.createVVQuestBtn();
targetElement.parentNode.insertBefore(btn, targetElement.nextSibling);
}
} else {
// 创建悬浮按钮
this.createFloatingButton();
}
});
});
}
getContent() {
let content = '';
// 获取帖子标题
const titleElement = document.querySelector('.core_title_txt');
if (titleElement) {
content += titleElement.textContent.trim() + ' ';
}
// 获取楼主发帖内容
const postContentElement = document.querySelector('.d_post_content');
if (postContentElement) {
content += postContentElement.textContent.trim();
}
console.log('获取到帖子内容:', content);
return content;
}
createVVQuestBtn() {
const vvquestBtn = document.createElement('button');
vvquestBtn.className = 'vvquest-btn';
vvquestBtn.id = 'vvquest-btn';
vvquestBtn.textContent = 'VVQuest';
vvquestBtn.title = '使用VVQuest搜索表情包';
return vvquestBtn;
}
createFloatingButton() {
const floatingBtn = this.createVVQuestBtn();
floatingBtn.classList.add('vvquest-floating-btn');
document.body.appendChild(floatingBtn);
}
}
// 知乎集成
class ZhihuIntegration extends SiteIntegration {
createButton() {
const zhihuBtn = document.createElement('button');
zhihuBtn.className = 'vvquest-btn vvquest-zhihu-btn';
zhihuBtn.id = 'vvquest-btn';
zhihuBtn.textContent = 'VVQuest';
zhihuBtn.title = '使用VVQuest搜索表情包';
document.body.appendChild(zhihuBtn);
console.log('成功创建知乎左下角固定按钮');
}
getContent() {
let title = '';
// 尝试多个可能的选择器来获取知乎问题标题
const selectors = [
'.QuestionHeader-title',
'h1.QuestionHeader-title',
'.QuestionPage .QuestionHeader .QuestionHeader-title',
'.Question-title',
'.zm-item-title',
'h1[data-zop-question]'
];
for (const selector of selectors) {
const titleElement = document.querySelector(selector);
if (titleElement) {
title = titleElement.textContent.trim();
console.log('获取到知乎问题标题:', title);
break;
}
}
// 如果没找到标题,尝试获取当前页面标题
if (!title) {
title = document.title.replace(' - 知乎', '').trim();
console.log('使用页面标题作为知乎问题:', title);
}
return title;
}
}
// 初始化函数
function init() {
console.log('VVQuest 表情包助手已加载');
// 检测当前网站并初始化对应的集成类
if (location.hostname.includes('tieba.baidu.com')) {
console.log('检测到百度贴吧网站');
new TiebaIntegration();
} else if (location.hostname.includes('zhihu.com')) {
console.log('检测到知乎网站');
new ZhihuIntegration();
}
}
// 启动脚本
init();
})();