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': '其他' };