diff --git a/assets/css/frontend/pages/submit-editor.css b/assets/css/frontend/pages/submit-editor.css
index 2303712e..d61e97e8 100644
--- a/assets/css/frontend/pages/submit-editor.css
+++ b/assets/css/frontend/pages/submit-editor.css
@@ -42,3 +42,149 @@
.tinymce-hide > [contenteditable="true"] > :last-child {
margin-bottom: 0;
}
+
+.quote_q {
+ position: relative;
+ margin: 10px 0 20px;
+ padding: 1.2em;
+ border-radius: 6px;
+ color: var(--quote-color);
+ --quote-color: #555;
+}
+
+.quote_q::before {
+ position: absolute;
+ inset: 0;
+ border-radius: 6px;
+ background: var(--quote-color);
+ content: '';
+ opacity: 0.04;
+}
+
+.quote_q.quote-mce::after {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ color: var(--quote-color);
+ content: '\201c';
+ font-size: 24px;
+ line-height: 1;
+ opacity: 0.6;
+}
+
+.quote_q > div {
+ position: relative;
+ min-height: 24px;
+ padding-left: 2em;
+ opacity: 0.7;
+}
+
+.quote_q > div > :first-child {
+ margin-top: 0;
+}
+
+.quote_q > div > :last-child {
+ margin-bottom: 0;
+}
+
+.quote_q [contenteditable="true"] {
+ outline: 0;
+}
+
+.qe_wzk_c-red {
+ --quote-color: #dc3f3f;
+}
+
+.qe_wzk_lan {
+ --quote-color: #0092ff;
+}
+
+.qe_wzk_lv {
+ --quote-color: #23790e;
+}
+
+.enlighter-pre {
+ position: relative;
+ max-height: 300px;
+ overflow: auto;
+ padding: 32px 12px 12px;
+ border-radius: 6px;
+ background: #f2f4f5;
+}
+
+.enlighter-pre code.gl {
+ display: block;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font-family: Consolas, Monaco, monospace;
+ line-height: 1.45;
+ white-space: break-spaces;
+}
+
+.enlighter-pre code.gl::before {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ padding: 3px 8px;
+ border-radius: 4px;
+ background: linear-gradient(135deg, #3dcabd 10%, #254bdd 100%);
+ color: #fff;
+ content: '高亮代码';
+ font-size: 12px;
+}
+
+.file-download-box {
+ margin-bottom: 20px;
+ padding: 15px;
+ border: 1px dashed #bbb;
+ border-radius: 6px;
+ background: #f5f5f5;
+}
+
+.file-download-box .file-download-icon {
+ font-size: 24px;
+}
+
+.file-download-box .file-download-name {
+ min-width: 80px;
+ outline: 0;
+}
+
+.new-dplayer.post-dplayer {
+ position: relative;
+ min-height: 220px;
+ margin-bottom: 20px;
+ border-radius: 6px;
+ background: #222;
+}
+
+.new-dplayer.post-dplayer::before {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ content: '视频';
+ opacity: 0.65;
+}
+
+.wp-block-embed.is-type-video {
+ margin-bottom: 20px;
+}
+
+.iframe-absbox {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ border-radius: 6px;
+ background: #222;
+}
+
+.iframe-absbox iframe {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
diff --git a/assets/js/front-submit-editor.js b/assets/js/front-submit-editor.js
index 69a469bd..c37a793d 100644
--- a/assets/js/front-submit-editor.js
+++ b/assets/js/front-submit-editor.js
@@ -21,7 +21,7 @@
message,
type,
duration === undefined ? 5 : duration,
- id || 'aksrt-front-submit-editor-image-toast'
+ id || 'aksrt-front-submit-editor-media-toast'
);
}
}
@@ -34,7 +34,7 @@
.replace(/>/g, '>');
}
- function validImageUrl(value) {
+ function validHttpUrl(value) {
var raw = String(value).trim();
if (!/^https?:\/\//i.test(raw)) {
return '';
@@ -48,7 +48,7 @@
}
function imageMarkup(url, attachmentId, alt) {
- var safeUrl = validImageUrl(url);
+ var safeUrl = validHttpUrl(url);
if (!safeUrl) {
return '';
}
@@ -57,7 +57,49 @@
return '
 + ')
';
}
- function closeImageModal() {
+ function videoMarkup(url, attachmentId, name) {
+ var safeUrl = validHttpUrl(url);
+ if (!safeUrl) {
+ return '';
+ }
+ var idAttr = attachmentId ? ' data-edit-file-id="' + Number(attachmentId) + '"' : '';
+ return '';
+ }
+
+ function fileMarkup(url, attachmentId, name) {
+ var safeUrl = validHttpUrl(url);
+ if (!safeUrl) {
+ return '';
+ }
+ var filename = String(name || safeUrl.split('/').pop() || i18n.attachment || '附件');
+ var idAttr = attachmentId ? ' data-download-file="' + Number(attachmentId) + '"' : '';
+ return '' +
+ '
' +
+ '
' + escapeAttr(filename) + '
' +
+ '
';
+ }
+
+ function embedVideoMarkup(value) {
+ var holder = document.createElement('div');
+ holder.innerHTML = String(value).trim();
+ var iframe = holder.querySelector('iframe');
+ var safeUrl = validHttpUrl(iframe ? iframe.getAttribute('src') : value);
+ if (!safeUrl) {
+ return '';
+ }
+ return '';
+ }
+
+ function closeMediaModal() {
if (!activeModal) {
return;
}
@@ -67,13 +109,27 @@
document.body.classList.remove('aksrt-editor-media-open');
}
- function uploadImage(file) {
- var upload = (cfg.uploads || {}).image || null;
- if (!upload || !upload.url) {
- return Promise.reject(new Error(i18n.uploadUnavailable || '当前账号不能上传图片'));
+ function acceptsFile(file, type) {
+ if (!file) {
+ return false;
}
- if (!file || !/^image\//.test(file.type || '')) {
- return Promise.reject(new Error(i18n.invalidImage || '请选择有效的图片文件'));
+ if (type === 'image') {
+ return /^image\//.test(file.type || '');
+ }
+ if (type === 'video') {
+ return /^video\//.test(file.type || '');
+ }
+ return true;
+ }
+
+ function uploadMedia(file, type) {
+ var uploadKey = type === 'file' ? 'resource' : type;
+ var upload = (cfg.uploads || {})[uploadKey] || null;
+ if (!upload || !upload.url) {
+ return Promise.reject(new Error(i18n.uploadUnavailable || '当前账号不能上传此类文件'));
+ }
+ if (!acceptsFile(file, type)) {
+ return Promise.reject(new Error(i18n.invalidFile || '请选择有效文件'));
}
if (upload.maxSize && file.size > upload.maxSize) {
return Promise.reject(new Error(i18n.fileTooLarge || '文件大小超过限制'));
@@ -91,19 +147,76 @@
return {};
}).then(function (payload) {
if (!response.ok || !payload || !payload.success || !payload.url) {
- throw new Error((payload && payload.message) || i18n.error || '图片上传失败');
+ throw new Error((payload && payload.message) || i18n.error || '文件上传失败');
}
return payload;
});
});
}
- function createImageModal(editor) {
- closeImageModal();
+ function mediaDefinition(type) {
+ if (type === 'video') {
+ return {
+ title: i18n.insertVideo || '插入视频',
+ uploadTab: i18n.uploadVideo || '上传视频',
+ externalTab: i18n.externalVideo || '外链视频',
+ embedTab: i18n.embedVideo || '嵌入视频',
+ accept: 'video/*',
+ multiple: false,
+ canUpload: !!editorCfg.canUploadVideo && !!((cfg.uploads || {}).video),
+ canEmbed: !!editorCfg.canEmbedVideo,
+ select: i18n.selectVideo || '选择或拖入视频',
+ hint: i18n.videoUploadHint || '上传完成后自动插入正文',
+ externalHint: i18n.externalVideoHint || '请填写视频文件地址:',
+ };
+ }
+ if (type === 'file') {
+ return {
+ title: i18n.insertFile || '上传附件',
+ uploadTab: i18n.uploadFile || '上传附件',
+ externalTab: i18n.externalFile || '输入地址',
+ accept: '*/*',
+ multiple: false,
+ canUpload: !!editorCfg.canUploadFile && !!((cfg.uploads || {}).resource),
+ canEmbed: false,
+ select: i18n.selectFile || '选择或拖入附件',
+ hint: i18n.fileUploadHint || '上传完成后自动插入正文',
+ externalHint: i18n.externalFileHint || '请填写附件地址:',
+ };
+ }
+ return {
+ title: i18n.insertImage || '插入图片',
+ uploadTab: i18n.uploadImage || '上传图片',
+ externalTab: i18n.externalImage || '外链图片',
+ accept: 'image/gif,image/jpeg,image/png,image/webp',
+ multiple: true,
+ canUpload: !!editorCfg.canUploadImage && !!((cfg.uploads || {}).image),
+ canEmbed: false,
+ select: i18n.selectImages || '选择或拖入图片',
+ hint: i18n.imageUploadHint || '可批量选择,上传完成后自动插入正文',
+ externalHint: i18n.externalImageHint || '每行填写一个图片地址:',
+ };
+ }
- var canUpload = !!editorCfg.canUploadImage && !!((cfg.uploads || {}).image);
+ function mediaMarkup(type, item) {
+ if (type === 'video') {
+ return videoMarkup(item.url, item.attachment_id, item.filename || item.name || '');
+ }
+ if (type === 'file') {
+ return fileMarkup(item.url, item.attachment_id, item.filename || item.name || '');
+ }
+ return imageMarkup(item.url, item.attachment_id, item.name || '');
+ }
+
+ function createMediaModal(editor, type) {
+ closeMediaModal();
+
+ var definition = mediaDefinition(type);
+ var canUpload = definition.canUpload;
+ var firstTab = canUpload ? 'upload' : 'external';
var root = document.createElement('div');
root.className = 'aksrt-editor-media';
+ root.setAttribute('data-editor-media-type', type);
root.setAttribute('role', 'dialog');
root.setAttribute('aria-modal', 'true');
root.setAttribute('aria-labelledby', 'aksrt-editor-media-title');
@@ -112,38 +225,51 @@
escapeAttr(i18n.close || '关闭') + '">' +
'';
@@ -161,15 +287,24 @@
return;
}
selection.textContent = files.length
- ? (i18n.selectedImages || '已选择 %d 张图片').replace('%d', String(files.length))
+ ? (definition.multiple
+ ? (i18n.selectedFiles || '已选择 %d 个文件').replace('%d', String(files.length))
+ : files[0].name)
: '';
uploadButton.disabled = files.length === 0;
}
function setFiles(nextFiles) {
- files = Array.prototype.filter.call(nextFiles || [], function (file) {
- return /^image\//.test(file.type || '');
+ var candidates = Array.prototype.slice.call(nextFiles || []);
+ files = candidates.filter(function (file) {
+ return acceptsFile(file, type);
});
+ if (candidates.length && !files.length) {
+ notify(i18n.invalidFile || '请选择有效文件', 'warning');
+ }
+ if (!definition.multiple && files.length > 1) {
+ files = files.slice(0, 1);
+ }
showSelection();
}
@@ -177,7 +312,7 @@
var close = event.target.closest('[data-editor-media-close]');
if (close) {
event.preventDefault();
- closeImageModal();
+ closeMediaModal();
return;
}
@@ -198,16 +333,32 @@
if (event.target.closest('[data-editor-media-external]')) {
var textarea = root.querySelector('[data-editor-media-urls]');
var urls = String(textarea ? textarea.value : '').split(/\r?\n/);
+ if (!definition.multiple) {
+ urls = urls.slice(0, 1);
+ }
var html = urls.map(function (url) {
- return imageMarkup(url, 0, '');
+ return mediaMarkup(type, { url: url });
}).join('');
if (!html) {
- notify(i18n.invalidImageUrl || '请填写有效的图片地址', 'warning');
+ notify(i18n.invalidMediaUrl || '请填写有效地址', 'warning');
return;
}
editor.insertContent(html + '');
editor.nodeChanged();
- closeImageModal();
+ closeMediaModal();
+ return;
+ }
+
+ if (event.target.closest('[data-editor-media-embed-submit]')) {
+ var embedInput = root.querySelector('[data-editor-media-embed]');
+ var embedHtml = embedVideoMarkup(embedInput ? embedInput.value : '');
+ if (!embedHtml) {
+ notify(i18n.invalidVideoUrl || '请填写有效的视频嵌入地址', 'warning');
+ return;
+ }
+ editor.insertContent(embedHtml + '');
+ editor.nodeChanged();
+ closeMediaModal();
return;
}
@@ -217,20 +368,22 @@
}
uploadButton.disabled = true;
uploadButton.textContent = i18n.uploading || '正在上传…';
- notify(i18n.uploading || '正在上传正文图片…', 'load', '');
- Promise.all(files.map(uploadImage)).then(function (items) {
+ notify(i18n.uploading || '正在上传…', 'load', '');
+ Promise.all(files.map(function (file) {
+ return uploadMedia(file, type);
+ })).then(function (items) {
var html = items.map(function (item) {
- return imageMarkup(item.url, item.attachment_id, item.name || '');
+ return mediaMarkup(type, item);
}).join('');
if (!html) {
- throw new Error(i18n.error || '图片上传失败');
+ throw new Error(i18n.error || '文件上传失败');
}
editor.insertContent(html + '');
editor.nodeChanged();
- notify(i18n.uploaded || '正文图片上传成功', 'success');
- closeImageModal();
+ notify(i18n.uploaded || '上传成功', 'success');
+ closeMediaModal();
}).catch(function (error) {
- notify(error.message || i18n.error || '正文图片上传失败', 'danger');
+ notify(error.message || i18n.error || '文件上传失败', 'danger');
uploadButton.disabled = false;
uploadButton.textContent = i18n.confirmUpload || '确认上传';
});
@@ -263,7 +416,7 @@
var onKeydown = function (event) {
if (event.key === 'Escape') {
- closeImageModal();
+ closeMediaModal();
}
};
activeModal = { root: root, onKeydown: onKeydown };
@@ -300,6 +453,143 @@
editor.nodeChanged();
}
+ function insertQuote(editor, colorClass) {
+ var node = editor.selection.getNode();
+ var quote = editor.dom.getParent(node, '.quote_q');
+ if (quote) {
+ quote.className = 'quote_q quote-mce' + (colorClass ? ' ' + colorClass : '');
+ editor.nodeChanged();
+ return;
+ }
+
+ var content = String(editor.selection.getContent() || '').trim();
+ if (node && node.nodeName === 'P' && node.parentNode === editor.getBody()) {
+ content = node.outerHTML;
+ editor.dom.remove(node);
+ } else {
+ content = content ? '' + content + '
' : '';
+ }
+ editor.insertContent(
+ ''
+ );
+ editor.nodeChanged();
+ }
+
+ var codeLanguages = [
+ ['yaml', 'yaml'],
+ ['xml/html', 'xml'],
+ ['visualbasic', 'visualbasic'],
+ ['vhdl', 'vhdl'],
+ ['typescript', 'typescript'],
+ ['swift', 'swift'],
+ ['sql', 'sql'],
+ ['shell', 'shell'],
+ ['scss/sass', 'scss'],
+ ['rust', 'rust'],
+ ['ruby', 'ruby'],
+ ['python', 'python'],
+ ['powershell', 'powershell'],
+ ['php', 'php'],
+ ['markdown', 'markdown'],
+ ['lua', 'lua'],
+ ['less', 'less'],
+ ['kotlin', 'kotlin'],
+ ['json', 'json'],
+ ['javascript', 'javascript'],
+ ['java', 'java'],
+ ['ini/conf', 'ini'],
+ ['go/golang', 'go'],
+ ['docker', 'dockerfile'],
+ ['diff', 'diff'],
+ ['css', 'css'],
+ ['csharp', 'csharp'],
+ ['Cpp/C++/C', 'cpp'],
+ ['assembly', 'assembly'],
+ [i18n.codeGeneric || '通用高亮', 'generic'],
+ ];
+
+ var codeThemes = [
+ ['enlighter', 'enlighter'],
+ ['classic', 'classic'],
+ ['beyond', 'beyond'],
+ ['mowtwo', 'mowtwo'],
+ ['eclipse', 'eclipse'],
+ ['droide', 'droide'],
+ ['minimal', 'minimal'],
+ ['atomic', 'atomic'],
+ ['dracula', 'dracula'],
+ ['bootstrap4', 'bootstrap4'],
+ ['rowhammer', 'rowhammer'],
+ ['godzilla', 'godzilla'],
+ [i18n.codeFollowGlobal || '跟随全局设置', 'qj'],
+ ];
+
+ function listboxValues(items) {
+ return items.map(function (item) {
+ return { text: item[0], value: item[1] };
+ });
+ }
+
+ function openCodeModal(editor) {
+ var selected = editor.dom.getParent(editor.selection.getNode(), 'pre.enlighter-pre');
+ var code = selected ? selected.querySelector('code') : null;
+ var width = window.innerWidth > 800 ? Math.round(window.innerWidth * 0.5) :
+ (window.innerWidth > 640 ? Math.round(window.innerWidth * 0.7) : window.innerWidth - 20);
+ var height = Math.round(window.innerHeight * 0.6);
+
+ editor.windowManager.open({
+ title: (selected ? (i18n.edit || '编辑') : (i18n.insert || '插入')) +
+ (i18n.highlightCode || '高亮代码'),
+ width: width,
+ height: height,
+ body: [
+ {
+ type: 'listbox',
+ name: 'language',
+ label: i18n.selectLanguage || '选择语言',
+ value: code ? (code.getAttribute('data-enlighter-language') || 'generic') : 'generic',
+ values: listboxValues(codeLanguages),
+ },
+ {
+ type: 'listbox',
+ name: 'theme',
+ label: i18n.codeTheme || '主题',
+ value: code ? (code.getAttribute('data-enlighter-theme') || 'qj') : 'qj',
+ values: listboxValues(codeThemes),
+ },
+ {
+ type: 'textbox',
+ name: 'content',
+ label: i18n.codeLabel || '代码:',
+ value: code ? (code.textContent || '') : '',
+ multiline: true,
+ minHeight: Math.max(160, height - 115),
+ },
+ ],
+ onsubmit: function (event) {
+ var content = String(event.data.content || '').replace(/\r\n/g, '\n');
+ if (!content) {
+ return;
+ }
+ var encoded = window.tinymce.html.Entities.encodeAllRaw(content);
+ var language = event.data.language === 'generic' ? '' :
+ ' data-enlighter-language="' + escapeAttr(event.data.language) + '"';
+ var theme = event.data.theme === 'qj' ? '' :
+ ' data-enlighter-theme="' + escapeAttr(event.data.theme) + '"';
+ var markup = '' + encoded + '
';
+ if (selected) {
+ editor.dom.setOuterHTML(selected, markup);
+ } else {
+ editor.insertContent(markup + '');
+ }
+ editor.nodeChanged();
+ editor.undoManager.add();
+ },
+ });
+ }
+
window.tinymce.PluginManager.add('aksrt_editor_tools', function (editor) {
if (editorCfg.canUploadImage) {
editor.addButton('aksrt_image', {
@@ -307,11 +597,55 @@
icon: 'image',
tooltip: i18n.insertImage || '插入图片',
onclick: function () {
- createImageModal(editor);
+ createMediaModal(editor, 'image');
},
});
}
+ editor.addButton('aksrt_video', {
+ text: '',
+ icon: 'media',
+ tooltip: i18n.insertVideo || '插入视频',
+ onclick: function () {
+ createMediaModal(editor, 'video');
+ },
+ });
+
+ if (editorCfg.canUploadFile) {
+ editor.addButton('aksrt_file', {
+ text: '',
+ icon: 'upload',
+ tooltip: i18n.insertFile || '上传附件',
+ onclick: function () {
+ createMediaModal(editor, 'file');
+ },
+ });
+ }
+
+ editor.addButton('aksrt_quote', {
+ text: '',
+ icon: 'blockquote',
+ tooltip: i18n.quote || '引言',
+ type: 'menubutton',
+ stateSelector: '.quote_q',
+ menu: [
+ { text: i18n.quoteGray || '灰色', onclick: function () { insertQuote(editor, ''); } },
+ { text: i18n.quoteRed || '红色', onclick: function () { insertQuote(editor, 'qe_wzk_c-red'); } },
+ { text: i18n.quoteBlue || '蓝色', onclick: function () { insertQuote(editor, 'qe_wzk_lan'); } },
+ { text: i18n.quoteGreen || '绿色', onclick: function () { insertQuote(editor, 'qe_wzk_lv'); } },
+ ],
+ });
+
+ editor.addButton('aksrt_precode', {
+ text: '',
+ icon: 'code',
+ tooltip: i18n.highlightCode || '高亮代码',
+ stateSelector: 'pre.enlighter-pre',
+ onclick: function () {
+ openCodeModal(editor);
+ },
+ });
+
if (editorCfg.canHide) {
var menu = [
{
diff --git a/src/Admin/Schema/SchemaPermissions.php b/src/Admin/Schema/SchemaPermissions.php
index 2c03d5b0..cf8927a9 100644
--- a/src/Admin/Schema/SchemaPermissions.php
+++ b/src/Admin/Schema/SchemaPermissions.php
@@ -65,6 +65,7 @@ class SchemaPermissions {
]],
['key' => 'content', 'title' => '内容上传限制', 'fields' => [
['key' => 'submit_image_max_size', 'label' => '投稿图片大小上限 (MB)', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
+ ['key' => 'submit_video_max_size', 'label' => '投稿视频大小上限 (MB)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 200],
['key' => 'comment_image_max_size', 'label' => '评论图片大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
]],
['key' => 'resource', 'title' => '资源附件限制', 'fields' => [
diff --git a/src/Core/FrontSubmit.php b/src/Core/FrontSubmit.php
index 2135127c..55b46c06 100644
--- a/src/Core/FrontSubmit.php
+++ b/src/Core/FrontSubmit.php
@@ -45,14 +45,14 @@ class FrontSubmit {
}
public function register_rest_routes(): void {
- register_rest_route('aksrt/v1', '/front-submit/upload/(?Pimage|resource)', [
+ register_rest_route('aksrt/v1', '/front-submit/upload/(?Pimage|video|resource)', [
'methods' => 'POST',
'callback' => [$this, 'rest_upload_attachment'],
'permission_callback' => [$this, 'can_upload_attachment'],
'args' => [
'kind' => [
'validate_callback' => static function ($value): bool {
- return in_array((string) $value, ['image', 'resource'], true);
+ return in_array((string) $value, ['image', 'video', 'resource'], true);
},
],
],
@@ -69,7 +69,15 @@ class FrontSubmit {
}
$kind = (string) $request->get_param('kind');
- $cap = $kind === 'resource' ? 'upload_resource_attachment' : 'upload_submit_image';
+ $caps = [
+ 'image' => 'upload_submit_image',
+ 'video' => 'new_post_upload_video',
+ 'resource' => 'upload_resource_attachment',
+ ];
+ $cap = $caps[$kind] ?? '';
+ if ($cap === '') {
+ return false;
+ }
return $this->user_can_cap($cap, $user_id);
}
@@ -84,9 +92,11 @@ class FrontSubmit {
}
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
- $max_mb = $kind === 'resource'
- ? max(1, min(200, (int) ($settings['resource_max_size'] ?? 50)))
- : max(1, min(20, (int) ($settings['submit_image_max_size'] ?? 5)));
+ $max_mb = match ($kind) {
+ 'resource' => max(1, min(200, (int) ($settings['resource_max_size'] ?? 50))),
+ 'video' => max(1, min(200, (int) ($settings['submit_video_max_size'] ?? 30))),
+ default => max(1, min(20, (int) ($settings['submit_image_max_size'] ?? 5))),
+ };
$max_size = $max_mb * MB_IN_BYTES;
$size = (int) ($file['size'] ?? 0);
@@ -119,6 +129,8 @@ class FrontSubmit {
return new \WP_Error('aksrt_upload_image_rejected', __('图片未通过内容审核。', 'aksrt'), ['status' => 422]);
}
}
+ } elseif ($kind === 'video' && strpos($mime, 'video/') !== 0) {
+ return new \WP_Error('aksrt_upload_invalid_video', __('请上传有效的视频文件。', 'aksrt'), ['status' => 400]);
}
if (!function_exists('media_handle_upload')) {
@@ -150,6 +162,7 @@ class FrontSubmit {
'attachment_id' => $attachment_id,
'url' => $url,
'name' => get_the_title($attachment_id),
+ 'filename' => wp_basename((string) get_attached_file($attachment_id)),
'mime' => $mime,
], 201);
}
diff --git a/templates/pages/page-submit.php b/templates/pages/page-submit.php
index d0ce51b6..dccceef6 100644
--- a/templates/pages/page-submit.php
+++ b/templates/pages/page-submit.php
@@ -73,6 +73,7 @@ if (file_exists($js)) {
}
$attachment_settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$submit_image_max_size = max(1, min(20, (int) ($attachment_settings['submit_image_max_size'] ?? 5))) * MB_IN_BYTES;
+ $submit_video_max_size = max(1, min(200, (int) ($attachment_settings['submit_video_max_size'] ?? 30))) * MB_IN_BYTES;
$resource_max_size = max(1, min(200, (int) ($attachment_settings['resource_max_size'] ?? 50))) * MB_IN_BYTES;
wp_localize_script('aksrt-front-submit', 'aksrtSubmitData', [
@@ -88,6 +89,9 @@ if (file_exists($js)) {
],
'editor' => [
'canUploadImage' => !empty($ctx['can_upload_img']),
+ 'canUploadVideo' => !empty($ctx['can_upload_video']),
+ 'canEmbedVideo' => !empty($ctx['can_iframe']),
+ 'canUploadFile' => !empty($ctx['can_upload_file']),
'canHide' => !empty($ctx['can_hide']),
'canPay' => !empty($ctx['can_pay']),
],
@@ -96,6 +100,10 @@ if (file_exists($js)) {
'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/image')),
'maxSize' => $submit_image_max_size,
] : null,
+ 'video' => !empty($ctx['can_upload_video']) ? [
+ 'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/video')),
+ 'maxSize' => $submit_video_max_size,
+ ] : null,
'resource' => !empty($ctx['can_upload_file']) ? [
'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/resource')),
'maxSize' => $resource_max_size,
@@ -127,15 +135,45 @@ if (file_exists($js)) {
'insertImage' => __('插入图片', 'aksrt'),
'uploadImage' => __('上传图片', 'aksrt'),
'externalImage' => __('外链图片', 'aksrt'),
+ 'insertVideo' => __('插入视频', 'aksrt'),
+ 'uploadVideo' => __('上传视频', 'aksrt'),
+ 'externalVideo' => __('外链视频', 'aksrt'),
+ 'embedVideo' => __('嵌入视频', 'aksrt'),
+ 'selectVideo' => __('选择或拖入视频', 'aksrt'),
+ 'videoUploadHint' => __('上传完成后自动插入正文', 'aksrt'),
+ 'externalVideoHint' => __('请填写视频文件地址:', 'aksrt'),
+ 'embedVideoHint' => __('请输入嵌入地址或者直接粘贴 iframe 嵌入代码:', 'aksrt'),
+ 'confirmEmbed' => __('确认嵌入', 'aksrt'),
+ 'insertFile' => __('上传附件', 'aksrt'),
+ 'uploadFile' => __('上传附件', 'aksrt'),
+ 'externalFile' => __('输入地址', 'aksrt'),
+ 'selectFile' => __('选择或拖入附件', 'aksrt'),
+ 'fileUploadHint' => __('上传完成后自动插入正文', 'aksrt'),
+ 'externalFileHint' => __('请填写附件地址:', 'aksrt'),
'selectImages' => __('选择或拖入图片', 'aksrt'),
'imageUploadHint' => __('可批量选择,上传完成后自动插入正文', 'aksrt'),
- 'selectedImages' => __('已选择 %d 张图片', 'aksrt'),
+ 'selectedFiles' => __('已选择 %d 个文件', 'aksrt'),
'confirmUpload' => __('确认上传', 'aksrt'),
'externalImageHint' => __('每行填写一个图片地址:', 'aksrt'),
'confirmInsert' => __('确认插入', 'aksrt'),
- 'invalidImage' => __('请选择有效的图片文件', 'aksrt'),
- 'invalidImageUrl' => __('请填写有效的图片地址', 'aksrt'),
- 'uploadUnavailable' => __('当前账号不能上传图片', 'aksrt'),
+ 'invalidMediaUrl' => __('请填写有效地址', 'aksrt'),
+ 'invalidVideoUrl' => __('请填写有效的视频嵌入地址', 'aksrt'),
+ 'invalidFile' => __('请选择有效文件', 'aksrt'),
+ 'uploadUnavailable' => __('当前账号不能上传此类文件', 'aksrt'),
+ 'download' => __('下载', 'aksrt'),
+ 'quote' => __('引言', 'aksrt'),
+ 'quoteGray' => __('灰色引言', 'aksrt'),
+ 'quoteRed' => __('红色引言', 'aksrt'),
+ 'quoteBlue' => __('蓝色引言', 'aksrt'),
+ 'quoteGreen' => __('绿色引言', 'aksrt'),
+ 'edit' => __('编辑', 'aksrt'),
+ 'insert' => __('插入', 'aksrt'),
+ 'highlightCode' => __('高亮代码', 'aksrt'),
+ 'selectLanguage' => __('选择语言', 'aksrt'),
+ 'codeGeneric' => __('通用高亮', 'aksrt'),
+ 'codeTheme' => __('主题', 'aksrt'),
+ 'codeFollowGlobal' => __('跟随全局设置', 'aksrt'),
+ 'codeLabel' => __('代码:', 'aksrt'),
'hiddenContent' => __('隐藏内容', 'aksrt'),
'hiddenPlaceholder' => __('隐藏内容', 'aksrt'),
'replyVisible' => __('回复可见', 'aksrt'),
diff --git a/templates/parts/submit/editor-main.php b/templates/parts/submit/editor-main.php
index 1dc72d68..a6ca7e2e 100644
--- a/templates/parts/submit/editor-main.php
+++ b/templates/parts/submit/editor-main.php
@@ -47,11 +47,15 @@ $is_can_new = !empty($ctx['is_can_new']);
if (!empty($ctx['can_upload_img'])) {
$toolbar1[] = 'aksrt_image';
}
+ $toolbar1[] = 'aksrt_video';
+ if (!empty($ctx['can_upload_file'])) {
+ $toolbar1[] = 'aksrt_file';
+ }
if (!empty($ctx['can_hide'])) {
$toolbar1[] = 'aksrt_hide';
}
- $toolbar1 = implode(',', array_merge($toolbar1, ['blockquote', 'code', 'fullscreen']));
- $plugins = 'paste,link,lists,fullscreen,wordpress,wplink,charmap';
+ $toolbar1 = implode(',', array_merge($toolbar1, ['aksrt_quote', 'aksrt_precode', 'fullscreen']));
+ $plugins = 'paste,link,lists,fullscreen,wordpress,wplink,charmap,colorpicker,textcolor';
$editor_plugin_path = get_template_directory() . '/assets/js/front-submit-editor.js';
$editor_plugin_url = get_template_directory_uri() . '/assets/js/front-submit-editor.js';
if (file_exists($editor_plugin_path)) {
diff --git a/tests/widget-system/post-editor-reference-contract.test.cjs b/tests/widget-system/post-editor-reference-contract.test.cjs
index c2d687f5..3dc7e8e1 100644
--- a/tests/widget-system/post-editor-reference-contract.test.cjs
+++ b/tests/widget-system/post-editor-reference-contract.test.cjs
@@ -75,6 +75,46 @@ test('front submit image and visibility actions live in the TinyMCE toolbar', ()
assert.doesNotMatch(editor, /hidecontent|shop|forum|bbs_/i);
});
+test('front submit exposes the complete reference media and formatting toolset', () => {
+ const main = read('templates/parts/submit/editor-main.php');
+ const page = read('templates/pages/page-submit.php');
+ const editor = read('assets/js/front-submit-editor.js');
+ const editorCss = read('assets/css/frontend/pages/submit-editor.css');
+ const submit = read('src/Core/FrontSubmit.php');
+
+ ['aksrt_video', 'aksrt_file', 'aksrt_quote', 'aksrt_precode'].forEach((button) => {
+ assert.match(main, new RegExp(`['"]${button}['"]`));
+ assert.match(editor, new RegExp(`addButton\\(['"]${button}['"]`));
+ });
+ assert.doesNotMatch(main, /array_merge\(\$toolbar1, \['blockquote'/);
+ assert.match(main, /toolbar2[\s\S]*forecolor/);
+ assert.match(main, /colorpicker,textcolor/);
+
+ assert.match(page, /'canUploadVideo'\s*=>\s*!empty\(\$ctx\['can_upload_video'\]\)/);
+ assert.match(page, /'canEmbedVideo'\s*=>\s*!empty\(\$ctx\['can_iframe'\]\)/);
+ assert.match(page, /'canUploadFile'\s*=>\s*!empty\(\$ctx\['can_upload_file'\]\)/);
+ assert.match(page, /'video'\s*=>\s*!empty\(\$ctx\['can_upload_video'\]\)/);
+ assert.match(submit, /\(\?Pimage\|video\|resource\)/);
+ assert.match(submit, /new_post_upload_video/);
+
+ assert.match(editor, /data-editor-media-tab="upload"/);
+ assert.match(editor, /data-editor-media-tab="external"/);
+ assert.match(editor, /data-editor-media-tab="embed"/);
+ assert.match(editor, /new-dplayer post-dplayer dplayer/);
+ assert.match(editor, /file-download-box/);
+ assert.match(editor, /wp-block-embed is-type-video/);
+ assert.match(editor, /data-enlighter-language/);
+ assert.match(editor, /data-enlighter-theme/);
+ assert.match(editor, /enlighter-pre/);
+ assert.match(editor, /qe_wzk_c-red/);
+ assert.match(editor, /qe_wzk_lan/);
+ assert.match(editor, /qe_wzk_lv/);
+ assert.match(editorCss, /\.quote_q/);
+ assert.match(editorCss, /\.file-download-box/);
+ assert.match(editorCss, /\.enlighter-pre/);
+ assert.doesNotMatch(editor, /shop|forum|bbs_/i);
+});
+
test('article paid-content metabox follows the reference field set without shop enhancements', () => {
const meta = read('src/Core/ResourceMetaBox.php');
const fields = [
diff --git a/tests/widget-system/toast-contract.test.cjs b/tests/widget-system/toast-contract.test.cjs
index 04bb2edd..85e4be79 100644
--- a/tests/widget-system/toast-contract.test.cjs
+++ b/tests/widget-system/toast-contract.test.cjs
@@ -381,7 +381,7 @@ test('front submit actions use direct fixed-id notyf flows', () => {
'aksrt-front-submit-slide-upload-toast',
'aksrt-front-submit-video-cover-toast',
'aksrt-front-submit-resource-upload-toast',
- 'aksrt-front-submit-editor-image-toast',
+ 'aksrt-front-submit-editor-media-toast',
'aksrt-front-submit-ai-tags-toast',
'aksrt-front-submit-ai-summary-toast',
].forEach((id) => assert.match(source, new RegExp("['\"]" + id + "['\"]")));
@@ -400,7 +400,7 @@ test('front submit actions use direct fixed-id notyf flows', () => {
});
assert.match(submit, /aksrt-front-submit-resource-upload-toast[\s\S]*?'load'[\s\S]*?'success'[\s\S]*?'danger'/);
- assert.match(editor, /aksrt-front-submit-editor-image-toast[\s\S]*?'load'[\s\S]*?'success'[\s\S]*?'danger'/);
+ assert.match(editor, /aksrt-front-submit-editor-media-toast[\s\S]*?'load'[\s\S]*?'success'[\s\S]*?'danger'/);
assert.match(submit, /aksrt-front-submit-ai-tags-toast[\s\S]*?aksrt-front-submit-ai-summary-toast[\s\S]*?'load'[\s\S]*?'success'[\s\S]*?'danger'/);
assert.match(submit, /needContent[^;]+?'warning'/);
assert.doesNotMatch(submit, /\b(?:window\.)?alert\s*\(/);