Files
wordpress-theme-aksrt/src/Frontend/PageAdminSet.php
T
kate522 12f60897d9 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 覆盖门禁、渲染与各保存分支。
2026-10-06 12:22:43 +08:00

494 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 前端页面编辑(后台「扩展&增强 → 系统工具」的 admin_frontend_set)。
*
* 对齐参考主题 zib-frontend-set.php:wp_footer 直出 #modal_admin_set 弹窗
* (bootstrap modal + wp-ajax-submit 走 vendored main.js 的 aksrt_ajax 契约),
* 管理员在前台快速修改当前文章/页面的参数或分类术语的名称与描述。
*
* @package AKSRT
*/
namespace AKSRT\Frontend;
defined('ABSPATH') || exit;
class PageAdminSet {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_filter('aksrt_float_right_buttons', [$this, 'float_button'], 10, 2);
add_action('wp_footer', [$this, 'render_modal'], 10);
add_action('wp_ajax_aksrt_frontend_set_save', [$this, 'ajax_save']);
}
/** 开启条件:设置开关 + 管理员 + 单页/术语页 + 有效对象(排除投稿与导航页模板)。 */
public static function enabled(): bool {
if (!class_exists('\AKSRT\Settings\ExtensionRuntime') || !is_user_logged_in()) {
return false;
}
$settings = \AKSRT\Settings\ExtensionRuntime::get_system_tools_settings();
if (empty($settings['admin_frontend_set']) || !current_user_can('manage_options')) {
return false;
}
if (!get_queried_object_id() || (!is_single() && !is_page() && !is_category() && !is_tag() && !is_tax())) {
return false;
}
if (is_page_template('templates/pages/page-submit.php') || is_page_template('templates/pages/page-posts-navs.php')) {
return false;
}
return true;
}
/**
* 悬浮按钮:追加到右侧悬浮按钮栏(对齐参考主题 zib_admin_frontend_set_botton)。
*
* 参考主题对移动端与桌面端共用同一按钮,$is_mobile 仅用于保持 filter 签名。
*/
public function float_button($buttons, $is_mobile = null) {
if (!self::enabled()) {
return $buttons;
}
$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>';
return (string) $buttons . $button;
}
/** wp_footer 输出 #modal_admin_set(对齐参考主题 zib_frontend_set_modal)。 */
public function render_modal(): void {
if (!self::enabled()) {
return;
}
$object_id = (int) get_queried_object_id();
if (is_category() || is_tag() || is_tax()) {
$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 {
$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;
}
// 对齐参考主题 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 -- 各片段已转义
}
/** 关闭按钮图标(本站 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');
$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' => 1, 'ys' => 'danger', 'msg' => __('页面数据出错', 'aksrt')]);
}
$type = isset($_POST['type']) ? sanitize_key((string) $_POST['type']) : '';
if ($type === 'tax') {
$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(['msg' => __('保存成功,正在刷新页面', 'aksrt'), 'reload' => true]);
}
private function save_post(int $post_id): void {
$post = get_post($post_id);
if (!$post) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('未找到可用post数据', 'aksrt')]);
}
if (!current_user_can('edit_post', $post_id)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('权限不足', 'aksrt')]);
}
$postarr = ['ID' => $post_id];
if (isset($_POST['post_title'])) {
$title = sanitize_text_field(wp_unslash((string) $_POST['post_title']));
if ($title !== '') {
$postarr['post_title'] = $title;
}
}
if (isset($_POST['comments_open'])) {
$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' => 1, 'ys' => 'danger', 'msg' => $result->get_error_message()]);
}
}
if (isset($_POST['post_format']) && post_type_supports($post->post_type, 'post-formats')) {
$format = sanitize_key((string) wp_unslash($_POST['post_format']));
set_post_format($post_id, $format === 'standard' ? '' : $format);
}
if ($post->post_type === 'post' && class_exists('\AKSRT\Core\PostMetaBox')) {
// 保留白名单之外的既有配置键(封面、视频等),仅覆盖弹窗提交的字段。
// 复选框未勾选时浏览器不会提交键名,两个开关必须无条件写入。
$meta = get_post_meta($post_id, \AKSRT\Core\PostMetaBox::META_KEY, true);
$meta = is_array($meta) ? $meta : [];
$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);
if (isset($_POST['views'])) {
update_post_meta($post_id, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, max(0, (int) $_POST['views']));
}
if (isset($_POST['like'])) {
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' => 1, 'ys' => 'danger', 'msg' => __('未找到可用term数据', 'aksrt')]);
}
if (!current_user_can('manage_categories') && !current_user_can('manage_terms')) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('权限不足', 'aksrt')]);
}
$data = [];
if (isset($_POST['term_name'])) {
$name = sanitize_text_field(wp_unslash((string) $_POST['term_name']));
if ($name !== '') {
$data['name'] = $name;
}
}
if (isset($_POST['term_description'])) {
$data['description'] = sanitize_textarea_field(wp_unslash((string) $_POST['term_description']));
}
if (!$data) {
return;
}
$result = wp_update_term($term_id, (string) $term->taxonomy, $data);
if (is_wp_error($result)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => $result->get_error_message()]);
}
}
/** 文章/页面的弹窗字段(对齐参考主题 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)
: [];
$inputs = [];
$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'),
],
];
$inputs[] = [
'name' => __('标题', 'aksrt'),
'id' => 'post_title',
'std' => (string) get_the_title($post_id),
'type' => 'text',
];
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'),
];
}
$inputs[] = [
'name' => __('评论', 'aksrt'),
'id' => 'comments_open',
'std' => comments_open($post_id) ? 'open' : '',
'type' => 'radio',
'options' => [
'open' => __('开启', 'aksrt'),
'' => __('关闭', 'aksrt'),
],
];
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->render_inputs($inputs);
}
/** 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 '<p class="muted-3-color">' . esc_html__('未找到可用term数据', 'aksrt') . '</p>';
}
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',
],
]);
}
/** 表单控件构建(对齐参考主题 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;
}
/**
* 描述文案:允许 <br>、强调标签与来源可信的主题文案(对齐参考主题的富文本描述)。
*
* @param string $desc 内部生成的描述文案。
*/
private function rich_desc(string $desc): string {
return wp_kses($desc, [
'br' => [],
'div' => ['class' => []],
'span' => ['class' => []],
'em' => [],
'strong' => [],
'b' => [],
'i' => [],
]);
}
}