留言板:添加、搜索、删除(单条/批量)日间/夜间/自动三种主题模式
·



<!DOCTYPE html>
<html lang="zh-CN" data-theme="auto">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>留言板</title>
<style>
:root {
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
--btn-add: #4CAF50;
--btn-delete: #f44336;
--btn-action: #2196F3;
--btn-export: #9C27B0;
--card-bg: rgba(255, 255, 255, 0.8);
}
[data-theme="dark"] {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--text-primary: #ffffff;
--text-secondary: #bbbbbb;
--border-color: #333333;
--card-bg: rgba(30, 30, 30, 0.9);
}
* {
box-sizing: border-box;
}
body {
font-family: "Segoe UI", system-ui, sans-serif;
background-color: var(--bg-primary);
margin: 0;
padding: 20px 10px;
color: var(--text-primary);
transition: background-color 0.3s, color 0.3s;
font-size: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background-color: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 16px;
}
.header-title {
font-size: 1.5rem;
font-weight: bold;
}
.controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.btn {
border: none;
border-radius: 5px;
padding: 6px 12px;
cursor: pointer;
font-size: 14px;
color: white;
}
.btn-add {
background-color: var(--btn-add);
}
.btn-delete,
.btn-batch-delete {
background-color: var(--btn-delete);
}
.btn-action {
background-color: var(--btn-action);
}
.btn-export {
background-color: var(--btn-export);
}
.btn-theme {
background-color: #795548;
}
.search-box {
display: flex;
gap: 8px;
margin: 12px 0;
}
.search-box input {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-primary);
}
dialog {
border-radius: 8px;
padding: 20px;
width: 90%;
max-width: 500px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-primary);
}
dialog textarea {
width: 100%;
height: 100px;
padding: 10px;
margin: 10px 0;
border: 1px solid var(--border-color);
border-radius: 5px;
resize: vertical;
font-family: inherit;
background: var(--bg-primary);
color: var(--text-primary);
}
#message-list {
list-style: none;
padding: 0;
}
.message-item {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 12px 12px 12px 56px;
margin-bottom: 16px;
position: relative;
transition: opacity 0.4s, transform 0.4s;
}
.message-checkbox {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
display: none;
cursor: pointer;
}
.delete-mode .message-checkbox {
display: block;
}
.message-content {
margin: 0 0 8px;
word-break: break-word;
}
.message-content.deleted {
text-decoration: line-through;
color: var(--text-secondary);
}
.message-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85em;
color: var(--text-secondary);
}
.message-meta span {
/* 靠右对齐 */
flex-grow: 1;
text-align: right;
}
/* 单条删除按钮 */
.message-actions {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
display: none;
}
.delete-mode .message-actions,
.message-item:hover .message-actions {
display: block;
}
.message-delete-btn {
background: none;
border: none;
color: var(--btn-delete);
font-size: 18px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.message-delete-btn:hover {
opacity: 1;
}
/* 删除动画 */
.message-item.deleting {
animation: fadeOut 0.4s forwards;
}
@keyframes fadeOut {
to {
opacity: 0;
transform: translateX(20px);
}
}
.pagination {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 16px;
}
.pagination button {
padding: 4px 10px;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
}
.pagination button.active {
background: var(--btn-action);
color: white;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status-info {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: var(--text-secondary);
}
@media (max-width: 600px) {
.header {
flex-direction: column;
align-items: stretch;
}
.controls {
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<div id="message-board">
<div class="header">
<h1 class="header-title">留言板</h1>
<div class="search-box">
<input type="text" id="search-input" placeholder="搜索留言内容...">
</div>
<div class="controls">
<button class="btn btn-add" id="open-dialog-btn">添加</button>
<button class="btn btn-delete" id="toggle-delete-mode-btn">删除</button>
<button class="btn btn-export" id="export-btn">导出</button>
<button class="btn btn-action" id="import-btn">导入</button>
<button class="btn btn-theme" id="theme-toggle-btn">🌓 主题</button>
</div>
</div>
<dialog id="add-message-dialog">
<textarea id="message-input" placeholder="请输入留言内容..."></textarea>
<div style="text-align: right;">
<button id="submit-btn">提交</button>
<button id="close-dialog-btn">返回</button>
</div>
</dialog>
<ul id="message-list"></ul>
<div class="select-controls" id="select-controls" style="display:none; margin:12px 0; text-align:center;">
<button class="btn btn-action" id="select-all-btn">全选</button>
<button class="btn btn-action" id="unselect-all-btn">取消全选</button>
<button class="btn btn-action" id="invert-select-btn">反选</button>
</div>
<button class="btn btn-batch-delete" id="batch-delete-btn" style="display:none; width:100%;">删除选中项</button>
<div class="pagination" id="pagination"></div>
</div>
<p class="status-info">
宽度: <span id="window-width"></span>px,
高度: <span id="window-height"></span>px
</p>
</div>
<script>
(function () {
// ======================
// 工具函数
// ======================
function generateId(prefix = 'id') {
if (window.crypto && crypto.randomUUID) {
return `${prefix}_${crypto.randomUUID().slice(0, 8)}`;
}
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
}
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// ======================
// 主应用状态
// ======================
const ITEMS_PER_PAGE = 5;
let messages = JSON.parse(localStorage.getItem('message_board_app_v1_messages') || '[]');
let currentPage = 1;
let filteredMessages = [];
let isSubmitting = false;
let deleteMode = false;
let isDeleting = false;
// DOM 引用
const el = {
messageList: document.getElementById('message-list'),
addDialog: document.getElementById('add-message-dialog'),
messageInput: document.getElementById('message-input'),
submitBtn: document.getElementById('submit-btn'),
closeDialogBtn: document.getElementById('close-dialog-btn'),
openDialogBtn: document.getElementById('open-dialog-btn'),
toggleDeleteModeBtn: document.getElementById('toggle-delete-mode-btn'),
batchDeleteBtn: document.getElementById('batch-delete-btn'),
selectControls: document.getElementById('select-controls'),
selectAllBtn: document.getElementById('select-all-btn'),
unselectAllBtn: document.getElementById('unselect-all-btn'),
invertSelectBtn: document.getElementById('invert-select-btn'),
exportBtn: document.getElementById('export-btn'),
importBtn: document.getElementById('import-btn'),
searchInput: document.getElementById('search-input'),
pagination: document.getElementById('pagination'),
themeToggleBtn: document.getElementById('theme-toggle-btn'),
windowWidth: document.getElementById('window-width'),
windowHeight: document.getElementById('window-height')
};
// ======================
// 主题切换
// ======================
function initTheme() {
const saved = localStorage.getItem('theme') || 'auto';
document.documentElement.setAttribute('data-theme', saved);
updateThemeButtonText();
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
let next;
if (current === 'auto') next = 'dark';
else if (current === 'dark') next = 'light';
else next = 'auto';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
updateThemeButtonText();
}
function updateThemeButtonText() {
const theme = document.documentElement.getAttribute('data-theme');
const label = { auto: '🌓 自动', dark: '🌙 夜间', light: '☀️ 日间' };
el.themeToggleBtn.textContent = label[theme] || '🌓 主题';
}
// ======================
// 数据处理
// ======================
function prepareFilteredMessages() {
const keyword = el.searchInput.value.trim().toLowerCase();
filteredMessages = messages.filter(msg =>
msg.content.toLowerCase().includes(keyword)
);
}
function saveMessages(resetPage = true) {
localStorage.setItem('message_board_app_v1_messages', JSON.stringify(messages));
prepareFilteredMessages();
if (resetPage) currentPage = 1;
render();
}
// ======================
// 渲染
// ======================
function renderPagination() {
const totalPages = Math.ceil(filteredMessages.length / ITEMS_PER_PAGE);
el.pagination.innerHTML = '';
if (totalPages <= 1) {
el.pagination.style.display = 'none';
return;
}
el.pagination.style.display = 'flex';
const createButton = (text, disabled, onClick) => {
const btn = document.createElement('button');
btn.textContent = text;
btn.disabled = disabled;
if (!disabled) btn.addEventListener('click', onClick);
return btn;
};
el.pagination.appendChild(
createButton('‹', currentPage === 1, () => { if (currentPage > 1) { currentPage--; render(); } })
);
let start = Math.max(1, currentPage - 2);
let end = Math.min(totalPages, start + 4);
if (end - start < 4) start = Math.max(1, end - 4);
for (let i = start; i <= end; i++) {
const btn = createButton(i, false, () => { currentPage = i; render(); });
if (i === currentPage) btn.classList.add('active');
el.pagination.appendChild(btn);
}
el.pagination.appendChild(
createButton('›', currentPage === totalPages, () => { if (currentPage < totalPages) { currentPage++; render(); } })
);
}
function renderMessageItem(msg) {
const div = document.createElement('div');
div.className = 'message-item';
div.setAttribute('data-id', msg.id);
const checkboxId = `chk-${msg.id}`;
div.innerHTML = `
<input type="checkbox" id="${checkboxId}" class="message-checkbox" data-id="${msg.id}">
<label for="${checkboxId}" style="cursor:pointer;user-select:none;">
<p class="message-content">${escapeHtml(msg.content)}</p>
<div class="message-meta">
<span>${msg.timestamp}</span>
</div>
</label>
<div class="message-actions">
<button class="message-delete-btn" data-id="${msg.id}">🔴×🔴</button>
</div>
`;
return div;
}
function render() {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const pageData = filteredMessages.slice(startIndex, startIndex + ITEMS_PER_PAGE);
el.messageList.innerHTML = '';
if (pageData.length === 0) {
el.messageList.innerHTML = '<li style="text-align:center;color:var(--text-secondary);padding-left:0;">暂无留言</li>';
el.toggleDeleteModeBtn.disabled = true;
el.batchDeleteBtn.style.display = 'none';
el.selectControls.style.display = 'none';
el.pagination.style.display = 'none';
return;
}
el.toggleDeleteModeBtn.disabled = false;
pageData.forEach(msg => {
el.messageList.appendChild(renderMessageItem(msg));
});
if (deleteMode) {
el.selectControls.style.display = 'block';
el.batchDeleteBtn.style.display = 'block';
updateDeleteButtonText();
} else {
el.selectControls.style.display = 'none';
el.batchDeleteBtn.style.display = 'none';
}
renderPagination();
}
// ======================
// 交互逻辑
// ======================
function addMessage() {
if (isSubmitting) return;
const content = el.messageInput.value.trim();
if (!content) {
alert('留言内容不能为空!');
return;
}
isSubmitting = true;
messages.push({
id: generateId('msg'),
content,
timestamp: new Date().toLocaleString('zh-CN')
});
saveMessages(true);
el.messageInput.value = '';
el.addDialog.close();
isSubmitting = false;
}
function toggleDeleteMode() {
deleteMode = !deleteMode;
if (deleteMode) {
prepareFilteredMessages();
el.selectControls.style.display = 'block';
el.batchDeleteBtn.style.display = 'block';
updateDeleteButtonText();
} else {
exitDeleteMode();
}
}
function exitDeleteMode() {
deleteMode = false;
el.toggleDeleteModeBtn.textContent = '删除';
el.selectControls.style.display = 'none';
el.batchDeleteBtn.style.display = 'none';
document.querySelectorAll('.message-checkbox').forEach(cb => cb.checked = false);
}
function updateDeleteButtonText() {
const count = document.querySelectorAll('.message-checkbox:checked').length;
el.toggleDeleteModeBtn.textContent = count > 0 ? `删除 (${count})` : '删除';
}
function deleteSelectedMessages() {
if (isDeleting) return;
isDeleting = true;
const checked = document.querySelectorAll('.message-checkbox:checked');
const idsToDelete = Array.from(checked).map(cb => cb.dataset.id);
if (idsToDelete.length === 0) {
alert('请至少选择一条留言!');
isDeleting = false;
return;
}
if (!confirm(`确定要删除选中的 ${idsToDelete.length} 条留言吗?`)) {
isDeleting = false;
return;
}
// 先加删除线和动画
idsToDelete.forEach(id => {
const item = document.querySelector(`.message-item[data-id="${id}"]`);
if (item) {
const content = item.querySelector('.message-content');
if (content) content.classList.add('deleted');
item.classList.add('deleting');
}
});
setTimeout(() => {
messages = messages.filter(msg => !idsToDelete.includes(msg.id));
saveMessages(false); // 不重置分页
// 如果当前页空了但还有数据,自动跳回上一页
const totalPages = Math.ceil(filteredMessages.length / ITEMS_PER_PAGE);
if (el.messageList.children.length === 0 && totalPages > 0 && currentPage > totalPages) {
currentPage = totalPages;
render();
}
exitDeleteMode();
isDeleting = false;
}, 400);
}
function deleteSingleMessage(id) {
if (isDeleting) return;
const item = document.querySelector(`.message-item[data-id="${id}"]`);
if (!item) return;
if (!confirm('确定要删除这条留言吗?')) return;
isDeleting = true;
const content = item.querySelector('.message-content');
if (content) content.classList.add('deleted');
item.classList.add('deleting');
setTimeout(() => {
messages = messages.filter(msg => msg.id !== id);
saveMessages(false);
const totalPages = Math.ceil(filteredMessages.length / ITEMS_PER_PAGE);
if (el.messageList.children.length === 0 && totalPages > 0 && currentPage > totalPages) {
currentPage = totalPages;
render();
}
isDeleting = false;
}, 400);
}
function exportMessages() {
if (messages.length === 0) {
alert('没有留言可导出!');
return;
}
const dataStr = JSON.stringify(messages, null, 2);
const blob = new Blob([dataStr], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `messages_${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
}
function importMessages(event) {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const imported = JSON.parse(e.target.result);
if (!Array.isArray(imported)) throw new Error('不是数组');
const valid = imported.every(item =>
typeof item === 'object' &&
typeof item.id === 'string' &&
typeof item.content === 'string' &&
typeof item.timestamp === 'string'
);
if (!valid) throw new Error('数据格式不正确');
messages = imported;
saveMessages(true);
alert('导入成功!');
} catch (err) {
alert('导入失败:' + err.message);
}
};
reader.readAsText(file);
event.target.value = '';
}
function handleSearch() {
currentPage = 1;
prepareFilteredMessages();
render();
}
function updateWindowSize() {
el.windowWidth.textContent = window.innerWidth;
el.windowHeight.textContent = window.innerHeight;
}
// ======================
// 初始化
// ======================
function init() {
initTheme();
prepareFilteredMessages();
render();
el.openDialogBtn.addEventListener('click', () => el.addDialog.showModal());
el.closeDialogBtn.addEventListener('click', () => el.addDialog.close());
el.submitBtn.addEventListener('click', addMessage);
el.toggleDeleteModeBtn.addEventListener('click', toggleDeleteMode);
el.batchDeleteBtn.addEventListener('click', deleteSelectedMessages);
el.selectAllBtn.addEventListener('click', () => setCheckboxes(true));
el.unselectAllBtn.addEventListener('click', () => setCheckboxes(false));
el.invertSelectBtn.addEventListener('click', invertCheckboxes);
el.exportBtn.addEventListener('click', exportMessages);
el.themeToggleBtn.addEventListener('click', toggleTheme);
el.searchInput.addEventListener('input', debounce(handleSearch, 300));
el.messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
addMessage();
}
});
// 批量删除:复选框变化 → 添加/移除删除线
document.addEventListener('change', (e) => {
if (e.target.classList.contains('message-checkbox')) {
const isChecked = e.target.checked;
const messageItem = e.target.closest('.message-item');
const contentEl = messageItem?.querySelector('.message-content');
if (contentEl) {
if (isChecked) {
contentEl.classList.add('deleted');
} else {
contentEl.classList.remove('deleted');
}
}
updateDeleteButtonText();
}
});
// 单条删除:事件委托
document.addEventListener('click', (e) => {
if (e.target.classList.contains('message-delete-btn')) {
const id = e.target.dataset.id;
deleteSingleMessage(id);
}
});
window.addEventListener('resize', updateWindowSize);
updateWindowSize();
}
function setCheckboxes(checked) {
document.querySelectorAll('.message-checkbox').forEach(cb => cb.checked = checked);
// 同步删除线
document.querySelectorAll('.message-checkbox').forEach(cb => {
const content = cb.closest('.message-item')?.querySelector('.message-content');
if (content) {
if (checked) {
content.classList.add('deleted');
} else {
content.classList.remove('deleted');
}
}
});
updateDeleteButtonText();
}
function invertCheckboxes() {
document.querySelectorAll('.message-checkbox').forEach(cb => {
cb.checked = !cb.checked;
const content = cb.closest('.message-item')?.querySelector('.message-content');
if (content) {
if (cb.checked) {
content.classList.add('deleted');
} else {
content.classList.remove('deleted');
}
}
});
updateDeleteButtonText();
}
const importInput = document.createElement('input');
importInput.type = 'file';
importInput.accept = '.json';
importInput.style.display = 'none';
importInput.addEventListener('change', importMessages);
document.body.appendChild(importInput);
el.importBtn.addEventListener('click', () => importInput.click());
init();
})();
</script>
</body>
</html>
更多推荐



所有评论(0)