Files
go_blog/templates/admin/article_create.html
T
dsh 5c55a76acb 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:01:20 +08:00

191 lines
8.7 KiB
HTML
Raw 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.
{{define "article_create"}}
{{template "header" .}}
{{template "markdown_assets" .}}
<section class="max-w-3xl mx-auto px-4 py-10">
<h2 class="text-2xl font-bold text-gray-900 mb-8">{{.FormTitleText}}</h2>
<div id="articleError" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm {{if not .Error}}hidden{{end}}">
{{.Error}}
</div>
<form id="articleForm" action="{{.FormAction}}" method="post" class="space-y-6">
<input type="hidden" name="_csrf" value="{{.CSRFToken}}">
<!-- Hidden: attachment ownership (token on create, id on edit) -->
<input type="hidden" name="session_token" value="{{.SessionToken}}">
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_title"}}</label>
<input type="text" name="title" value="{{.FormTitle}}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
placeholder="{{index .Tr "article_title"}}">
</div>
<!-- Slug -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_slug"}}</label>
<input type="text" name="slug" value="{{.FormSlug}}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
placeholder="{{index .Tr "article_slug_hint"}}">
<p class="text-xs text-gray-400 mt-1">{{index .Tr "article_slug_hint"}}</p>
</div>
<!-- Summary -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_summary"}}</label>
<textarea name="summary" rows="3"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors resize-y"
placeholder="{{index .Tr "article_summary"}}">{{.FormSummary}}</textarea>
</div>
<!-- Content (EasyMDE Markdown Editor) -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_content"}}</label>
<textarea id="articleContent" name="content">{{.FormContent}}</textarea>
</div>
<!-- Cover -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_cover"}}</label>
<input type="text" name="cover" value="{{.FormCover}}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
placeholder="https://...">
</div>
<!-- Tags -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_tags"}}</label>
<input type="text" name="tags" value="{{.FormTags}}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
placeholder="{{index .Tr "article_tags_hint"}}">
<p class="text-xs text-gray-400 mt-1">{{index .Tr "article_tags_hint"}}</p>
</div>
<!-- Attachments -->
{{template "article_attachments" .}}
<!-- Published At -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_published_at"}}</label>
<input type="datetime-local" name="published_at" value="{{.FormPublishedAt}}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
<p class="text-xs text-gray-400 mt-1">{{index .Tr "article_published_at_hint"}}</p>
</div>
<!-- IsTop -->
<div class="flex items-center gap-2">
<input type="checkbox" name="is_top" value="1" id="isTopCheckbox" {{if .FormIsTop}}checked{{end}}
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<label for="isTopCheckbox" class="text-sm font-medium text-gray-700">{{index .Tr "article_is_top"}}</label>
</div>
<!-- Submit Buttons -->
<div class="flex gap-3">
<button type="submit" name="status" value="0"
class="px-6 py-3 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 transition-colors cursor-pointer">
{{index .Tr "article_save_draft"}}
</button>
<button type="submit" name="status" value="1"
class="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors cursor-pointer">
{{index .Tr "article_publish"}}
</button>
</div>
</form>
</section>
{{template "footer" .}}
<script src="/static/js/article-attachments.js?v=1"></script>
<script>
// 文章附件接口的上下文:新建页用 session_token,编辑页用 article_id。
var ATT = {
articleID: {{ if .FormArticleID }}{{ .FormArticleID }}{{ else }}0{{ end }},
sessionToken: "{{ .SessionToken }}",
uploadURL: "/api/admin/articles/attachments"
};
// 顶层作用域:供表单提交(PUT/POST 路由选择)与附件共享逻辑共同使用。
var articleID = ATT.articleID;
var sessionToken = ATT.sessionToken;
// 编辑器“上传图片”按钮:选择本地图片 → 上传(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('![' + r.filename + '](' + r.url + ')\n');
editor.codemirror.focus();
});
};
input.click();
}
var easyMDE = new EasyMDE({
element: document.getElementById('articleContent'),
autoDownloadFontAwesome: false,
spellChecker: false,
autosave: { enabled: false },
placeholder: '{{index .Tr "article_content"}}',
previewRender: function (plainText, preview) {
return '<div class="md-body">' + BlogMD.render(plainText) + '</div>';
},
toolbar: [
'bold', 'italic', 'heading', '|',
'quote', 'unordered-list', 'ordered-list', '|',
'link', { name: 'uploadImage', className: 'fa fa-image', title: '{{index .Tr "article_image_upload"}}', action: uploadImageAction }, 'code', 'table', '|',
'preview', 'side-by-side', 'fullscreen', '|',
'guide'
],
status: false,
minHeight: '300px'
});
// ---- Attachments(共享逻辑,见 static/js/article-attachments.js ----
initArticleAttachments({
uploadURL: ATT.uploadURL,
listURL: '/api/admin/articles/:id/attachments',
editor: easyMDE,
articleID: ATT.articleID,
sessionToken: ATT.sessionToken,
texts: {
pick: '{{index .Tr "article_att_pick"}}',
uploading: '{{index .Tr "article_att_uploading"}}',
insert: '{{index .Tr "article_att_insert"}}',
setCover: '{{index .Tr "article_att_set_cover"}}',
coverSet: '{{index .Tr "article_att_cover_set"}}',
del: '{{index .Tr "settings_delete"}}',
delConfirm: '{{index .Tr "article_att_delete_confirm"}}',
err: '{{index .Tr "article_att_error"}}'
}
});
// ---- Article form submitJSON API;草稿/发布按钮由 e.submitter 分流) ----
(function () {
var form = document.getElementById('articleForm');
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = e.submitter || null;
// EasyMDE 隐藏了 textarea:先同步编辑器内容再提交。
var ta = document.getElementById('articleContent');
if (ta && easyMDE) { ta.value = easyMDE.value(); }
var method = articleID ? 'PUT' : 'POST';
var url = articleID ? '/api/admin/articles/' + articleID : '/api/admin/articles';
blogAPI(method, url, blogForm(form, btn)).then(function (r) {
if (r.ok) { window.location.href = r.redirect || '/admin'; }
else { blogShowError('articleError', r.error || 'Failed to save article.'); }
});
});
})();
</script>
{{end}}