<!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 = {
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    '"': '&quot;',
                    "'": '&#039;'
                };
                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>

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐