diff --git a/css/pages/announcements.css b/css/pages/announcements.css index 7480ff2..aef3d31 100644 --- a/css/pages/announcements.css +++ b/css/pages/announcements.css @@ -383,12 +383,41 @@ border: none; } -.detail-edit-btn-row { +.detail-action-btn-row { margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(0,0,0,0.05); display: flex; justify-content: flex-end; + gap: 10px; +} + +.btn-share-announcement { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 16px; + background: transparent; + color: var(--text-secondary); + border: 1.5px solid rgba(0,0,0,0.12); + border-radius: 18px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; +} + +.btn-share-announcement:hover { + color: var(--accent-color); + border-color: var(--accent-color); + background: rgba(0,113,227,0.04); +} + +.btn-share-announcement.shared { + color: #15803d; + border-color: #34c759; + background: #e8fceb; } .btn-edit-announcement { diff --git a/js/announcements_script.js b/js/announcements_script.js index 8c7cb42..e19f42c 100644 --- a/js/announcements_script.js +++ b/js/announcements_script.js @@ -59,6 +59,7 @@ document.addEventListener('DOMContentLoaded', () => { // Sort by time descending (newest first) announcementsData.sort((a, b) => new Date(b.time) - new Date(a.time)); renderTimeline(); + handleHashNavigation(); }) .catch(err => { console.error('Error loading announcements:', err); @@ -100,8 +101,10 @@ document.addEventListener('DOMContentLoaded', () => { } filtered.forEach((item, index) => { + const anchorId = generateAnchorId(item); const timelineItem = document.createElement('div'); timelineItem.className = 'timeline-item category-' + item.category; + timelineItem.id = anchorId; const card = document.createElement('div'); card.className = 'announcement-card'; @@ -148,20 +151,52 @@ document.addEventListener('DOMContentLoaded', () => { detailInner.className = 'detail-content'; renderContentBlocks(detailInner, item.content); - // Edit button inside detail (hidden by default) - const editRow = document.createElement('div'); - editRow.className = 'detail-edit-btn-row ' + (editModeEnabled ? 'edit-visible' : 'edit-hidden'); + // Action buttons row inside detail + const actionRow = document.createElement('div'); + actionRow.className = 'detail-action-btn-row'; + + // Share button + const shareBtn = document.createElement('button'); + shareBtn.className = 'btn-share-announcement'; + shareBtn.innerHTML = ' 分享'; + shareBtn.addEventListener('click', (e) => { + e.stopPropagation(); + var url = location.origin + location.pathname + '#' + anchorId; + navigator.clipboard.writeText(url).then(() => { + shareBtn.innerHTML = ' 已复制链接'; + shareBtn.classList.add('shared'); + setTimeout(() => { + shareBtn.innerHTML = ' 分享'; + shareBtn.classList.remove('shared'); + }, 2000); + }).catch(() => { + // Fallback: use a temporary input + var tmp = document.createElement('input'); + tmp.value = url; + document.body.appendChild(tmp); + tmp.select(); + document.execCommand('copy'); + document.body.removeChild(tmp); + shareBtn.innerHTML = ' 已复制链接'; + setTimeout(() => { + shareBtn.innerHTML = ' 分享'; + }, 2000); + }); + }); + actionRow.appendChild(shareBtn); + + // Edit button (hidden by default) const editBtn = document.createElement('button'); - editBtn.className = 'btn-edit-announcement'; + editBtn.className = 'btn-edit-announcement ' + (editModeEnabled ? 'edit-visible' : 'edit-hidden'); editBtn.innerHTML = ' 编辑'; editBtn.addEventListener('click', (e) => { e.stopPropagation(); openEditor(item); }); - editRow.appendChild(editBtn); + actionRow.appendChild(editBtn); detail.appendChild(detailInner); - detail.appendChild(editRow); + detail.appendChild(actionRow); card.appendChild(summary); card.appendChild(detail); @@ -208,6 +243,36 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // ========== Generate stable ID for announcement ========== + function generateAnchorId(item) { + // Use title + time to create a stable hash-friendly ID + var raw = (item.time || '') + '_' + (item.title || ''); + var hash = 0; + for (var i = 0; i < raw.length; i++) { + hash = ((hash << 5) - hash) + raw.charCodeAt(i); + hash |= 0; + } + return 'a' + Math.abs(hash).toString(36); + } + + // ========== Handle URL hash on load ========== + function handleHashNavigation() { + var hash = location.hash.replace('#', ''); + if (!hash) return; + var target = document.getElementById(hash); + if (!target) return; + // Expand this card + var card = target.querySelector('.announcement-card'); + if (card) { + timeline.querySelectorAll('.announcement-card.expanded').forEach(function(c) { c.classList.remove('expanded'); }); + card.classList.add('expanded'); + } + // Scroll into view with a small delay for layout + setTimeout(function() { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 100); + } + // ========== Helpers ========== function getCategoryText(cat) { const map = { 'activity': '活动', 'maintenance': '维护', 'other': '其他' };