From 588fd66bb535e7715b9278018eec00d0ce935f7a Mon Sep 17 00:00:00 2001 From: zhangyuheng Date: Tue, 10 Mar 2026 15:05:41 +0800 Subject: [PATCH] feat: add announcements feature with JSON data and interactive UI - Created a new JSON file for announcements containing activity and maintenance updates. - Implemented a JavaScript module to fetch, filter, and display announcements dynamically. - Added functionality for editing announcements with a modal interface and live preview. - Included drag-and-drop support for content blocks within the announcement editor. - Enhanced user experience with category filtering and search capabilities. --- announcements.html | 189 ++++++ css/pages/announcements.css | 1095 +++++++++++++++++++++++++++++++++++ data/announcements.json | 86 +++ js/announcements_script.js | 540 +++++++++++++++++ js/components.js | 2 + sitemap.xml | 8 + 6 files changed, 1920 insertions(+) create mode 100644 announcements.html create mode 100644 css/pages/announcements.css create mode 100644 data/announcements.json create mode 100644 js/announcements_script.js diff --git a/announcements.html b/announcements.html new file mode 100644 index 0000000..5c9832a --- /dev/null +++ b/announcements.html @@ -0,0 +1,189 @@ + + + + + + 活动公告 - 白鹿原 Minecraft 服务器 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+

公告列表

+ +
+ +
+ +
+
+
类别
+
+ + + + +
+
+
+
+ + +
+ +
+ + +
+ + + + + + + + + + + + diff --git a/css/pages/announcements.css b/css/pages/announcements.css new file mode 100644 index 0000000..7480ff2 --- /dev/null +++ b/css/pages/announcements.css @@ -0,0 +1,1095 @@ +/* Page-Specific Styles for Announcements */ + +.announcements-hero-bg { + background-image: url('https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png'); +} + +/* Container */ +.announcements-container { + max-width: 900px; + margin: 0 auto; + padding: 40px 20px; +} + +/* Controls */ +.controls-section { + background: var(--card-bg); + padding: 30px; + border-radius: var(--radius-large); + box-shadow: 0 4px 20px rgba(0,0,0,0.05); + margin-bottom: 40px; +} + +.controls-header-row { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 20px; +} + +.title-with-action { + display: flex; + align-items: center; + gap: 16px; +} + +.section-title { + font-size: 24px; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.btn-add-announcement { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 18px; + background: var(--accent-color); + color: #fff; + border: none; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; +} + +.btn-add-announcement:hover { + background: #005bb5; + transform: translateY(-1px); +} + +.search-box { + position: relative; + max-width: 400px; + width: 100%; +} + +.search-box i { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); +} + +.search-box input { + width: 100%; + padding: 10px 16px 10px 44px; + border: 1px solid rgba(0,0,0,0.1); + border-radius: 12px; + font-size: 15px; + background: #f5f5f7; + transition: var(--transition); +} + +.search-box input:focus { + outline: none; + background: #fff; + border-color: var(--accent-color); + box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1); +} + +.filters-wrapper { + display: flex; + flex-direction: column; + gap: 16px; + border-top: 1px solid rgba(0,0,0,0.05); + padding-top: 20px; + margin-top: 24px; +} + +.filter-group { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; +} + +.filter-label { + font-weight: 600; + font-size: 14px; + color: var(--text-secondary); + min-width: 70px; + display: flex; + align-items: center; + gap: 6px; +} + +.filter-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.filter-tag { + background: #fff; + border: 1px solid rgba(0,0,0,0.1); + padding: 6px 14px; + border-radius: 18px; + font-size: 13px; + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 6px; +} + +.filter-tag:hover { + background: #f5f5f7; + color: var(--text-primary); + border-color: rgba(0,0,0,0.2); +} + +.filter-tag.active { + background: var(--text-primary); + color: white; + border-color: var(--text-primary); +} + +/* ========== Timeline ========== */ +.timeline { + position: relative; + padding-left: 32px; +} + +.timeline::before { + content: ''; + position: absolute; + left: 7px; + top: 0; + bottom: 0; + width: 2px; + background: linear-gradient(to bottom, var(--accent-color), rgba(0,113,227,0.1)); + border-radius: 2px; +} + +.timeline-item { + position: relative; + margin-bottom: 24px; +} + +.timeline-item::before { + content: ''; + position: absolute; + left: -32px; + top: 28px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + border: 3px solid var(--accent-color); + z-index: 1; + transition: var(--transition); +} + +.timeline-item.category-activity::before { + border-color: var(--brand-green); +} + +.timeline-item.category-maintenance::before { + border-color: #f59e0b; +} + +.timeline-item.category-other::before { + border-color: #8b5cf6; +} + +/* Card */ +.announcement-card { + background: var(--card-bg); + border-radius: var(--radius-medium); + box-shadow: 0 2px 12px rgba(0,0,0,0.04); + border: 1px solid rgba(0,0,0,0.03); + overflow: hidden; + transition: var(--transition); + cursor: pointer; +} + +.announcement-card:hover { + box-shadow: 0 8px 28px rgba(0,0,0,0.08); + transform: translateY(-2px); +} + +.announcement-card.expanded { + cursor: default; + transform: none; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); + border-color: rgba(0,0,0,0.06); +} + +.card-summary { + padding: 24px 28px; + display: flex; + align-items: center; + gap: 16px; +} + +.announcement-card.expanded .card-summary { + border-bottom: 1px solid rgba(0,0,0,0.05); + background: linear-gradient(to bottom, #fff, #fafafa); +} + +.card-summary-main { + flex: 1; + min-width: 0; +} + +.card-summary-top { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 6px; + flex-wrap: wrap; +} + +.category-badge { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: 10px; + white-space: nowrap; + flex-shrink: 0; +} + +.category-badge.badge-activity { + background: #e8fceb; + color: #15803d; +} + +.category-badge.badge-maintenance { + background: #fff8d6; + color: #b45309; +} + +.category-badge.badge-other { + background: #f3f0ff; + color: #6d28d9; +} + +.announcement-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.4; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.announcement-card.expanded .announcement-title { + white-space: normal; + overflow: visible; +} + +.announcement-intro { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.5; + margin: 4px 0 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.announcement-card.expanded .announcement-intro { + white-space: normal; + overflow: visible; +} + +.card-summary-time { + font-size: 13px; + color: var(--text-secondary); + white-space: nowrap; + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; +} + +.expand-icon { + color: var(--text-secondary); + font-size: 14px; + transition: transform 0.3s ease; + flex-shrink: 0; + opacity: 0.4; +} + +.announcement-card.expanded .expand-icon { + transform: rotate(180deg); + opacity: 0.6; +} + +/* Expanded Detail */ +.card-detail { + max-height: 0; + overflow: hidden; + transition: max-height 0.45s cubic-bezier(0.25, 1, 0.5, 1), padding 0.35s ease; + padding: 0 28px; +} + +.announcement-card.expanded .card-detail { + max-height: 2000px; + padding: 28px 28px 32px; +} + +.detail-content { + line-height: 1.8; + font-size: 15px; + color: var(--text-primary); +} + +.detail-content p { + margin-bottom: 14px; +} + +.detail-content p:last-child { + margin-bottom: 0; +} + +.detail-content img { + max-width: 100%; + border-radius: 12px; + margin: 12px 0 16px; + border: 1px solid rgba(0,0,0,0.05); + box-shadow: 0 4px 12px rgba(0,0,0,0.05); +} + +.detail-content .video-embed-wrapper { + position: relative; + width: 100%; + padding-bottom: 56.25%; + margin: 12px 0 16px; + border-radius: 12px; + overflow: hidden; + background: #000; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} + +.detail-content .video-embed-wrapper iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; +} + +.detail-edit-btn-row { + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgba(0,0,0,0.05); + display: flex; + justify-content: flex-end; +} + +.btn-edit-announcement { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 16px; + background: transparent; + color: var(--accent-color); + border: 1.5px solid var(--accent-color); + border-radius: 18px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; +} + +.btn-edit-announcement:hover { + background: var(--accent-color); + color: #fff; +} + +/* No results */ +.no-results-message { + text-align: center; + padding: 60px; + color: var(--text-secondary); + font-size: 16px; + background: var(--card-bg); + border-radius: var(--radius-medium); +} + +.is-hidden { + display: none !important; +} + +/* Edit mode hidden class */ +.edit-hidden { + display: none !important; +} + +/* ========== Editor Modal ========== */ +.modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.editor-modal-content { + background-color: #fff; + margin: 20px auto; + border-radius: var(--radius-large); + max-width: 1100px; + width: 95%; + padding: 0; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.3); + position: relative; + max-height: calc(100vh - 40px); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.close-editor-modal, +.close-json-modal { + position: absolute; + top: 16px; + right: 20px; + font-size: 24px; + color: var(--text-secondary); + cursor: pointer; + transition: 0.2s; + z-index: 10; + background: rgba(255,255,255,0.9); + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.close-editor-modal:hover, +.close-json-modal:hover { + background: #f0f0f0; + color: var(--text-primary); +} + +.editor-modal-header { + padding: 20px 28px; + border-bottom: 1px solid rgba(0,0,0,0.08); + background: linear-gradient(to bottom, #fff, #fafafa); + border-radius: var(--radius-large) var(--radius-large) 0 0; + flex-shrink: 0; +} + +.editor-modal-header h3 { + font-size: 22px; + font-weight: 700; + display: flex; + align-items: center; + gap: 10px; +} + +.editor-layout { + display: flex; + flex: 1; + overflow: hidden; + min-height: 0; +} + +.editor-preview { + flex: 0 0 45%; + display: flex; + flex-direction: column; + border-right: 1px solid rgba(0,0,0,0.08); + background: #f5f5f7; +} + +.editor-panel-title { + font-size: 12px; + font-weight: 700; + color: var(--text-secondary); + padding: 14px 24px; + border-bottom: 1px solid rgba(0,0,0,0.05); + display: flex; + align-items: center; + gap: 8px; + text-transform: uppercase; + letter-spacing: 0.6px; + flex-shrink: 0; + background: rgba(255,255,255,0.6); +} + +.editor-preview-content { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.editor-preview-content::-webkit-scrollbar, +.editor-form-scroll::-webkit-scrollbar { + width: 5px; +} + +.editor-preview-content::-webkit-scrollbar-track, +.editor-form-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.editor-preview-content::-webkit-scrollbar-thumb, +.editor-form-scroll::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.12); + border-radius: 10px; +} + +/* Preview Card in Editor */ +.preview-announcement { + background: #fff; + border-radius: 16px; + box-shadow: 0 2px 12px rgba(0,0,0,0.06); + overflow: hidden; +} + +.preview-header { + padding: 24px 24px 16px; + background: linear-gradient(to bottom, #fff, #fafafa); + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +.preview-title { + font-size: 22px; + font-weight: 700; + margin-bottom: 12px; + line-height: 1.2; +} + +.preview-meta { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.preview-body { + padding: 20px 24px 24px; +} + +.preview-body .detail-content p { + margin-bottom: 12px; +} + +.preview-intro-text { + font-size: 15px; + line-height: 1.6; + color: var(--text-secondary); + margin-bottom: 16px; + font-style: italic; +} + +/* Editor Form */ +.editor-form { + flex: 0 0 55%; + display: flex; + flex-direction: column; + min-width: 0; +} + +.editor-form-scroll { + flex: 1; + overflow-y: auto; + padding: 24px 28px 40px; +} + +.form-group { + margin-bottom: 22px; +} + +.form-group > label { + display: block; + font-size: 12px; + font-weight: 700; + color: var(--text-secondary); + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.form-group input[type="text"], +.form-group input[type="date"], +.form-group textarea { + width: 100%; + padding: 12px 16px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + font-size: 14px; + font-family: inherit; + background-color: #f9f9fa; + transition: all 0.2s ease; + color: var(--text-primary); + box-sizing: border-box; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--accent-color); + background-color: #fff; + box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1); +} + +.form-group textarea { + resize: vertical; + min-height: 60px; +} + +.form-row { + display: flex; + gap: 14px; +} + +.form-row .form-group { + flex: 1; +} + +/* Custom Select */ +.custom-select { + position: relative; + width: 100%; + user-select: none; + font-size: 14px; +} + +.custom-select-trigger { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 12px 16px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + background-color: #f9f9fa; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.custom-select-trigger i { + color: var(--text-secondary); + font-size: 12px; + transition: transform 0.3s ease; +} + +.custom-select:hover .custom-select-trigger { + background-color: #fff; + border-color: rgba(0,0,0,0.2); +} + +.custom-select.open .custom-select-trigger { + border-color: var(--accent-color); + background-color: #fff; + box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1); +} + +.custom-select.open .custom-select-trigger i { + transform: rotate(180deg); +} + +.custom-select-options { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0,0,0,0.15); + border: 1px solid rgba(0,0,0,0.08); + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1); + z-index: 100; + padding: 8px; +} + +.custom-select.open .custom-select-options { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.custom-option { + padding: 10px 14px; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s, color 0.2s; + color: var(--text-primary); + margin-bottom: 2px; +} + +.custom-option:last-child { + margin-bottom: 0; +} + +.custom-option:hover { + background: #f5f5f7; +} + +.custom-option.selected { + background: #e0f2fe; + color: #0369a1; + font-weight: 600; +} + +/* Sortable Items */ +.sortable-list { + min-height: 8px; + margin-bottom: 10px; +} + +.sortable-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + margin-bottom: 8px; + background: #fff; + border: 1.5px solid rgba(0,0,0,0.08); + border-radius: 12px; + transition: box-shadow 0.2s, border-color 0.2s, opacity 0.2s; +} + +.sortable-item:last-child { + margin-bottom: 0; +} + +.sortable-item:hover { + border-color: rgba(0,0,0,0.15); + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.sortable-item.dragging { + opacity: 0.4; + border-color: var(--accent-color); +} + +.sortable-item.drag-over { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 113, 227, 0.15); +} + +.drag-handle { + cursor: grab; + color: var(--text-secondary); + padding: 6px 2px; + font-size: 14px; + opacity: 0.35; + transition: 0.2s; + flex-shrink: 0; +} + +.drag-handle:active { + cursor: grabbing; +} + +.sortable-item:hover .drag-handle { + opacity: 0.7; +} + +.item-type-badge { + font-size: 10px; + font-weight: 700; + padding: 3px 8px; + border-radius: 6px; + white-space: nowrap; + flex-shrink: 0; + margin-top: 6px; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.badge-text { + background: #e8f5e9; + color: #2e7d32; +} + +.badge-image { + background: #e3f2fd; + color: #1565c0; +} + +.badge-video { + background: #fce4ec; + color: #c62828; +} + +.sortable-item .item-content { + flex: 1; + border: 1px solid rgba(0,0,0,0.06) !important; + border-radius: 8px !important; + padding: 8px 10px !important; + font-size: 13px !important; + background: #fafafa !important; + min-height: unset; + font-family: inherit; + resize: vertical; +} + +.sortable-item .item-content:focus { + border-color: var(--accent-color) !important; + background: #fff !important; + box-shadow: none !important; +} + +.remove-item-btn { + background: none; + border: none; + color: #ccc; + cursor: pointer; + padding: 6px; + border-radius: 8px; + transition: 0.2s; + flex-shrink: 0; + margin-top: 3px; + font-size: 13px; +} + +.remove-item-btn:hover { + color: #ef4444; + background: #fef2f2; +} + +.add-item-row { + display: flex; + gap: 8px; +} + +.add-item-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: #f5f5f7; + border: 1.5px dashed rgba(0,0,0,0.12); + border-radius: 10px; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition); +} + +.add-item-btn:hover { + border-color: var(--accent-color); + color: var(--accent-color); + background: #f0f7ff; +} + +/* Editor Actions */ +.editor-actions { + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid rgba(0,0,0,0.08); + display: flex; + justify-content: flex-end; +} + +.btn-save-announcement { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 28px; + background: var(--brand-green); + color: #fff; + border: none; + border-radius: 14px; + font-size: 15px; + font-weight: 700; + cursor: pointer; + transition: var(--transition); +} + +.btn-save-announcement:hover { + background: #2db84d; + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(52, 199, 89, 0.3); +} + +/* JSON Output Modal */ +.json-output-content { + background-color: #fff; + margin: 60px auto; + border-radius: var(--radius-large); + max-width: 640px; + width: 90%; + padding: 36px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.3); + position: relative; +} + +.json-output-content h3 { + font-size: 20px; + font-weight: 700; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 10px; +} + +.json-output-hint { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 20px; + line-height: 1.5; +} + +#json-output { + width: 100%; + height: 300px; + padding: 16px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + background: #f5f5f7; + color: var(--text-primary); + resize: vertical; + margin-bottom: 16px; +} + +#json-output:focus { + outline: none; + border-color: var(--accent-color); +} + +.btn-copy-json { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 24px; + background: var(--accent-color); + color: #fff; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + width: 100%; + justify-content: center; +} + +.btn-copy-json:hover { + background: #005bb5; +} + +/* ========== Responsive ========== */ +@media (max-width: 900px) { + .editor-modal-content { + margin: 0; + width: 100%; + max-width: 100%; + max-height: 100%; + height: 100%; + border-radius: 0; + } + + .editor-layout { + flex-direction: column; + } + + .editor-preview { + flex: none; + max-height: 35vh; + border-right: none; + border-bottom: 1px solid rgba(0,0,0,0.08); + } + + .editor-form { + flex: 1; + min-height: 0; + } + + .form-row { + flex-direction: column; + gap: 0; + } +} + +@media (max-width: 768px) { + .announcements-container { + padding: 24px 16px; + } + + .controls-header-row { + flex-direction: column; + align-items: stretch; + } + + .search-box { + max-width: 100%; + } + + .title-with-action { + flex-wrap: wrap; + gap: 10px; + } + + .timeline { + padding-left: 24px; + } + + .timeline-item::before { + left: -24px; + width: 12px; + height: 12px; + top: 26px; + } + + .card-summary { + padding: 18px 20px; + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .card-summary-time { + align-self: flex-end; + } + + .announcement-card.expanded .card-detail { + padding: 20px; + } + + .json-output-content { + margin: 0; + width: 100%; + height: 100%; + max-height: 100%; + border-radius: 0; + } + + .editor-modal-header { + padding: 16px 20px; + } + + .editor-form-scroll { + padding: 20px; + } + + .filter-label { + min-width: auto; + margin-bottom: 4px; + } +} diff --git a/data/announcements.json b/data/announcements.json new file mode 100644 index 0000000..2e7579c --- /dev/null +++ b/data/announcements.json @@ -0,0 +1,86 @@ +[ + { + "title": "【26Q2Q3】官网首页封面图片投稿", + "intro": "服务器官方网站首页封面图片征集投稿活动。", + "time": "2026-03-10", + "category": "activity", + "content": [ + { + "type": "text", + "content": "▸参与投稿" + }, + { + "type": "text", + "content": "1. 打开服务器官网相册:https://bailuyuan.lunadeer.cn/photo.html" + }, + { + "type": "text", + "content": "2. 注册或登录账号;" + }, + { + "type": "text", + "content": "3. 上传图片,在标签处选择或输入:#26Q2Q3官网封面活动;" + }, + { + "type": "text", + "content": "4. 回车确认标签被添加,最后点击上传图片;" + }, + { + "type": "text", + "content": "---------------------------------" + }, + { + "type": "text", + "content": "▸参与投票" + }, + { + "type": "text", + "content": "1. 打开服务器官网相册:https://bailuyuan.lunadeer.cn/photo.html" + }, + { + "type": "text", + "content": "2. 注册或登录账号;" + }, + { + "type": "text", + "content": "3. 筛选图片,选择:#26Q2Q3官网封面活动;" + }, + { + "type": "text", + "content": "4. 为你喜爱的图片点赞;" + }, + { + "type": "text", + "content": "---------------------------------" + }, + { + "type": "text", + "content": "▸活动截止日期" + }, + { + "type": "text", + "content": "2026年3月31日" + } + ] + }, + { + "title": "服务器新增假人、无限末地门结构", + "intro": "服务器维护更新。", + "time": "2026-03-10", + "category": "maintenance", + "content": [ + { + "type": "text", + "content": "服务器更新维护,本次维护新增如下内容:" + }, + { + "type": "text", + "content": "1. 服务器新人假人系统(Bot/FakePlayer),详情参考文档里对假人的说明:https://bailuyuan.lunadeer.cn/doc.html" + }, + { + "type": "text", + "content": "2. 服务器重写了末地传送门结构生成规则,重写后与基岩版一致,为不限数量随机生成,便于生活在新出生点的玩家有便捷的末地传送门可用;" + } + ] + } +] \ No newline at end of file diff --git a/js/announcements_script.js b/js/announcements_script.js new file mode 100644 index 0000000..8c7cb42 --- /dev/null +++ b/js/announcements_script.js @@ -0,0 +1,540 @@ +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 = ` +
+
+ + ${categoryText} + +

${escapeHtml(item.title)}

+
+

${escapeHtml(item.intro)}

+
+ ${escapeHtml(item.time)} + + `; + + summary.addEventListener('click', () => { + const wasExpanded = card.classList.contains('expanded'); + // Collapse all + timeline.querySelectorAll('.announcement-card.expanded').forEach(c => c.classList.remove('expanded')); + // Toggle current + if (!wasExpanded) { + card.classList.add('expanded'); + } + }); + + // Detail + const detail = document.createElement('div'); + detail.className = 'card-detail'; + const detailInner = document.createElement('div'); + 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'); + const editBtn = document.createElement('button'); + editBtn.className = 'btn-edit-announcement'; + editBtn.innerHTML = ' 编辑'; + editBtn.addEventListener('click', (e) => { + e.stopPropagation(); + openEditor(item); + }); + editRow.appendChild(editBtn); + + detail.appendChild(detailInner); + detail.appendChild(editRow); + + card.appendChild(summary); + card.appendChild(detail); + timelineItem.appendChild(card); + timeline.appendChild(timelineItem); + }); + } + + function renderContentBlocks(container, blocks) { + container.innerHTML = ''; + if (!blocks || blocks.length === 0) { + container.innerHTML = '

暂无内容

'; + 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 = '
'; + html += '
'; + html += '
' + escapeHtml(title) + '
'; + html += '
'; + html += ' ' + categoryText + ''; + html += ' ' + escapeHtml(time) + ''; + html += '
'; + html += '
'; + + html += '
'; + html += '

' + escapeHtml(intro) + '

'; + html += '
'; + if (editorContentBlocks.length > 0) { + editorContentBlocks.forEach(block => { + if (block.type === 'text') { + html += '

' + (escapeHtml(block.content) || '空文字') + '

'; + } else if (block.type === 'image') { + html += block.content ? '' : '

空图片

'; + } else if (block.type === 'video') { + html += renderVideoPreviewHtml(block.content); + } + }); + } else { + html += '

暂无正文内容

'; + } + html += '
'; + preview.innerHTML = html; + } + + function renderVideoPreviewHtml(content) { + var bv = parseBVNumber(content); + if (bv) { + return '
'; + } + return '

请输入有效的 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 @@ 0.5 + + + https://mcpure.lunadeer.cn/announcements.html + 2026-03-10 + weekly + 0.7 + +