PageSourceSearch

https://xeone3.pages.dev/js/douban.js

js xeone3.pages.dev collected 2026-10-03 06:01:35 UTC 29,458 bytes, 788 lines download raw bytes

1// 豆瓣热门电影电视剧推荐功能
2
3// 豆瓣标签列表 - 修改为默认标签
4let defaultMovieTags = ['热门', '最新', '经典', '豆瓣高分', '冷门佳片', '华语', '欧美', '韩国', '日本', '动作', '喜剧', '爱情', '科幻', '悬疑', '恐怖', '治愈'];
5let defaultTvTags = ['热门', '美剧', '英剧', '韩剧', '日剧', '国产剧', '港剧', '日本动画', '综艺', '纪录片'];
6
7// 用户标签列表 - 存储用户实际使用的标签(包含保留的系统标签和用户添加的自定义标签)
8let movieTags = [];
9let tvTags = [];
10
11// 加载用户标签
12function loadUserTags() {
13    try {
14        // 尝试从本地存储加载用户保存的标签
15        const savedMovieTags = localStorage.getItem('userMovieTags');
16        const savedTvTags = localStorage.getItem('userTvTags');
17        
18        // 如果本地存储中有标签数据,则使用它
19        if (savedMovieTags) {
20            movieTags = JSON.parse(savedMovieTags);
21        } else {
22            // 否则使用默认标签
23            movieTags = [...defaultMovieTags];
24        }
25        
26        if (savedTvTags) {
27            tvTags = JSON.parse(savedTvTags);
28        } else {
29            // 否则使用默认标签
30            tvTags = [...defaultTvTags];
31        }
32    } catch (e) {
33        console.error('加载标签失败:', e);
34        // 初始化为默认值,防止错误
35        movieTags = [...defaultMovieTags];
36        tvTags = [...defaultTvTags];
37    }
38}
39
40// 保存用户标签
41function saveUserTags() {
42    try {
43        localStorage.setItem('userMovieTags', JSON.stringify(movieTags));
44        localStorage.setItem('userTvTags', JSON.stringify(tvTags));
45    } catch (e) {
46        console.error('保存标签失败:', e);
47        showToast('保存标签失败', 'error');
48    }
49}
50
51let doubanMovieTvCurrentSwitch = 'movie';
52let doubanCurrentTag = '热门';
53let doubanPageStart = 0;
54const doubanPageSize = 16; // 一次显示的项目数量
55
56// 初始化豆瓣功能
57function initDouban() {
58    // 设置豆瓣开关的初始状态
59    const doubanToggle = document.getElementById('doubanToggle');
60    if (doubanToggle) {
61        const isEnabled = localStorage.getItem('doubanEnabled') === 'true';
62        doubanToggle.checked = isEnabled;
63        
64        // 设置开关外观
65        const toggleBg = doubanToggle.nextElementSibling;
66        const toggleDot = toggleBg.nextElementSibling;
67        if (isEnabled) {
68            toggleBg.classList.add('bg-pink-600');
69            toggleDot.classList.add('translate-x-6');
70        }
71        
72        // 添加事件监听
73        doubanToggle.addEventListener('change', function(e) {
74            const isChecked = e.target.checked;
75            localStorage.setItem('doubanEnabled', isChecked);
76            
77            // 更新开关外观
78            if (isChecked) {
79                toggleBg.classList.add('bg-pink-600');
80                toggleDot.classList.add('translate-x-6');
81            } else {
82                toggleBg.classList.remove('bg-pink-600');
83                toggleDot.classList.remove('translate-x-6');
84            }
85            
86            // 更新显示状态
87            updateDoubanVisibility();
88        });
89        
90        // 初始更新显示状态
91        updateDoubanVisibility();
92
93        // 滚动到页面顶部
94        window.scrollTo(0, 0);
95    }
96
97    // 加载用户标签
98    loadUserTags();
99
100    // 渲染电影/电视剧切换
101    renderDoubanMovieTvSwitch();
102    
103    // 渲染豆瓣标签
104    renderDoubanTags();
105    
106    // 换一批按钮事件监听
107    setupDoubanRefreshBtn();
108    
109    // 初始加载热门内容
110    if (localStorage.getItem('doubanEnabled') === 'true') {
111        renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
112    }
113}
114
115// 根据设置更新豆瓣区域的显示状态
116function updateDoubanVisibility() {
117    const doubanArea = document.getElementById('doubanArea');
118    if (!doubanArea) return;
119    
120    const isEnabled = localStorage.getItem('doubanEnabled') === 'true';
121    const isSearching = document.getElementById('resultsArea') && 
122        !document.getElementById('resultsArea').classList.contains('hidden');
123    
124    // 只有在启用且没有搜索结果显示时才显示豆瓣区域
125    if (isEnabled && !isSearching) {
126        doubanArea.classList.remove('hidden');
127        // 如果豆瓣结果为空,重新加载
128        if (document.getElementById('douban-results').children.length === 0) {
129            renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
130        }
131    } else {
132        doubanArea.classList.add('hidden');
133    }
134}
135
136// 只填充搜索框,不执行搜索,让用户自主决定搜索时机
137function fillSearchInput(title) {
138    if (!title) return;
139    
140    // 安全处理标题,防止XSS
141    const safeTitle = title
142        .replace(/</g, '&lt;')
143        .replace(/>/g, '&gt;')
144        .replace(/"/g, '&quot;');
145    
146    const input = document.getElementById('searchInput');
147    if (input) {
148        input.value = safeTitle;
149        
150        // 聚焦搜索框,便于用户立即使用键盘操作
151        input.focus();
152        
153        // 显示一个提示,告知用户点击搜索按钮进行搜索
154        showToast('已填充搜索内容,点击搜索按钮开始搜索', 'info');
155    }
156}
157
158// 填充搜索框并执行搜索
159function fillAndSearch(title) {
160    if (!title) return;
161    
162    // 安全处理标题,防止XSS
163    const safeTitle = title
164        .replace(/</g, '&lt;')
165        .replace(/>/g, '&gt;')
166        .replace(/"/g, '&quot;');
167    
168    const input = document.getElementById('searchInput');
169    if (input) {
170        input.value = safeTitle;
171        search(); // 使用已有的search函数执行搜索
172        
173        // 同时更新浏览器URL,使其反映当前的搜索状态
174        try {
175            // 使用URI编码确保特殊字符能够正确显示
176            const encodedQuery = encodeURIComponent(safeTitle);
177            // 使用HTML5 History API更新URL,不刷新页面
178            window.history.pushState(
179                { search: safeTitle }, 
180                `搜索: ${safeTitle} - LibreTV`, 
181                `/s=${encodedQuery}`
182            );
183            // 更新页面标题
184            document.title = `搜索: ${safeTitle} - LibreTV`;
185        } catch (e) {
186            console.error('更新浏览器历史失败:', e);
187        }
188    }
189}
190
191// 填充搜索框,确保豆瓣资源API被选中,然后执行搜索
192async function fillAndSearchWithDouban(title) {
193    if (!title) return;
194    
195    // 安全处理标题,防止XSS
196    const safeTitle = title
197        .replace(/</g, '&lt;')
198        .replace(/>/g, '&gt;')
199        .replace(/"/g, '&quot;');
200    
201    // 确保豆瓣资源API被选中
202    if (typeof selectedAPIs !== 'undefined' && !selectedAPIs.includes('dbzy')) {
203        // 在设置中勾选豆瓣资源API复选框
204        const doubanCheckbox = document.querySelector('input[id="api_dbzy"]');
205        if (doubanCheckbox) {
206            doubanCheckbox.checked = true;
207            
208            // 触发updateSelectedAPIs函数以更新状态
209            if (typeof updateSelectedAPIs === 'function') {
210                updateSelectedAPIs();
211            } else {
212                // 如果函数不可用,则手动添加到selectedAPIs
213                selectedAPIs.push('dbzy');
214                localStorage.setItem('selectedAPIs', JSON.stringify(selectedAPIs));
215                
216                // 更新选中API计数(如果有这个元素)
217                const countEl = document.getElementById('selectedAPICount');
218                if (countEl) {
219                    countEl.textContent = selectedAPIs.length;
220                }
221            }
222            
223            showToast('已自动选择豆瓣资源API', 'info');
224        }
225    }
226    
227    // 填充搜索框并执行搜索
228    const input = document.getElementById('searchInput');
229    if (input) {
230        input.value = safeTitle;
231        await search(); // 使用已有的search函数执行搜索
232        
233        // 更新浏览器URL,使其反映当前的搜索状态
234        try {
235            // 使用URI编码确保特殊字符能够正确显示
236            const encodedQuery = encodeURIComponent(safeTitle);
237            // 使用HTML5 History API更新URL,不刷新页面
238            window.history.pushState(
239                { search: safeTitle }, 
240                `搜索: ${safeTitle} - LibreTV`, 
241                `/s=${encodedQuery}`
242            );
243            // 更新页面标题
244            document.title = `搜索: ${safeTitle} - LibreTV`;
245        } catch (e) {
246            console.error('更新浏览器历史失败:', e);
247        }
248
249        if (window.innerWidth <= 768) {
250          window.scrollTo({
251              top: 0,
252              behavior: 'smooth'
253          });
254        }
255    }
256}
257
258// 渲染电影/电视剧切换器
259function renderDoubanMovieTvSwitch() {
260    // 获取切换按钮元素
261    const movieToggle = document.getElementById('douban-movie-toggle');
262    const tvToggle = document.getElementById('douban-tv-toggle');
263
264    if (!movieToggle ||!tvToggle) return;
265
266    movieToggle.addEventListener('click', function() {
267        if (doubanMovieTvCurrentSwitch !== 'movie') {
268            // 更新按钮样式
269            movieToggle.classList.add('bg-pink-600', 'text-white');
270            movieToggle.classList.remove('text-gray-300');
271            
272            tvToggle.classList.remove('bg-pink-600', 'text-white');
273            tvToggle.classList.add('text-gray-300');
274            
275            doubanMovieTvCurrentSwitch = 'movie';
276            doubanCurrentTag = '热门';
277
278            // 重新加载豆瓣内容
279            renderDoubanTags(movieTags);
280
281            // 换一批按钮事件监听
282            setupDoubanRefreshBtn();
283            
284            // 初始加载热门内容
285            if (localStorage.getItem('doubanEnabled') === 'true') {
286                renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
287            }
288        }
289    });
290    
291    // 电视剧按钮点击事件
292    tvToggle.addEventListener('click', function() {
293        if (doubanMovieTvCurrentSwitch !== 'tv') {
294            // 更新按钮样式
295            tvToggle.classList.add('bg-pink-600', 'text-white');
296            tvToggle.classList.remove('text-gray-300');
297            
298            movieToggle.classList.remove('bg-pink-600', 'text-white');
299            movieToggle.classList.add('text-gray-300');
300            
301            doubanMovieTvCurrentSwitch = 'tv';
302            doubanCurrentTag = '热门';
303
304            // 重新加载豆瓣内容
305            renderDoubanTags(tvTags);
306
307            // 换一批按钮事件监听
308            setupDoubanRefreshBtn();
309            
310            // 初始加载热门内容
311            if (localStorage.getItem('doubanEnabled') === 'true') {
312                renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
313            }
314        }
315    });
316}
317
318// 渲染豆瓣标签选择器
319function renderDoubanTags(tags) {
320    const tagContainer = document.getElementById('douban-tags');
321    if (!tagContainer) return;
322    
323    // 确定当前应该使用的标签列表
324    const currentTags = doubanMovieTvCurrentSwitch === 'movie' ? movieTags : tvTags;
325    
326    // 清空标签容器
327    tagContainer.innerHTML = '';
328
329    // 先添加标签管理按钮
330    const manageBtn = document.createElement('button');
331    manageBtn.className = 'py-1.5 px-3.5 rounded text-sm font-medium transition-all duration-300 bg-[#1a1a1a] text-gray-300 hover:bg-pink-700 hover:text-white border border-[#333] hover:border-white';
332    manageBtn.innerHTML = '<span class="flex items-center"><svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
332<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>管理标签</span>';
333    manageBtn.onclick = function() {
334        showTagManageModal();
335    };
336    tagContainer.appendChild(manageBtn);
337
338    // 添加所有标签
339    currentTags.forEach(tag => {
340        const btn = document.createElement('button');
341        
342        // 设置样式
343        let btnClass = 'py-1.5 px-3.5 rounded text-sm font-medium transition-all duration-300 border ';
344        
345        // 当前选中的标签使用高亮样式
346        if (tag === doubanCurrentTag) {
347            btnClass += 'bg-pink-600 text-white shadow-md border-white';
348        } else {
349            btnClass += 'bg-[#1a1a1a] text-gray-300 hover:bg-pink-700 hover:text-white border-[#333] hover:border-white';
350        }
351        
352        btn.className = btnClass;
353        btn.textContent = tag;
354        
355        btn.onclick = function() {
356            if (doubanCurrentTag !== tag) {
357                doubanCurrentTag = tag;
358                doubanPageStart = 0;
359                renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
360                renderDoubanTags();
361            }
362        };
363        
364        tagContainer.appendChild(btn);
365    });
366}
367
368// 设置换一批按钮事件
369function setupDoubanRefreshBtn() {
370    // 修复ID,使用正确的ID douban-refresh 而不是 douban-refresh-btn
371    const btn = document.getElementById('douban-refresh');
372    if (!btn) return;
373    
374    btn.onclick = function() {
375        doubanPageStart += doubanPageSize;
376        if (doubanPageStart > 9 * doubanPageSize) {
377            doubanPageStart = 0;
378        }
379        
380        renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
381    };
382}
383
384function fetchDoubanTags() {
385    const movieTagsTarget = `https://movie.douban.com/j/search_tags?type=movie`
386    fetchDoubanData(movieTagsTarget)
387        .then(data => {
388            movieTags = data.tags;
389            if (doubanMovieTvCurrentSwitch === 'movie') {
390                renderDoubanTags(movieTags);
391            }
392        })
393        .catch(error => {
394            console.error("获取豆瓣热门电影标签失败:", error);
395        });
396    const tvTagsTarget = `https://movie.douban.com/j/search_tags?type=tv`
397    fetchDoubanData(tvTagsTarget)
398       .then(data => {
399            tvTags = data.tags;
400            if (doubanMovieTvCurrentSwitch === 'tv') {
401                renderDoubanTags(tvTags);
402            }
403        })
404       .catch(error => {
405            console.error("获取豆瓣热门电视剧标签失败:", error);
406        });
407}
408
409// 渲染热门推荐内容
410function renderRecommend(tag, pageLimit, pageStart) {
411    const container = document.getElementById("douban-results");
412    if (!container) return;
413
414    const loadingOverlayHTML = `
415        <div class="absolute inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-10">
416            <div class="flex items-center justify-center">
417                <div class="w-6 h-6 border-2 border-pink-500 border-t-transparent rounded-full animate-spin inline-block"></div>
418                <span class="text-pink-500 ml-4">加载中...</span>
419            </div>
420        </div>
421    `;
422
423    container.classList.add("relative");
424    container.insertAdjacentHTML('beforeend', loadingOverlayHTML);
425    
426    const target = `https://movie.douban.com/j/search_subjects?type=${doubanMovieTvCurrentSwitch}&tag=${tag}&sort=recommend&page_limit=${pageLimit}&page_start=${pageStart}`;
427    
428    // 使用通用请求函数
429    fetchDoubanData(target)
430        .then(data => {
431            renderDoubanCards(data, container);
432        })
433        .catch(error => {
434            console.error("获取豆瓣数据失败:", error);
435            container.innerHTML = `
436                <div class="col-span-full text-center py-8">
437                    <div class="text-red-400">❌ 获取豆瓣数据失败,请稍后重试</div>
438                    <div class="text-gray-500 text-sm mt-2">提示:使用VPN可能有助于解决此问题</div>
439                </div>
440            `;
441        });
442}
443
444async function fetchDoubanData(url) {
445    // 添加超时控制
446    const controller = new AbortController();
447    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
448    
449    // 设置请求选项,包括信号和头部
450    const fetchOptions = {
451        signal: controller.signal,
452        headers: {
453            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
454            'Referer': 'https://movie.douban.com/',
455            'Accept': 'application/json, text/plain, */*',
456        }
457    };
458
459    try {
460        // 尝试直接访问(豆瓣API可能允许部分CORS请求)
461        const response = await fetch(PROXY_URL + encodeURIComponent(url), fetchOptions);
462        clearTimeout(timeoutId);
463        
464        if (!response.ok) {
465            throw new Error(`HTTP error! Status: ${response.status}`);
466        }
467        
468        return await response.json();
469    } catch (err) {
470        console.error("豆瓣 API 请求失败(直接代理):", err);
471        
472        // 失败后尝试备用方法:作为备选
473        const fallbackUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
474        
475        try {
476            const fallbackResponse = await fetch(fallbackUrl);
477            
478            if (!fallbackResponse.ok) {
479                throw new Error(`备用API请求失败! 状态: ${fallbackResponse.status}`);
480            }
481            
482            const data = await fallbackResponse.json();
483            
484            // 解析原始内容
485            if (data && data.contents) {
486                return JSON.parse(data.contents);
487            } else {
488                throw new Error("无法获取有效数据");
489            }
490        } catch (fallbackErr) {
491            console.error("豆瓣 API 备用请求也失败:", fallbackErr);
492            throw fallbackErr; // 向上抛出错误,让调用者处理
493        }
494    }
495}
496
497// 抽取渲染豆瓣卡片的逻辑到单独函数
498function renderDoubanCards(data, container) {
499    // 创建文档片段以提高性能
500    const fragment = document.createDocumentFragment();
501    
502    // 如果没有数据
503    if (!data.subjects || data.subjects.length === 0) {
504        const emptyEl = document.createElement("div");
505        emptyEl.className = "col-span-full text-center py-8";
506        emptyEl.innerHTML = `
507            <div class="text-pink-500">❌ 暂无数据,请尝试其他分类或刷新</div>
508        `;
509        fragment.appendChild(emptyEl);
510    } else {
511        // 循环创建每个影视卡片
512        data.subjects.forEach(item => {
513            const card = document.createElement("div");
514            card.className = "bg-[#111] hover:bg-[#222] transition-all duration-300 rounded-lg overflow-hidden flex flex-col transform hover:scale-105 shadow-md hover:shadow-lg";
515            
516            // 生成卡片内容,确保安全显示(防止XSS)
517            const safeTitle = item.title
518                .replace(/</g, '&lt;')
519                .replace(/>/g, '&gt;')
520                .replace(/"/g, '&quot;');
521            
522            const safeRate = (item.rate || "暂无")
523                .replace(/</g, '&lt;')
524                .replace(/>/g, '&gt;');
525            
526            // 处理图片URL
527            // 1. 直接使用豆瓣图片URL (添加no-referrer属性)
528            const originalCoverUrl = item.cover;
529            
530            // 2. 也准备代理URL作为备选
531            const proxiedCoverUrl = PROXY_URL + encodeURIComponent(originalCoverUrl);
532            
533            // 为不同设备优化卡片布局
534            card.innerHTML = `
535                <div class="relative w-full aspect-[2/3] overflow-hidden cursor-pointer" onclick="fillAndSearchWithDouban('${safeTitle}')">
536                    <img src="${originalCoverUrl}" alt="${safeTitle}" 
537                        class="w-full h-full object-cover transition-transform duration-500 hover:scale-110"
538                        onerror="this.onerror=null; this.src='${proxiedCoverUrl}'; this.classList.add('object-contain');"
539                        loading="lazy" referrerpolicy="no-referrer">
540                    <div class="absolute inset-0 bg-gradient-to-t from-black to-transparent opacity-60"></div>
541                    <div class="absolute bottom-2 left-2 bg-black/70 text-white text-xs px-2 py-1 rounded-sm">
542                        <span class="text-yellow-400">★</span> ${safeRate}
543                    </div>
544                    <div class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded-sm hover:bg-[#333] transition-colors">
545                        <a href="${item.url}" target="_blank" rel="noopener noreferrer" title="在豆瓣查看" onclick="event.stopPropagation();">
546                            🔗
547                        </a>
548                    </div>
549                </div>
550                <div class="p-2 text-center bg-[#111]">
551                    <button onclick="fillAndSearchWithDouban('${safeTitle}')" 
552                            class="text-sm font-medium text-white truncate w-full hover:text-pink-400 transition"
553                            title="${safeTitle}">
554                        ${safeTitle}
555                    </button>
556                </div>
557            `;
558            
559            fragment.appendChild(card);
560        });
561    }
562    
563    // 清空并添加所有新元素
564    container.innerHTML = "";
565    container.appendChild(fragment);
566}
567
568// 重置到首页
569function resetToHome() {
570    resetSearchArea();
571    updateDoubanVisibility();
572}
573
574// 加载豆瓣首页内容
575document.addEventListener('DOMContentLoaded', initDouban);
576
577// 显示标签管理模态框
578function showTagManageModal() {
579    // 确保模态框在页面上只有一个实例
580    let modal = document.getElementById('tagManageModal');
581    if (modal) {
582        document.body.removeChild(modal);
583    }
584    
585    // 创建模态框元素
586    modal = document.createElement('div');
587    modal.id = 'tagManageModal';
588    modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40';
589    
590    // 当前使用的标签类型和默认标签
591    const isMovie = doubanMovieTvCurrentSwitch === 'movie';
592    const currentTags = isMovie ? movieTags : tvTags;
593    const defaultTags = isMovie ? defaultMovieTags : defaultTvTags;
594    
595    // 模态框内容
596    modal.innerHTML = `
597        <div class="bg-[#191919] rounded-lg p-6 max-w-md w-full max-h-[90vh] overflow-y-auto relative">
598            <button id="closeTagModal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-xl">&times;</button>
599            
600            <h3 class="text-xl font-bold text-white mb-4">标签管理 (${isMovie ? '电影' : '电视剧'})</h3>
601            
602            <div class="mb-4">
603                <div class="flex justify-between items-center mb-2">
604                    <h4 class="text-lg font-medium text-gray-300">标签列表</h4>
605                    <button id="resetTagsBtn" class="text-xs px-2 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded">
606                        恢复默认标签
607                    </button>
608                </div>
609                <div class="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-4" id="tagsGrid">
610                    ${currentTags.length ? currentTags.map(tag => {
611                        // "热门"标签不能删除
612                        const canDelete = tag !== '热门';
613                        return `
614                            <div class="bg-[#1a1a1a] text-gray-300 py-1.5 px-3 rounded text-sm font-medium flex justify-between items-center group">
615                                <span>${tag}</span>
616                                ${canDelete ? 
617                                    `<button class="delete-tag-btn text-gray-500 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity" 
618                                        data-tag="${tag}">✕</button>` : 
619                                    `<span class="text-gray-500 text-xs italic opacity-0 group-hover:opacity-100">必需</span>`
620                                }
621                            </div>
622                        `;
623                    }).join('') : 
624                    `<div class="col-span-full text-center py-4 text-gray-500">无标签,请添加或恢复默认</div>`}
625                </div>
626            </div>
627            
628            <div class="border-t border-gray-700 pt-4">
629                <h4 class="text-lg font-medium text-gray-300 mb-3">添加新标签</h4>
630                <form id="addTagForm" class="flex items-center">
631                    <input type="text" id="newTagInput" placeholder="输入标签名称..." 
632                           class="flex-1 bg-[#222] text-white border border-gray-700 rounded px-3 py-2 focus:outline-none focus:border-pink-500">
633                    <button type="submit" class="ml-2 bg-pink-600 hover:bg-pink-700 text-white px-4 py-2 rounded">添加</button>
634                </form>
635                <p class="text-xs text-gray-500 mt-2">提示:标签名称不能为空,不能重复,不能包含特殊字符</p>
636            </div>
637        </div>
638    `;
639    
640    // 添加模态框到页面
641    document.body.appendChild(modal);
642    
643    // 焦点放在输入框上
644    setTimeout(() => {
645        document.getElementById('newTagInput').focus();
646    }, 100);
647    
648    // 添加事件监听器 - 关闭按钮
649    document.getElementById('closeTagModal').addEventListener('click', function() {
650        document.body.removeChild(modal);
651    });
652    
653    // 添加事件监听器 - 点击模态框外部关闭
654    modal.addEventListener('click', function(e) {
655        if (e.target === modal) {
656            document.body.removeChild(modal);
657        }
658    });
659    
660    // 添加事件监听器 - 恢复默认标签按钮
661    document.getElementById('resetTagsBtn').addEventListener('click', function() {
662        resetTagsToDefault();
663        showTagManageModal(); // 重新加载模态框
664    });
665    
666    // 添加事件监听器 - 删除标签按钮
667    const deleteButtons = document.querySelectorAll('.delete-tag-btn');
668    deleteButtons.forEach(btn => {
669        btn.addEventListener('click', function() {
670            const tagToDelete = this.getAttribute('data-tag');
671            deleteTag(tagToDelete);
672            showTagManageModal(); // 重新加载模态框
673        });
674    });
675    
676    // 添加事件监听器 - 表单提交
677    document.getElementById('addTagForm').addEventListener('submit', function(e) {
678        e.preventDefault();
679        const input = document.getElementById('newTagInput');
680        const newTag = input.value.trim();
681        
682        if (newTag) {
683            addTag(newTag);
684            input.value = '';
685            showTagManageModal(); // 重新加载模态框
686        }
687    });
688}
689
690// 添加标签
691function addTag(tag) {
692    // 安全处理标签名,防止XSS
693    const safeTag = tag
694        .replace(/</g, '&lt;')
695        .replace(/>/g, '&gt;')
696        .replace(/"/g, '&quot;');
697    
698    // 确定当前使用的是电影还是电视剧标签
699    const isMovie = doubanMovieTvCurrentSwitch === 'movie';
700    const currentTags = isMovie ? movieTags : tvTags;
701    
702    // 检查是否已存在(忽略大小写)
703    const exists = currentTags.some(
704        existingTag => existingTag.toLowerCase() === safeTag.toLowerCase()
705    );
706    
707    if (exists) {
708        showToast('标签已存在', 'warning');
709        return;
710    }
711    
712    // 添加到对应的标签数组
713    if (isMovie) {
714        movieTags.push(safeTag);
715    } else {
716        tvTags.push(safeTag);
717    }
718    
719    // 保存到本地存储
720    saveUserTags();
721    
722    // 重新渲染标签
723    renderDoubanTags();
724    
725    showToast('标签添加成功', 'success');
726}
727
728// 删除标签
729function deleteTag(tag) {
730    // 热门标签不能删除
731    if (tag === '热门') {
732        showToast('热门标签不能删除', 'warning');
733        return;
734    }
735    
736    // 确定当前使用的是电影还是电视剧标签
737    const isMovie = doubanMovieTvCurrentSwitch === 'movie';
738    const currentTags = isMovie ? movieTags : tvTags;
739    
740    // 寻找标签索引
741    const index = currentTags.indexOf(tag);
742    
743    // 如果找到标签,则删除
744    if (index !== -1) {
745        currentTags.splice(index, 1);
746        
747        // 保存到本地存储
748        saveUserTags();
749        
750        // 如果当前选中的是被删除的标签,则重置为"热门"
751        if (doubanCurrentTag === tag) {
752            doubanCurrentTag = '热门';
753            doubanPageStart = 0;
754            renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
755        }
756        
757        // 重新渲染标签
758        renderDoubanTags();
759        
760        showToast('标签删除成功', 'success');
761    }
762}
763
764// 重置为默认标签
765function resetTagsToDefault() {
766    // 确定当前使用的是电影还是电视剧
767    const isMovie = doubanMovieTvCurrentSwitch === 'movie';
768    
769    // 重置为默认标签
770    if (isMovie) {
771        movieTags = [...defaultMovieTags];
772    } else {
773        tvTags = [...defaultTvTags];
774    }
775    
776    // 设置当前标签为热门
777    doubanCurrentTag = '热门';
778    doubanPageStart = 0;
779    
780    // 保存到本地存储
781    saveUserTags();
782    
783    // 重新渲染标签和内容
784    renderDoubanTags();
785    renderRecommend(doubanCurrentTag, doubanPageSize, doubanPageStart);
786    
787    showToast('已恢复默认标签', 'success');
788}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.