Files
go_blog/static/js/article-attachments.js
dsh 134943e4fb 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),支持一键设封面/插入正文
2026-08-28 20:02:46 +08:00

135 lines
6.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 文章附件区共享逻辑(管理员与普通用户的文章新建/编辑页共用):
// 上传(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 + ')'
: '[' + 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);
});
}
};