421a8aa1f7
1. 新增知资札记完整站点:包含首页、详情页、管理后台 2. 添加站点图标、鉴权配置与后端API鉴权逻辑 3. 在市场页面新增知资札记入口跳转 4. 新增经典乡村音乐播放列表文件 5. 临时添加养生谚语批量导入脚本
696 lines
26 KiB
JavaScript
696 lines
26 KiB
JavaScript
/**
|
||
* 知资札记 - 后台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\b[^>]*>(.*?)<\/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 = '<tr><td colspan="10" style="text-align:center;padding:40px;color:#999">暂无数据</td></tr>';
|
||
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 = '<div class="text-icon">文</div>';
|
||
}
|
||
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 = '<div class="za-modal">' +
|
||
'<h3>' + escapeHtml(title) + '</h3>' +
|
||
'<div class="modal-body">' + escapeHtml(message) + '</div>' +
|
||
'<div class="modal-actions">' +
|
||
'<button class="btn btn-cancel" id="za-confirm-cancel">取消</button>' +
|
||
'<button class="btn btn-danger" id="za-confirm-ok">确认删除</button>' +
|
||
'</div></div>';
|
||
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 = '<div class="za-modal form-modal">' +
|
||
'<h3>' + (id ? '编辑存档' : '新增存档') + '</h3>' +
|
||
'<div class="form-group"><label>标题</label><input id="za-form-title" value="' + escapeHtml(article ? article.title : '') + '" placeholder="输入文章标题" /></div>' +
|
||
'<div class="form-group"><label>分类</label><select id="za-form-category"><option value="">选择分类</option></select></div>' +
|
||
'<div class="form-group"><label>来源备注</label><input id="za-form-source" value="' + escapeHtml(article ? article.source : '') + '" placeholder="来源信息(纯文字)" /></div>' +
|
||
'<div class="form-group"><label>标签</label><div class="tag-input" id="za-form-tags"><input id="za-tag-input" placeholder="输入标签后回车" /></div></div>' +
|
||
'<div class="form-group"><label>正文</label><textarea id="za-form-content" placeholder="粘贴正文内容(自动清除外链)">' + escapeHtml(article ? article.content : '') + '</textarea></div>' +
|
||
'<div class="form-group"><label>内容类型</label><select id="za-form-contentType"><option value="text">纯文字</option><option value="image">图文</option></select></div>' +
|
||
'<div class="form-group"><label>置顶</label><select id="za-form-pinned"><option value="0">否</option><option value="1">是</option></select></div>' +
|
||
'<div class="form-group"><label>图片上传</label>' +
|
||
'<div class="upload-area" id="za-upload-area">' +
|
||
'<input type="file" id="za-file-input" accept="image/*" multiple style="display:none" />' +
|
||
'<button type="button" class="btn btn-outline" id="za-upload-btn">选择图片(可多选)</button>' +
|
||
'<div id="za-upload-preview" class="upload-preview"></div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="modal-actions">' +
|
||
'<button class="btn btn-cancel" id="za-form-cancel">取消</button>' +
|
||
'<button class="btn btn-primary" id="za-form-save">保存</button>' +
|
||
'</div></div>';
|
||
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 + '<button class="remove-tag" data-idx="' + i + '">×</button>';
|
||
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 = '<img src="/uploads/zhizi_notes/' + img.filename + '" alt="" />' +
|
||
'<button class="preview-remove" data-filename="' + img.filename + '" data-id="' + (img.id || '') + '">×</button>';
|
||
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 = '<img src="' + e.target.result + '" alt="" />' +
|
||
'<button class="preview-remove">×</button>';
|
||
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, '>').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);
|
||
})(); |