Files
go_blog/templates/partials/article_form.html
T
dsh d78ed8e2fe refactor: 文章新建/编辑表单收敛为单一模板 article_form,删除两套重复模板
- 新增 templates/partials/article_form.html(define article_form):
  按 .FormIsMy 分支渲染——管理员变体(标签/置顶勾选/草稿发布双按钮/
  /api/admin/articles)与作者变体(状态下拉/保存取消//api/my/articles)
- renderArticleForm / renderMyArticleForm 均渲染 article_form,各自
  设置 FormIsMy=false/true;JS 统一(API_BASE/redirectBase/editor/
  错误提示均单份)
- 删除 templates/admin/article_create.html 与
  templates/user/my_article_form.html(约 360 行 → 一份 ~230 行)
- 新增 TestArticleFormTemplateVariants 断言两个变体渲染差异;
  测试环境注册两个工作区的新建页路由
2026-08-28 20:02:46 +08:00

222 lines
10 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_form"}}
{{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" id="articleSessionToken" 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}}" required
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(管理员工作区;后端两者均支持,作者页暂无入口) -->
{{if not .FormIsMy}}
<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>
{{end}}
<!-- 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(管理员工作区:普通作者不可置顶全站文章,SECURITY_TODO #31 -->
{{if not .FormIsMy}}
<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>
{{end}}
{{if .FormIsMy}}
<!-- My workspace:状态下拉 + 保存/取消 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_field_status"}}</label>
<select name="status"
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">
<option value="0" {{if eq .FormStatus "0"}}selected{{end}}>{{index .Tr "article_draft"}}</option>
<option value="1" {{if eq .FormStatus "1"}}selected{{end}}>{{index .Tr "article_published"}}</option>
</select>
</div>
<div class="flex gap-3">
<button type="submit"
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_save"}}
</button>
<a href="/my/articles"
class="px-6 py-3 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 transition-colors">
{{index .Tr "article_cancel"}}
</a>
</div>
{{else}}
<!-- Admin workspace:草稿/发布双按钮 -->
<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>
{{end}}
</form>
</section>
{{template "footer" .}}
<script src="/static/js/article-attachments.js?v=1"></script>
<script>
// 工作区上下文:管理员 /api/admin/articles,作者 /api/my/articles。
// 新建页用 session_token,编辑页用 article_id。
var API_BASE = "{{if .FormIsMy}}/api/my/articles{{else}}/api/admin/articles{{end}}";
var sessEl = document.getElementById('articleSessionToken');
var ATT = {
articleID: {{ if .FormArticleID }}{{ .FormArticleID }}{{ else }}0{{ end }},
sessionToken: sessEl ? sessEl.value : '',
uploadURL: API_BASE + '/attachments',
listURL: API_BASE + '/:id/attachments'
};
// 顶层作用域:供表单提交(PUT/POST 路由选择)与附件共享逻辑共同使用。
var articleID = ATT.articleID;
var redirectBase = "{{if .FormIsMy}}/my/articles{{else}}/admin{{end}}";
// 编辑器“上传图片”按钮:选择本地图片 → 上传(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: ATT.listURL,
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 APIadmin 的草稿/发布按钮由 e.submitter 分流,
// my 的 status 取自 select 字段) ----
(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_BASE + '/' + articleID : API_BASE;
blogAPI(method, url, blogForm(form, btn)).then(function (r) {
if (r.ok) { window.location.href = r.redirect || redirectBase; }
else { blogShowError('articleError', r.error || 'Failed to save article.'); }
});
});
})();
</script>
{{end}}