feat: 文章新建/编辑页编辑器支持本地图片上传并插入正文
- 两套页面(admin + 普通用户)EasyMDE 的 image 按钮改为“上传图片”: 选择本地图片 → 走既有附件接口(files 表 type=attachments,新建页 session_token / 编辑页 article_id)→ 在正文光标处插入  - templates/layouts/base.html:新增共享 window.blogUploadImage 上传助手 - templates/user/my_article_form.html:新建页 session_token 输入框加 id (编辑器上传需要读取);补齐原本缺失的上传能力 - i18n:新增 article_image_upload / article_image_not_image(中英) - 服务端依既有上传策略校验(扩展名/MIME/魔数/大小),客户端限 image/*
This commit is contained in:
4 files changed
+82
-5
No files matched your search
@@ -185,6 +185,8 @@ var translations = map[Lang]map[string]string{
|
||||
"article_att_uploading": "Uploading...",
|
||||
"article_att_error": "Upload failed. Please try again.",
|
||||
"article_att_delete_confirm": "Delete this attachment?",
|
||||
"article_image_upload": "Upload image",
|
||||
"article_image_not_image": "The file is not a valid image.",
|
||||
|
||||
// 文章管理
|
||||
"article_list_title": "Articles",
|
||||
@@ -626,6 +628,8 @@ var translations = map[Lang]map[string]string{
|
||||
"article_att_uploading": "上传中...",
|
||||
"article_att_error": "上传失败,请重试。",
|
||||
"article_att_delete_confirm": "删除该附件?",
|
||||
"article_image_upload": "上传图片",
|
||||
"article_image_not_image": "该文件不是有效的图片。",
|
||||
|
||||
// 文章管理
|
||||
"article_list_title": "文章管理",
|
||||
|
||||
@@ -118,6 +118,32 @@
|
||||
{{template "footer" .}}
|
||||
|
||||
<script>
|
||||
// 文章附件接口的上下文:新建页用 session_token,编辑页用 article_id。
|
||||
var ATT = {
|
||||
articleID: {{ if .FormArticleID }}{{ .FormArticleID }}{{ else }}0{{ end }},
|
||||
sessionToken: "{{ .SessionToken }}",
|
||||
uploadURL: "/api/admin/articles/attachments"
|
||||
};
|
||||
|
||||
// 编辑器“上传图片”按钮:选择本地图片 → 上传(files 表,type=attachments)→ 插入正文光标处。
|
||||
function uploadImageAction(editor) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = function () {
|
||||
var f = input.files && input.files[0];
|
||||
if (!f) return;
|
||||
blogUploadImage({ url: ATT.uploadURL, file: f, articleID: ATT.articleID, sessionToken: ATT.sessionToken })
|
||||
.then(function (r) {
|
||||
if (r.error) { blogShowError('articleError', r.error); return; }
|
||||
if (!r.is_image) { blogShowError('articleError', '{{index .Tr "article_image_not_image"}}'); return; }
|
||||
editor.codemirror.replaceSelection('\n');
|
||||
editor.codemirror.focus();
|
||||
});
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
var easyMDE = new EasyMDE({
|
||||
element: document.getElementById('articleContent'),
|
||||
autoDownloadFontAwesome: false,
|
||||
@@ -130,7 +156,7 @@ var easyMDE = new EasyMDE({
|
||||
toolbar: [
|
||||
'bold', 'italic', 'heading', '|',
|
||||
'quote', 'unordered-list', 'ordered-list', '|',
|
||||
'link', 'image', 'code', 'table', '|',
|
||||
'link', { name: 'uploadImage', className: 'fa fa-image', title: '{{index .Tr "article_image_upload"}}', action: uploadImageAction }, 'code', 'table', '|',
|
||||
'preview', 'side-by-side', 'fullscreen', '|',
|
||||
'guide'
|
||||
],
|
||||
@@ -140,8 +166,8 @@ var easyMDE = new EasyMDE({
|
||||
|
||||
// ---- Attachments ----
|
||||
(function () {
|
||||
var articleID = {{ if .FormArticleID }}{{ .FormArticleID }}{{ else }}0{{ end }};
|
||||
var sessionToken = "{{ .SessionToken }}";
|
||||
var articleID = ATT.articleID;
|
||||
var sessionToken = ATT.sessionToken;
|
||||
var csrfTokenEl = document.querySelector('meta[name="csrf-token"]');
|
||||
var csrfToken = csrfTokenEl ? csrfTokenEl.getAttribute('content') : '';
|
||||
var uploadBtn = document.getElementById('attachmentUploadBtn');
|
||||
|
||||
@@ -203,6 +203,26 @@
|
||||
});
|
||||
};
|
||||
|
||||
// 上传本地图片(multipart)到文章附件接口(files 表,type=attachments)。
|
||||
// 新建页传 session_token,编辑页传 article_id;成功 resolve {…, url, is_image}。
|
||||
// 供文章新建/编辑页编辑器“上传图片”按钮使用。
|
||||
window.blogUploadImage = function (opts) {
|
||||
var meta = document.querySelector('meta[name="csrf-token"]');
|
||||
var csrf = meta ? meta.getAttribute('content') : '';
|
||||
var fd = new FormData();
|
||||
fd.append('file', opts.file);
|
||||
if (opts.articleID) { fd.append('article_id', opts.articleID); }
|
||||
if (!opts.articleID && opts.sessionToken) { fd.append('session_token', opts.sessionToken); }
|
||||
return fetch(opts.url, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': csrf, 'Accept': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: fd
|
||||
}).then(function (r) {
|
||||
return r.json();
|
||||
});
|
||||
};
|
||||
|
||||
// 将表单序列化为 JSON 数据对象:
|
||||
// - 文本/select/textarea 从 FormData 取值(checkboxes 在下述循环覆盖)
|
||||
// - checkbox 一律转 bool(未选中也发送 false,匹配服务端 JSON 绑定)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<form id="myArticleForm" action="{{.FormAction}}" method="post" class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
|
||||
{{if .SessionToken}}
|
||||
<input type="hidden" name="session_token" value="{{.SessionToken}}">
|
||||
<input type="hidden" name="session_token" id="myArticleSessionToken" value="{{.SessionToken}}">
|
||||
{{end}}
|
||||
|
||||
<div>
|
||||
@@ -82,6 +82,33 @@
|
||||
<script>
|
||||
var myEasyMDE = null;
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 文章附件接口的上下文:新建页用 session_token,编辑页用 article_id。
|
||||
var sessEl = document.getElementById('myArticleSessionToken');
|
||||
var ATT = {
|
||||
articleID: {{ if .FormArticleID }}{{ .FormArticleID }}{{ else }}0{{ end }},
|
||||
sessionToken: sessEl ? sessEl.value : '',
|
||||
uploadURL: "/api/my/articles/attachments"
|
||||
};
|
||||
|
||||
// 编辑器“上传图片”按钮:选择本地图片 → 上传(files 表)→ 插入正文光标处。
|
||||
function uploadImageAction(editor) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = function () {
|
||||
var f = input.files && input.files[0];
|
||||
if (!f) return;
|
||||
blogUploadImage({ url: ATT.uploadURL, file: f, articleID: ATT.articleID, sessionToken: ATT.sessionToken })
|
||||
.then(function (r) {
|
||||
if (r.error) { blogShowError('myArticleError', r.error); return; }
|
||||
if (!r.is_image) { blogShowError('myArticleError', '{{index .Tr "article_image_not_image"}}'); return; }
|
||||
editor.codemirror.replaceSelection('\n');
|
||||
editor.codemirror.focus();
|
||||
});
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
myEasyMDE = new EasyMDE({
|
||||
element: document.getElementById('content'),
|
||||
autoDownloadFontAwesome: false,
|
||||
@@ -91,7 +118,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return '<div class="md-body">' + BlogMD.render(plainText) + '</div>';
|
||||
},
|
||||
toolbar: ["bold", "italic", "heading", "|", "quote", "unordered-list", "ordered-list", "|",
|
||||
"link", "image", "|", "preview", "side-by-side", "fullscreen", "|", "guide"]
|
||||
"link", { name: 'uploadImage', className: 'fa fa-image', title: '{{index .Tr "article_image_upload"}}', action: uploadImageAction }, "|", "preview", "side-by-side", "fullscreen", "|", "guide"]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user