document.addEventListener('DOMContentLoaded', () => { let announcementsData = []; const timeline = document.getElementById('announcements-timeline'); const noResults = document.getElementById('no-results'); const categoryFilters = document.getElementById('category-filters'); const searchInput = document.getElementById('announcement-search'); let currentFilters = { category: 'all', search: '' }; let editModeEnabled = false; let currentEditItem = null; // ========== Secret "edit" keyboard shortcut ========== let secretBuffer = ''; document.addEventListener('keydown', (e) => { // Ignore if user is typing in an input/textarea if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; secretBuffer += e.key.toLowerCase(); // Keep only last 4 characters if (secretBuffer.length > 4) { secretBuffer = secretBuffer.slice(-4); } if (secretBuffer === 'edit') { editModeEnabled = !editModeEnabled; secretBuffer = ''; toggleEditButtons(editModeEnabled); if (editModeEnabled) { console.log('%c[公告管理] 编辑模式已启用', 'color: #34c759; font-weight: bold; font-size: 14px;'); console.log('%c再次输入 "edit" 可隐藏编辑按钮', 'color: #86868b;'); } else { console.log('%c[公告管理] 编辑模式已隐藏', 'color: #f59e0b; font-weight: bold; font-size: 14px;'); } } }); // Log hint on page load console.log('%c[公告管理] 提示:在页面中键入 "edit" 可显示编辑按钮', 'color: #0071e3; font-weight: bold; font-size: 13px;'); function toggleEditButtons(show) { document.querySelectorAll('.edit-hidden').forEach(el => { if (show) { el.classList.remove('edit-hidden'); el.classList.add('edit-visible'); } else { el.classList.remove('edit-visible'); el.classList.add('edit-hidden'); } }); } // ========== Fetch Data ========== fetch('data/announcements.json') .then(response => response.json()) .then(data => { announcementsData = data; // Sort by time descending (newest first) announcementsData.sort((a, b) => new Date(b.time) - new Date(a.time)); renderTimeline(); }) .catch(err => { console.error('Error loading announcements:', err); timeline.innerHTML = '
无法加载公告数据。
'; }); // ========== Event Listeners ========== categoryFilters.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON') { Array.from(categoryFilters.children).forEach(btn => btn.classList.remove('active')); e.target.classList.add('active'); currentFilters.category = e.target.dataset.filter; renderTimeline(); } }); searchInput.addEventListener('input', (e) => { currentFilters.search = e.target.value.toLowerCase().trim(); renderTimeline(); }); // ========== Render ========== function renderTimeline() { timeline.innerHTML = ''; const filtered = announcementsData.filter(item => { const matchCategory = currentFilters.category === 'all' || item.category === currentFilters.category; const matchSearch = !currentFilters.search || item.title.toLowerCase().includes(currentFilters.search) || item.intro.toLowerCase().includes(currentFilters.search); return matchCategory && matchSearch; }); if (filtered.length === 0) { noResults.classList.remove('is-hidden'); return; } else { noResults.classList.add('is-hidden'); } filtered.forEach((item, index) => { const timelineItem = document.createElement('div'); timelineItem.className = 'timeline-item category-' + item.category; const card = document.createElement('div'); card.className = 'announcement-card'; // Expand the first (newest) item by default if (index === 0) { card.classList.add('expanded'); } const categoryBadgeClass = getCategoryBadgeClass(item.category); const categoryText = getCategoryText(item.category); const categoryIcon = getCategoryIcon(item.category); // Summary const summary = document.createElement('div'); summary.className = 'card-summary'; summary.innerHTML = `${escapeHtml(item.intro)}
暂无内容
'; return; } blocks.forEach(block => { if (block.type === 'text') { const p = document.createElement('p'); p.innerText = block.content; container.appendChild(p); } else if (block.type === 'image') { const img = document.createElement('img'); img.src = block.content; img.loading = 'lazy'; container.appendChild(img); } else if (block.type === 'video') { const bv = parseBVNumber(block.content); if (bv) { const wrapper = document.createElement('div'); wrapper.className = 'video-embed-wrapper'; const iframe = document.createElement('iframe'); iframe.src = 'https://player.bilibili.com/player.html?bvid=' + bv + '&autoplay=0&high_quality=1'; iframe.allowFullscreen = true; iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups'); iframe.loading = 'lazy'; wrapper.appendChild(iframe); container.appendChild(wrapper); } else { const p = document.createElement('p'); p.className = 'text-secondary'; p.innerText = '无效的视频 BV 号'; container.appendChild(p); } } }); } // ========== Helpers ========== function getCategoryText(cat) { const map = { 'activity': '活动', 'maintenance': '维护', 'other': '其他' }; return map[cat] || cat; } function getCategoryIcon(cat) { const map = { 'activity': 'fa-calendar-check', 'maintenance': 'fa-wrench', 'other': 'fa-info-circle' }; return map[cat] || 'fa-info-circle'; } function getCategoryBadgeClass(cat) { const map = { 'activity': 'badge-activity', 'maintenance': 'badge-maintenance', 'other': 'badge-other' }; return map[cat] || 'badge-other'; } function parseBVNumber(input) { if (!input) return null; input = input.trim(); var bvPattern = /^(BV[A-Za-z0-9]+)$/; var directMatch = input.match(bvPattern); if (directMatch) return directMatch[1]; var urlPattern = /bilibili\.com\/video\/(BV[A-Za-z0-9]+)/; var urlMatch = input.match(urlPattern); if (urlMatch) return urlMatch[1]; var generalPattern = /(BV[A-Za-z0-9]{10,})/; var generalMatch = input.match(generalPattern); if (generalMatch) return generalMatch[1]; return null; } function escapeHtml(text) { if (!text) return ''; var div = document.createElement('div'); div.appendChild(document.createTextNode(text)); return div.innerHTML; } // ========== Editor Modal ========== const editorModal = document.getElementById('editor-modal'); const jsonOutputModal = document.getElementById('json-output-modal'); const closeEditorModal = document.querySelector('.close-editor-modal'); const closeJsonModal = document.querySelector('.close-json-modal'); document.getElementById('btn-add-announcement').addEventListener('click', () => { openEditor(null); }); closeEditorModal.addEventListener('click', () => { editorModal.style.display = 'none'; document.body.style.overflow = 'auto'; }); window.addEventListener('click', (e) => { if (e.target === editorModal) { editorModal.style.display = 'none'; document.body.style.overflow = 'auto'; } if (e.target === jsonOutputModal) { jsonOutputModal.style.display = 'none'; } }); closeJsonModal.addEventListener('click', () => { jsonOutputModal.style.display = 'none'; }); let editorContentBlocks = []; // Custom select init document.querySelectorAll('.custom-select').forEach(select => { const trigger = select.querySelector('.custom-select-trigger'); const options = select.querySelectorAll('.custom-option'); const input = select.querySelector('input[type="hidden"]'); const text = select.querySelector('.custom-select-text'); trigger.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = select.classList.contains('open'); document.querySelectorAll('.custom-select').forEach(s => s.classList.remove('open')); if (!isOpen) { select.classList.add('open'); } }); options.forEach(option => { option.addEventListener('click', (e) => { e.stopPropagation(); options.forEach(opt => opt.classList.remove('selected')); option.classList.add('selected'); text.innerText = option.innerText; input.value = option.dataset.value; input.dispatchEvent(new Event('change')); select.classList.remove('open'); }); }); }); document.addEventListener('click', () => { document.querySelectorAll('.custom-select').forEach(s => s.classList.remove('open')); }); function setCustomSelectValue(id, value) { var input = document.getElementById(id); if (!input) return; var select = input.closest('.custom-select'); var option = select.querySelector('.custom-option[data-value="' + value + '"]'); if (option) { input.value = value; select.querySelector('.custom-select-text').innerText = option.innerText; select.querySelectorAll('.custom-option').forEach(opt => opt.classList.remove('selected')); option.classList.add('selected'); } } function openEditor(item) { currentEditItem = item; editorContentBlocks = item ? item.content.map(c => ({...c})) : []; document.getElementById('editor-title').value = item ? item.title : ''; document.getElementById('editor-intro').value = item ? item.intro : ''; document.getElementById('editor-time').value = item ? item.time : new Date().toISOString().slice(0, 10); setCustomSelectValue('editor-category', item ? item.category : 'activity'); renderSortableList('editor-content-list', editorContentBlocks); updatePreview(); editorModal.style.display = 'block'; document.body.style.overflow = 'hidden'; } // ========== Sortable List (drag-and-drop) ========== let dragState = { listId: null, fromIdx: null }; function renderSortableList(listId, items) { var container = document.getElementById(listId); container.innerHTML = ''; items.forEach((item, idx) => { var div = document.createElement('div'); div.className = 'sortable-item'; div.draggable = true; div.dataset.idx = idx; div.dataset.listId = listId; var typeBadgeClass = item.type === 'text' ? 'badge-text' : item.type === 'image' ? 'badge-image' : 'badge-video'; var typeBadgeLabel = item.type === 'text' ? '文字' : item.type === 'image' ? '图片' : '视频'; var contentHtml; if (item.type === 'text') { contentHtml = ''; } else if (item.type === 'image') { contentHtml = ''; } else { contentHtml = ''; } div.innerHTML = '' + '' + typeBadgeLabel + '' + contentHtml + ''; container.appendChild(div); div.addEventListener('dragstart', onDragStart); div.addEventListener('dragover', onDragOver); div.addEventListener('dragenter', onDragEnter); div.addEventListener('dragleave', onDragLeave); div.addEventListener('drop', onDrop); div.addEventListener('dragend', onDragEnd); var contentEl = div.querySelector('.item-content'); contentEl.addEventListener('input', () => { items[idx].content = contentEl.value; updatePreview(); }); div.querySelector('.remove-item-btn').addEventListener('click', () => { items.splice(idx, 1); renderSortableList(listId, items); updatePreview(); }); }); } function onDragStart(e) { var item = e.target.closest('.sortable-item'); if (!item) return; dragState.listId = item.dataset.listId; dragState.fromIdx = parseInt(item.dataset.idx); item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); } function onDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } function onDragEnter(e) { var item = e.target.closest('.sortable-item'); if (item && item.dataset.listId === dragState.listId) item.classList.add('drag-over'); } function onDragLeave(e) { var item = e.target.closest('.sortable-item'); if (item) item.classList.remove('drag-over'); } function onDrop(e) { e.preventDefault(); var item = e.target.closest('.sortable-item'); if (!item || item.dataset.listId !== dragState.listId) return; var toIdx = parseInt(item.dataset.idx); var fromIdx = dragState.fromIdx; if (fromIdx === toIdx) return; var moved = editorContentBlocks.splice(fromIdx, 1)[0]; editorContentBlocks.splice(toIdx, 0, moved); renderSortableList('editor-content-list', editorContentBlocks); updatePreview(); } function onDragEnd() { document.querySelectorAll('.sortable-item').forEach(el => el.classList.remove('dragging', 'drag-over')); dragState = { listId: null, fromIdx: null }; } // Add content buttons document.querySelectorAll('.add-item-btn').forEach(btn => { btn.addEventListener('click', () => { var type = btn.dataset.type; editorContentBlocks.push({ type: type, content: '' }); renderSortableList('editor-content-list', editorContentBlocks); updatePreview(); }); }); // Live Preview ['editor-title', 'editor-intro', 'editor-time', 'editor-category'].forEach(id => { document.getElementById(id).addEventListener('input', updatePreview); document.getElementById(id).addEventListener('change', updatePreview); }); function updatePreview() { var preview = document.getElementById('editor-preview-area'); var title = document.getElementById('editor-title').value || '未命名公告'; var intro = document.getElementById('editor-intro').value || '暂无简介'; var time = document.getElementById('editor-time').value || '未设定'; var category = document.getElementById('editor-category').value; var categoryText = getCategoryText(category); var categoryIcon = getCategoryIcon(category); var badgeClass = getCategoryBadgeClass(category); var html = '' + escapeHtml(intro) + '
'; html += '' + (escapeHtml(block.content) || '空文字') + '
'; } else if (block.type === 'image') { html += block.content ? '空图片
'; } else if (block.type === 'video') { html += renderVideoPreviewHtml(block.content); } }); } else { html += '暂无正文内容
'; } html += '请输入有效的 BV 号或 bilibili 视频地址
'; } // ========== Save / Generate JSON ========== document.getElementById('btn-save-announcement').addEventListener('click', () => { var title = document.getElementById('editor-title').value.trim(); if (!title) { alert('请填写公告标题'); document.getElementById('editor-title').focus(); return; } var announcementObj = { title: title, intro: document.getElementById('editor-intro').value.trim(), time: document.getElementById('editor-time').value, category: document.getElementById('editor-category').value, content: editorContentBlocks.filter(i => i.content.trim() !== '').map(i => { if (i.type === 'video') { return { type: 'video', content: parseBVNumber(i.content) || i.content }; } return { ...i }; }) }; var jsonStr = JSON.stringify(announcementObj, null, 4); document.getElementById('json-output').value = jsonStr; jsonOutputModal.style.display = 'block'; }); // Copy JSON document.getElementById('btn-copy-json').addEventListener('click', () => { var textArea = document.getElementById('json-output'); textArea.select(); textArea.setSelectionRange(0, 99999); navigator.clipboard.writeText(textArea.value).then(() => { var btn = document.getElementById('btn-copy-json'); var originalHTML = btn.innerHTML; btn.innerHTML = ' 已复制!'; btn.style.background = '#34c759'; setTimeout(() => { btn.innerHTML = originalHTML; btn.style.background = ''; }, 2000); }).catch(() => { document.execCommand('copy'); alert('已复制到剪贴板'); }); }); });