feat: 普通用户文章页补齐附件区(上传/列表/插入正文/设封面/删除)
- 新增共享部分模板 templates/partials/article_attachments.html (附件区标记:文件名/大小/操作列 + 上传按钮) - 新增共享 static/js/article-attachments.js:上传(multipart→files 表 type=attachments)、编辑页回填、插入正文、图片一键设封面、删除; uploadURL/listURL/editor/i18n 文案均由页面注入 - 管理员页改用共享 partial+JS(行为不变);同时修复既有 bug: 表单提交闭包引用未声明的 articleID(作用域错误导致保存静默失败) - 普通用户页加入附件区,走 /api/my/articles/attachments(新建页 session_token / 编辑页 article_id),支持一键设封面/插入正文
This commit is contained in:
4 files changed
+208
-138
No files matched your search
@@ -0,0 +1,134 @@
|
||||
// 文章附件区共享逻辑(管理员与普通用户的文章新建/编辑页共用):
|
||||
// 上传(multipart → 附件接口,写 files 表 type=attachments)、编辑页回填列表、
|
||||
// 插入正文(图片 ![]() / 其他 []())、图片一键设封面、删除(引用计数由服务端处理)。
|
||||
//
|
||||
// 由页面调用:initArticleAttachments(cfg)
|
||||
// cfg:
|
||||
// uploadURL 上传接口,如 "/api/my/articles/attachments"(DELETE 为 uploadURL + "/<id>")
|
||||
// listURL 列表接口模板,":id" 会被替换为文章 id,如 "/api/my/articles/:id/attachments"
|
||||
// editor EasyMDE 实例(用于在光标处插入 Markdown)
|
||||
// articleID 文章 id(编辑页);新建页为 0
|
||||
// sessionToken 新建页的临时归属令牌
|
||||
// texts 页面 i18n 文案:{pick, uploading, insert, setCover, coverSet, del, delConfirm, err}
|
||||
window.initArticleAttachments = function (cfg) {
|
||||
var uploadBtn = document.getElementById('attachmentUploadBtn');
|
||||
var fileInput = document.getElementById('attachmentInput');
|
||||
var msgEl = document.getElementById('attachmentMsg');
|
||||
var listEl = document.getElementById('attachmentList');
|
||||
var texts = cfg.texts || {};
|
||||
if (!uploadBtn || !listEl || !fileInput) { return; }
|
||||
|
||||
var meta = document.querySelector('meta[name="csrf-token"]');
|
||||
var csrfToken = meta ? meta.getAttribute('content') : '';
|
||||
|
||||
// Enable the uploader (uploads allowed only while logged in, which is true here).
|
||||
uploadBtn.disabled = false;
|
||||
fileInput.disabled = false;
|
||||
|
||||
function fmtSize(b) {
|
||||
if (b < 1024) { return b + ' B'; }
|
||||
var u = ['KiB', 'MiB', 'GiB'], i = -1;
|
||||
do { b /= 1024; i++; } while (b >= 1024 && i < u.length - 1);
|
||||
return b.toFixed(1) + ' ' + u[i];
|
||||
}
|
||||
|
||||
function insertMd(md) {
|
||||
var cm = cfg.editor && cfg.editor.codemirror;
|
||||
if (!cm) { return; }
|
||||
cm.replaceSelection(md + '\n');
|
||||
cm.focus();
|
||||
}
|
||||
|
||||
function addRow(att) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-gray-50';
|
||||
tr.dataset.id = att.id;
|
||||
var nameTd = document.createElement('td');
|
||||
nameTd.className = 'px-3 py-2 text-sm text-gray-800';
|
||||
var link = document.createElement('a');
|
||||
link.href = att.url; link.target = '_blank'; link.textContent = att.filename;
|
||||
nameTd.appendChild(link);
|
||||
var sizeTd = document.createElement('td');
|
||||
sizeTd.className = 'px-3 py-2 text-sm text-gray-500';
|
||||
sizeTd.textContent = fmtSize(att.size);
|
||||
var actTd = document.createElement('td');
|
||||
actTd.className = 'px-3 py-2 text-sm text-right whitespace-nowrap';
|
||||
var insBtn = document.createElement('button');
|
||||
insBtn.type = 'button';
|
||||
insBtn.textContent = texts.insert || 'Insert';
|
||||
insBtn.className = 'text-blue-600 hover:text-blue-800 font-medium mr-3 cursor-pointer bg-transparent border-none';
|
||||
insBtn.onclick = function () {
|
||||
var md = att.is_image
|
||||
? ''
|
||||
: '[' + att.filename + '](' + att.url + ')';
|
||||
insertMd(md);
|
||||
};
|
||||
|
||||
// Images: extra button to copy the URL into the cover field.
|
||||
var coverBtn = null;
|
||||
if (att.is_image) {
|
||||
coverBtn = document.createElement('button');
|
||||
coverBtn.type = 'button';
|
||||
coverBtn.textContent = texts.setCover || 'Cover';
|
||||
coverBtn.className = 'text-green-600 hover:text-green-800 font-medium mr-3 cursor-pointer bg-transparent border-none';
|
||||
coverBtn.onclick = function () {
|
||||
var cover = document.querySelector('input[name="cover"]');
|
||||
if (cover) { cover.value = att.url; msgEl.textContent = texts.coverSet || ''; }
|
||||
};
|
||||
}
|
||||
var delBtn = document.createElement('button');
|
||||
delBtn.type = 'button';
|
||||
delBtn.textContent = texts.del || 'Delete';
|
||||
delBtn.className = 'text-red-600 hover:text-red-800 font-medium cursor-pointer bg-transparent border-none';
|
||||
delBtn.onclick = function () {
|
||||
if (!confirm(texts.delConfirm || 'Delete?')) { return; }
|
||||
fetch(cfg.uploadURL + '/' + att.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': csrfToken }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (r) {
|
||||
if (r.ok) { tr.remove(); }
|
||||
else { msgEl.textContent = r.error || 'error'; }
|
||||
});
|
||||
};
|
||||
actTd.appendChild(insBtn);
|
||||
if (coverBtn) { actTd.appendChild(coverBtn); }
|
||||
actTd.appendChild(delBtn);
|
||||
tr.appendChild(nameTd);
|
||||
tr.appendChild(sizeTd);
|
||||
tr.appendChild(actTd);
|
||||
listEl.appendChild(tr);
|
||||
}
|
||||
|
||||
uploadBtn.addEventListener('click', function () {
|
||||
if (!fileInput.files.length) { msgEl.textContent = texts.pick || 'Pick a file.'; return; }
|
||||
var fd = new FormData();
|
||||
fd.append('file', fileInput.files[0]);
|
||||
if (cfg.articleID) { fd.append('article_id', cfg.articleID); }
|
||||
else if (cfg.sessionToken) { fd.append('session_token', cfg.sessionToken); }
|
||||
msgEl.textContent = texts.uploading || 'Uploading...';
|
||||
fetch(cfg.uploadURL, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': csrfToken },
|
||||
body: fd
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (r) {
|
||||
if (r.error) { msgEl.textContent = r.error; return; }
|
||||
msgEl.textContent = '';
|
||||
addRow(r);
|
||||
fileInput.value = '';
|
||||
})
|
||||
.catch(function () { msgEl.textContent = texts.err || 'Upload failed.'; });
|
||||
});
|
||||
|
||||
// Edit page: load existing attachments.
|
||||
if (cfg.articleID) {
|
||||
fetch(cfg.listURL.replace(':id', cfg.articleID))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (r) {
|
||||
(r.attachments || []).forEach(addRow);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user