Files
wordpress-theme-aksrt/src/Core/PostMetaBox.php
T
2026-08-26 16:32:18 +08:00

316 lines
16 KiB
PHP

<?php
/**
* Article extension fields shared by the block and classic editors.
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class PostMetaBox {
private static $instance = null;
public const META_KEY = DomainModel::POST_META_KEY;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('add_meta_boxes', [$this, 'register_meta_boxes']);
add_action('save_post_post', [$this, 'save_meta_box'], 10, 2);
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
}
/** @return array<string, mixed> */
public static function get_meta(int $post_id): array {
$saved = get_post_meta($post_id, self::META_KEY, true);
$defaults = DomainModel::default_post_meta();
$meta = array_merge($defaults, is_array($saved) ? array_intersect_key($saved, $defaults) : []);
$meta['views'] = max(0, (int) get_post_meta($post_id, DomainModel::POST_VIEW_COUNT_KEY, true));
$meta['like'] = max(0, (int) get_post_meta($post_id, '_aksrt_like_count', true));
$meta['featured_video_episode'] = self::sanitize_video_episodes($meta['featured_video_episode'] ?? []);
return $meta;
}
public static function get_meta_value(int $post_id, string $key, $default = '') {
$meta = self::get_meta($post_id);
return array_key_exists($key, $meta) ? $meta[$key] : $default;
}
/**
* @param mixed $raw Raw episode rows.
* @return array<int, array{title:string,url:string}>
*/
public static function sanitize_video_episodes($raw): array {
if (!is_array($raw)) {
return [];
}
$episodes = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url === '') {
continue;
}
$episodes[] = [
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
'url' => $url,
];
}
return array_slice($episodes, 0, 50);
}
public function register_meta_boxes(): void {
add_meta_box('post_main', __('文章扩展', 'aksrt'), [$this, 'render_meta_box'], 'post', 'side', 'high');
$settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_post_filter_settings()
: [];
if (!empty($settings['custom_filter'])) {
add_meta_box(
'aksrt_custom_filter_meta',
__('高级自定义筛选', 'aksrt'),
[$this, 'render_custom_filter_meta_box'],
'post',
'side',
'default'
);
}
}
public function enqueue_assets(string $hook): void {
if (!in_array($hook, ['post.php', 'post-new.php'], true)) {
return;
}
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->post_type !== 'post') {
return;
}
wp_enqueue_media();
$css = AKSRT_DIR . '/assets/css/admin/post-extension.css';
$js = AKSRT_DIR . '/assets/js/admin-post-extension.js';
if (is_file($css)) {
wp_enqueue_style('aksrt-post-extension', AKSRT_URI . '/assets/css/admin/post-extension.css', [], (string) filemtime($css));
}
if (is_file($js)) {
wp_enqueue_script('aksrt-post-extension', AKSRT_URI . '/assets/js/admin-post-extension.js', ['jquery'], (string) filemtime($js), true);
}
}
public function render_meta_box(\WP_Post $post): void {
$meta = self::get_meta($post->ID);
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$show_image = !array_key_exists('post_article_image_cover', $settings) || !empty($settings['post_article_image_cover']);
$show_slide = !array_key_exists('list_thumb_slides_s', $settings)
|| !empty($settings['list_thumb_slides_s'])
|| !empty($settings['post_article_slide_cover']);
$show_video = !array_key_exists('list_thumb_video_s', $settings)
|| !empty($settings['list_thumb_video_s'])
|| !empty($settings['post_article_video_cover']);
wp_nonce_field('aksrt_post_extension_save', 'aksrt_post_extension_nonce');
?>
<div class="aksrt-post-extension csf-fields" data-aksrt-post-extension>
<?php
$this->render_upload_field(
'thumbnail_url',
__('外链特色图像', 'aksrt'),
(string) $meta['thumbnail_url'],
'image',
__('支持直接输入链接用作特色图像', 'aksrt') . '<br><span class="aksrt-post-extension__danger">' . __('此处设置仅在未设置 WordPress 特色图片时有效', 'aksrt') . '</span>'
);
if ($show_image) {
$this->render_upload_field('cover_image', __('封面图', 'aksrt'), (string) $meta['cover_image'], 'image', __('在文章页顶部显示封面图', 'aksrt'));
}
?>
<?php if ($show_slide) : ?>
<div class="aksrt-post-extension__field">
<div class="aksrt-post-extension__title"><label for="aksrt-featured-slide"><?php esc_html_e('特色幻灯片', 'aksrt'); ?></label></div>
<div class="aksrt-post-extension__control">
<input id="aksrt-featured-slide" type="hidden" name="aksrt_post_extension[featured_slide]" value="<?php echo esc_attr((string) $meta['featured_slide']); ?>" data-gallery-input>
<div class="aksrt-post-extension__gallery" data-gallery-preview></div>
<div class="aksrt-post-extension__buttons">
<button type="button" class="button" data-gallery-select><?php esc_html_e('添加图像', 'aksrt'); ?></button>
<button type="button" class="button-link-delete" data-gallery-clear><?php esc_html_e('清空图像', 'aksrt'); ?></button>
</div>
<p class="description"><?php esc_html_e('为该文章显示幻灯片封面或幻灯片缩略图(优先级 > 特色图像及封面图)', 'aksrt'); ?></p>
</div>
</div>
<?php endif; ?>
<?php if ($show_video) : ?>
<?php $this->render_upload_field('featured_video', __('特色视频', 'aksrt'), (string) $meta['featured_video'], 'video', __('为该文章显示视频封面(优先级 > 幻灯片封面)', 'aksrt'), __('选择视频或填写视频地址', 'aksrt')); ?>
<div class="aksrt-post-extension__field compact" data-video-dependent>
<div class="aksrt-post-extension__title"><span><?php esc_html_e('本集标题', 'aksrt'); ?></span></div>
<div class="aksrt-post-extension__control">
<input type="text" name="aksrt_post_extension[featured_video_title]" value="<?php echo esc_attr((string) $meta['featured_video_title']); ?>" placeholder="<?php esc_attr_e('第1集', 'aksrt'); ?>">
<p class="description"><?php esc_html_e('如需添加剧集则需填写此处', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-post-extension__field compact" data-video-dependent>
<div class="aksrt-post-extension__title"><span><?php esc_html_e('视频剧集', 'aksrt'); ?></span></div>
<div class="aksrt-post-extension__control">
<p class="description"><?php esc_html_e('为视频封面添加更多剧集', 'aksrt'); ?></p>
<div data-episode-list>
<?php foreach ((array) $meta['featured_video_episode'] as $index => $episode) : ?>
<?php $this->render_episode_row((int) $index, (array) $episode); ?>
<?php endforeach; ?>
</div>
<button type="button" class="button" data-episode-add><?php esc_html_e('添加剧集', 'aksrt'); ?></button>
<template data-episode-template><?php $this->render_episode_row('__INDEX__', []); ?></template>
</div>
</div>
<?php endif; ?>
<?php $this->render_text_field('subtitle', __('副标题', 'aksrt'), (string) $meta['subtitle']); ?>
<?php $this->render_number_field('views', __('阅读量', 'aksrt'), (int) $meta['views']); ?>
<?php $this->render_number_field('like', __('点赞数', 'aksrt'), (int) $meta['like']); ?>
<fieldset class="aksrt-post-extension__field">
<legend class="aksrt-post-extension__title"><?php esc_html_e('显示布局', 'aksrt'); ?></legend>
<div class="aksrt-post-extension__control aksrt-post-extension__radios">
<?php
$layouts = [
'false' => __('跟随主题', 'aksrt'),
'no_sidebar' => __('无侧边栏', 'aksrt'),
'sidebar_left' => __('侧边栏靠左', 'aksrt'),
'sidebar_right' => __('侧边栏靠右', 'aksrt'),
];
foreach ($layouts as $value => $label) :
?>
<label><input type="radio" name="aksrt_post_extension[show_layout]" value="<?php echo esc_attr($value); ?>" <?php checked((string) $meta['show_layout'], $value); ?>> <?php echo esc_html($label); ?></label>
<?php endforeach; ?>
</div>
</fieldset>
<div class="aksrt-post-extension__field aksrt-post-extension__field--check"><div class="aksrt-post-extension__control"><label><input type="checkbox" name="aksrt_post_extension[no_article-navs]" value="1" <?php checked(!empty($meta['no_article-navs'])); ?>> <?php esc_html_e('不显示目录树', 'aksrt'); ?></label></div></div>
<div class="aksrt-post-extension__field aksrt-post-extension__field--check"><div class="aksrt-post-extension__control"><label><input type="checkbox" name="aksrt_post_extension[article_maxheight_xz]" value="1" <?php checked(!empty($meta['article_maxheight_xz'])); ?>> <?php esc_html_e('限制内容最大高度', 'aksrt'); ?></label></div></div>
</div>
<?php
}
private function render_upload_field(string $id, string $title, string $value, string $library, string $description, string $placeholder = ''): void {
?>
<div class="aksrt-post-extension__field">
<div class="aksrt-post-extension__title"><label for="aksrt-post-<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></label></div>
<div class="aksrt-post-extension__control">
<div class="aksrt-post-extension__upload">
<input id="aksrt-post-<?php echo esc_attr($id); ?>" type="url" name="aksrt_post_extension[<?php echo esc_attr($id); ?>]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr($placeholder); ?>" data-media-input>
<button type="button" class="button" data-media-select data-library="<?php echo esc_attr($library); ?>"><?php esc_html_e('选择', 'aksrt'); ?></button>
<button type="button" class="button-link-delete" data-media-clear><?php esc_html_e('移除', 'aksrt'); ?></button>
</div>
<p class="description"><?php echo wp_kses_post($description); ?></p>
</div>
</div>
<?php
}
private function render_text_field(string $id, string $title, string $value): void {
?>
<div class="aksrt-post-extension__field"><div class="aksrt-post-extension__title"><label for="aksrt-post-<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></label></div><div class="aksrt-post-extension__control"><input id="aksrt-post-<?php echo esc_attr($id); ?>" type="text" name="aksrt_post_extension[<?php echo esc_attr($id); ?>]" value="<?php echo esc_attr($value); ?>"></div></div>
<?php
}
private function render_number_field(string $id, string $title, int $value): void {
?>
<div class="aksrt-post-extension__field"><div class="aksrt-post-extension__title"><label for="aksrt-post-<?php echo esc_attr($id); ?>"><?php echo esc_html($title); ?></label></div><div class="aksrt-post-extension__control"><input id="aksrt-post-<?php echo esc_attr($id); ?>" type="number" min="0" step="1" name="aksrt_post_extension[<?php echo esc_attr($id); ?>]" value="<?php echo esc_attr((string) $value); ?>"></div></div>
<?php
}
/** @param int|string $index */
private function render_episode_row($index, array $episode): void {
?>
<div class="aksrt-post-extension__episode" data-episode-row>
<input type="text" name="aksrt_post_extension[featured_video_episode][<?php echo esc_attr((string) $index); ?>][title]" value="<?php echo esc_attr((string) ($episode['title'] ?? '')); ?>" placeholder="<?php esc_attr_e('剧集标题', 'aksrt'); ?>">
<div class="aksrt-post-extension__upload"><input type="url" name="aksrt_post_extension[featured_video_episode][<?php echo esc_attr((string) $index); ?>][url]" value="<?php echo esc_attr((string) ($episode['url'] ?? '')); ?>" placeholder="<?php esc_attr_e('选择视频或填写视频地址', 'aksrt'); ?>" data-media-input><button type="button" class="button" data-media-select data-library="video"><?php esc_html_e('选择', 'aksrt'); ?></button></div>
<button type="button" class="button-link-delete" data-episode-remove><?php esc_html_e('移除', 'aksrt'); ?></button>
</div>
<?php
}
public function render_custom_filter_meta_box(\WP_Post $post): void {
wp_nonce_field('aksrt_custom_filter_save', 'aksrt_custom_filter_nonce');
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
foreach ($dimensions as $dimension) {
$key = sanitize_key((string) ($dimension['key'] ?? ''));
$name = sanitize_text_field((string) ($dimension['name'] ?? $key));
$vals = is_array($dimension['vals'] ?? null) ? $dimension['vals'] : [];
if ($key === '') {
continue;
}
$current = (array) get_post_meta($post->ID, '_aksrt_cf_' . $key, true);
echo '<div class="aksrt-custom-filter-field"><strong>' . esc_html($name) . '</strong>';
foreach ($vals as $value) {
$value_key = sanitize_key((string) ($value['key'] ?? ''));
$value_name = sanitize_text_field((string) ($value['name'] ?? $value_key));
if ($value_key === '') {
continue;
}
printf('<label><input type="checkbox" name="aksrt_custom_filter[%s][]" value="%s"%s> %s</label>', esc_attr($key), esc_attr($value_key), checked(in_array($value_key, array_map('strval', $current), true), true, false), esc_html($value_name));
}
echo '</div>';
}
}
public function save_meta_box(int $post_id, \WP_Post $post): void {
if ($post->post_type !== 'post' || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) || wp_is_post_revision($post_id) || !current_user_can('edit_post', $post_id)) {
return;
}
if (isset($_POST['aksrt_post_extension_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_post_extension_nonce'])), 'aksrt_post_extension_save')) {
$raw = isset($_POST['aksrt_post_extension']) && is_array($_POST['aksrt_post_extension']) ? wp_unslash($_POST['aksrt_post_extension']) : [];
$layout = sanitize_key((string) ($raw['show_layout'] ?? 'false'));
if (!in_array($layout, ['false', 'no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
$layout = 'false';
}
$slide_ids = array_values(array_filter(array_map('absint', preg_split('/[\s,]+/', (string) ($raw['featured_slide'] ?? '')) ?: [])));
$meta = [
'thumbnail_url' => esc_url_raw((string) ($raw['thumbnail_url'] ?? '')),
'cover_image' => esc_url_raw((string) ($raw['cover_image'] ?? '')),
'featured_slide' => implode(',', array_slice($slide_ids, 0, 50)),
'featured_video' => esc_url_raw((string) ($raw['featured_video'] ?? '')),
'featured_video_title' => sanitize_text_field((string) ($raw['featured_video_title'] ?? '')),
'featured_video_episode' => self::sanitize_video_episodes($raw['featured_video_episode'] ?? []),
'subtitle' => sanitize_text_field((string) ($raw['subtitle'] ?? '')),
'show_layout' => $layout,
'no_article-navs' => !empty($raw['no_article-navs']),
'article_maxheight_xz' => !empty($raw['article_maxheight_xz']),
];
update_post_meta($post_id, self::META_KEY, $meta);
update_post_meta($post_id, DomainModel::POST_VIEW_COUNT_KEY, max(0, (int) ($raw['views'] ?? 0)));
update_post_meta($post_id, '_aksrt_like_count', max(0, (int) ($raw['like'] ?? 0)));
}
if (isset($_POST['aksrt_custom_filter_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_custom_filter_nonce'])), 'aksrt_custom_filter_save')) {
$settings = \AKSRT\Settings\Settings::get_post_filter_settings();
$dimensions = is_array($settings['custom_filter'] ?? null) ? $settings['custom_filter'] : [];
$submitted = isset($_POST['aksrt_custom_filter']) && is_array($_POST['aksrt_custom_filter']) ? wp_unslash($_POST['aksrt_custom_filter']) : [];
foreach ($dimensions as $dimension) {
$key = sanitize_key((string) ($dimension['key'] ?? ''));
if ($key === '') {
continue;
}
$allowed = array_values(array_filter(array_map(static fn($value): string => sanitize_key((string) ($value['key'] ?? '')), (array) ($dimension['vals'] ?? []))));
$values = array_values(array_intersect($allowed, array_map('sanitize_key', (array) ($submitted[$key] ?? []))));
if ($values) {
update_post_meta($post_id, '_aksrt_cf_' . $key, $values);
} else {
delete_post_meta($post_id, '_aksrt_cf_' . $key);
}
}
}
}
}