fix(frontend): 修复前台页面编辑弹窗并 1:1 复刻参考主题
远端加载的弹窗在 admin-ajax 上下文里拿不到查询对象,enabled() 恒为假,直接返回 success:false;而 AksrtRequest.responseError 只读 message/data.message,读不到 msg,遂报「请求失败(HTTP 200)」。 改为参考主题原生架构:wp_footer 直出 #modal_admin_set,提交走 vendored main.js 的 aksrt_ajax 契约,不再有 AJAX 取 HTML。 对齐参考主题 zib-frontend-set.php: - 弹窗补齐 aria-hidden=false、win-ajax-replace、role=document - 隐藏字段改用 taxonomy + post_type - 字段顺序与 is_page 分支(标题样式、widgets_register、容器位置) - 多选 checkbox 改 name="id[]",描述允许富文本 - 模块布局指向定制器小工具面板 - term 保存使用 POST 的 taxonomy 字段 清理不再使用的 assets/js/page-admin-set.js。 新增 frontend-page-edit-harness.php 覆盖门禁、渲染与各保存分支。
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* AKSRT Page Admin Set — 前端页面编辑弹窗提交(后台「扩展&增强 → 系统工具」)。
|
||||
*
|
||||
* 弹窗内容由 PageAdminSet::ajax_modal() 经 aksrt-modal-runtime 注入,
|
||||
* 这里只负责表单提交:序列化 → admin-ajax → 成功后刷新页面。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
|
||||
function notify(message, type) {
|
||||
if (typeof window.notyf === 'function') {
|
||||
window.notyf(message, type || 'error', 4, 'aksrt-page-admin-set-toast');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target.closest('form.aksrt-admin-set-form');
|
||||
if (!form) return;
|
||||
event.preventDefault();
|
||||
|
||||
var submitButton = form.querySelector('.aksrt-admin-set-submit');
|
||||
if (submitButton && submitButton.disabled) return;
|
||||
if (submitButton) submitButton.disabled = true;
|
||||
|
||||
var action = (form.querySelector('input[name="action"]') || {}).value || '';
|
||||
var ajaxUrl = (window.aksrtData && window.aksrtData.ajaxUrl) || (window._win && window._win.ajax_url) || '';
|
||||
if (!ajaxUrl) {
|
||||
notify('请求地址缺失', 'error');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var url = ajaxUrl + (ajaxUrl.indexOf('?') === -1 ? '?' : '&') + 'action=' + encodeURIComponent(action);
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: new FormData(form),
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().catch(function () { return {}; }).then(function (payload) {
|
||||
if (!response.ok || !payload || !payload.success) {
|
||||
throw new Error((payload && payload.data && payload.data.msg) || '保存失败,请重试');
|
||||
}
|
||||
return payload;
|
||||
});
|
||||
})
|
||||
.then(function (payload) {
|
||||
var data = payload.data || {};
|
||||
notify(data.msg || '保存成功', 'success');
|
||||
if (data.reload !== false) {
|
||||
window.setTimeout(function () { window.location.reload(); }, 600);
|
||||
} else if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
notify(error.message || '保存失败,请重试', 'error');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
});
|
||||
});
|
||||
})(window, document);
|
||||
+318
-130
@@ -2,9 +2,9 @@
|
||||
/**
|
||||
* 前端页面编辑(后台「扩展&增强 → 系统工具」的 admin_frontend_set)。
|
||||
*
|
||||
* 对齐参考主题 zib-frontend-set.php:管理员在前台通过悬浮按钮打开弹窗,
|
||||
* 快速修改当前文章/页面的标题、副标题、布局、计数、评论开关或分类术语的
|
||||
* 名称与描述;保存走白名单,行为与后台 PostMetaBox / wp_update_term 一致。
|
||||
* 对齐参考主题 zib-frontend-set.php:wp_footer 直出 #modal_admin_set 弹窗
|
||||
* (bootstrap modal + wp-ajax-submit 走 vendored main.js 的 aksrt_ajax 契约),
|
||||
* 管理员在前台快速修改当前文章/页面的参数或分类术语的名称与描述。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -26,9 +26,8 @@ class PageAdminSet {
|
||||
|
||||
public function init(): void {
|
||||
add_filter('aksrt_float_right_buttons', [$this, 'float_button'], 10, 2);
|
||||
add_action('wp_ajax_aksrt_frontend_set_modal', [$this, 'ajax_modal']);
|
||||
add_action('wp_footer', [$this, 'render_modal'], 10);
|
||||
add_action('wp_ajax_aksrt_frontend_set_save', [$this, 'ajax_save']);
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_script']);
|
||||
}
|
||||
|
||||
/** 开启条件:设置开关 + 管理员 + 单页/术语页 + 有效对象(排除投稿与导航页模板)。 */
|
||||
@@ -49,91 +48,119 @@ class PageAdminSet {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 悬浮按钮:注入右侧悬浮按钮栏(对齐参考主题 zib_admin_frontend_set_botton)。 */
|
||||
public function float_button($buttons, $is_mobile) {
|
||||
/**
|
||||
* 悬浮按钮:追加到右侧悬浮按钮栏(对齐参考主题 zib_admin_frontend_set_botton)。
|
||||
*
|
||||
* 参考主题对移动端与桌面端共用同一按钮,$is_mobile 仅用于保持 filter 签名。
|
||||
*/
|
||||
public function float_button($buttons, $is_mobile = null) {
|
||||
if (!self::enabled()) {
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
$url = add_query_arg(
|
||||
[
|
||||
'action' => 'aksrt_frontend_set_modal',
|
||||
'_wpnonce' => wp_create_nonce('aksrt_frontend_set'),
|
||||
],
|
||||
admin_url('admin-ajax.php')
|
||||
);
|
||||
$button = '<a href="javascript:;" data-toggle="modal" data-target="#modal_admin_set"'
|
||||
. ' title="' . esc_attr__('设置页面参数', 'aksrt') . '" class="admin-set-page float-btn">'
|
||||
. '<i class="fa fa-cog fa-spin" aria-hidden="true"></i>'
|
||||
. '<div class="abs-right badg c-red px12 admin-set-info" style="width:187px;">' . esc_html__('在此编辑此页面参数', 'aksrt') . '</div></a>';
|
||||
|
||||
$button = '<a class="float-btn admin-set-btn" href="javascript:;"'
|
||||
. ' data-aksrt-modal="remote" data-aksrt-modal-cache="false" data-aksrt-modal-kind="admin-set"'
|
||||
. ' data-aksrt-modal-url="' . esc_url($url) . '"'
|
||||
. aksrt_float_btn_tooltip_attr(__('在此编辑此页面参数', 'aksrt'), (bool) $is_mobile)
|
||||
. '><i class="fa fas fa-cog"></i></a>';
|
||||
|
||||
return $button . (string) $buttons;
|
||||
return (string) $buttons . $button;
|
||||
}
|
||||
|
||||
/** 条件加载弹窗提交脚本。 */
|
||||
public function enqueue_script(): void {
|
||||
/** wp_footer 输出 #modal_admin_set(对齐参考主题 zib_frontend_set_modal)。 */
|
||||
public function render_modal(): void {
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
}
|
||||
$src = get_template_directory() . '/assets/js/page-admin-set.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-page-admin-set',
|
||||
get_template_directory_uri() . '/assets/js/page-admin-set.js',
|
||||
[],
|
||||
file_exists($src) ? (string) filemtime($src) : null,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/** GET 弹窗内容(注入 aksrt-modal-runtime)。 */
|
||||
public function ajax_modal(): void {
|
||||
check_ajax_referer('aksrt_frontend_set', '_wpnonce');
|
||||
if (!self::enabled()) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$object_id = (int) get_queried_object_id();
|
||||
if (is_category() || is_tag() || is_tax()) {
|
||||
$html = $this->term_modal_html($object_id, (string) get_queried_object()->taxonomy);
|
||||
$type = 'tax';
|
||||
$taxonomy = (string) get_queried_object()->taxonomy;
|
||||
$post_type = '';
|
||||
$edit_link = get_edit_term_link($object_id, $taxonomy);
|
||||
$type_label = (string) get_taxonomy($taxonomy)->labels->singular_name;
|
||||
} else {
|
||||
$html = $this->post_modal_html($object_id, get_post_type($object_id) ?: 'post');
|
||||
$type = 'post';
|
||||
$taxonomy = '';
|
||||
$post_type = (string) get_post_type();
|
||||
$edit_link = (string) get_edit_post_link($object_id);
|
||||
$type_label = (string) get_post_type_object($post_type)->labels->singular_name;
|
||||
}
|
||||
|
||||
wp_send_json_success(['html' => $html]);
|
||||
// 对齐参考主题 zib_get_customize_widgets_url:跳转定制器小工具面板并返回当前页。
|
||||
$current_url = (string) home_url(add_query_arg(null, null));
|
||||
$widgets_url = add_query_arg([
|
||||
'url' => rawurlencode($current_url),
|
||||
'autofocus' => ['panel' => 'widgets'],
|
||||
'return' => rawurlencode($current_url),
|
||||
], admin_url('customize.php'));
|
||||
|
||||
$header = '<div class="modal-header"><strong class="modal-title">'
|
||||
. '<i class="fa fa-sliders mr10" aria-hidden="true"></i>' . esc_html($type_label . __('设置', 'aksrt'))
|
||||
. '</strong><button class="close" data-dismiss="modal">' . $this->close_icon() . '</button></div>';
|
||||
|
||||
$footer = '<div class="modal-footer">'
|
||||
. '<a target="_blank" title="' . esc_attr__('使用可视化布局配置此页面', 'aksrt') . '" data-toggle="tooltip" href="' . esc_url($widgets_url) . '" class="but c-yellow-2 padding-lg"><i class="fa fa-pie-chart" aria-hidden="true"></i>' . esc_html__('模块布局', 'aksrt') . '</a>'
|
||||
. ($edit_link ? '<a target="_blank" href="' . esc_url($edit_link) . '" class="but c-yellow padding-lg"><i class="fa fa-wordpress" aria-hidden="true"></i>' . esc_html__('后台编辑', 'aksrt') . '</a>' : '')
|
||||
. '<button class="but jb-blue padding-lg wp-ajax-submit"><i class="fa fa-check" aria-hidden="true"></i>' . esc_html__('确认修改', 'aksrt') . '</button>'
|
||||
. '</div>';
|
||||
|
||||
$body = '<div class="modal-body">'
|
||||
. ($type === 'tax' ? $this->term_inputs($object_id, $taxonomy) : $this->post_inputs($object_id))
|
||||
. '<input type="hidden" name="action" value="aksrt_frontend_set_save">'
|
||||
. '<input type="hidden" name="type" value="' . esc_attr($type) . '">'
|
||||
. '<input type="hidden" name="taxonomy" value="' . esc_attr($taxonomy) . '">'
|
||||
. '<input type="hidden" name="post_type" value="' . esc_attr($post_type) . '">'
|
||||
. '<input type="hidden" name="id" value="' . esc_attr((string) $object_id) . '">'
|
||||
. wp_nonce_field('aksrt_frontend_set', '_wpnonce', true, false)
|
||||
. '</div>';
|
||||
|
||||
echo '<div class="modal fade" id="modal_admin_set" tabindex="-1" role="dialog" aria-hidden="false" win-ajax-replace="modal_admin_set">'
|
||||
. '<div class="modal-dialog" role="document">'
|
||||
. '<div class="modal-content page-set-modal"><form>' . $header . $body . $footer . '</form></div>'
|
||||
. '</div></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各片段已转义
|
||||
}
|
||||
|
||||
/** POST 保存:仅接受白名单字段。 */
|
||||
/** 关闭按钮图标(本站 FA5 惯例,对齐参考主题 close 按钮位)。 */
|
||||
private function close_icon(): string {
|
||||
return '<i class="fa fas fa-times" aria-hidden="true"></i>';
|
||||
}
|
||||
|
||||
/** 保存(legacy aksrt_ajax 契约:{msg, reload} / {error, ys, msg})。 */
|
||||
public function ajax_save(): void {
|
||||
check_ajax_referer('aksrt_frontend_set', '_wpnonce');
|
||||
if (!self::enabled()) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
|
||||
$settings = class_exists('\AKSRT\Settings\ExtensionRuntime')
|
||||
? \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings()
|
||||
: [];
|
||||
if (empty($settings['admin_frontend_set']) || !current_user_can('manage_options')) {
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$object_id = isset($_POST['id']) ? absint($_POST['id']) : 0;
|
||||
if (!$object_id) {
|
||||
wp_send_json_error(['msg' => __('页面数据出错', 'aksrt')]);
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('页面数据出错', 'aksrt')]);
|
||||
}
|
||||
|
||||
$type = isset($_POST['type']) ? sanitize_key((string) $_POST['type']) : '';
|
||||
if ($type === 'tax') {
|
||||
$taxonomy = isset($_POST['subtype']) ? sanitize_key((string) $_POST['subtype']) : '';
|
||||
$this->save_term($object_id, $taxonomy);
|
||||
} else {
|
||||
$this->save_term($object_id, isset($_POST['taxonomy']) ? sanitize_key((string) wp_unslash($_POST['taxonomy'])) : '');
|
||||
} elseif ($type === 'post') {
|
||||
$this->save_post($object_id);
|
||||
} else {
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('页面数据出错', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_success(['msg' => __('保存成功,正在刷新页面', 'aksrt'), 'reload' => true]);
|
||||
wp_send_json(['msg' => __('保存成功,正在刷新页面', 'aksrt'), 'reload' => true]);
|
||||
}
|
||||
|
||||
private function save_post(int $post_id): void {
|
||||
$post = get_post($post_id);
|
||||
if (!$post) {
|
||||
wp_send_json_error(['msg' => __('未找到可用数据', 'aksrt')]);
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('未找到可用post数据', 'aksrt')]);
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$postarr = ['ID' => $post_id];
|
||||
@@ -144,12 +171,12 @@ class PageAdminSet {
|
||||
}
|
||||
}
|
||||
if (isset($_POST['comments_open'])) {
|
||||
$postarr['comment_status'] = !empty($_POST['comments_open']) ? 'open' : 'closed';
|
||||
$postarr['comment_status'] = $_POST['comments_open'] === 'open' ? 'open' : 'closed';
|
||||
}
|
||||
if (count($postarr) > 1) {
|
||||
$result = wp_update_post($postarr, true);
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['msg' => $result->get_error_message()]);
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => $result->get_error_message()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,21 +186,21 @@ class PageAdminSet {
|
||||
}
|
||||
|
||||
if ($post->post_type === 'post' && class_exists('\AKSRT\Core\PostMetaBox')) {
|
||||
$layout = isset($_POST['show_layout']) ? sanitize_key((string) wp_unslash($_POST['show_layout'])) : '';
|
||||
if ($layout !== '' && !in_array($layout, ['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
|
||||
$layout = 'false';
|
||||
}
|
||||
|
||||
// 保留白名单之外的既有配置键(封面、视频等),仅覆盖弹窗提交的字段。
|
||||
// 复选框未勾选时浏览器不会提交键名,必须无条件写入两个开关。
|
||||
// 复选框未勾选时浏览器不会提交键名,两个开关必须无条件写入。
|
||||
$meta = get_post_meta($post_id, \AKSRT\Core\PostMetaBox::META_KEY, true);
|
||||
$meta = is_array($meta) ? $meta : [];
|
||||
if ($layout !== '') {
|
||||
$meta['show_layout'] = $layout;
|
||||
|
||||
$layout = isset($_POST['show_layout']) ? sanitize_key((string) wp_unslash($_POST['show_layout'])) : 'false';
|
||||
if (!in_array($layout, ['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
|
||||
$layout = 'false';
|
||||
}
|
||||
$meta['show_layout'] = $layout;
|
||||
|
||||
if (isset($_POST['subtitle'])) {
|
||||
$meta['subtitle'] = sanitize_text_field(wp_unslash((string) $_POST['subtitle']));
|
||||
}
|
||||
|
||||
$meta['no_article-navs'] = !empty($_POST['no_article-navs']);
|
||||
$meta['article_maxheight_xz'] = !empty($_POST['article_maxheight_xz']);
|
||||
update_post_meta($post_id, \AKSRT\Core\PostMetaBox::META_KEY, $meta);
|
||||
@@ -185,15 +212,36 @@ class PageAdminSet {
|
||||
update_post_meta($post_id, '_aksrt_like_count', max(0, (int) $_POST['like']));
|
||||
}
|
||||
}
|
||||
|
||||
if ($post->post_type === 'page') {
|
||||
if (isset($_POST['page_header_style'])) {
|
||||
$style = sanitize_key((string) wp_unslash($_POST['page_header_style']));
|
||||
update_post_meta($post_id, 'page_header_style', in_array($style, ['', 'not', '1', '2', '3'], true) ? $style : '');
|
||||
}
|
||||
|
||||
$register = isset($_POST['widgets_register']) && $_POST['widgets_register'] === '1';
|
||||
update_post_meta($post_id, 'widgets_register', $register ? '1' : '');
|
||||
|
||||
// 关闭小工具容器时清空容器位置;容器位置与参考主题的注册语义保持一致。
|
||||
$containers = [];
|
||||
if ($register && !empty($_POST['widgets_register_container']) && is_array($_POST['widgets_register_container'])) {
|
||||
$allowed = ['sidebar', 'top_fluid', 'top_content', 'bottom_content', 'bottom_fluid'];
|
||||
$containers = array_values(array_unique(array_intersect(
|
||||
array_map('sanitize_key', wp_unslash($_POST['widgets_register_container'])),
|
||||
$allowed
|
||||
)));
|
||||
}
|
||||
update_post_meta($post_id, 'widgets_register_container', $containers);
|
||||
}
|
||||
}
|
||||
|
||||
private function save_term(int $term_id, string $taxonomy = ''): void {
|
||||
$term = $taxonomy !== '' ? get_term($term_id, $taxonomy) : get_term($term_id);
|
||||
if (!$term || is_wp_error($term)) {
|
||||
wp_send_json_error(['msg' => __('未找到可用数据', 'aksrt')]);
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('未找到可用term数据', 'aksrt')]);
|
||||
}
|
||||
if (!current_user_can('manage_categories') && !current_user_can('manage_terms')) {
|
||||
wp_send_json_error(['msg' => __('权限不足', 'aksrt')]);
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
@@ -212,94 +260,234 @@ class PageAdminSet {
|
||||
|
||||
$result = wp_update_term($term_id, (string) $term->taxonomy, $data);
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['msg' => $result->get_error_message()]);
|
||||
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => $result->get_error_message()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function post_modal_html(int $post_id, string $post_type): string {
|
||||
/** 文章/页面的弹窗字段(对齐参考主题 zib_get_frontend_set_input)。 */
|
||||
private function post_inputs(int $post_id): string {
|
||||
$meta = class_exists('\AKSRT\Core\PostMetaBox')
|
||||
? \AKSRT\Core\PostMetaBox::get_meta($post_id)
|
||||
: [];
|
||||
$views = (int) ($meta['views'] ?? 0);
|
||||
$like = (int) ($meta['like'] ?? 0);
|
||||
$is_single = is_single();
|
||||
$inputs = [];
|
||||
|
||||
$html = '<div class="aksrt-admin-set">';
|
||||
$html .= '<p class="em09 muted-3-color mb10">' . esc_html(get_the_title($post_id)) . '</p>';
|
||||
$inputs[] = [
|
||||
'name' => __('显示布局', 'aksrt'),
|
||||
'id' => 'show_layout',
|
||||
'std' => (string) ($meta['show_layout'] ?? 'false'),
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'false' => __('跟随主题', 'aksrt'),
|
||||
'no_sidebar' => __('无侧边栏', 'aksrt'),
|
||||
'sidebar_left' => __('侧边栏靠左', 'aksrt'),
|
||||
'sidebar_right' => __('侧边栏靠右', 'aksrt'),
|
||||
],
|
||||
];
|
||||
|
||||
$html .= $this->text_field('post_title', __('标题', 'aksrt'), (string) get_the_title($post_id));
|
||||
$inputs[] = [
|
||||
'name' => __('标题', 'aksrt'),
|
||||
'id' => 'post_title',
|
||||
'std' => (string) get_the_title($post_id),
|
||||
'type' => 'text',
|
||||
];
|
||||
|
||||
if ($is_single) {
|
||||
$html .= $this->text_field('subtitle', __('副标题', 'aksrt'), (string) ($meta['subtitle'] ?? ''));
|
||||
$layout = (string) ($meta['show_layout'] ?? 'false');
|
||||
$html .= '<div class="relative line-form mb10 modal-line-field"><select name="show_layout" class="line-form-input">'
|
||||
. '<option value="false"' . selected($layout, 'false', false) . '>' . esc_html__('跟随主题', 'aksrt') . '</option>'
|
||||
. '<option value="no_sidebar"' . selected($layout, 'no_sidebar', false) . '>' . esc_html__('无侧边栏', 'aksrt') . '</option>'
|
||||
. '<option value="sidebar_left"' . selected($layout, 'sidebar_left', false) . '>' . esc_html__('侧边栏靠左', 'aksrt') . '</option>'
|
||||
. '<option value="sidebar_right"' . selected($layout, 'sidebar_right', false) . '>' . esc_html__('侧边栏靠右', 'aksrt') . '</option>'
|
||||
. '</select><i class="line-form-line"></i></div>';
|
||||
$html .= $this->number_field('like', __('点赞数', 'aksrt'), $like);
|
||||
$html .= $this->number_field('views', __('阅读数', 'aksrt'), $views);
|
||||
$html .= $this->checkbox_field('no_article-navs', __('不显示目录树', 'aksrt'), !empty($meta['no_article-navs']));
|
||||
$html .= $this->checkbox_field('article_maxheight_xz', __('限制文章最大高度', 'aksrt'), !empty($meta['article_maxheight_xz']));
|
||||
if (is_single()) {
|
||||
$inputs[] = [
|
||||
'name' => __('副标题', 'aksrt'),
|
||||
'id' => 'subtitle',
|
||||
'std' => (string) ($meta['subtitle'] ?? ''),
|
||||
'type' => 'text',
|
||||
];
|
||||
$inputs[] = [
|
||||
'name' => __('文章格式', 'aksrt'),
|
||||
'id' => 'post_format',
|
||||
'std' => (string) get_post_format($post_id),
|
||||
'type' => 'select',
|
||||
'options' => $this->post_format_options(),
|
||||
];
|
||||
$inputs[] = [
|
||||
'name' => __('点赞数', 'aksrt'),
|
||||
'id' => 'like',
|
||||
'std' => (int) ($meta['like'] ?? 0),
|
||||
'type' => 'number',
|
||||
];
|
||||
$inputs[] = [
|
||||
'name' => __('阅读数', 'aksrt'),
|
||||
'id' => 'views',
|
||||
'std' => (int) ($meta['views'] ?? 0),
|
||||
'type' => 'number',
|
||||
];
|
||||
$inputs[] = [
|
||||
'name' => __('目录树', 'aksrt'),
|
||||
'id' => 'no_article-navs',
|
||||
'std' => !empty($meta['no_article-navs']),
|
||||
'type' => 'checkbox',
|
||||
'desc' => __('不显示', 'aksrt'),
|
||||
];
|
||||
$inputs[] = [
|
||||
'name' => __('文章高度', 'aksrt'),
|
||||
'id' => 'article_maxheight_xz',
|
||||
'std' => !empty($meta['article_maxheight_xz']),
|
||||
'type' => 'checkbox',
|
||||
'desc' => __('限制文章最大高度', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
$html .= '<div class="relative line-form mb10 modal-line-field"><select name="comments_open" class="line-form-input">'
|
||||
. '<option value="open"' . selected(comments_open($post_id), true, false) . '>' . esc_html__('开启评论', 'aksrt') . '</option>'
|
||||
. '<option value=""' . selected(comments_open($post_id), false, false) . '>' . esc_html__('关闭评论', 'aksrt') . '</option>'
|
||||
. '</select><i class="line-form-line"></i></div>';
|
||||
$inputs[] = [
|
||||
'name' => __('评论', 'aksrt'),
|
||||
'id' => 'comments_open',
|
||||
'std' => comments_open($post_id) ? 'open' : '',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'open' => __('开启', 'aksrt'),
|
||||
'' => __('关闭', 'aksrt'),
|
||||
],
|
||||
];
|
||||
|
||||
$html .= $this->hidden_fields('post', $post_id, $post_type);
|
||||
$html .= '<div class="flex jsb mt10"><button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>'
|
||||
. '<button type="submit" class="but jb-blue aksrt-admin-set-submit"><i class="fa fas fa-check"></i> ' . esc_html__('确认修改', 'aksrt') . '</button></div>';
|
||||
$html .= '</div>';
|
||||
if (is_page()) {
|
||||
$inputs[] = [
|
||||
'name' => __('标题样式', 'aksrt'),
|
||||
'id' => 'page_header_style',
|
||||
'std' => (string) get_post_meta($post_id, 'page_header_style', true),
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'' => __('跟随主题', 'aksrt'),
|
||||
'not' => __('不显示', 'aksrt'),
|
||||
'1' => __('简单样式', 'aksrt'),
|
||||
'2' => __('卡片样式', 'aksrt'),
|
||||
'3' => __('封面图样式', 'aksrt'),
|
||||
],
|
||||
];
|
||||
$inputs[] = [
|
||||
'name' => __('模块布局', 'aksrt'),
|
||||
'id' => 'widgets_register',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'1' => __('开启', 'aksrt'),
|
||||
'0' => __('关闭', 'aksrt'),
|
||||
],
|
||||
'std' => (string) get_post_meta($post_id, 'widgets_register', true),
|
||||
];
|
||||
$inputs[] = [
|
||||
'id' => 'widgets_register_container',
|
||||
'type' => 'checkbox',
|
||||
'class' => 'compact',
|
||||
'std' => (array) get_post_meta($post_id, 'widgets_register_container', true),
|
||||
'name' => __('创建容器位置', 'aksrt'),
|
||||
'desc' => __('请根据需要合理开启,如果用不到则不要开启', 'aksrt') . '<br>' . __('保存页面后即可进入小工具或模块配置添加模块', 'aksrt') . '<div class="c-yellow">' . __('注意:开启此功能的页面不能太多,太多会影响性能,建议控制在10个以内', 'aksrt') . '</div>',
|
||||
'options' => [
|
||||
'sidebar' => __('侧边栏', 'aksrt'),
|
||||
'top_fluid' => __('顶部全宽度', 'aksrt'),
|
||||
'top_content' => __('主内容上面', 'aksrt'),
|
||||
'bottom_content' => __('主内容下面', 'aksrt'),
|
||||
'bottom_fluid' => __('底部全宽度', 'aksrt'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->wrap_form($html);
|
||||
return $this->render_inputs($inputs);
|
||||
}
|
||||
|
||||
private function term_modal_html(int $term_id, string $taxonomy): string {
|
||||
/** AKSRT 注册的文章格式(standard + add_theme_support 注册项)。 */
|
||||
private function post_format_options(): array {
|
||||
$options = ['standard' => __('标准', 'aksrt')];
|
||||
foreach (get_theme_support('post-formats') ? (array) get_theme_support('post-formats')[0] : [] as $format) {
|
||||
$options[$format] = esc_html(get_post_format_string($format));
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/** 分类术语的弹窗字段(对齐参考主题 tax 分支)。 */
|
||||
private function term_inputs(int $term_id, string $taxonomy): string {
|
||||
$term = get_term($term_id, $taxonomy);
|
||||
if (!$term || is_wp_error($term)) {
|
||||
return '<div class="aksrt-admin-set"><p class="muted-3-color">' . esc_html__('未找到可用数据', 'aksrt') . '</p></div>';
|
||||
return '<p class="muted-3-color">' . esc_html__('未找到可用term数据', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
$html = '<div class="aksrt-admin-set">';
|
||||
$html .= '<p class="em09 muted-3-color mb10">' . esc_html($term->name) . '</p>';
|
||||
$html .= $this->text_field('term_name', __('名称', 'aksrt'), (string) $term->name);
|
||||
$html .= $this->text_field('term_description', __('描述', 'aksrt'), (string) $term->description);
|
||||
$html .= $this->hidden_fields('tax', $term_id, $taxonomy);
|
||||
$html .= '<div class="flex jsb mt10"><button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>'
|
||||
. '<button type="submit" class="but jb-blue aksrt-admin-set-submit"><i class="fa fas fa-check"></i> ' . esc_html__('确认修改', 'aksrt') . '</button></div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $this->wrap_form($html);
|
||||
return $this->render_inputs([
|
||||
[
|
||||
'name' => __('名称', 'aksrt'),
|
||||
'id' => 'term_name',
|
||||
'std' => (string) $term->name,
|
||||
'type' => 'text',
|
||||
],
|
||||
[
|
||||
'name' => __('描述', 'aksrt'),
|
||||
'id' => 'term_description',
|
||||
'std' => (string) $term->description,
|
||||
'type' => 'text',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function wrap_form(string $inner): string {
|
||||
return '<form class="aksrt-admin-set-form">'
|
||||
. '<input type="hidden" name="action" value="aksrt_frontend_set_save">'
|
||||
. wp_nonce_field('aksrt_frontend_set', '_wpnonce', true, false)
|
||||
. $inner
|
||||
. '</form>';
|
||||
/** 表单控件构建(对齐参考主题 zib_edit_input_construct)。 */
|
||||
private function render_inputs(array $inputs): string {
|
||||
$output = '';
|
||||
foreach ($inputs as $meta) {
|
||||
$type = (string) ($meta['type'] ?? '');
|
||||
$value = $meta['std'] ?? '';
|
||||
$desc = (string) ($meta['desc'] ?? '');
|
||||
$class = $type !== '' ? ' option-' . $type : '';
|
||||
|
||||
if (isset($meta['class'])) {
|
||||
$class .= ' ' . $meta['class'];
|
||||
}
|
||||
|
||||
$control = '';
|
||||
if ($type === 'text') {
|
||||
$control = '<input class="form-control" name="' . esc_attr((string) ($meta['id'] ?? '')) . '" type="text" value="' . esc_attr((string) $value) . '"/>';
|
||||
} elseif ($type === 'number') {
|
||||
$control = '<input class="form-control" name="' . esc_attr((string) ($meta['id'] ?? '')) . '" type="number" value="' . esc_attr((string) $value) . '"/>';
|
||||
} elseif ($type === 'select') {
|
||||
$control = '<div class="form-select"><select class="form-control" name="' . esc_attr((string) ($meta['id'] ?? '')) . '">';
|
||||
foreach ((array) ($meta['options'] ?? []) as $key => $option) {
|
||||
$control .= '<option' . selected((string) $value, (string) $key, false) . ' value="' . esc_attr((string) $key) . '">' . esc_html((string) $option) . '</option>';
|
||||
}
|
||||
$control .= '</select></div>';
|
||||
} elseif ($type === 'radio') {
|
||||
foreach ((array) ($meta['options'] ?? []) as $key => $option) {
|
||||
$control .= '<label class="mr10"><input type="radio" name="' . esc_attr((string) ($meta['id'] ?? '')) . '" value="' . esc_attr((string) $key) . '" ' . checked((string) $value, (string) $key, false) . ' /><span class="ml6 em09 muted-color" style=" font-weight: normal; ">' . esc_html((string) $option) . '</span></label>';
|
||||
}
|
||||
} elseif ($type === 'checkbox') {
|
||||
$options = (array) ($meta['options'] ?? []);
|
||||
if ($options) {
|
||||
foreach ($options as $key => $option) {
|
||||
$checked = is_array($value) ? (in_array($key, $value, true) ? ' checked' : '') : ((string) $value === (string) $key ? ' checked' : '');
|
||||
$control .= '<span class="form-checkbox"><input name="' . esc_attr((string) ($meta['id'] ?? '')) . '[]" id="' . esc_attr((string) ($meta['id'] ?? '') . $key) . '" type="checkbox"' . $checked . ' value="' . esc_attr((string) $key) . '"/><label for="' . esc_attr((string) ($meta['id'] ?? '') . $key) . '" class="em09 muted-color ml6 mr10" style=" font-weight: normal; ">' . esc_html((string) $option) . '</label></span>';
|
||||
}
|
||||
} else {
|
||||
$control = '<span class="form-checkbox"><input value="1" name="' . esc_attr((string) ($meta['id'] ?? '')) . '" id="' . esc_attr((string) ($meta['id'] ?? '')) . '" type="checkbox" ' . checked(!empty($value), true, false) . '/><label for="' . esc_attr((string) ($meta['id'] ?? '')) . '" class="em09 muted-color ml6" style=" font-weight: normal; ">' . ($desc !== '' ? $this->rich_desc($desc) : '') . '</label></span>';
|
||||
}
|
||||
}
|
||||
|
||||
$output .= '<div class="mb10 row' . esc_attr($class) . '">' . "\n";
|
||||
$output .= '<div class="heading col-xs-3 text-right">' . esc_html((string) ($meta['name'] ?? '')) . '</div>' . "\n";
|
||||
$output .= '<div class="option col-xs-8">' . "\n";
|
||||
$output .= $control;
|
||||
if ($desc !== '' && $type !== 'checkbox') {
|
||||
$output .= '<div class="em09 muted-2-color">' . $this->rich_desc($desc) . '</div>' . "\n";
|
||||
}
|
||||
$output .= '</div>' . "\n";
|
||||
$output .= '</div>' . "\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function hidden_fields(string $type, int $id, string $subtype): string {
|
||||
return '<input type="hidden" name="type" value="' . esc_attr($type) . '">'
|
||||
. '<input type="hidden" name="subtype" value="' . esc_attr($subtype) . '">'
|
||||
. '<input type="hidden" name="id" value="' . esc_attr((string) $id) . '">';
|
||||
}
|
||||
|
||||
private function text_field(string $name, string $label, string $value): string {
|
||||
return '<div class="relative line-form mb10 modal-line-field"><input type="text" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" class="line-form-input">'
|
||||
. '<div class="scale-placeholder">' . esc_html($label) . '</div><i class="line-form-line"></i></div>';
|
||||
}
|
||||
|
||||
private function number_field(string $name, string $label, int $value): string {
|
||||
return '<div class="relative line-form mb10 modal-line-field"><input type="number" min="0" step="1" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" class="line-form-input">'
|
||||
. '<div class="scale-placeholder">' . esc_html($label) . '</div><i class="line-form-line"></i></div>';
|
||||
}
|
||||
|
||||
private function checkbox_field(string $name, string $label, bool $checked): string {
|
||||
return '<div class="mb10 form-switch-label"><label><input type="checkbox" name="' . esc_attr($name) . '" value="1"' . checked($checked, true, false) . '> ' . esc_html($label) . '</label></div>';
|
||||
/**
|
||||
* 描述文案:允许 <br>、强调标签与来源可信的主题文案(对齐参考主题的富文本描述)。
|
||||
*
|
||||
* @param string $desc 内部生成的描述文案。
|
||||
*/
|
||||
private function rich_desc(string $desc): string {
|
||||
return wp_kses($desc, [
|
||||
'br' => [],
|
||||
'div' => ['class' => []],
|
||||
'span' => ['class' => []],
|
||||
'em' => [],
|
||||
'strong' => [],
|
||||
'b' => [],
|
||||
'i' => [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
/** Frontend page edit runtime harness: modal gating, input rendering, and ajax-save outside the query. */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
|
||||
$GLOBALS['aksrt_set_settings'] = [];
|
||||
$GLOBALS['aksrt_set_meta'] = [];
|
||||
$GLOBALS['aksrt_set_json'] = null;
|
||||
$GLOBALS['aksrt_set_output'] = '';
|
||||
$GLOBALS['aksrt_set_term'] = null;
|
||||
$GLOBALS['aksrt_set_post'] = null;
|
||||
$GLOBALS['aksrt_set_queried'] = 0;
|
||||
$GLOBALS['aksrt_set_post_type'] = 'post';
|
||||
$GLOBALS['aksrt_set_is_single'] = false;
|
||||
$GLOBALS['aksrt_set_is_page'] = false;
|
||||
$GLOBALS['aksrt_set_is_category'] = false;
|
||||
$GLOBALS['aksrt_set_object'] = null;
|
||||
$GLOBALS['aksrt_set_page_templates'] = [];
|
||||
$GLOBALS['aksrt_set_can'] = true;
|
||||
|
||||
function __(string $text, string $domain = ''): string { return $text; }
|
||||
function esc_html__($t, $d = ''): string { return (string) $t; }
|
||||
function esc_attr__($t, $d = ''): string { return (string) $t; }
|
||||
function esc_html($t): string { return (string) $t; }
|
||||
function esc_attr($t): string { return (string) $t; }
|
||||
function esc_url($t): string { return (string) $t; }
|
||||
function sanitize_key(string $key): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)) ?? ''; }
|
||||
function sanitize_text_field($v): string { return trim(strip_tags((string) $v)); }
|
||||
function sanitize_textarea_field($v): string { return trim(strip_tags((string) $v)); }
|
||||
function wp_unslash($v) { return $v; }
|
||||
function absint($v): int { return max(0, (int) $v); }
|
||||
function is_user_logged_in(): bool { return true; }
|
||||
function current_user_can($cap, ...$args): bool { return (bool) $GLOBALS['aksrt_set_can']; }
|
||||
function is_single(): bool { return $GLOBALS['aksrt_set_is_single']; }
|
||||
function is_page(): bool { return $GLOBALS['aksrt_set_is_page']; }
|
||||
function is_category(): bool { return $GLOBALS['aksrt_set_is_category']; }
|
||||
function is_tag(): bool { return false; }
|
||||
function is_tax(): bool { return false; }
|
||||
function get_queried_object_id(): int { return $GLOBALS['aksrt_set_queried']; }
|
||||
function get_queried_object() { return $GLOBALS['aksrt_set_object']; }
|
||||
function get_post_type(): string { return $GLOBALS['aksrt_set_post_type']; }
|
||||
function get_post($id) { return $GLOBALS['aksrt_set_post']; }
|
||||
function get_post_meta($id, $key = '', $single = false) { return $GLOBALS['aksrt_set_meta'][$key] ?? ''; }
|
||||
function update_post_meta($id, $key, $value): void { $GLOBALS['aksrt_set_meta'][$key] = $value; }
|
||||
function get_the_title($id): string { return '示例标题'; }
|
||||
function get_the_permalink($id): string { return 'https://example.test/post'; }
|
||||
function get_post_format($id) { return ''; }
|
||||
function set_post_format($id, $format): void { $GLOBALS['aksrt_set_meta']['__format'] = $format; }
|
||||
function post_type_supports($type, $feature): bool { return $feature === 'post-formats'; }
|
||||
function get_theme_support($feature) { return $feature === 'post-formats' ? [['image', 'video']] : false; }
|
||||
function get_post_format_string($format): string { return ucfirst((string) $format); }
|
||||
function comments_open($id): bool { return true; }
|
||||
function get_edit_post_link($id, $context = '') { return 'https://example.test/wp-admin/post.php'; }
|
||||
function get_edit_term_link($id, $tax = '') { return 'https://example.test/wp-admin/term.php'; }
|
||||
function get_taxonomy($tax) { return (object) ['labels' => (object) ['singular_name' => '分类']]; }
|
||||
function get_post_type_object($type) { return (object) ['labels' => (object) ['singular_name' => $type === 'page' ? '页面' : '文章']]; }
|
||||
function get_term($id, $tax = '') { return $GLOBALS['aksrt_set_term']; }
|
||||
function wp_update_term($id, $tax, $args) { $GLOBALS['aksrt_set_meta']['__term'] = $args; return 1; }
|
||||
function wp_update_post($args, $wp_error = false) { $GLOBALS['aksrt_set_meta']['__post'] = $args; return 1; }
|
||||
function is_wp_error($v): bool { return false; }
|
||||
function wp_is_mobile(): bool { return false; }
|
||||
function is_page_template($tpl): bool { return in_array($tpl, $GLOBALS['aksrt_set_page_templates'], true); }
|
||||
function home_url($path = ''): string { return 'https://example.test' . $path; }
|
||||
function add_query_arg($args, $url = null) {
|
||||
if ($args === null) { return 'https://example.test/current'; }
|
||||
if ($url === null) { $url = 'https://example.test/current'; }
|
||||
$query = is_array($args) ? http_build_query($args) : (string) $args;
|
||||
return $url . (strpos($url, '?') === false ? '?' : '&') . $query;
|
||||
}
|
||||
function admin_url($path = ''): string { return 'https://example.test/wp-admin/' . ltrim((string) $path, '/'); }
|
||||
function wp_nonce_field($action = -1, $name = '_wpnonce', $referer = true, $echo = true) {
|
||||
$html = '<input type="hidden" name="' . $name . '" value="nonce">';
|
||||
if ($echo) { echo $html; return null; }
|
||||
return $html;
|
||||
}
|
||||
function check_ajax_referer($action = -1, $name = '_wpnonce'): bool { return true; }
|
||||
function wp_send_json($data, $code = null): void {
|
||||
$GLOBALS['aksrt_set_json'] = $data;
|
||||
throw new \RuntimeException('__sent__');
|
||||
}
|
||||
function wp_kses($t, $allowed = null): string { return (string) $t; }
|
||||
function checked($a, $b = true, $echo = true): string { return (string) $a === (string) $b ? ' checked="checked"' : ''; }
|
||||
function selected($a, $b = true, $echo = true): string { return (string) $a === (string) $b ? ' selected="selected"' : ''; }
|
||||
|
||||
function set_check(bool $condition, string $message): void {
|
||||
if (!$condition) throw new \RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
final class ExtensionRuntime {
|
||||
public static array $settings = [];
|
||||
public static function get_system_tools_settings(): array { return self::$settings; }
|
||||
}
|
||||
}
|
||||
|
||||
namespace AKSRT\Core {
|
||||
final class PostMetaBox {
|
||||
public const META_KEY = 'aksrt_post_extension';
|
||||
public static function get_meta(int $post_id): array { return $GLOBALS['aksrt_set_meta']; }
|
||||
}
|
||||
final class DomainModel {
|
||||
public const POST_VIEW_COUNT_KEY = 'aksrt_post_views';
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
use AKSRT\Frontend\PageAdminSet;
|
||||
|
||||
require dirname(__DIR__, 2) . '/src/Frontend/PageAdminSet.php';
|
||||
|
||||
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
|
||||
$GLOBALS['aksrt_set_queried'] = 0;
|
||||
$GLOBALS['aksrt_set_is_single'] = true;
|
||||
|
||||
// --- Gate: a non-admin-visible context must not render anything. ---
|
||||
set_check(PageAdminSet::enabled() === false, 'enabled() must be false without a queried object');
|
||||
|
||||
$GLOBALS['aksrt_set_queried'] = 12;
|
||||
set_check(PageAdminSet::enabled() === true, 'enabled() must be true on a single post for an admin');
|
||||
|
||||
$GLOBALS['aksrt_set_can'] = false;
|
||||
set_check(PageAdminSet::enabled() === false, 'enabled() must respect manage_options');
|
||||
$GLOBALS['aksrt_set_can'] = true;
|
||||
|
||||
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
|
||||
set_check(PageAdminSet::enabled() === false, 'enabled() must respect the admin_frontend_set switch');
|
||||
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
|
||||
|
||||
$GLOBALS['aksrt_set_page_templates'] = ['templates/pages/page-submit.php'];
|
||||
set_check(PageAdminSet::enabled() === false, 'enabled() must exclude the submit page template');
|
||||
$GLOBALS['aksrt_set_page_templates'] = [];
|
||||
|
||||
$GLOBALS['aksrt_set_post'] = (object) ['ID' => 12, 'post_type' => 'post'];
|
||||
|
||||
// --- Modal: reference markup parity. ---
|
||||
ob_start();
|
||||
(new PageAdminSet())->render_modal();
|
||||
$modal = (string) ob_get_clean();
|
||||
|
||||
set_check(strpos($modal, 'id="modal_admin_set"') !== false, 'modal must render #modal_admin_set');
|
||||
set_check(strpos($modal, 'win-ajax-replace="modal_admin_set"') !== false, 'modal must carry the reference win-ajax-replace key');
|
||||
set_check(strpos($modal, 'aria-hidden="false"') !== false, 'modal must expose aria-hidden="false"');
|
||||
set_check(strpos($modal, 'class="modal-content page-set-modal"') !== false, 'modal must keep the page-set-modal class');
|
||||
set_check(strpos($modal, 'class="modal-dialog" role="document"') !== false, 'modal must keep the reference dialog/document roles');
|
||||
set_check(strpos($modal, 'data-dismiss="modal"') !== false, 'modal header must expose the dismiss button');
|
||||
set_check(strpos($modal, 'name="action" value="aksrt_frontend_set_save"') !== false, 'modal must post aksrt_frontend_set_save');
|
||||
set_check(strpos($modal, 'name="taxonomy" value=""') !== false, 'modal must post the taxonomy field');
|
||||
set_check(strpos($modal, 'name="post_type" value="post"') !== false, 'modal must post the post_type field');
|
||||
set_check(strpos($modal, 'name="id" value="12"') !== false, 'modal must post the object id');
|
||||
set_check(strpos($modal, 'customize.php') !== false, '模块布局 must target the customizer');
|
||||
set_check(strpos($modal, 'wp-ajax-submit') !== false, 'modal footer must submit via wp-ajax-submit');
|
||||
set_check(strpos($modal, 'but jb-blue padding-lg') !== false, 'submit button must keep reference classes');
|
||||
set_check(strpos($modal, 'but c-yellow padding-lg') !== false, '后台编辑 must keep reference classes');
|
||||
set_check(strpos($modal, 'but c-yellow-2 padding-lg') !== false, '模块布局 must keep reference classes');
|
||||
set_check(strpos($modal, 'col-xs-3 text-right') !== false, 'inputs must use the reference heading column');
|
||||
set_check(strpos($modal, 'col-xs-8') !== false, 'inputs must use the reference option column');
|
||||
set_check(strpos($modal, 'name="show_layout"') !== false, 'post inputs must include show_layout');
|
||||
set_check(strpos($modal, 'name="no_article-navs"') !== false, 'post inputs must include the 目录树 checkbox');
|
||||
set_check(strpos($modal, 'value="checkbox"') === false, 'checkbox inputs must render type="checkbox" not value="checkbox"');
|
||||
|
||||
// --- Save: the reference contract returns {msg, reload} outside any query context. ---
|
||||
$_POST = [
|
||||
'id' => '12',
|
||||
'type' => 'post',
|
||||
'post_title' => '新标题',
|
||||
'comments_open' => '',
|
||||
'show_layout' => 'no_sidebar',
|
||||
'subtitle' => '副标题',
|
||||
'views' => '42',
|
||||
'like' => '7',
|
||||
'no_article-navs' => '1',
|
||||
];
|
||||
try {
|
||||
(new PageAdminSet())->ajax_save();
|
||||
set_check(false, 'ajax_save must terminate the request');
|
||||
} catch (\RuntimeException $e) {
|
||||
set_check($e->getMessage() === '__sent__', 'ajax_save must terminate via wp_send_json');
|
||||
}
|
||||
|
||||
set_check(is_array($GLOBALS['aksrt_set_json']), 'save must send a JSON payload');
|
||||
set_check(($GLOBALS['aksrt_set_json']['msg'] ?? '') === '保存成功,正在刷新页面', 'save must reuse the reference success message');
|
||||
set_check(!empty($GLOBALS['aksrt_set_json']['reload']), 'save must ask the client to reload');
|
||||
set_check(($GLOBALS['aksrt_set_meta']['__post']['post_title'] ?? '') === '新标题', 'save must update the post title');
|
||||
set_check(($GLOBALS['aksrt_set_meta']['__post']['comment_status'] ?? '') === 'closed', 'save must close comments when empty');
|
||||
|
||||
$saved_meta = $GLOBALS['aksrt_set_meta']['aksrt_post_extension'] ?? [];
|
||||
set_check(($saved_meta['show_layout'] ?? '') === 'no_sidebar', 'save must persist show_layout');
|
||||
set_check(($saved_meta['subtitle'] ?? '') === '副标题', 'save must persist the subtitle');
|
||||
set_check(!empty($saved_meta['no_article-navs']), 'checked checkbox must persist as on');
|
||||
set_check(empty($saved_meta['article_maxheight_xz']), 'unchecked checkbox must persist as off');
|
||||
set_check((int) $GLOBALS['aksrt_set_meta']['aksrt_post_views'] === 42, 'save must persist the view count');
|
||||
set_check((int) $GLOBALS['aksrt_set_meta']['_aksrt_like_count'] === 7, 'save must persist the like count');
|
||||
|
||||
// --- Permission: the switch off must reject the save even with a valid object. ---
|
||||
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => false];
|
||||
$GLOBALS['aksrt_set_json'] = null;
|
||||
try {
|
||||
(new PageAdminSet())->ajax_save();
|
||||
} catch (\RuntimeException $e) {
|
||||
// expected
|
||||
}
|
||||
set_check(($GLOBALS['aksrt_set_json']['error'] ?? 0) === 1, 'disabled switch must reject the save');
|
||||
set_check(!empty($GLOBALS['aksrt_set_json']['ys']), 'rejection must carry the danger type for the legacy client');
|
||||
|
||||
// --- Term save path. ---
|
||||
\AKSRT\Settings\ExtensionRuntime::$settings = ['admin_frontend_set' => true];
|
||||
$GLOBALS['aksrt_set_json'] = null;
|
||||
$GLOBALS['aksrt_set_meta'] = [];
|
||||
$GLOBALS['aksrt_set_term'] = (object) ['term_id' => 5, 'taxonomy' => 'category', 'name' => '旧名称', 'description' => ''];
|
||||
$_POST = ['id' => '5', 'type' => 'tax', 'taxonomy' => 'category', 'term_name' => '新分类', 'term_description' => '描述'];
|
||||
try {
|
||||
(new PageAdminSet())->ajax_save();
|
||||
} catch (\RuntimeException $e) {
|
||||
// expected
|
||||
}
|
||||
set_check(($GLOBALS['aksrt_set_meta']['__term']['name'] ?? '') === '新分类', 'term save must update the name');
|
||||
set_check(($GLOBALS['aksrt_set_meta']['__term']['description'] ?? '') === '描述', 'term save must update the description');
|
||||
|
||||
// --- Page branch: 标题样式 + 模块布局 + 容器位置. ---
|
||||
$GLOBALS['aksrt_set_queried'] = 21;
|
||||
$GLOBALS['aksrt_set_is_single'] = false;
|
||||
$GLOBALS['aksrt_set_is_page'] = true;
|
||||
$GLOBALS['aksrt_set_post_type'] = 'page';
|
||||
$GLOBALS['aksrt_set_post'] = (object) ['ID' => 21, 'post_type' => 'page'];
|
||||
$GLOBALS['aksrt_set_json'] = null;
|
||||
$GLOBALS['aksrt_set_meta'] = [];
|
||||
$_POST = [
|
||||
'id' => '21',
|
||||
'type' => 'post',
|
||||
'post_title' => '页面标题',
|
||||
'page_header_style' => '2',
|
||||
'widgets_register' => '1',
|
||||
'widgets_register_container' => ['sidebar', 'top_fluid'],
|
||||
];
|
||||
try {
|
||||
(new PageAdminSet())->ajax_save();
|
||||
} catch (\RuntimeException $e) {
|
||||
// expected
|
||||
}
|
||||
set_check(($GLOBALS['aksrt_set_meta']['page_header_style'] ?? '') === '2', 'page save must persist the header style');
|
||||
set_check(($GLOBALS['aksrt_set_meta']['widgets_register'] ?? '') === '1', 'page save must enable the widget container');
|
||||
set_check(($GLOBALS['aksrt_set_meta']['widgets_register_container'] ?? []) === ['sidebar', 'top_fluid'], 'page save must persist container positions');
|
||||
|
||||
$GLOBALS['aksrt_set_json'] = null;
|
||||
$GLOBALS['aksrt_set_meta'] = [];
|
||||
$_POST = ['id' => '21', 'type' => 'post', 'post_title' => '页面标题', 'widgets_register' => '0'];
|
||||
try {
|
||||
(new PageAdminSet())->ajax_save();
|
||||
} catch (\RuntimeException $e) {
|
||||
// expected
|
||||
}
|
||||
set_check(($GLOBALS['aksrt_set_meta']['widgets_register'] ?? '') === '', 'closing the widget container must clear the flag');
|
||||
set_check(($GLOBALS['aksrt_set_meta']['widgets_register_container'] ?? ['x']) === [], 'closing the widget container must clear positions');
|
||||
|
||||
echo "Frontend page edit harness passed\n";
|
||||
}
|
||||
@@ -150,11 +150,11 @@ test('split upload ships a full chunk protocol: routes, merge pipeline, and clie
|
||||
assert.doesNotMatch(read('src/Core/ScriptsPart1c.php'), /is_split_upload|split_minimum_size/);
|
||||
});
|
||||
|
||||
test('frontend page edit ships gate, float button, modal, and whitelisted save', () => {
|
||||
test('frontend page edit ships gate, footer modal, and legacy ajax-save contract', () => {
|
||||
const pageAdminSet = read('src/Frontend/PageAdminSet.php');
|
||||
const pageAdminJs = read('assets/js/page-admin-set.js');
|
||||
const floatButtons = read('inc/helpers/aksrt-float-buttons.php');
|
||||
const bootstrap = read('functions.php');
|
||||
const mainJs = read('assets/js/runtime/main.js');
|
||||
|
||||
assert.match(bootstrap, /\\AKSRT\\Frontend\\PageAdminSet::get_instance\(\)->init\(\)/);
|
||||
assert.match(pageAdminSet, /public static function enabled\(\): bool/);
|
||||
@@ -163,27 +163,59 @@ test('frontend page edit ships gate, float button, modal, and whitelisted save',
|
||||
assert.match(pageAdminSet, /is_page_template\('templates\/pages\/page-submit\.php'\)/);
|
||||
assert.match(pageAdminSet, /is_page_template\('templates\/pages\/page-posts-navs\.php'\)/);
|
||||
|
||||
// Float bar gains the same extension point the reference theme hooks into.
|
||||
// Reference parity: the gear float button opens the footer-rendered bootstrap modal.
|
||||
assert.match(floatButtons, /apply_filters\('aksrt_float_right_buttons', \$btn, \$is_mobile\)/);
|
||||
assert.match(pageAdminSet, /add_filter\('aksrt_float_right_buttons', \[\$this, 'float_button'\], 10, 2\)/);
|
||||
assert.match(pageAdminSet, /data-aksrt-modal="remote"/);
|
||||
assert.match(pageAdminSet, /check_ajax_referer\('aksrt_frontend_set', '_wpnonce'\)/);
|
||||
assert.match(pageAdminSet, /add_action\('wp_footer', \[\$this, 'render_modal'\], 10\)/);
|
||||
assert.match(pageAdminSet, /data-toggle="modal" data-target="#modal_admin_set"/);
|
||||
assert.match(pageAdminSet, /class="modal fade" id="modal_admin_set" tabindex="-1" role="dialog" aria-hidden="false" win-ajax-replace="modal_admin_set"/);
|
||||
assert.match(pageAdminSet, /class="modal-content page-set-modal"/);
|
||||
assert.match(pageAdminSet, /wp_nonce_field\('aksrt_frontend_set', '_wpnonce', true, false\)/);
|
||||
// Reference hidden inputs: type + taxonomy + post_type + id travel with the submit.
|
||||
assert.match(pageAdminSet, /name="action" value="aksrt_frontend_set_save"/);
|
||||
assert.match(pageAdminSet, /name="type" value="' . esc_attr\(\$type\)/);
|
||||
assert.match(pageAdminSet, /name="taxonomy" value="' . esc_attr\(\$taxonomy\)/);
|
||||
assert.match(pageAdminSet, /name="post_type" value="' . esc_attr\(\$post_type\)/);
|
||||
// 模块布局 opens the customizer widgets panel and returns to the current URL.
|
||||
assert.match(pageAdminSet, /admin_url\('customize\.php'\)/);
|
||||
assert.match(pageAdminSet, /'autofocus' => \['panel' => 'widgets'\]/);
|
||||
|
||||
// Modal + save endpoints; save is whitelisted and preserves other meta keys.
|
||||
assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_modal', \[\$this, 'ajax_modal'\]\)/);
|
||||
assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_save', \[\$this, 'ajax_save'\]\)/);
|
||||
// Modal footer mirrors the reference: 模块布局 / 后台编辑 / 确认修改 (wp-ajax-submit).
|
||||
assert.match(pageAdminSet, /esc_html__\('模块布局', 'aksrt'\)/);
|
||||
assert.match(pageAdminSet, /esc_html__\('后台编辑', 'aksrt'\)/);
|
||||
assert.match(pageAdminSet, /esc_html__\('确认修改', 'aksrt'\)/);
|
||||
assert.match(pageAdminSet, /class="but c-yellow-2 padding-lg"/);
|
||||
assert.match(pageAdminSet, /class="but c-yellow padding-lg"/);
|
||||
assert.match(pageAdminSet, /class="but jb-blue padding-lg wp-ajax-submit"/);
|
||||
|
||||
// Fields replicate the reference input construct and map to AKSRT's post meta.
|
||||
assert.match(pageAdminSet, /private function render_inputs\(array \$inputs\): string/);
|
||||
assert.match(pageAdminSet, /class="heading col-xs-3 text-right"/);
|
||||
assert.match(pageAdminSet, /class="option col-xs-8"/);
|
||||
assert.match(pageAdminSet, /\\AKSRT\\Core\\PostMetaBox::META_KEY/);
|
||||
assert.match(pageAdminSet, /\\AKSRT\\Core\\DomainModel::POST_VIEW_COUNT_KEY/);
|
||||
assert.match(pageAdminSet, /update_post_meta\(\$post_id, '_aksrt_like_count', max\(0, \(int\) \$_POST\['like'\]\)\)/);
|
||||
assert.match(pageAdminSet, /wp_update_term\(\$term_id, \(string\) \$term->taxonomy, \$data\)/);
|
||||
assert.match(pageAdminSet, /set_post_format\(\$post_id, \$format === 'standard' \? '' : \$format\)/);
|
||||
// Page branch: 标题样式 + 模块布局(widgets_register) + 容器位置.
|
||||
assert.match(pageAdminSet, /esc_attr\(\(string\) \(\$meta\['id'\] \?\? ''\)\) \. '\[\]" id="'/);
|
||||
assert.match(pageAdminSet, /'page_header_style'/);
|
||||
assert.match(pageAdminSet, /update_post_meta\(\$post_id, 'widgets_register', \$register \? '1' : ''\)/);
|
||||
assert.match(pageAdminSet, /update_post_meta\(\$post_id, 'widgets_register_container', \$containers\)/);
|
||||
// Reference checkbox semantics: unchecked boxes never submit, so both are written unconditionally.
|
||||
assert.match(pageAdminSet, /\$meta\['no_article-navs'\] = !empty\(\$_POST\['no_article-navs'\]\)/);
|
||||
assert.match(pageAdminSet, /\$meta\['article_maxheight_xz'\] = !empty\(\$_POST\['article_maxheight_xz'\]\)/);
|
||||
// The geometry mirrors the reference construct (row + heading + option, \n separators).
|
||||
assert.match(pageAdminSet, /'<div class="mb10 row' \. esc_attr\(\$class\) \. '">' \. "\\n"/);
|
||||
|
||||
// Client submits through the shared modal runtime contract; the ajax action
|
||||
// comes from the form's hidden field that the modal endpoint embeds.
|
||||
assert.match(pageAdminJs, /form\.aksrt-admin-set-form/);
|
||||
assert.match(pageAdminJs, /input\[name="action"\]/);
|
||||
assert.match(pageAdminJs, /window\.location\.reload/);
|
||||
assert.match(pageAdminSet, /'html' => \$html/);
|
||||
// Save runs under the vendored main.js aksrt_ajax contract ({msg,reload}).
|
||||
assert.match(pageAdminSet, /add_action\('wp_ajax_aksrt_frontend_set_save', \[\$this, 'ajax_save'\]\)/);
|
||||
assert.match(pageAdminSet, /check_ajax_referer\('aksrt_frontend_set', '_wpnonce'\)/);
|
||||
assert.match(pageAdminSet, /wp_send_json\(\['msg' => __\('保存成功,正在刷新页面', 'aksrt'\), 'reload' => true\]\)/);
|
||||
assert.match(mainJs, /jQuery\.fn\.serializeObject = function/);
|
||||
assert.match(mainJs, /if \(n\.reload\) \{/);
|
||||
// The dedicated fetch-submit client is gone: vendored main.js owns the flow.
|
||||
assert.equal(fs.existsSync(path.join(root, 'assets/js/page-admin-set.js')), false);
|
||||
});
|
||||
|
||||
test('float toolbar config reaches the tinymce plugin with reference-context buttons', () => {
|
||||
@@ -227,3 +259,9 @@ test('split upload runtime contract harness passes', () => {
|
||||
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||
assert.match(output, /Split upload harness passed/);
|
||||
});
|
||||
|
||||
test('frontend page edit runtime contract harness passes', () => {
|
||||
const harness = path.join(root, 'tests', 'widget-system', 'frontend-page-edit-harness.php');
|
||||
const output = execFileSync('php', [harness], { encoding: 'utf8' });
|
||||
assert.match(output, /Frontend page edit harness passed/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user