Files
wordpress-theme-aksrt/src/Frontend/PageAdminSet.php
T

526 lines
21 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_footer', [self::class, 'enqueue_hint_script'], 9);
add_action('wp_ajax_aksrt_frontend_set_save', [$this, 'ajax_save']);
}
/**
* 提示横条的自动淡出(对齐参考主题 js/page-edit.js)。
*
* 参考主题在 2 秒后把 .admin-set-info 淡出 1 秒,避免提示常驻遮挡悬浮栏。
* 挂在 wp_footer 早期:wp_print_footer_scripts 在此之前执行,脚本仍能入队。
*/
public static function enqueue_hint_script(): void {
if (!self::enabled()) {
return;
}
$path = get_template_directory() . '/assets/js/front-page-admin-set.js';
if (!file_exists($path)) {
return;
}
wp_enqueue_script(
'aksrt-front-page-admin-set',
get_template_directory_uri() . '/assets/js/front-page-admin-set.js',
['jquery'],
(string) filemtime($path),
true
);
}
/** 开启条件:设置开关 + 管理员 + 单页/术语页 + 有效对象(排除投稿与导航页模板)。 */
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 fas 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 fas fa-sliders-h 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 fas fa-chart-pie" 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 fab fa-wordpress" aria-hidden="true"></i>' . esc_html__('后台编辑', 'aksrt') . '</a>' : '')
. '<button class="but jb-blue padding-lg wp-ajax-submit"><i class="fa fas 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);
}
/**
* 文章格式选项(1:1 对齐参考主题的固定列表)。
*
* 参考主题把标准/图像/画廊/视频四个格式写死在弹窗里,不读
* get_theme_support('post-formats')。本站注册的格式比参考主题多
* (aside/quote/link 等),动态枚举会让选项集偏离参考主题,
* 因此同样固定这四项。
*/
private function post_format_options(): array {
return [
'standard' => __('标准', 'aksrt'),
'image' => __('图像', 'aksrt'),
'gallery' => __('画廊', 'aksrt'),
'video' => __('视频', 'aksrt'),
];
}
/** 分类术语的弹窗字段(对齐参考主题 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' => [],
]);
}
}