mirror of
https://github.com/Coldsmiles/infstarweb.git
synced 2026-04-23 02:30:41 +08:00
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.
This commit is contained in:
189
announcements.html
Normal file
189
announcements.html
Normal file
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>活动公告 - 白鹿原 Minecraft 服务器</title>
|
||||
<meta name="description" content="白鹿原Minecraft服务器活动公告,了解最新的服务器活动、维护通知和重要公告信息。">
|
||||
<meta name="keywords" content="Minecraft公告,MC活动,白鹿原公告,服务器活动,维护通知">
|
||||
<meta name="author" content="白鹿原 Minecraft 服务器">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://mcpure.lunadeer.cn/announcements.html">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://mcpure.lunadeer.cn/announcements.html">
|
||||
<meta property="og:title" content="活动公告 - 白鹿原 Minecraft 服务器">
|
||||
<meta property="og:description" content="白鹿原Minecraft服务器活动公告,了解最新的服务器活动、维护通知和重要公告信息。">
|
||||
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
|
||||
<meta property="og:locale" content="zh_CN">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary">
|
||||
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/announcements.html">
|
||||
<meta property="twitter:title" content="活动公告 - 白鹿原 Minecraft 服务器">
|
||||
<meta property="twitter:description" content="白鹿原Minecraft服务器活动公告,了解最新的服务器活动、维护通知和重要公告信息。">
|
||||
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/pages/announcements.css">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "活动公告",
|
||||
"description": "白鹿原Minecraft服务器活动公告",
|
||||
"url": "https://mcpure.lunadeer.cn/announcements.html",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "白鹿原 Minecraft 服务器",
|
||||
"url": "https://mcpure.lunadeer.cn/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="navbar-component"></div>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<header id="hero-component" data-title="活动公告" data-subtitle="了解服务器最新动态、活动安排与维护通知。" data-class="announcements-hero-bg"></header>
|
||||
|
||||
<div class="announcements-container">
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls-section">
|
||||
<div class="controls-header-row">
|
||||
<div class="title-with-action">
|
||||
<h2 class="section-title">公告列表</h2>
|
||||
<button class="btn-add-announcement edit-hidden" id="btn-add-announcement">
|
||||
<i class="fas fa-plus"></i> 新增公告
|
||||
</button>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="announcement-search" placeholder="搜索公告标题或简介...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-wrapper">
|
||||
<div class="filter-group">
|
||||
<div class="filter-label"><i class="fas fa-tag"></i> 类别</div>
|
||||
<div class="filter-tags" id="category-filters">
|
||||
<button class="filter-tag active" data-filter="all">全部</button>
|
||||
<button class="filter-tag" data-filter="activity"><i class="fas fa-calendar-check"></i> 活动</button>
|
||||
<button class="filter-tag" data-filter="maintenance"><i class="fas fa-wrench"></i> 维护</button>
|
||||
<button class="filter-tag" data-filter="other"><i class="fas fa-info-circle"></i> 其他</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="timeline" id="announcements-timeline">
|
||||
<!-- JS will inject timeline items here -->
|
||||
</div>
|
||||
<div id="no-results" class="no-results-message is-hidden">
|
||||
没有找到匹配的公告
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Editor Modal -->
|
||||
<div id="editor-modal" class="modal">
|
||||
<div class="modal-content editor-modal-content">
|
||||
<span class="close-editor-modal">×</span>
|
||||
<div class="editor-modal-header">
|
||||
<h3><i class="fas fa-bullhorn"></i> 公告编辑器</h3>
|
||||
</div>
|
||||
<div class="editor-layout">
|
||||
<!-- Left: Preview -->
|
||||
<div class="editor-preview">
|
||||
<div class="editor-panel-title"><i class="fas fa-eye"></i> 实时预览</div>
|
||||
<div class="editor-preview-content" id="editor-preview-area"></div>
|
||||
</div>
|
||||
<!-- Right: Editor Form -->
|
||||
<div class="editor-form">
|
||||
<div class="editor-panel-title"><i class="fas fa-edit"></i> 编辑内容</div>
|
||||
<div class="editor-form-scroll">
|
||||
<div class="form-group">
|
||||
<label for="editor-title">公告标题</label>
|
||||
<input type="text" id="editor-title" placeholder="输入公告标题...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editor-intro">简介</label>
|
||||
<textarea id="editor-intro" placeholder="输入公告简介..." rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="editor-time">时间</label>
|
||||
<input type="date" id="editor-time">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>类别</label>
|
||||
<div class="custom-select">
|
||||
<input type="hidden" id="editor-category" value="activity">
|
||||
<div class="custom-select-trigger">
|
||||
<span class="custom-select-text">活动</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="custom-select-options">
|
||||
<div class="custom-option selected" data-value="activity">活动</div>
|
||||
<div class="custom-option" data-value="maintenance">维护</div>
|
||||
<div class="custom-option" data-value="other">其他</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>正文内容</label>
|
||||
<div class="sortable-list" id="editor-content-list"></div>
|
||||
<div class="add-item-row">
|
||||
<button type="button" class="add-item-btn" data-type="text">
|
||||
<i class="fas fa-plus"></i> 添加文字
|
||||
</button>
|
||||
<button type="button" class="add-item-btn" data-type="image">
|
||||
<i class="fas fa-image"></i> 添加图片
|
||||
</button>
|
||||
<button type="button" class="add-item-btn" data-type="video">
|
||||
<i class="fas fa-video"></i> 添加视频
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<button type="button" class="btn-save-announcement" id="btn-save-announcement">
|
||||
<i class="fas fa-save"></i> 生成 JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JSON Output Modal -->
|
||||
<div id="json-output-modal" class="modal">
|
||||
<div class="modal-content json-output-content">
|
||||
<span class="close-json-modal">×</span>
|
||||
<h3><i class="fas fa-code"></i> 生成完成</h3>
|
||||
<p class="json-output-hint">请复制以下 JSON 内容,更新到 data/announcements.json 文件中。</p>
|
||||
<textarea id="json-output" readonly></textarea>
|
||||
<button type="button" class="btn-copy-json" id="btn-copy-json">
|
||||
<i class="fas fa-copy"></i> 复制到剪贴板
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer-component"></div>
|
||||
<script src="js/components.js"></script>
|
||||
<script src="js/announcements_script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1095
css/pages/announcements.css
Normal file
1095
css/pages/announcements.css
Normal file
File diff suppressed because it is too large
Load Diff
86
data/announcements.json
Normal file
86
data/announcements.json
Normal file
@@ -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. 服务器重写了末地传送门结构生成规则,重写后与基岩版一致,为不限数量随机生成,便于生活在新出生点的玩家有便捷的末地传送门可用;"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
540
js/announcements_script.js
Normal file
540
js/announcements_script.js
Normal file
@@ -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 = '<p class="error" style="text-align:center;color:var(--text-secondary);padding:40px;">无法加载公告数据。</p>';
|
||||
});
|
||||
|
||||
// ========== 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 = `
|
||||
<div class="card-summary-main">
|
||||
<div class="card-summary-top">
|
||||
<span class="category-badge ${categoryBadgeClass}">
|
||||
<i class="fas ${categoryIcon}"></i> ${categoryText}
|
||||
</span>
|
||||
<h3 class="announcement-title">${escapeHtml(item.title)}</h3>
|
||||
</div>
|
||||
<p class="announcement-intro">${escapeHtml(item.intro)}</p>
|
||||
</div>
|
||||
<span class="card-summary-time"><i class="far fa-clock"></i> ${escapeHtml(item.time)}</span>
|
||||
<i class="fas fa-chevron-down expand-icon"></i>
|
||||
`;
|
||||
|
||||
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 = '<i class="fas fa-pen"></i> 编辑';
|
||||
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 = '<p style="color:var(--text-secondary);">暂无内容</p>';
|
||||
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 = '<textarea class="item-content" rows="2" placeholder="输入文字内容...">' + escapeHtml(item.content) + '</textarea>';
|
||||
} else if (item.type === 'image') {
|
||||
contentHtml = '<input type="text" class="item-content" placeholder="输入图片URL..." value="' + escapeHtml(item.content) + '">';
|
||||
} else {
|
||||
contentHtml = '<input type="text" class="item-content" placeholder="BV1xxxxxxxxxx 或 bilibili 视频地址" value="' + escapeHtml(item.content) + '">';
|
||||
}
|
||||
|
||||
div.innerHTML =
|
||||
'<span class="drag-handle"><i class="fas fa-grip-vertical"></i></span>' +
|
||||
'<span class="item-type-badge ' + typeBadgeClass + '">' + typeBadgeLabel + '</span>' +
|
||||
contentHtml +
|
||||
'<button type="button" class="remove-item-btn" title="删除"><i class="fas fa-trash-alt"></i></button>';
|
||||
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 = '<div class="preview-announcement">';
|
||||
html += '<div class="preview-header">';
|
||||
html += '<div class="preview-title">' + escapeHtml(title) + '</div>';
|
||||
html += '<div class="preview-meta">';
|
||||
html += '<span class="category-badge ' + badgeClass + '"><i class="fas ' + categoryIcon + '"></i> ' + categoryText + '</span>';
|
||||
html += '<span class="card-summary-time"><i class="far fa-clock"></i> ' + escapeHtml(time) + '</span>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="preview-body">';
|
||||
html += '<p class="preview-intro-text">' + escapeHtml(intro) + '</p>';
|
||||
html += '<div class="detail-content">';
|
||||
if (editorContentBlocks.length > 0) {
|
||||
editorContentBlocks.forEach(block => {
|
||||
if (block.type === 'text') {
|
||||
html += '<p>' + (escapeHtml(block.content) || '<span class="text-secondary">空文字</span>') + '</p>';
|
||||
} else if (block.type === 'image') {
|
||||
html += block.content ? '<img src="' + escapeHtml(block.content) + '" loading="lazy">' : '<p class="text-secondary">空图片</p>';
|
||||
} else if (block.type === 'video') {
|
||||
html += renderVideoPreviewHtml(block.content);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
html += '<p class="text-secondary">暂无正文内容</p>';
|
||||
}
|
||||
html += '</div></div></div>';
|
||||
preview.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderVideoPreviewHtml(content) {
|
||||
var bv = parseBVNumber(content);
|
||||
if (bv) {
|
||||
return '<div class="video-embed-wrapper"><iframe src="https://player.bilibili.com/player.html?bvid=' + bv + '&autoplay=0&high_quality=1" allowfullscreen sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe></div>';
|
||||
}
|
||||
return '<p class="text-secondary">请输入有效的 BV 号或 bilibili 视频地址</p>';
|
||||
}
|
||||
|
||||
// ========== 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 = '<i class="fas fa-check"></i> 已复制!';
|
||||
btn.style.background = '#34c759';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.style.background = '';
|
||||
}, 2000);
|
||||
}).catch(() => {
|
||||
document.execCommand('copy');
|
||||
alert('已复制到剪贴板');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ const Components = {
|
||||
<a href="/doc.html">文档</a>
|
||||
<a href="/map.html">地图</a>
|
||||
<a href="/facilities.html">设施</a>
|
||||
<a href="/announcements.html">公告</a>
|
||||
<a href="/photo.html">相册</a>
|
||||
<a href="/stats.html">数据</a>
|
||||
<a href="/sponsor.html">赞助</a>
|
||||
@@ -32,6 +33,7 @@ const Components = {
|
||||
<a href="/doc.html">文档</a>
|
||||
<a href="/map.html">地图</a>
|
||||
<a href="/facilities.html">设施</a>
|
||||
<a href="/announcements.html">公告</a>
|
||||
<a href="/photo.html">相册</a>
|
||||
<a href="/stats.html">数据</a>
|
||||
<a href="/sponsor.html">赞助</a>
|
||||
|
||||
@@ -68,4 +68,12 @@
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
|
||||
<!-- 活动公告 -->
|
||||
<url>
|
||||
<loc>https://mcpure.lunadeer.cn/announcements.html</loc>
|
||||
<lastmod>2026-03-10</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
|
||||
</urlset>
|
||||
|
||||
Reference in New Issue
Block a user