/** * 知资札记 - 后台CMS管理逻辑 * 图文内容管理列表页面 */ (function () { 'use strict'; const API_BASE = '/api/zhizi_notes'; const $ = function (sel) { return document.querySelector(sel); }; const $$ = function (sel) { return document.querySelectorAll(sel); }; // ── 状态 ── var adminState = { page: 1, pageSize: 20, totalPages: 0, total: 0, filter: { search: '', category: '', contentType: '', status: '' }, selectedIds: new Set(), editingId: null }; // ── Toast ── function showToast(msg, type) { type = type || 'info'; var t = $('#za-toast'); if (!t) return; t.textContent = msg; t.className = 'za-toast ' + type + ' show'; clearTimeout(t._timer); t._timer = setTimeout(function () { t.classList.remove('show'); }, type === 'error' ? 4000 : 2000); } // ── 链接清洗 ── function clearAllLinks(html) { if (!html || typeof html !== 'string') return ''; return html.replace(/]*>(.*?)<\/a>/gi, '$1'); } // ── API ── async function apiGet(path) { try { var res = await fetch(API_BASE + path, { credentials: 'same-origin' }); return await res.json(); } catch (e) { return { ok: false, error: e.message }; } } async function apiPost(path, body) { try { var res = await fetch(API_BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(body) }); return await res.json(); } catch (e) { return { ok: false, error: e.message }; } } async function apiPut(path, body) { try { var res = await fetch(API_BASE + path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(body) }); return await res.json(); } catch (e) { return { ok: false, error: e.message }; } } async function apiDelete(path) { try { var res = await fetch(API_BASE + path, { method: 'DELETE', credentials: 'same-origin' }); return await res.json(); } catch (e) { return { ok: false, error: e.message }; } } // ── 加载列表 ── async function loadTableData() { var params = new URLSearchParams(); params.set('page', adminState.page); params.set('pageSize', adminState.pageSize); if (adminState.filter.search) params.set('search', adminState.filter.search); if (adminState.filter.category) params.set('category', adminState.filter.category); if (adminState.filter.contentType) params.set('contentType', adminState.filter.contentType); if (adminState.filter.status) params.set('status', adminState.filter.status); var data = await apiGet('/articles?' + params.toString()); if (!data.ok) { showToast('加载失败:' + (data.error || '未知错误'), 'error'); return; } adminState.totalPages = data.pagination ? data.pagination.totalPages : 0; adminState.total = data.pagination ? data.pagination.total : 0; adminState.selectedIds.clear(); renderTable(data.data || []); renderPagination(); } // ── 渲染表格 ── function renderTable(articles) { var tbody = $('#za-tbody'); if (!tbody) return; tbody.innerHTML = ''; if (articles.length === 0) { tbody.innerHTML = '暂无数据'; return; } articles.forEach(function (a) { var tr = document.createElement('tr'); // 复选框 var tdCb = document.createElement('td'); var cb = document.createElement('input'); cb.type = 'checkbox'; cb.setAttribute('data-id', a.id); cb.addEventListener('change', function () { toggleSelect(a.id, cb.checked); }); tdCb.appendChild(cb); // 封面 var tdCover = document.createElement('td'); var coverDiv = document.createElement('div'); coverDiv.className = 'cell-cover'; if (a.cover_url) { var img = document.createElement('img'); img.src = a.cover_url; img.alt = ''; coverDiv.appendChild(img); } else { coverDiv.innerHTML = '
文
'; } tdCover.appendChild(coverDiv); // 标题 var tdTitle = document.createElement('td'); var titleDiv = document.createElement('div'); titleDiv.className = 'cell-title'; titleDiv.textContent = a.title; titleDiv.title = a.title; tdTitle.appendChild(titleDiv); // 分类 var tdCat = document.createElement('td'); tdCat.textContent = a.category; // 内容类型 var tdType = document.createElement('td'); var typeSpan = document.createElement('span'); typeSpan.className = 'type-badge ' + (a.content_type === 'text' ? 'text' : 'image'); var imgCount = a.image_count || 0; typeSpan.textContent = a.content_type === 'text' ? '纯文字' : '图文(' + imgCount + '张)'; tdType.appendChild(typeSpan); // 来源 var tdSrc = document.createElement('td'); tdSrc.textContent = a.source || '-'; tdSrc.style.maxWidth = '100px'; tdSrc.style.overflow = 'hidden'; tdSrc.style.textOverflow = 'ellipsis'; tdSrc.style.whiteSpace = 'nowrap'; // 创建时间 var tdTime = document.createElement('td'); tdTime.textContent = (a.created_at || '').slice(0, 16); tdTime.style.whiteSpace = 'nowrap'; // 阅读量 var tdViews = document.createElement('td'); tdViews.textContent = a.views || 0; // 状态 var tdStatus = document.createElement('td'); var statusSpan = document.createElement('span'); statusSpan.className = 'status-badge ' + (a.status === 'normal' ? 'normal' : 'offline'); statusSpan.textContent = a.status === 'normal' ? '正常' : '已下架'; tdStatus.appendChild(statusSpan); // 操作 var tdActions = document.createElement('td'); tdActions.className = 'actions'; var btnPreview = document.createElement('button'); btnPreview.className = 'act-preview'; btnPreview.textContent = '预览'; btnPreview.addEventListener('click', function () { window.open('/tools/zhizi_notes/detail.html?id=' + a.id, '_blank'); }); var btnEdit = document.createElement('button'); btnEdit.className = 'act-edit'; btnEdit.textContent = '编辑'; btnEdit.addEventListener('click', function () { openEditForm(a.id); }); var btnToggle = document.createElement('button'); btnToggle.className = 'act-toggle'; btnToggle.textContent = a.status === 'normal' ? '下架' : '上架'; btnToggle.addEventListener('click', function () { toggleStatus(a.id, a.status); }); var btnDel = document.createElement('button'); btnDel.className = 'act-delete'; btnDel.textContent = '删除'; btnDel.addEventListener('click', function () { confirmDelete(a.id, a.title); }); tdActions.appendChild(btnPreview); tdActions.appendChild(btnEdit); tdActions.appendChild(btnToggle); tdActions.appendChild(btnDel); tr.appendChild(tdCb); tr.appendChild(tdCover); tr.appendChild(tdTitle); tr.appendChild(tdCat); tr.appendChild(tdType); tr.appendChild(tdSrc); tr.appendChild(tdTime); tr.appendChild(tdViews); tr.appendChild(tdStatus); tr.appendChild(tdActions); tbody.appendChild(tr); }); } // ── 分页 ── function renderPagination() { var container = $('#za-pagination'); if (!container) return; var info = $('#za-page-info'); if (info) info.textContent = '共 ' + adminState.total + ' 条,第 ' + adminState.page + '/' + adminState.totalPages + ' 页'; var btns = $('#za-page-btns'); if (!btns) return; btns.innerHTML = ''; var total = adminState.totalPages; var cur = adminState.page; var prevBtn = document.createElement('button'); prevBtn.textContent = '上一页'; prevBtn.disabled = cur <= 1; prevBtn.addEventListener('click', function () { if (cur > 1) { adminState.page--; loadTableData(); } }); btns.appendChild(prevBtn); var start = Math.max(1, cur - 2); var end = Math.min(total, cur + 2); for (var i = start; i <= end; i++) { var pageBtn = document.createElement('button'); pageBtn.textContent = i; if (i === cur) pageBtn.classList.add('active'); pageBtn.addEventListener('click', function (p) { return function () { adminState.page = p; loadTableData(); }; }(i)); btns.appendChild(pageBtn); } var nextBtn = document.createElement('button'); nextBtn.textContent = '下一页'; nextBtn.disabled = cur >= total; nextBtn.addEventListener('click', function () { if (cur < total) { adminState.page++; loadTableData(); } }); btns.appendChild(nextBtn); } // ── 选择行 ── function toggleSelect(id, checked) { if (checked) adminState.selectedIds.add(id); else adminState.selectedIds.delete(id); updateBatchBtns(); } function toggleSelectAll(checked) { $$('#za-tbody input[type="checkbox"]').forEach(function (cb) { cb.checked = checked; var id = parseInt(cb.getAttribute('data-id')); if (checked) adminState.selectedIds.add(id); else adminState.selectedIds.delete(id); }); updateBatchBtns(); } function updateBatchBtns() { var hasSelection = adminState.selectedIds.size > 0; var btns = $$('.za-toolbar .btn:disabled, .za-toolbar .btn:not(:disabled)'); $$('#za-btn-batch-offline, #za-btn-batch-delete, #za-btn-batch-export').forEach(function (btn) { btn.disabled = !hasSelection; }); } // ── 批量操作 ── async function batchAction(action) { if (adminState.selectedIds.size === 0) { showToast('请先选择文章', 'info'); return; } var ids = Array.from(adminState.selectedIds); var labels = { delete: '删除', offline: '下架', online: '上架' }; var label = labels[action] || action; if (action === 'delete') { showConfirm('确认批量' + label, '确定要批量' + label + ' ' + ids.length + ' 篇文章吗?此操作不可恢复。', async function () { var data = await apiPost('/articles/batch', { ids: ids, action: action }); if (data.ok) { showToast('批量' + label + '成功', 'success'); loadTableData(); } else showToast('操作失败:' + (data.error || '未知错误'), 'error'); }); } else { var data = await apiPost('/articles/batch', { ids: ids, action: action }); if (data.ok) { showToast('批量' + label + '成功', 'success'); loadTableData(); } else showToast('操作失败:' + (data.error || '未知错误'), 'error'); } } // ── 状态切换 ── async function toggleStatus(id, currentStatus) { var newStatus = currentStatus === 'normal' ? 'offline' : 'normal'; var label = newStatus === 'normal' ? '上架' : '下架'; var data = await apiPut('/articles/' + id, { status: newStatus }); if (data.ok) { showToast(label + '成功', 'success'); loadTableData(); } else showToast('操作失败:' + (data.error || '未知错误'), 'error'); } // ── 删除确认 ── function confirmDelete(id, title) { showConfirm('确认删除', '确定要永久删除文章「' + title + '」吗?关联图片也会一并删除,此操作不可恢复。', async function () { var data = await apiDelete('/articles/' + id); if (data.ok) { showToast('删除成功', 'success'); loadTableData(); } else showToast('删除失败:' + (data.error || '未知错误'), 'error'); }); } // ── 确认弹窗 ── function showConfirm(title, message, onConfirm) { var overlay = document.createElement('div'); overlay.className = 'za-modal-overlay'; overlay.innerHTML = '
' + '

' + escapeHtml(title) + '

' + '' + '
'; document.body.appendChild(overlay); overlay.querySelector('#za-confirm-cancel').addEventListener('click', function () { overlay.remove(); }); overlay.querySelector('#za-confirm-ok').addEventListener('click', function () { overlay.remove(); if (onConfirm) onConfirm(); }); overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); }); } // ── 上传文件 ── async function uploadFiles(files, articleId) { var formData = new FormData(); formData.append('articleId', articleId || 0); for (var i = 0; i < files.length; i++) { formData.append('images', files[i]); } try { var res = await fetch(API_BASE + '/upload', { method: 'POST', credentials: 'same-origin', body: formData }); return await res.json(); } catch (e) { return { ok: false, error: e.message }; } } // ── 新增/编辑表单 ── async function openEditForm(id) { adminState.editingId = id; var article = null; if (id) { var data = await apiGet('/articles/' + id); if (data.ok) article = data.data; else { showToast('加载文章失败', 'error'); return; } } var overlay = document.createElement('div'); overlay.className = 'za-modal-overlay'; overlay.innerHTML = '
' + '

' + (id ? '编辑存档' : '新增存档') + '

' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
'; document.body.appendChild(overlay); // 填充分类 var cats = await apiGet('/categories'); var catSelect = overlay.querySelector('#za-form-category'); if (cats.ok && cats.data) { cats.data.forEach(function (c) { var opt = document.createElement('option'); opt.value = c.name; opt.textContent = c.name; if (article && article.category === c.name) opt.selected = true; catSelect.appendChild(opt); }); } if (article) { overlay.querySelector('#za-form-contentType').value = article.content_type || 'text'; overlay.querySelector('#za-form-pinned').value = article.is_pinned ? '1' : '0'; } // 标签管理 var tagContainer = overlay.querySelector('#za-form-tags'); var tagInput = overlay.querySelector('#za-tag-input'); var currentTags = []; try { if (article && article.tags_json) currentTags = JSON.parse(article.tags_json); } catch (e) {} function renderTagChips() { var existing = tagContainer.querySelectorAll('.tag-chip'); existing.forEach(function (el) { el.remove(); }); currentTags.forEach(function (t, i) { var chip = document.createElement('span'); chip.className = 'tag-chip'; chip.innerHTML = t + ''; chip.querySelector('.remove-tag').addEventListener('click', function () { currentTags.splice(i, 1); renderTagChips(); }); tagContainer.insertBefore(chip, tagInput); }); } renderTagChips(); tagInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); var val = tagInput.value.trim(); if (val && currentTags.indexOf(val) === -1) { currentTags.push(val); renderTagChips(); } tagInput.value = ''; } }); // ── 图片上传逻辑 ── var selectedFiles = []; var removedImageIds = []; // 编辑时删除的已有图片ID var uploadPreview = overlay.querySelector('#za-upload-preview'); var fileInput = overlay.querySelector('#za-file-input'); var uploadBtn = overlay.querySelector('#za-upload-btn'); // 编辑时加载已有图片 if (article && article.images) { article.images.forEach(function (img) { var item = document.createElement('div'); item.className = 'preview-item'; item.innerHTML = '' + ''; uploadPreview.appendChild(item); }); } // 为所有已有图片的删除按钮绑定事件 uploadPreview.querySelectorAll('.preview-remove').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-id'); if (id) removedImageIds.push(parseInt(id)); btn.closest('.preview-item').remove(); }); }); uploadBtn.addEventListener('click', function () { fileInput.click(); }); fileInput.addEventListener('change', function () { if (!fileInput.files) return; Array.from(fileInput.files).forEach(function (file, idx) { if (!file.type.startsWith('image/')) return; selectedFiles.push(file); var reader = new FileReader(); reader.onload = (function (i) { return function (e) { var item = document.createElement('div'); item.className = 'preview-item'; item.innerHTML = '' + ''; item.querySelector('.preview-remove').addEventListener('click', function () { selectedFiles[i] = null; item.remove(); }); uploadPreview.appendChild(item); }; })(idx); reader.readAsDataURL(file); }); fileInput.value = ''; }); overlay.querySelector('#za-form-cancel').addEventListener('click', function () { overlay.remove(); }); overlay.querySelector('#za-form-save').addEventListener('click', async function () { var saveBtn = overlay.querySelector('#za-form-save'); saveBtn.disabled = true; saveBtn.textContent = '保存中…'; var title = overlay.querySelector('#za-form-title').value.trim(); if (!title) { showToast('标题不能为空', 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; return; } // 先保存文章获取 ID var rawContent = overlay.querySelector('#za-form-content').value; var content = clearAllLinks(rawContent); var body = { title: title, content: content, category: overlay.querySelector('#za-form-category').value || '未分类', source: overlay.querySelector('#za-form-source').value.trim(), tags: currentTags, contentType: overlay.querySelector('#za-form-contentType').value, isPinned: overlay.querySelector('#za-form-pinned').value === '1' }; var result; if (id) { result = await apiPut('/articles/' + id, body); result.data = { id: id }; } else { result = await apiPost('/articles', body); } if (!result.ok) { showToast('保存失败:' + (result.error || '未知错误'), 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; return; } var savedId = result.data ? result.data.id : id; if (!savedId) savedId = id; // 编辑时删除已被移除的图片 if (id && removedImageIds.length > 0) { for (var k = 0; k < removedImageIds.length; k++) { await apiDelete('/images/' + removedImageIds[k]); } } // 过滤已删除的文件(标记为 null) var filesToUpload = selectedFiles.filter(function (f) { return f !== null; }); // 上传图片 if (filesToUpload.length > 0 && savedId) { saveBtn.textContent = '上传图片中…'; var uploadResult = await uploadFiles(filesToUpload, savedId); if (!uploadResult.ok) { showToast('图片上传失败:' + (uploadResult.error || '未知错误'), 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; return; } // 更新封面为新上传的第一张图 if (uploadResult.data && uploadResult.data.length > 0) { body.coverUrl = uploadResult.data[0].url; await apiPut('/articles/' + savedId, body); } } showToast(id ? '更新成功' : '创建成功', 'success'); overlay.remove(); loadTableData(); }); overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); }); } function escapeHtml(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // ── 导出 ── async function exportSelected() { if (adminState.selectedIds.size === 0) { showToast('请先选择文章', 'info'); return; } var ids = Array.from(adminState.selectedIds); try { var res = await fetch(API_BASE + '/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ ids: ids }) }); if (res.ok) { var blob = await res.blob(); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = '知资札记_导出.txt'; a.click(); URL.revokeObjectURL(url); showToast('导出成功', 'success'); } else { showToast('导出失败', 'error'); } } catch (e) { showToast('导出失败:' + e.message, 'error'); } } // ── 初始化 ── function init() { // 全选 var selectAllCb = $('#za-select-all'); if (selectAllCb) { selectAllCb.addEventListener('change', function () { toggleSelectAll(selectAllCb.checked); }); } // 搜索 var searchInput = $('#za-search'); var searchBtn = $('#za-search-btn'); if (searchBtn && searchInput) { searchBtn.addEventListener('click', function () { adminState.filter.search = searchInput.value.trim(); adminState.page = 1; loadTableData(); }); searchInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') { adminState.filter.search = searchInput.value.trim(); adminState.page = 1; loadTableData(); } }); } // 分类筛选 var catFilter = $('#za-filter-category'); if (catFilter) { catFilter.addEventListener('change', function () { adminState.filter.category = catFilter.value; adminState.page = 1; loadTableData(); }); } // 内容类型筛选 var typeFilter = $('#za-filter-type'); if (typeFilter) { typeFilter.addEventListener('change', function () { adminState.filter.contentType = typeFilter.value; adminState.page = 1; loadTableData(); }); } // 状态筛选 var statusFilter = $('#za-filter-status'); if (statusFilter) { statusFilter.addEventListener('change', function () { adminState.filter.status = statusFilter.value; adminState.page = 1; loadTableData(); }); } // 每页条数 var pageSizeSel = $('#za-page-size'); if (pageSizeSel) { pageSizeSel.addEventListener('change', function () { adminState.pageSize = parseInt(pageSizeSel.value) || 20; adminState.page = 1; loadTableData(); }); } // 批量操作按钮 var btnBatchOffline = $('#za-btn-batch-offline'); var btnBatchDelete = $('#za-btn-batch-delete'); var btnBatchExport = $('#za-btn-batch-export'); if (btnBatchOffline) btnBatchOffline.addEventListener('click', function () { batchAction('offline'); }); if (btnBatchDelete) btnBatchDelete.addEventListener('click', function () { batchAction('delete'); }); if (btnBatchExport) btnBatchExport.addEventListener('click', function () { exportSelected(); }); // 新增按钮 var btnNew = $('#za-btn-new'); if (btnNew) btnNew.addEventListener('click', function () { openEditForm(null); }); // 加载分类选项 loadFilterOptions(); loadTableData(); } async function loadFilterOptions() { var cats = await apiGet('/categories'); var catFilter = $('#za-filter-category'); if (catFilter && cats.ok && cats.data) { cats.data.forEach(function (c) { var opt = document.createElement('option'); opt.value = c.name; opt.textContent = c.name; catFilter.appendChild(opt); }); } } document.addEventListener('DOMContentLoaded', init); })();