refactor: flatten src/AKSRT → src (drop theme-identifier dir)

Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
2026-08-02 22:06:06 +08:00
parent 56f1a8175f
commit 8c5b1555c9
285 changed files with 26 additions and 3600 deletions
+858
View File
@@ -0,0 +1,858 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class PostMetaBox {
private static $instance = null;
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_box']);
add_action('save_post', [$this, 'save_meta_box']);
// §3.8 Quick / Bulk edit
add_filter('manage_post_posts_columns', [$this, 'add_columns']);
add_action('manage_post_posts_custom_column', [$this, 'render_column'], 10, 2);
add_action('quick_edit_custom_box', [$this, 'render_quick_edit'], 10, 2);
add_action('bulk_edit_custom_box', [$this, 'render_quick_edit'], 10, 2);
add_action('admin_footer-edit.php', [$this, 'admin_quick_edit_script']);
// Pay-type dropdown on posts list (zibll zibpay_type filter).
add_action('restrict_manage_posts', [$this, 'render_pay_type_filter']);
add_action('pre_get_posts', [$this, 'filter_posts_by_pay_type']);
}
/**
* Get post meta with defaults.
*/
public static function get_meta(int $post_id): array {
$defaults = DomainModel::default_post_meta();
$saved = get_post_meta($post_id, self::META_KEY, true);
if (!is_array($saved)) {
$saved = [];
}
$meta = array_merge($defaults, $saved);
// New route only: episodes is always a list of {url,title}.
$meta['featured_video_episode'] = self::sanitize_video_episodes($meta['featured_video_episode'] ?? []);
if (!in_array((string) ($meta['show_toc'] ?? ''), ['', '0', '1'], true)) {
$meta['show_toc'] = '';
}
return $meta;
}
/**
* Sanitize video episode list (new route: array of url/title rows).
*
* @param mixed $raw Raw form / meta value.
* @return array<int, array{url:string,title:string}>
*/
public static function sanitize_video_episodes($raw): array {
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
$url = esc_url_raw((string) ($row['url'] ?? ''));
if ($url === '') {
continue;
}
$out[] = [
'url' => $url,
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
];
if (count($out) >= 50) {
break;
}
}
return $out;
}
/**
* Get a single post meta value.
*/
public static function get_meta_value(int $post_id, string $key, $default = '') {
$meta = self::get_meta($post_id);
return $meta[$key] ?? $default;
}
/**
* Register the meta box.
*/
public function register_meta_box(): void {
add_meta_box(
'aksrt_post_meta',
__('AKSRT 文章设置', 'aksrt'),
[$this, 'render_meta_box'],
'post',
'side',
'default'
);
add_meta_box(
'aksrt_page_meta',
__('AKSRT 页面设置', 'aksrt'),
[$this, 'render_page_meta_box'],
'page',
'side',
'default'
);
// §3.8 custom filter metabox (side)
$filter_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_post_filter_settings()
: ['custom_filter' => []];
if (!empty($filter_settings['custom_filter'])) {
add_meta_box(
'aksrt_custom_filter_meta',
__('高级筛选', 'aksrt'),
[$this, 'render_custom_filter_meta_box'],
'post',
'side',
'default'
);
}
}
/**
* Render the page meta box (page_header_style / cover override).
*/
public function render_page_meta_box(\WP_Post $post): void {
$header_style = get_post_meta($post->ID, 'page_header_style', true);
$cover_img = get_post_meta($post->ID, 'page_header_cover_img', true);
wp_nonce_field('aksrt_page_meta', 'aksrt_page_meta_nonce');
?>
<p><strong><?php esc_html_e('页面标题样式覆盖', 'aksrt'); ?></strong></p>
<select name="page_header_style" class="widefat">
<option value="" <?php selected($header_style, ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="not" <?php selected($header_style, 'not'); ?>><?php esc_html_e('不显示', 'aksrt'); ?></option>
<option value="1" <?php selected($header_style, '1'); ?>><?php esc_html_e('简单', 'aksrt'); ?></option>
<option value="2" <?php selected($header_style, '2'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="3" <?php selected($header_style, '3'); ?>><?php esc_html_e('封面', 'aksrt'); ?></option>
</select>
<small class="description"><?php esc_html_e('覆盖全局「其它页面 → 统一页头」设置。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('页面封面图覆盖', 'aksrt'); ?></strong></p>
<input type="text" name="page_header_cover_img" value="<?php echo esc_attr($cover_img); ?>" class="widefat" placeholder="<?php esc_attr_e('图片 URL', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('覆盖全局默认封面图,仅封面样式(样式3)生效。', 'aksrt'); ?></small>
<?php
}
/**
* Render the meta box.
*/
public function render_meta_box(\WP_Post $post): void {
$meta = self::get_meta($post->ID);
wp_nonce_field('aksrt_post_meta', 'aksrt_post_meta_nonce');
?>
<p><strong><?php esc_html_e('附加封面图', 'aksrt'); ?></strong></p>
<p>
<input type="text" name="aksrt_post_meta[cover_images]" value="<?php echo esc_attr($meta['cover_images']); ?>" class="widefat" placeholder="<?php esc_attr_e('附件ID,逗号分隔', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('用于列表多图模式,填写附件 ID(逗号分隔)。', 'aksrt'); ?></small>
</p>
<p><strong><?php esc_html_e('列表布局覆盖', 'aksrt'); ?></strong></p>
<select name="aksrt_post_meta[layout_override]" class="widefat">
<option value="" <?php selected($meta['layout_override'], ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="card" <?php selected($meta['layout_override'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="list" <?php selected($meta['layout_override'], 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="mini" <?php selected($meta['layout_override'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
</select>
<p><strong><?php esc_html_e('侧栏布局(Zibll show_layout)', 'aksrt'); ?></strong></p>
<?php
$show_layout = (string) get_post_meta($post->ID, '_aksrt_show_layout', true);
if (!in_array($show_layout, ['no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
$show_layout = '';
}
?>
<select name="aksrt_show_layout" class="widefat">
<option value="" <?php selected($show_layout, ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="no_sidebar" <?php selected($show_layout, 'no_sidebar'); ?>><?php esc_html_e('无侧栏', 'aksrt'); ?></option>
<option value="sidebar_right" <?php selected($show_layout, 'sidebar_right'); ?>><?php esc_html_e('右侧栏', 'aksrt'); ?></option>
<option value="sidebar_left" <?php selected($show_layout, 'sidebar_left'); ?>><?php esc_html_e('左侧栏', 'aksrt'); ?></option>
</select>
<p class="description"><?php esc_html_e('覆盖全局侧栏开关与左右位置,仅影响本文/本页。', 'aksrt'); ?></p>
<p><strong><?php esc_html_e('头图模式', 'aksrt'); ?></strong></p>
<select name="aksrt_post_meta[header_image_mode]" class="widefat">
<option value="" <?php selected($meta['header_image_mode'], ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="featured" <?php selected($meta['header_image_mode'], 'featured'); ?>><?php esc_html_e('特色图片', 'aksrt'); ?></option>
<option value="auto" <?php selected($meta['header_image_mode'], 'auto'); ?>><?php esc_html_e('自动(内容首图)', 'aksrt'); ?></option>
<option value="none" <?php selected($meta['header_image_mode'], 'none'); ?>><?php esc_html_e('不显示', 'aksrt'); ?></option>
</select>
<p>
<label for="aksrt_show_toc"><strong><?php esc_html_e('目录树', 'aksrt'); ?></strong></label>
<select name="aksrt_post_meta[show_toc]" id="aksrt_show_toc" class="widefat">
<option value="" <?php selected((string) ($meta['show_toc'] ?? ''), ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="1" <?php selected((string) ($meta['show_toc'] ?? ''), '1'); ?>><?php esc_html_e('强制开启', 'aksrt'); ?></option>
<option value="0" <?php selected((string) ($meta['show_toc'] ?? ''), '0'); ?>><?php esc_html_e('强制关闭', 'aksrt'); ?></option>
</select>
</p>
<p><strong><?php esc_html_e('单篇交互覆盖', 'aksrt'); ?></strong></p>
<?php
$toggle_fields = [
'show_reading_progress' => __('阅读进度条', 'aksrt'),
'enable_lightbox' => __('图片灯箱', 'aksrt'),
'show_related' => __('相关推荐', 'aksrt'),
'enable_like' => __('点赞按钮', 'aksrt'),
'enable_favorite' => __('收藏按钮', 'aksrt'),
'enable_share' => __('分享按钮', 'aksrt'),
'enable_print' => __('打印按钮', 'aksrt'),
'enable_pdf' => __('PDF 导出按钮', 'aksrt'),
];
foreach ($toggle_fields as $field_key => $field_label) :
?>
<p>
<label for="aksrt_post_meta_<?php echo esc_attr($field_key); ?>" class="screen-reader-text"><?php echo esc_html($field_label); ?></label>
<select name="aksrt_post_meta[<?php echo esc_attr($field_key); ?>]" id="aksrt_post_meta_<?php echo esc_attr($field_key); ?>" class="widefat">
<option value="" <?php selected($meta[$field_key], ''); ?>><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="1" <?php selected($meta[$field_key], '1'); ?>><?php esc_html_e('强制开启', 'aksrt'); ?></option>
<option value="0" <?php selected($meta[$field_key], '0'); ?>><?php esc_html_e('强制关闭', 'aksrt'); ?></option>
</select>
<small class="description"><?php echo esc_html($field_label); ?></small>
</p>
<?php endforeach; ?>
<p><strong><?php esc_html_e('相关推荐数量覆盖', 'aksrt'); ?></strong></p>
<p>
<input type="number" name="aksrt_post_meta[related_count]" value="<?php echo esc_attr((string) (int) $meta['related_count']); ?>" class="widefat" min="0" max="20" step="1">
<small class="description"><?php esc_html_e('0 表示跟随全局设置。', 'aksrt'); ?></small>
</p>
<p>
<label>
<input type="checkbox" name="aksrt_post_meta[is_featured]" value="1" <?php checked(!empty($meta['is_featured'])); ?>>
<?php esc_html_e('标记为推荐文章', 'aksrt'); ?>
</label>
</p>
<p><strong><?php esc_html_e('自定义摘要', 'aksrt'); ?></strong></p>
<textarea name="aksrt_post_meta[custom_excerpt]" rows="3" class="widefat" placeholder="<?php esc_attr_e('留空使用自动摘要', 'aksrt'); ?>"><?php echo esc_textarea($meta['custom_excerpt']); ?></textarea>
<p><strong><?php esc_html_e('版权声明覆盖', 'aksrt'); ?></strong></p>
<textarea name="aksrt_post_meta[copyright_text]" rows="3" class="widefat" placeholder="<?php esc_attr_e('留空使用默认版权文案', 'aksrt'); ?>"><?php echo esc_textarea($meta['copyright_text']); ?></textarea>
<hr>
<p><strong><?php esc_html_e('副标题', 'aksrt'); ?></strong></p>
<input type="text" name="aksrt_post_meta[subtitle]" value="<?php echo esc_attr($meta['subtitle']); ?>" class="widefat" placeholder="<?php esc_attr_e('可选,显示在标题下方', 'aksrt'); ?>">
<p><strong><?php esc_html_e('外链特色图', 'aksrt'); ?></strong></p>
<input type="url" name="aksrt_post_meta[thumbnail_url]" value="<?php echo esc_url($meta['thumbnail_url']); ?>" class="widefat" placeholder="<?php esc_attr_e('外部图片 URL(覆盖特色图片)', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('填入外部图片 URL 将替代特色图片。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('幻灯片封面', 'aksrt'); ?></strong></p>
<input type="text" name="aksrt_post_meta[featured_slide]" value="<?php echo esc_attr($meta['featured_slide']); ?>" class="widefat" placeholder="<?php esc_attr_e('图片 URL,逗号分隔', 'aksrt'); ?>">
<small class="description"><?php esc_html_e('多张图片 URL 用逗号分隔,用于封面轮播。', 'aksrt'); ?></small>
<p><strong><?php esc_html_e('特色视频', 'aksrt'); ?></strong></p>
<input type="url" name="aksrt_post_meta[featured_video]" value="<?php echo esc_url($meta['featured_video']); ?>" class="widefat" placeholder="<?php esc_attr_e('第 1 集视频 URL(直链)', 'aksrt'); ?>">
<p><strong><?php esc_html_e('视频剧集列表', 'aksrt'); ?></strong></p>
<?php
$episodes = is_array($meta['featured_video_episode'] ?? null) ? $meta['featured_video_episode'] : [];
// Always show at least 3 editable rows for new-route authoring.
$rows = max(3, count($episodes));
for ($i = 0; $i < $rows; $i++) :
$ep = is_array($episodes[$i] ?? null) ? $episodes[$i] : [];
$ep_url = (string) ($ep['url'] ?? '');
$ep_title = (string) ($ep['title'] ?? '');
?>
<p class="aksrt-episode-row">
<input type="text" name="aksrt_post_meta[featured_video_episode][<?php echo (int) $i; ?>][title]" value="<?php echo esc_attr($ep_title); ?>" class="widefat" placeholder="<?php echo esc_attr(sprintf(__('第 %d 集标题(可选)', 'aksrt'), $i + 1)); ?>" style="margin-bottom:4px;">
<input type="url" name="aksrt_post_meta[featured_video_episode][<?php echo (int) $i; ?>][url]" value="<?php echo esc_url($ep_url); ?>" class="widefat" placeholder="<?php echo esc_attr(sprintf(__('第 %d 集视频 URL', 'aksrt'), $i + 1)); ?>">
</p>
<?php endfor; ?>
<small class="description"><?php esc_html_e('新路线:每行一集(标题 + URL)。留空 URL 的行会被忽略。第 1 集可与上方「特色视频」二选一填写。', 'aksrt'); ?></small>
<hr>
<p><strong><?php esc_html_e('手动计数覆盖', 'aksrt'); ?></strong></p>
<p>
<label><?php esc_html_e('阅读量', 'aksrt'); ?></label>
<input type="number" name="aksrt_post_meta[views]" value="<?php echo esc_attr((string) (int) $meta['views']); ?>" class="widefat" min="0" step="1">
</p>
<p>
<label><?php esc_html_e('点赞数', 'aksrt'); ?></label>
<input type="number" name="aksrt_post_meta[likes]" value="<?php echo esc_attr((string) (int) $meta['likes']); ?>" class="widefat" min="0" step="1">
</p>
<small class="description"><?php esc_html_e('0 表示不覆盖,使用系统统计值。', 'aksrt'); ?></small>
<?php
}
/**
* Render the custom filter metabox (per-dimension value selectors).
* §3.8 — stores values as individual post meta keys (_aksrt_cf_{dim_key}).
*/
public function render_custom_filter_meta_box(\WP_Post $post): void {
wp_nonce_field('aksrt_custom_filter_meta', 'aksrt_custom_filter_meta_nonce');
$filter_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_post_filter_settings()
: ['custom_filter' => []];
$dimensions = $filter_settings['custom_filter'];
if (empty($dimensions)) {
echo '<p class="description">' . esc_html__('尚未定义筛选维度,请前往 文章设置 → 高级筛选 添加。', 'aksrt') . '</p>';
return;
}
foreach ($dimensions as $dim) :
$meta_key = '_aksrt_cf_' . $dim['key'];
$current = get_post_meta($post->ID, $meta_key, true);
?>
<p>
<label for="<?php echo esc_attr($meta_key); ?>"><strong><?php echo esc_html($dim['name']); ?></strong></label>
<select name="<?php echo esc_attr($meta_key); ?>" id="<?php echo esc_attr($meta_key); ?>" class="widefat">
<option value=""><?php esc_html_e('未选择', 'aksrt'); ?></option>
<?php foreach ($dim['vals'] as $val) : ?>
<option value="<?php echo esc_attr($val['key']); ?>" <?php selected($current, $val['key']); ?>><?php echo esc_html($val['name']); ?></option>
<?php endforeach; ?>
</select>
</p>
<?php
endforeach;
}
/**
* Save meta box data.
*/
public function save_meta_box(int $post_id): void {
// Save page meta (page_header_style / page_header_cover_img)
if (isset($_POST['aksrt_page_meta_nonce']) && wp_verify_nonce($_POST['aksrt_page_meta_nonce'], 'aksrt_page_meta')) {
if (!defined('DOING_AUTOSAVE') && current_user_can('edit_post', $post_id)) {
$style = isset($_POST['page_header_style']) ? sanitize_key((string) $_POST['page_header_style']) : '';
if (!in_array($style, ['', 'not', '1', '2', '3'], true)) {
$style = '';
}
if ($style !== '') {
update_post_meta($post_id, 'page_header_style', $style);
} else {
delete_post_meta($post_id, 'page_header_style');
}
$cover = isset($_POST['page_header_cover_img']) ? esc_url_raw((string) $_POST['page_header_cover_img']) : '';
if ($cover !== '') {
update_post_meta($post_id, 'page_header_cover_img', $cover);
} else {
delete_post_meta($post_id, 'page_header_cover_img');
}
}
}
// Save post meta (aksrt_post_meta group)
if (!isset($_POST['aksrt_post_meta_nonce'])) return;
if (!wp_verify_nonce($_POST['aksrt_post_meta_nonce'], 'aksrt_post_meta')) return;
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (!current_user_can('edit_post', $post_id)) return;
// Zibll show_layout (independent of list layout_override).
$show_layout = isset($_POST['aksrt_show_layout']) ? sanitize_key((string) wp_unslash($_POST['aksrt_show_layout'])) : '';
if (!in_array($show_layout, ['no_sidebar', 'sidebar_left', 'sidebar_right'], true)) {
$show_layout = '';
}
if ($show_layout !== '') {
update_post_meta($post_id, '_aksrt_show_layout', $show_layout);
} else {
delete_post_meta($post_id, '_aksrt_show_layout');
}
// §3.8 Quick / Bulk edit — separate code path (merge mode)
if (!empty($_POST['aksrt_qe_flag'])) {
$this->save_quick_edit($post_id);
// Bulk pay fields may be submitted together with QE flag on bulk form.
$this->save_bulk_pay_fields($post_id);
return;
}
// Bulk-only pay fields without aksrt_qe_flag (when only pay row used).
if (!empty($_POST['aksrt_bulk_pay_flag'])) {
$this->save_bulk_pay_fields($post_id);
// Continue only if full meta also posted; otherwise done.
if (!isset($_POST['aksrt_post_meta'])) {
return;
}
}
$raw = $_POST['aksrt_post_meta'] ?? [];
$sanitized = [
'cover_images' => isset($raw['cover_images']) ? sanitize_text_field($raw['cover_images']) : '',
'custom_excerpt' => isset($raw['custom_excerpt']) ? sanitize_textarea_field($raw['custom_excerpt']) : '',
'layout_override' => isset($raw['layout_override']) ? sanitize_key($raw['layout_override']) : '',
'header_image_mode' => isset($raw['header_image_mode']) ? sanitize_key($raw['header_image_mode']) : '',
// New route: '' follow | '1' force on | '0' force off.
'show_toc' => isset($raw['show_toc']) && in_array((string) $raw['show_toc'], ['', '0', '1'], true) ? (string) $raw['show_toc'] : '',
'show_reading_progress' => isset($raw['show_reading_progress']) ? sanitize_key((string) $raw['show_reading_progress']) : '',
'enable_lightbox' => isset($raw['enable_lightbox']) ? sanitize_key((string) $raw['enable_lightbox']) : '',
'show_related' => isset($raw['show_related']) ? sanitize_key((string) $raw['show_related']) : '',
'related_count' => isset($raw['related_count']) ? (int) $raw['related_count'] : 0,
'enable_like' => isset($raw['enable_like']) ? sanitize_key((string) $raw['enable_like']) : '',
'enable_favorite' => isset($raw['enable_favorite']) ? sanitize_key((string) $raw['enable_favorite']) : '',
'enable_share' => isset($raw['enable_share']) ? sanitize_key((string) $raw['enable_share']) : '',
'enable_print' => isset($raw['enable_print']) ? sanitize_key((string) $raw['enable_print']) : '',
'enable_pdf' => isset($raw['enable_pdf']) ? sanitize_key((string) $raw['enable_pdf']) : '',
'copyright_text' => isset($raw['copyright_text']) ? sanitize_textarea_field($raw['copyright_text']) : '',
'is_featured' => !empty($raw['is_featured']),
// §3.8 文章扩展字段
'thumbnail_url' => isset($raw['thumbnail_url']) ? esc_url_raw((string) $raw['thumbnail_url']) : '',
'featured_slide' => isset($raw['featured_slide']) ? sanitize_text_field($raw['featured_slide']) : '',
'featured_video' => isset($raw['featured_video']) ? esc_url_raw((string) $raw['featured_video']) : '',
'featured_video_episode' => self::sanitize_video_episodes($raw['featured_video_episode'] ?? []),
'subtitle' => isset($raw['subtitle']) ? sanitize_text_field($raw['subtitle']) : '',
'views' => isset($raw['views']) ? max(0, (int) $raw['views']) : 0,
'likes' => isset($raw['likes']) ? max(0, (int) $raw['likes']) : 0,
'maxheight' => isset($raw['maxheight']) ? max(0, (int) $raw['maxheight']) : 0,
];
$valid_modes = ['', 'card', 'list', 'mini', 'grid-cms'];
if (!in_array($sanitized['layout_override'], $valid_modes, true)) {
$sanitized['layout_override'] = '';
}
$valid_header_modes = ['', 'featured', 'auto', 'none'];
if (!in_array($sanitized['header_image_mode'], $valid_header_modes, true)) {
$sanitized['header_image_mode'] = '';
}
foreach (['show_reading_progress', 'enable_lightbox', 'show_related', 'enable_like', 'enable_favorite', 'enable_share', 'enable_print', 'enable_pdf'] as $toggle_key) {
if (!in_array($sanitized[$toggle_key], ['', '0', '1'], true)) {
$sanitized[$toggle_key] = '';
}
}
$sanitized['related_count'] = max(0, min(20, (int) $sanitized['related_count']));
update_post_meta($post_id, self::META_KEY, $sanitized);
// §3.8 Save custom filter dimension values as individual post meta
if (isset($_POST['aksrt_custom_filter_meta_nonce']) && wp_verify_nonce($_POST['aksrt_custom_filter_meta_nonce'], 'aksrt_custom_filter_meta')) {
if (!defined('DOING_AUTOSAVE') && current_user_can('edit_post', $post_id)) {
$cf_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_post_filter_settings()
: ['custom_filter' => []];
foreach ($cf_settings['custom_filter'] as $dim) {
$meta_key = '_aksrt_cf_' . $dim['key'];
$value = isset($_POST[$meta_key]) ? sanitize_key((string) $_POST[$meta_key]) : '';
if ($value !== '') {
update_post_meta($post_id, $meta_key, $value);
} else {
delete_post_meta($post_id, $meta_key);
}
}
}
}
}
/**
* §3.8 Add custom columns to the posts list table.
*/
public function add_columns(array $columns): array {
$columns['aksrt_views'] = __('阅读量', 'aksrt');
$columns['aksrt_likes'] = __('点赞数', 'aksrt');
$columns['aksrt_sales'] = __('销售数据', 'aksrt');
return $columns;
}
/**
* §3.8 Render custom column content.
*/
public function render_column(string $column, int $post_id): void {
$meta = self::get_meta($post_id);
if ($column === 'aksrt_views') {
$views = (int) ($meta['views'] ?? 0);
echo esc_html($views > 0 ? (string) $views : '—');
} elseif ($column === 'aksrt_likes') {
$likes = (int) ($meta['likes'] ?? 0);
$layout = esc_attr($meta['layout_override'] ?? '');
$toc = esc_attr($meta['show_toc'] ?? '');
$mh = (int) ($meta['maxheight'] ?? 0);
echo '<span class="aksrt-col-data" data-views="' . (int) ($meta['views'] ?? 0) . '" data-likes="' . $likes . '" data-layout="' . $layout . '" data-toc="' . $toc . '" data-maxheight="' . $mh . '">' . esc_html($likes > 0 ? (string) $likes : '—') . '</span>';
} elseif ($column === 'aksrt_sales') {
$this->render_sales_column($post_id);
}
}
/**
* Content-pay sales summary for posts list (zibll 销售数据 column parity).
*/
private function render_sales_column(int $post_id): void {
if (!function_exists('aksrt_get_pay_meta') || !class_exists('AKSRT\\Core\\DomainModel')) {
echo '—';
return;
}
$pay = aksrt_get_pay_meta($post_id);
$pay_type = DomainModel::normalize_pay_type((string) ($pay['pay_type'] ?? DomainModel::PAY_TYPE_NO));
if ($pay_type === DomainModel::PAY_TYPE_NO) {
echo '<span class="description">' . esc_html__('免费', 'aksrt') . '</span>';
return;
}
$labels = DomainModel::pay_type_labels();
$type_label = $labels[$pay_type] ?? $pay_type;
$modo = (($pay['pay_modo'] ?? '0') === 'points') ? 'points' : '0';
if ($modo === 'points') {
$price_text = sprintf(__('%d 积分', 'aksrt'), (int) ceil((float) ($pay['points_price'] ?? $pay['price'] ?? 0)));
} else {
$price_text = '¥' . number_format((float) ($pay['price'] ?? 0), 2);
}
$meta_sales = (int) get_post_meta($post_id, DomainModel::RESOURCE_SALES_VOLUME_KEY, true);
$db_sales = 0;
$revenue = 0.0;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$map = \AKSRT\Payment\OrderRepository::get_instance()->get_content_sales_map(
[$post_id],
DomainModel::ORDER_PRODUCT_RESOURCE
);
if (isset($map[$post_id])) {
$db_sales = (int) ($map[$post_id]['sales_count'] ?? 0);
$revenue = (float) ($map[$post_id]['revenue'] ?? 0);
}
}
$orders_url = admin_url('admin.php?page=aksrt_orders&product_type=resource&product_id=' . $post_id);
$products_url = admin_url('admin.php?page=aksrt_pay_products&s=' . $post_id);
echo '<div style="font-size:12px;line-height:1.5;">';
echo '<strong style="color:#0c9cc8;">' . esc_html((string) $type_label) . '</strong>';
echo '<br>' . esc_html($price_text);
echo '<br>' . esc_html(sprintf(__('Meta销量 %d', 'aksrt'), max(0, $meta_sales)));
if ($db_sales > 0) {
printf(
'<br><a href="%s" title="%s">%s</a>',
esc_url($orders_url),
esc_attr__('查看销售明细', 'aksrt'),
esc_html(sprintf(__('真实销量 %d · ¥%s', 'aksrt'), $db_sales, number_format($revenue, 2)))
);
} else {
echo '<br><span class="description">' . esc_html__('暂无订单', 'aksrt') . '</span>';
}
echo '<br><a href="' . esc_url($products_url) . '">' . esc_html__('付费文章', 'aksrt') . '</a>';
if ($pay_type === DomainModel::PAY_TYPE_DOWNLOAD) {
$dl_url = admin_url('admin.php?page=aksrt_download_logs&post_id=' . $post_id);
echo ' · <a href="' . esc_url($dl_url) . '">' . esc_html__('下载记录', 'aksrt') . '</a>';
}
echo '</div>';
}
/**
* Dropdown filter for content pay_type on edit.php?post_type=post.
*/
public function render_pay_type_filter(string $post_type): void {
if ($post_type !== 'post' || !class_exists('AKSRT\\Core\\DomainModel')) {
return;
}
$current = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
$labels = DomainModel::pay_type_labels();
echo '<select name="aksrt_pay_type">';
echo '<option value="">' . esc_html__('全部付费类型', 'aksrt') . '</option>';
foreach ($labels as $key => $label) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr($key),
selected($current, $key, false),
esc_html($label)
);
}
echo '</select>';
}
/**
* Apply aksrt_pay_type filter via denormalized _aksrt_pay_type meta.
*/
public function filter_posts_by_pay_type(\WP_Query $query): void {
if (!is_admin() || !$query->is_main_query()) {
return;
}
$screen_post_type = $query->get('post_type');
if ($screen_post_type !== 'post') {
return;
}
if (empty($_GET['aksrt_pay_type'])) {
return;
}
$pay_type = sanitize_key((string) wp_unslash($_GET['aksrt_pay_type']));
if ($pay_type === '' || !class_exists('AKSRT\\Core\\DomainModel')) {
return;
}
$pay_type = DomainModel::normalize_pay_type($pay_type);
$meta_query = (array) $query->get('meta_query');
if ($pay_type === DomainModel::PAY_TYPE_NO) {
$meta_query[] = [
'relation' => 'OR',
[
'key' => DomainModel::PAY_TYPE_KEY,
'value' => DomainModel::PAY_TYPE_NO,
'compare' => '=',
],
[
'key' => DomainModel::PAY_TYPE_KEY,
'compare' => 'NOT EXISTS',
],
];
} else {
$meta_query[] = [
'key' => DomainModel::PAY_TYPE_KEY,
'value' => $pay_type,
'compare' => '=',
];
}
$query->set('meta_query', $meta_query);
}
/**
* §3.8 Render quick edit and bulk edit fields.
*/
public function render_quick_edit(string $column_name, string $post_type): void {
if ($column_name !== 'aksrt_views' || $post_type !== 'post') {
return;
}
wp_nonce_field('aksrt_post_meta', 'aksrt_post_meta_nonce_qe');
?>
<fieldset class="inline-edit-col-right" id="aksrt-quick-edit">
<div class="inline-edit-col">
<h4><?php esc_html_e('AKSRT 文章设置', 'aksrt'); ?></h4>
<input type="hidden" name="aksrt_qe_flag" value="1">
<label class="alignleft">
<span class="title"><?php esc_html_e('阅读量', 'aksrt'); ?></span>
<input type="number" name="aksrt_qe_views" min="0" step="1" value="" class="aksrt-qe-views">
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('点赞数', 'aksrt'); ?></span>
<input type="number" name="aksrt_qe_likes" min="0" step="1" value="" class="aksrt-qe-likes">
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('布局', 'aksrt'); ?></span>
<select name="aksrt_qe_layout" class="aksrt-qe-layout">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<option value=""><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="card"><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="list"><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="mini"><?php esc_html_e('迷你', 'aksrt'); ?></option>
<option value="grid-cms"><?php esc_html_e('CMS网格', 'aksrt'); ?></option>
</select>
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('目录树', 'aksrt'); ?></span>
<select name="aksrt_qe_toc" class="aksrt-qe-toc">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<option value=""><?php esc_html_e('跟随全局', 'aksrt'); ?></option>
<option value="1"><?php esc_html_e('显示', 'aksrt'); ?></option>
<option value="0"><?php esc_html_e('隐藏', 'aksrt'); ?></option>
</select>
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('限高(px)', 'aksrt'); ?></span>
<input type="number" name="aksrt_qe_maxheight" min="0" step="100" value="" class="aksrt-qe-maxheight" placeholder="<?php esc_attr_e('0=不限', 'aksrt'); ?>">
</label>
<br class="clear">
<h4 style="margin-top:10px;"><?php esc_html_e('内容付费(批量)', 'aksrt'); ?></h4>
<input type="hidden" name="aksrt_bulk_pay_flag" value="1">
<label class="alignleft">
<span class="title"><?php esc_html_e('付费类型', 'aksrt'); ?></span>
<select name="aksrt_bulk_pay_type" class="aksrt-bulk-pay-type">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<?php
$pay_labels = class_exists('AKSRT\\Core\\DomainModel') ? DomainModel::pay_type_labels() : [];
foreach ($pay_labels as $k => $lab) :
?>
<option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($lab); ?></option>
<?php endforeach; ?>
</select>
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('支付方式', 'aksrt'); ?></span>
<select name="aksrt_bulk_pay_modo" class="aksrt-bulk-pay-modo">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<option value="0"><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points"><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('现金价', 'aksrt'); ?></span>
<input type="number" min="0" step="0.01" name="aksrt_bulk_pay_price" value="" class="aksrt-bulk-pay-price" placeholder="<?php esc_attr_e('留空=不改', 'aksrt'); ?>">
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('积分价', 'aksrt'); ?></span>
<input type="number" min="0" step="1" name="aksrt_bulk_points_price" value="" class="aksrt-bulk-points-price" placeholder="<?php esc_attr_e('留空=不改', 'aksrt'); ?>">
</label>
<label class="alignleft">
<span class="title"><?php esc_html_e('虚拟销量', 'aksrt'); ?></span>
<input type="number" min="0" step="1" name="aksrt_bulk_virtual_sales" value="" class="aksrt-bulk-virtual-sales" placeholder="<?php esc_attr_e('留空=不改', 'aksrt'); ?>">
</label>
<br class="clear">
</div>
</fieldset>
<?php
}
/**
* Bulk/quick merge update for content pay meta (_aksrt_pay + denormalized keys).
* Only non-sentinel fields are written; omit to keep existing.
*/
private function save_bulk_pay_fields(int $post_id): void {
if (!class_exists('AKSRT\\Core\\DomainModel') || !function_exists('aksrt_get_pay_meta')) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$type_raw = isset($_POST['aksrt_bulk_pay_type']) ? (string) wp_unslash($_POST['aksrt_bulk_pay_type']) : '-1';
$modo_raw = isset($_POST['aksrt_bulk_pay_modo']) ? (string) wp_unslash($_POST['aksrt_bulk_pay_modo']) : '-1';
$price_raw = isset($_POST['aksrt_bulk_pay_price']) ? trim((string) wp_unslash($_POST['aksrt_bulk_pay_price'])) : '';
$points_raw = isset($_POST['aksrt_bulk_points_price']) ? trim((string) wp_unslash($_POST['aksrt_bulk_points_price'])) : '';
$virtual_raw = isset($_POST['aksrt_bulk_virtual_sales']) ? trim((string) wp_unslash($_POST['aksrt_bulk_virtual_sales'])) : '';
$has_change = ($type_raw !== '' && $type_raw !== '-1')
|| ($modo_raw !== '' && $modo_raw !== '-1')
|| $price_raw !== ''
|| $points_raw !== ''
|| $virtual_raw !== '';
if (!$has_change) {
return;
}
$meta = aksrt_get_pay_meta($post_id);
if ($type_raw !== '' && $type_raw !== '-1') {
$meta['pay_type'] = DomainModel::normalize_pay_type(sanitize_key($type_raw));
}
if ($modo_raw !== '' && $modo_raw !== '-1') {
$modo = sanitize_key($modo_raw);
$meta['pay_modo'] = ($modo === 'points') ? 'points' : '0';
}
if ($price_raw !== '' && is_numeric($price_raw)) {
$meta['price'] = max(0, (float) $price_raw);
}
if ($points_raw !== '' && is_numeric($points_raw)) {
$meta['points_price'] = max(0, (int) $points_raw);
}
if ($virtual_raw !== '' && is_numeric($virtual_raw)) {
$meta['virtual_sales'] = max(0, (int) $virtual_raw);
}
$meta = DomainModel::sanitize_pay_meta($meta);
update_post_meta($post_id, DomainModel::PAY_META_KEY, $meta);
update_post_meta($post_id, DomainModel::PAY_TYPE_KEY, (string) ($meta['pay_type'] ?? DomainModel::PAY_TYPE_NO));
update_post_meta($post_id, DomainModel::PAY_PRICE_KEY, (float) ($meta['price'] ?? 0));
if (class_exists('AKSRT\\Core\\DomainModel') && method_exists('AKSRT\\Core\\DomainModel', 'is_resource_pay_type')) {
update_post_meta(
$post_id,
DomainModel::RESOURCE_MARKER_KEY,
DomainModel::is_resource_pay_type((string) ($meta['pay_type'] ?? 'no')) ? 1 : 0
);
}
}
/**
* §3.8 Quick / Bulk edit merge-mode save.
* Only updates fields that were actually submitted (non-sentinel, non-empty).
* Existing meta values for omitted fields are preserved.
*/
private function save_quick_edit(int $post_id): void {
$existing = self::get_meta($post_id);
$views = isset($_POST['aksrt_qe_views']) && $_POST['aksrt_qe_views'] !== ''
? max(0, (int) $_POST['aksrt_qe_views'])
: (int) ($existing['views'] ?? 0);
$likes = isset($_POST['aksrt_qe_likes']) && $_POST['aksrt_qe_likes'] !== ''
? max(0, (int) $_POST['aksrt_qe_likes'])
: (int) ($existing['likes'] ?? 0);
$layout = $existing['layout_override'] ?? '';
if (isset($_POST['aksrt_qe_layout']) && $_POST['aksrt_qe_layout'] !== '-1') {
$layout = sanitize_key((string) $_POST['aksrt_qe_layout']);
if (!in_array($layout, ['', 'card', 'list', 'mini', 'grid-cms'], true)) {
$layout = $existing['layout_override'] ?? '';
}
}
$toc = $existing['show_toc'] ?? '';
if (isset($_POST['aksrt_qe_toc']) && $_POST['aksrt_qe_toc'] !== '-1') {
$toc_v = sanitize_key((string) $_POST['aksrt_qe_toc']);
$toc = in_array($toc_v, ['', '0', '1'], true) ? $toc_v : ($existing['show_toc'] ?? '');
}
$mh = isset($_POST['aksrt_qe_maxheight']) && $_POST['aksrt_qe_maxheight'] !== ''
? max(0, (int) $_POST['aksrt_qe_maxheight'])
: (int) ($existing['maxheight'] ?? 0);
$existing['views'] = $views;
$existing['likes'] = $likes;
$existing['layout_override'] = $layout;
$existing['show_toc'] = $toc;
$existing['maxheight'] = $mh;
update_post_meta($post_id, self::META_KEY, $existing);
}
/**
* §3.8 Output inline JS on edit.php to populate quick edit fields from column data attributes.
*/
public function admin_quick_edit_script(): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->base !== 'edit' || $screen->post_type !== 'post') {
return;
}
?>
<script>
(function () {
var wp_inline_edit = window.inlineEditPost ? window.inlineEditPost.edit : null;
if (!wp_inline_edit) return;
window.inlineEditPost.edit = function (id) {
wp_inline_edit.apply(this, arguments);
var postId = 0;
if (typeof id === 'object') {
postId = parseInt(this.getId(id), 10);
}
if (!postId) return;
var row = document.getElementById('post-' + postId);
if (!row) return;
var viewsEl = row.querySelector('.column-aksrt_views');
var likesEl = row.querySelector('.column-aksrt_likes');
var data = (likesEl && likesEl.querySelector('.aksrt-col-data'))
|| (viewsEl && viewsEl.querySelector('.aksrt-col-data'));
if (!data) return;
var form = document.getElementById('edit-' + postId);
if (!form) return;
var v = data.getAttribute('data-views') || '';
var l = data.getAttribute('data-likes') || '';
var layout = data.getAttribute('data-layout') || '';
var toc = data.getAttribute('data-toc') || '';
var mh = data.getAttribute('data-maxheight') || '';
var qv = form.querySelector('.aksrt-qe-views');
var ql = form.querySelector('.aksrt-qe-likes');
if (qv) qv.value = v;
if (ql) ql.value = l;
var qLayout = form.querySelector('.aksrt-qe-layout');
if (qLayout) qLayout.value = layout;
var qToc = form.querySelector('.aksrt-qe-toc');
if (qToc) qToc.value = toc;
var qMh = form.querySelector('.aksrt-qe-maxheight');
if (qMh) qMh.value = mh;
};
})();
</script>
<?php
}
}