From fb2bbb64c5a48d46c4003e7048b9bbd306dad000 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 26 Aug 2026 17:57:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E4=BA=86=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=B0=8F=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/frontend/pages/submit-editor.css | 146 ++++++ assets/js/front-submit-editor.js | 414 ++++++++++++++++-- src/Admin/Schema/SchemaPermissions.php | 1 + src/Core/FrontSubmit.php | 25 +- templates/pages/page-submit.php | 46 +- templates/parts/submit/editor-main.php | 8 +- .../post-editor-reference-contract.test.cjs | 40 ++ tests/widget-system/toast-contract.test.cjs | 4 +- 8 files changed, 630 insertions(+), 54 deletions(-) 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 || '关闭') + '">' + '
' + '' + + escapeAttr(i18n.close || '关闭') + '">×' + '

' + - escapeAttr(i18n.insertImage || '插入图片') + '

' + + escapeAttr(definition.title) + '' + '
' + (canUpload ? '' + escapeAttr(definition.uploadTab) + '' : '') + - '' + + escapeAttr(definition.externalTab) + '' + + (definition.canEmbed + ? '' + : '') + '
' + '
' + (canUpload ? '
' + '' + '
' + '' + '
' : '') + - '
' + - '

' + escapeAttr(i18n.externalImageHint || '每行填写一个图片地址:') + '

' + + '
' + + '

' + escapeAttr(definition.externalHint) + '

' + '' + '' + '
' + + (definition.canEmbed + ? '
' + + '

' + escapeAttr(i18n.embedVideoHint || '请输入嵌入地址或者直接粘贴 iframe 嵌入代码:') + '

' + + '' + + '' + + '
' + : '') + '
' + '
'; @@ -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( + '
' + content + '

' + ); + 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*\(/);