diff --git a/announcements.html b/announcements.html new file mode 100644 index 0000000..5c9832a --- /dev/null +++ b/announcements.html @@ -0,0 +1,189 @@ + + +
+ + +无法加载公告数据。
'; + }); + + // ========== 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('已复制到剪贴板'); + }); + }); +}); diff --git a/js/components.js b/js/components.js index d40b998..5872ae3 100644 --- a/js/components.js +++ b/js/components.js @@ -15,6 +15,7 @@ const Components = { 文档 地图 设施 + 公告 相册 数据 赞助 @@ -32,6 +33,7 @@ const Components = { 文档 地图 设施 + 公告 相册 数据 赞助 diff --git a/sitemap.xml b/sitemap.xml index b4ca4d0..309325f 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -68,4 +68,12 @@