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:
kate522 committed 2026-08-02 22:06:06 +08:00
1 parent 56f1a8175f
commit 8c5b1555c9
285 files changed
+26 -3600

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class CategoryRewrite {
private static $instance = null;
private function __construct() {}
private function rewrite_version(): string {
return AKSRT_VERSION . ':category-rewrite-v1';
}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!$this->is_enabled()) {
// If disabled, ensure old rules are cleaned up
add_action('init', [$this, 'maybe_restore']);
return;
}
add_filter('category_link', [$this, 'remove_category_base'], 10, 2);
add_filter('term_link', [$this, 'remove_category_base_term'], 10, 3);
add_action('init', [$this, 'add_rewrite_rules']);
add_action('template_redirect', [$this, 'redirect_old_urls']);
add_action('admin_init', [$this, 'maybe_flush']);
}
/**
* Check if feature is enabled
*/
private function is_enabled(): bool {
if (!class_exists('\AKSRT\Settings\Settings')) return false;
// schema key = seo_no_category; runtime alias = seo_remove_category
return (bool) (
\AKSRT\Settings\Settings::get_value('seo_remove_category', false)
?: \AKSRT\Settings\Settings::get_value('seo_no_category', false)
);
}
/**
* Remove /category/ from category link
*/
public function remove_category_base(string $link, $term): string {
$base = get_option('category_base');
if ($base && $base !== 'category') return $link;
// Only process category taxonomy
if ($term instanceof \WP_Term && $term->taxonomy !== 'category') {
return $link;
}
$category_base_url = preg_quote(trailingslashit(home_url('/category')), '#');
return preg_replace('#' . $category_base_url . '#', trailingslashit(home_url('/')), $link, 1);
}
/**
* Remove base from term_link for categories
*/
public function remove_category_base_term(string $link, $term, $taxonomy): string {
if ($taxonomy !== 'category') return $link;
return $this->remove_category_base($link, $term);
}
/**
* Add rewrite rules for category URLs without /category/
*/
public function add_rewrite_rules(): void {
// Get all category slugs
$categories = get_categories(['hide_empty' => false]);
foreach ($categories as $cat) {
if (empty($cat->slug)) continue;
$slug = $cat->slug;
// /slug/ → index.php?category_name=$slug
add_rewrite_rule(
'^' . preg_quote($slug, '/') . '/?$',
'index.php?category_name=' . $slug,
'top'
);
// /slug/page/N/ → index.php?category_name=$slug&paged=$N
add_rewrite_rule(
'^' . preg_quote($slug, '/') . '/page/([0-9]+)/?$',
'index.php?category_name=$slug&paged=$matches[1]',
'top'
);
// /slug/feed/ → index.php?category_name=$slug&feed=$feed
add_rewrite_rule(
'^' . preg_quote($slug, '/') . '/feed/?$',
'index.php?category_name=$slug&feed=rss2',
'top'
);
}
}
/**
* 301 redirect old /category/ URLs to new ones
*/
public function redirect_old_urls(): void {
$request = $_SERVER['REQUEST_URI'] ?? '';
$home_path = wp_parse_url(home_url('/'), PHP_URL_PATH);
// Remove home path prefix
if ($home_path && strpos($request, $home_path) === 0) {
$request = substr($request, strlen($home_path));
}
// Check if request starts with /category/
if (preg_match('#^category/(.+?)/?$#', $request, $matches)) {
$new_url = home_url('/' . $matches[1] . '/');
wp_redirect($new_url, 301);
exit;
}
}
/**
* Flush rewrite rules when setting changes
*/
public function maybe_flush(): void {
$rewrite_version = $this->rewrite_version();
$stored = (string) get_option('aksrt_category_rewrite_flushed', '');
if ($stored !== $rewrite_version) {
flush_rewrite_rules();
update_option('aksrt_category_rewrite_flushed', $rewrite_version);
}
}
/**
* Restore default rewrite rules when feature is disabled
*/
public function maybe_restore(): void {
$stored = (string) get_option('aksrt_category_rewrite_flushed', '');
if ($stored !== '') {
flush_rewrite_rules();
delete_option('aksrt_category_rewrite_flushed');
}
}
/**
* Force flush (call after enabling/disabling)
*/
public static function force_flush(): void {
flush_rewrite_rules();
update_option('aksrt_category_rewrite_flushed', AKSRT_VERSION . ':category-rewrite-v1');
}
}
+596
View File
@@ -0,0 +1,596 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class CommentActions {
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('rest_api_init', [$this, 'register_routes']);
}
public function register_routes(): void {
register_rest_route('aksrt/v1', '/comment/list', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'list_comments'],
'permission_callback' => '__return_true',
'args' => [
'post_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
'page' => ['required' => false, 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint'],
'order' => ['required' => false, 'type' => 'string', 'default' => 'newest', 'sanitize_callback' => 'sanitize_key'],
'author_only' => ['required' => false, 'type' => 'boolean', 'default' => false],
],
]);
register_rest_route('aksrt/v1', '/comment/submit', [
'methods' => 'POST',
'callback' => [$this, 'submit_comment'],
'permission_callback' => '__return_true',
]);
register_rest_route('aksrt/v1', '/comment/edit', [
'methods' => 'POST',
'callback' => [$this, 'edit_comment'],
'permission_callback' => [$this, 'check_edit_permission'],
'args' => [
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
'content' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field'],
],
]);
register_rest_route('aksrt/v1', '/comment/delete', [
'methods' => 'POST',
'callback' => [$this, 'delete_comment'],
'permission_callback' => [$this, 'check_delete_permission'],
'args' => [
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
],
]);
register_rest_route('aksrt/v1', '/comment/moderate', [
'methods' => 'POST',
'callback' => [$this, 'moderate_comment'],
'permission_callback' => [$this, 'check_moderate_permission'],
'args' => [
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
'status' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
],
]);
register_rest_route('aksrt/v1', '/comment/pin', [
'methods' => 'POST',
'callback' => [$this, 'pin_comment'],
'permission_callback' => [$this, 'check_pin_permission'],
'args' => [
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
'pinned' => ['required' => false, 'type' => 'boolean'],
],
]);
register_rest_route('aksrt/v1', '/comment/like', [
'methods' => 'POST',
'callback' => [$this, 'like_comment'],
// Guests may like; abuse is bounded by per-visitor toggle state.
'permission_callback' => '__return_true',
'args' => [
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
'active' => ['required' => false, 'type' => 'boolean'],
],
]);
register_rest_route('aksrt/v1', '/comment/get', [
'methods' => \WP_REST_Server::READABLE,
'callback' => [$this, 'get_comment'],
'permission_callback' => [$this, 'check_get_permission'],
'args' => [
'comment_id' => ['required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint'],
],
]);
}
/**
* Permission: can load raw content for edit?
*/
public function check_get_permission(\WP_REST_Request $request): bool {
$comment_id = (int) $request->get_param('comment_id');
if ($comment_id <= 0 || !is_user_logged_in()) {
return false;
}
$perms = $this->permissions($comment_id);
return !empty($perms['can_edit']);
}
/**
* Return raw comment content for moveForm edit.
*/
public function get_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = (int) $request->get_param('comment_id');
$comment = get_comment($comment_id);
if (!$comment) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comment_not_found',
'message' => __('评论不存在。', 'aksrt'),
], 404);
}
return new \WP_REST_Response([
'success' => true,
'content' => (string) $comment->comment_content,
'data' => [
'content' => (string) $comment->comment_content,
'comment_id' => $comment_id,
'parent' => (int) $comment->comment_parent,
],
]);
}
/**
* Resolve the per-comment action contract (single source of truth).
*
* @param int $comment_id Comment ID.
* @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool}
*/
private function permissions(int $comment_id): array {
if (function_exists('aksrt_comment_action_permissions')) {
return aksrt_comment_action_permissions($comment_id);
}
// Defensive fallback if the helper is unavailable.
$comment = get_comment($comment_id);
$is_mod = current_user_can('moderate_comments');
$is_owner = $comment && is_user_logged_in() && (int) $comment->user_id === get_current_user_id();
return [
'can_edit' => $is_mod || $is_owner,
'can_delete' => $is_mod || $is_owner,
'can_moderate' => $is_mod,
'can_pin' => $is_mod,
];
}
/**
* Permission: can edit? (author within configured window, or moderator)
*/
public function check_edit_permission(\WP_REST_Request $request): bool {
if (!is_user_logged_in()) return false;
return $this->permissions((int) $request->get_param('comment_id'))['can_edit'];
}
/**
* Permission: can delete? (author, or moderator)
*/
public function check_delete_permission(\WP_REST_Request $request): bool {
if (!is_user_logged_in()) return false;
return $this->permissions((int) $request->get_param('comment_id'))['can_delete'];
}
/**
* Permission: can pin? (moderator, top-level only)
*/
public function check_pin_permission(\WP_REST_Request $request): bool {
if (!is_user_logged_in()) return false;
return $this->permissions((int) $request->get_param('comment_id'))['can_pin'];
}
/**
* Fragment list for order / author / page without full HTML scrape.
*/
public function list_comments(\WP_REST_Request $request): \WP_REST_Response {
$post_id = (int) $request->get_param('post_id');
$post = get_post($post_id);
if (!$post) {
return new \WP_REST_Response([
'success' => false,
'code' => 'post_not_found',
'message' => __('文章不存在。', 'aksrt'),
], 404);
}
if (!function_exists('aksrt_comments_render_list_fragment')) {
return new \WP_REST_Response([
'success' => false,
'code' => 'fragment_unavailable',
'message' => __('评论片段渲染不可用。', 'aksrt'),
], 500);
}
$order = sanitize_key((string) ($request->get_param('order') ?: 'newest'));
$author_only = (bool) $request->get_param('author_only');
$page = max(1, (int) ($request->get_param('page') ?: 1));
$fragment = aksrt_comments_render_list_fragment($post_id, [
'order' => $order,
'author_only' => $author_only,
'page' => $page,
]);
return new \WP_REST_Response([
'success' => true,
'data' => $fragment,
]);
}
/**
* AJAX submit with HTML item for local insert.
* Runs through native WP comment pipeline (preprocess_comment / moderation).
*/
public function submit_comment(\WP_REST_Request $request): \WP_REST_Response {
$params = $request->get_json_params();
if (!is_array($params) || empty($params)) {
$params = $request->get_params();
}
if (!is_array($params)) {
$params = [];
}
$edit_id = absint($params['edit_comment_ID'] ?? $params['edit_comment_id'] ?? 0);
if ($edit_id > 0) {
$request->set_param('comment_id', $edit_id);
$request->set_param('content', (string) ($params['comment'] ?? $params['content'] ?? ''));
return $this->edit_comment($request);
}
$post_id = absint($params['comment_post_ID'] ?? $params['post_id'] ?? 0);
$content = isset($params['comment']) ? (string) $params['comment'] : '';
$parent = absint($params['comment_parent'] ?? 0);
if ($post_id <= 0 || trim(wp_strip_all_tags($content)) === '') {
return new \WP_REST_Response([
'success' => false,
'code' => 'invalid_payload',
'message' => __('请输入评论内容。', 'aksrt'),
], 400);
}
$post = get_post($post_id);
if (!$post || !comments_open($post_id)) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comments_closed',
'message' => __('评论已关闭。', 'aksrt'),
], 403);
}
$nonce = (string) ($params['_aksrt_comment_nonce'] ?? $params['_wpnonce'] ?? '');
if ($nonce !== '' && !wp_verify_nonce($nonce, 'aksrt_comment_nonce') && !wp_verify_nonce($nonce, 'wp_rest')) {
return new \WP_REST_Response([
'success' => false,
'code' => 'invalid_nonce',
'message' => __('安全校验失败,请刷新后重试。', 'aksrt'),
], 403);
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('comment', $params);
if ($captcha_result !== true) {
return new \WP_REST_Response([
'success' => false,
'code' => 'captcha_failed',
'message' => (string) $captcha_result,
], 400);
}
}
$user = wp_get_current_user();
$commentdata = [
'comment_post_ID' => $post_id,
'comment_content' => $content,
'comment_parent' => $parent,
'comment_type' => '',
// Prefer real client IP behind CDN/proxy (REMOTE_ADDR alone is often edge IP).
'comment_author_IP' => function_exists('aksrt_get_client_ip')
? (string) aksrt_get_client_ip()
: (isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REMOTE_ADDR'])) : ''),
'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_USER_AGENT'])) : '',
'comment_date' => current_time('mysql'),
'comment_date_gmt' => current_time('mysql', true),
];
if ($user && $user->ID > 0) {
$commentdata['user_id'] = (int) $user->ID;
$commentdata['comment_author'] = $user->display_name ?: $user->user_login;
$commentdata['comment_author_email'] = $user->user_email;
$commentdata['comment_author_url'] = $user->user_url;
} else {
$commentdata['user_id'] = 0;
$commentdata['comment_author'] = sanitize_text_field((string) ($params['author'] ?? ''));
$commentdata['comment_author_email'] = sanitize_email((string) ($params['email'] ?? ''));
$commentdata['comment_author_url'] = esc_url_raw((string) ($params['url'] ?? ''));
if ($commentdata['comment_author'] === '' || $commentdata['comment_author_email'] === '') {
return new \WP_REST_Response([
'success' => false,
'code' => 'guest_fields_required',
'message' => __('请填写昵称和邮箱。', 'aksrt'),
], 400);
}
}
foreach (['author', 'email', 'url', 'comment', 'comment_post_ID', 'comment_parent', '_aksrt_comment_nonce'] as $k) {
if (isset($params[$k])) {
$_POST[$k] = $params[$k];
}
}
$comment_id = wp_new_comment(wp_slash($commentdata), true);
if (is_wp_error($comment_id)) {
return new \WP_REST_Response([
'success' => false,
'code' => $comment_id->get_error_code() ?: 'submit_failed',
'message' => $comment_id->get_error_message() ?: __('评论提交失败。', 'aksrt'),
], 400);
}
$comment = get_comment((int) $comment_id);
if (!$comment) {
return new \WP_REST_Response([
'success' => false,
'code' => 'submit_failed',
'message' => __('评论提交失败。', 'aksrt'),
], 500);
}
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_display_settings() : [];
$html = function_exists('aksrt_comments_render_single_html')
? aksrt_comments_render_single_html($comment, $settings)
: '';
$needs_moderation = '0' === (string) $comment->comment_approved;
$wait_time = 0;
return new \WP_REST_Response([
'success' => true,
'code' => 'submitted',
'message' => $needs_moderation ? __('评论已提交,等待管理员审核', 'aksrt') : __('评论发布成功', 'aksrt'),
'html' => $html,
'is_reply'=> $parent > 0,
'parent_id' => $parent,
'needs_moderation' => $needs_moderation,
'wait_time' => $wait_time,
'data' => [
'comment_id' => (int) $comment_id,
'approved' => (string) $comment->comment_approved,
],
]);
}
/**
* Edit comment content
*/
public function edit_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = $request->get_param('comment_id');
$content = $request->get_param('content');
$comment = get_comment($comment_id);
if (!$comment) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comment_not_found',
'message' => __('评论不存在。', 'aksrt'),
], 404);
}
// Sanitize with wp_kses (allow basic HTML)
$allowed_html = [
'a' => ['href' => [], 'title' => [], 'target' => []],
'code' => [],
'pre' => [],
'strong' => [],
'em' => [],
'br' => [],
'p' => [],
];
$clean_content = wp_kses($content, $allowed_html);
$updated = wp_update_comment([
'comment_ID' => $comment_id,
'comment_content' => $clean_content,
]);
if (is_wp_error($updated)) {
return new \WP_REST_Response([
'success' => false,
'code' => 'update_failed',
'message' => __('更新失败。', 'aksrt'),
], 500);
}
$rendered = function_exists('aksrt_render_comment_content')
? aksrt_render_comment_content($clean_content)
: wpautop($clean_content);
return new \WP_REST_Response([
'success' => true,
'code' => 'updated',
'message' => __('评论已更新。', 'aksrt'),
'data' => [
'content' => $rendered,
],
]);
}
/**
* Delete comment
*/
public function delete_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = $request->get_param('comment_id');
$comment = get_comment($comment_id);
if (!$comment) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comment_not_found',
'message' => __('评论不存在。', 'aksrt'),
], 404);
}
$deleted = wp_delete_comment($comment_id, false); // soft delete
if (!$deleted) {
return new \WP_REST_Response([
'success' => false,
'code' => 'delete_failed',
'message' => __('删除失败。', 'aksrt'),
], 500);
}
return new \WP_REST_Response([
'success' => true,
'code' => 'deleted',
'message' => __('评论已删除。', 'aksrt'),
]);
}
/**
* Moderate comment (approve / hold / spam)
*/
public function moderate_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = $request->get_param('comment_id');
$status = $request->get_param('status');
$allowed_statuses = ['approve', 'hold', 'spam'];
if (!in_array($status, $allowed_statuses, true)) {
return new \WP_REST_Response([
'success' => false,
'code' => 'invalid_status',
'message' => __('无效状态。', 'aksrt'),
], 400);
}
$comment = get_comment($comment_id);
if (!$comment) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comment_not_found',
'message' => __('评论不存在。', 'aksrt'),
], 404);
}
$status_map = [
'approve' => 'approved',
'hold' => 'hold',
'spam' => 'spam',
];
$result = wp_set_comment_status($comment_id, $status_map[$status]);
if (is_wp_error($result)) {
return new \WP_REST_Response([
'success' => false,
'code' => 'moderate_failed',
'message' => __('操作失败。', 'aksrt'),
], 500);
}
return new \WP_REST_Response([
'success' => true,
'code' => 'moderated',
'message' => __('操作成功。', 'aksrt'),
'data' => [
'status' => $status_map[$status],
],
]);
}
/**
* Toggle a comment's pinned ("置顶") state. Top-level comments only.
*/
public function pin_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = (int) $request->get_param('comment_id');
$comment = get_comment($comment_id);
if (!$comment) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comment_not_found',
'message' => __('评论不存在。', 'aksrt'),
], 404);
}
if ((int) $comment->comment_parent !== 0) {
return new \WP_REST_Response([
'success' => false,
'code' => 'cannot_pin_reply',
'message' => __('仅顶层评论可置顶。', 'aksrt'),
], 400);
}
$current = '1' === (string) get_comment_meta($comment_id, '_aksrt_comment_pinned', true);
$target = null !== $request->get_param('pinned')
? (bool) $request->get_param('pinned')
: !$current;
if ($target) {
update_comment_meta($comment_id, '_aksrt_comment_pinned', '1');
} else {
delete_comment_meta($comment_id, '_aksrt_comment_pinned');
}
return new \WP_REST_Response([
'success' => true,
'code' => 'pinned',
'message' => $target ? __('评论已置顶。', 'aksrt') : __('已取消置顶。', 'aksrt'),
'data' => [
'pinned' => $target,
],
]);
}
/**
* Toggle the current visitor's like on a comment.
*/
public function like_comment(\WP_REST_Request $request): \WP_REST_Response {
$comment_id = (int) $request->get_param('comment_id');
$comment = get_comment($comment_id);
if (!$comment || '1' !== (string) $comment->comment_approved) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comment_unavailable',
'message' => __('评论不存在或未通过审核。', 'aksrt'),
], 404);
}
if (!function_exists('aksrt_is_comment_liked') || !function_exists('aksrt_set_comment_like_state') || !function_exists('aksrt_update_comment_counter')) {
return new \WP_REST_Response([
'success' => false,
'code' => 'like_unavailable',
'message' => __('点赞功能不可用。', 'aksrt'),
], 500);
}
$current = aksrt_is_comment_liked($comment_id);
$target = null !== $request->get_param('active')
? (bool) $request->get_param('active')
: !$current;
if ($target !== $current) {
aksrt_set_comment_like_state($comment_id, $target);
$count = aksrt_update_comment_counter($comment_id, 'aksrt_comment_likes', $target ? 1 : -1);
if (is_user_logged_in()) {
do_action('aksrt_comment_liked', $comment_id, get_current_user_id(), (bool) $target);
}
} else {
$count = aksrt_get_comment_likes($comment_id);
}
return new \WP_REST_Response([
'success' => true,
'code' => 'liked',
'data' => [
'active' => $target,
'count' => $count,
],
]);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class DBMigrator {
use DBMigratorPart2;
use DBMigratorPart3;
use DBMigratorIdentity;
/** Current schema version; changing it re-runs dbDelta. */
const SCHEMA_VERSION = '1.17.0';
const OPT_KEY = 'aksrt_db_schema_version';
/**
* Register hooks.
*/
public static function init(): void {
add_action('after_switch_theme', [self::class, 'maybe_migrate']);
add_action('admin_init', [self::class, 'maybe_migrate']);
}
/**
* Run table schema upgrade when version mismatches.
*/
public static function maybe_migrate(): void {
$current = get_option(self::OPT_KEY, '');
if ($current === self::SCHEMA_VERSION) {
return;
}
self::run();
update_option(self::OPT_KEY, self::SCHEMA_VERSION);
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
/**
* Identity verification tables.
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
trait DBMigratorIdentity {
private static function migrate_identity_tables(string $prefix, string $charset_collate): void {
$application = "CREATE TABLE {$prefix}aksrt_identity_application (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
application_uuid CHAR(36) NOT NULL,
client_request_id CHAR(36) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
type VARCHAR(20) NOT NULL,
channel VARCHAR(20) NOT NULL DEFAULT 'cloud',
provider VARCHAR(20) NOT NULL DEFAULT '',
provider_flow VARCHAR(80) NOT NULL DEFAULT '',
provider_session_id VARCHAR(191) NOT NULL DEFAULT '',
provider_request_id VARCHAR(191) NOT NULL DEFAULT '',
redirect_url TEXT DEFAULT NULL,
qr_payload TEXT DEFAULT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'created',
public_name VARCHAR(64) NOT NULL DEFAULT '',
public_desc VARCHAR(300) NOT NULL DEFAULT '',
person_name_masked VARCHAR(64) NOT NULL DEFAULT '',
id_number_masked VARCHAR(32) NOT NULL DEFAULT '',
id_number_hash CHAR(64) NOT NULL DEFAULT '',
company_name VARCHAR(191) NOT NULL DEFAULT '',
company_code_masked VARCHAR(32) NOT NULL DEFAULT '',
company_code_hash CHAR(64) NOT NULL DEFAULT '',
sensitive_blob LONGTEXT DEFAULT NULL,
provider_result_code VARCHAR(128) NOT NULL DEFAULT '',
provider_result_summary TEXT DEFAULT NULL,
manual_reason TEXT DEFAULT NULL,
review_note TEXT DEFAULT NULL,
reviewer_id BIGINT UNSIGNED DEFAULT NULL,
evidence_ids TEXT DEFAULT NULL,
fee_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
fee_status VARCHAR(20) NOT NULL DEFAULT 'not_required',
attempt_ordinal INT UNSIGNED NOT NULL DEFAULT 0,
retry_count INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
provider_started_at DATETIME DEFAULT NULL,
completed_at DATETIME DEFAULT NULL,
expires_at DATETIME DEFAULT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_application_uuid (application_uuid),
UNIQUE KEY uniq_user_client_request (user_id, client_request_id),
KEY idx_user_created (user_id, created_at),
KEY idx_user_type_status (user_id, type, status),
KEY idx_provider_session (provider, provider_session_id),
KEY idx_status_updated (status, updated_at),
KEY idx_id_number_hash (id_number_hash),
KEY idx_company_code_hash (company_code_hash)
) {$charset_collate};";
dbDelta($application);
$event = "CREATE TABLE {$prefix}aksrt_identity_event (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
application_id BIGINT UNSIGNED NOT NULL,
event_uuid CHAR(36) NOT NULL,
source VARCHAR(20) NOT NULL,
event_type VARCHAR(64) NOT NULL,
external_event_id VARCHAR(191) NOT NULL DEFAULT '',
dedupe_key CHAR(64) NOT NULL,
from_status VARCHAR(32) NOT NULL DEFAULT '',
to_status VARCHAR(32) NOT NULL DEFAULT '',
result_code VARCHAR(128) NOT NULL DEFAULT '',
summary_json LONGTEXT DEFAULT NULL,
actor_user_id BIGINT UNSIGNED DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_event_uuid (event_uuid),
UNIQUE KEY uniq_dedupe_key (dedupe_key),
KEY idx_application_created (application_id, created_at),
KEY idx_source_external (source, external_event_id)
) {$charset_collate};";
dbDelta($event);
}
}
+390
View File
@@ -0,0 +1,390 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
trait DBMigratorPart2 {
/**
* 立即运行(供激活钩子直接调用)。
*/
public static function run(): void {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset_collate = $wpdb->get_charset_collate();
$prefix = $wpdb->prefix;
// ---------- 1. 积分流水 ----------
$sql_credit_log = "CREATE TABLE {$prefix}aksrt_credit_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
delta INT NOT NULL,
balance_after INT NOT NULL,
reason VARCHAR(64) NOT NULL,
remark VARCHAR(255) DEFAULT NULL,
ref_type VARCHAR(32) DEFAULT NULL,
ref_id BIGINT UNSIGNED DEFAULT NULL,
ip VARCHAR(64) DEFAULT NULL,
ua VARCHAR(255) DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_user_time (user_id, created_at),
KEY idx_reason (reason)
) {$charset_collate};";
dbDelta($sql_credit_log);
// ---------- 1b. 现金余额流水(站内钱包,与积分分离) ----------
$sql_balance_log = "CREATE TABLE {$prefix}aksrt_balance_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
delta DECIMAL(18,2) NOT NULL,
balance_after DECIMAL(18,2) NOT NULL,
reason VARCHAR(64) NOT NULL,
remark VARCHAR(255) DEFAULT NULL,
ref_type VARCHAR(32) DEFAULT NULL,
ref_id BIGINT UNSIGNED DEFAULT NULL,
ip VARCHAR(64) DEFAULT NULL,
ua VARCHAR(255) DEFAULT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_user_time (user_id, created_at),
KEY idx_reason (reason)
) {$charset_collate};";
dbDelta($sql_balance_log);
// ---------- 2. 签到 ----------
$sql_signin = "CREATE TABLE {$prefix}aksrt_signin_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
signin_date DATE NOT NULL,
streak INT NOT NULL DEFAULT 1,
reward INT NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY uniq_user_date (user_id, signin_date),
KEY idx_user (user_id)
) {$charset_collate};";
dbDelta($sql_signin);
// ---------- 3. 邀请 ----------
$sql_invite = "CREATE TABLE {$prefix}aksrt_invite (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
inviter_id BIGINT UNSIGNED NOT NULL,
invitee_id BIGINT UNSIGNED DEFAULT NULL,
invite_code VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL,
rewarded_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_code (invite_code),
KEY idx_inviter (inviter_id),
KEY idx_invitee (invitee_id),
KEY idx_status (status)
) {$charset_collate};";
dbDelta($sql_invite);
// ---------- 4. 下载 token ----------
$sql_dl_token = "CREATE TABLE {$prefix}aksrt_dl_token (
token CHAR(48) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
post_id BIGINT UNSIGNED NOT NULL,
source_index INT NOT NULL DEFAULT 0,
cost INT NOT NULL DEFAULT 0,
ip_bind VARCHAR(64) DEFAULT NULL,
ua_hash CHAR(40) DEFAULT NULL,
used_count INT NOT NULL DEFAULT 0,
max_uses INT NOT NULL DEFAULT 1,
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (token),
KEY idx_expires (expires_at),
KEY idx_user (user_id),
KEY idx_post (post_id)
) {$charset_collate};";
dbDelta($sql_dl_token);
// ---------- 5. 用户通知收件箱 ----------
$sql_notification = "CREATE TABLE {$prefix}aksrt_notification (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
type VARCHAR(32) NOT NULL,
title VARCHAR(191) NOT NULL,
description TEXT DEFAULT NULL,
target_url TEXT DEFAULT NULL,
is_read TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
read_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_user_created (user_id, created_at),
KEY idx_user_read (user_id, is_read)
) {$charset_collate};";
dbDelta($sql_notification);
// ---------- 6. 私信会话 ----------
$sql_conversation = "CREATE TABLE {$prefix}aksrt_conversation (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_a BIGINT UNSIGNED NOT NULL,
user_b BIGINT UNSIGNED NOT NULL,
last_message VARCHAR(255) DEFAULT '',
last_message_at DATETIME DEFAULT NULL,
unread_a INT NOT NULL DEFAULT 0,
unread_b INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_pair (user_a, user_b),
KEY idx_user_a (user_a),
KEY idx_user_b (user_b),
KEY idx_last_msg (last_message_at)
) {$charset_collate};";
dbDelta($sql_conversation);
// ---------- 7. 私信消息 ----------
$sql_conv_message = "CREATE TABLE {$prefix}aksrt_conversation_message (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
conversation_id BIGINT UNSIGNED NOT NULL,
sender_id BIGINT UNSIGNED NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_conv_time (conversation_id, created_at),
KEY idx_sender (sender_id)
) {$charset_collate};";
dbDelta($sql_conv_message);
// ---------- 8. 订单表 ----------
$sql_order = "CREATE TABLE {$prefix}aksrt_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(32) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
product_type VARCHAR(32) NOT NULL DEFAULT '',
product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
currency VARCHAR(8) NOT NULL DEFAULT 'CNY',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
shipping_status TINYINT NOT NULL DEFAULT 0,
payment_method VARCHAR(32) NOT NULL DEFAULT '',
payment_transaction_id VARCHAR(128) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL,
paid_at DATETIME DEFAULT NULL,
delivered_at DATETIME DEFAULT NULL,
completed_at DATETIME DEFAULT NULL,
cancelled_at DATETIME DEFAULT NULL,
refunded_at DATETIME DEFAULT NULL,
expires_at DATETIME DEFAULT NULL,
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_order_number (order_number),
KEY idx_user (user_id),
KEY idx_status (status),
KEY idx_created (created_at),
KEY idx_product (product_type, product_id)
) {$charset_collate};";
dbDelta($sql_order);
// ---------- 9. 收益台账 ----------
$sql_revenue = "CREATE TABLE {$prefix}aksrt_revenue (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
order_number VARCHAR(32) NOT NULL DEFAULT '',
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
type VARCHAR(32) NOT NULL DEFAULT 'income',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL,
settled_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_user (user_id),
KEY idx_order (order_number),
KEY idx_status (status),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_revenue);
// ---------- 10. 提现台账 ----------
$sql_withdrawal = "CREATE TABLE {$prefix}aksrt_withdrawal (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
method VARCHAR(32) NOT NULL DEFAULT '',
account_info TEXT DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL,
processed_at DATETIME DEFAULT NULL,
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_user (user_id),
KEY idx_status (status),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_withdrawal);
// ---------- 11. 举报工单 ----------
$sql_report = "CREATE TABLE {$prefix}aksrt_report (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
reporter_id BIGINT UNSIGNED NOT NULL,
target_type VARCHAR(20) NOT NULL,
target_id BIGINT UNSIGNED NOT NULL,
reason TEXT NOT NULL,
category VARCHAR(40) NOT NULL DEFAULT '',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL,
updated_at DATETIME DEFAULT NULL,
notes_json TEXT DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_status (status),
KEY idx_target (target_type, target_id),
KEY idx_reporter (reporter_id),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_report);
// ---------- 12. 用户限制/封禁 ----------
$sql_restriction = "CREATE TABLE {$prefix}aksrt_user_restriction (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
type VARCHAR(20) NOT NULL DEFAULT 'warn',
reason TEXT NOT NULL,
scope VARCHAR(20) NOT NULL DEFAULT 'global',
expires_at DATETIME DEFAULT NULL,
created_by BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_user (user_id),
KEY idx_type (type),
KEY idx_scope (scope),
KEY idx_expires (expires_at),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_restriction);
// ── follow ──
$sql_follow = "CREATE TABLE {$prefix}aksrt_follow (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
follower_id BIGINT UNSIGNED NOT NULL,
following_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (id),
UNIQUE KEY idx_pair (follower_id, following_id),
KEY idx_following (following_id),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_follow);
// ── coupon ──
$sql_coupon = "CREATE TABLE {$prefix}aksrt_coupon (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(60) NOT NULL DEFAULT '',
type VARCHAR(20) NOT NULL DEFAULT 'fixed',
value DECIMAL(10,2) NOT NULL DEFAULT 0,
min_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
usage_limit INT UNSIGNED NOT NULL DEFAULT 0,
used_count INT UNSIGNED NOT NULL DEFAULT 0,
expires_at DATETIME DEFAULT NULL,
per_user_limit INT UNSIGNED NOT NULL DEFAULT 0,
product_scope TEXT DEFAULT NULL,
max_discount DECIMAL(10,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (id),
UNIQUE KEY idx_code (code),
KEY idx_status (status),
KEY idx_expires (expires_at)
) {$charset_collate};";
dbDelta($sql_coupon);
// ── coupon_usage (R68) ──
$sql_coupon_usage = "CREATE TABLE {$prefix}aksrt_coupon_usage (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
coupon_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
order_number VARCHAR(32) NOT NULL DEFAULT '',
discount DECIMAL(10,2) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (id),
KEY idx_coupon (coupon_id),
KEY idx_user (user_id),
KEY idx_order (order_number)
) {$charset_collate};";
dbDelta($sql_coupon_usage);
// ── charge_card (zibll-style typed card_pass) ──
$sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
card VARCHAR(60) NOT NULL DEFAULT '',
password VARCHAR(80) NOT NULL DEFAULT '',
type VARCHAR(20) NOT NULL DEFAULT 'balance',
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
balance DECIMAL(10,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
meta LONGTEXT DEFAULT NULL,
order_number VARCHAR(32) NOT NULL DEFAULT '',
used_by BIGINT UNSIGNED DEFAULT NULL,
used_at DATETIME DEFAULT NULL,
expires_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_password (password),
KEY idx_card (card),
KEY idx_type (type),
KEY idx_status (status),
KEY idx_used_by (used_by),
KEY idx_order_number (order_number),
KEY idx_expires (expires_at)
) {$charset_collate};";
dbDelta($sql_charge_card);
// ── rebate_log ──
$sql_rebate_log = "CREATE TABLE {$prefix}aksrt_rebate_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
order_number VARCHAR(32) NOT NULL DEFAULT '',
source_user_id BIGINT UNSIGNED DEFAULT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
type VARCHAR(32) NOT NULL DEFAULT 'order',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
settled_at DATETIME DEFAULT NULL,
note TEXT DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_user (user_id),
KEY idx_order (order_number),
KEY idx_source (source_user_id),
KEY idx_status (status),
KEY idx_type (type),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_rebate_log);
// ── order_items (§6.5 多商品多商家) ──
$sql_order_items = "CREATE TABLE {$prefix}aksrt_order_item (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(32) NOT NULL DEFAULT '',
product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
product_title VARCHAR(255) NOT NULL DEFAULT '',
options_key VARCHAR(255) NOT NULL DEFAULT '',
options_label VARCHAR(255) NOT NULL DEFAULT '',
quantity INT UNSIGNED NOT NULL DEFAULT 1,
unit_price DECIMAL(10,2) NOT NULL DEFAULT 0,
total_price DECIMAL(10,2) NOT NULL DEFAULT 0,
discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
shipping_fee DECIMAL(10,2) NOT NULL DEFAULT 0,
pay_price DECIMAL(10,2) NOT NULL DEFAULT 0,
shipping_type VARCHAR(20) NOT NULL DEFAULT 'express',
auto_delivery_content TEXT DEFAULT NULL,
discount_hit TEXT DEFAULT NULL,
gift_data TEXT DEFAULT NULL,
image VARCHAR(512) NOT NULL DEFAULT '',
PRIMARY KEY (id),
KEY idx_order (order_number),
KEY idx_product (product_id)
) {$charset_collate};";
dbDelta($sql_order_items);
self::migrate_identity_tables($prefix, $charset_collate);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
trait DBMigratorPart3 {
/**
* 表名访问器(其它模块通过此函数取,避免散落硬编码)。
*/
public static function table(string $name): string {
global $wpdb;
$allowed = [
'credit_log' => 'aksrt_credit_log',
'balance_log' => 'aksrt_balance_log',
'signin_log' => 'aksrt_signin_log',
'invite' => 'aksrt_invite',
'dl_token' => 'aksrt_dl_token',
'notification' => 'aksrt_notification',
'conversation' => 'aksrt_conversation',
'conv_message' => 'aksrt_conversation_message',
'order' => 'aksrt_order',
'order_item' => 'aksrt_order_item',
'revenue' => 'aksrt_revenue',
'withdrawal' => 'aksrt_withdrawal',
'report' => 'aksrt_report',
'follow' => 'aksrt_follow',
'coupon' => 'aksrt_coupon',
'charge_card' => 'aksrt_charge_card',
'rebate_log' => 'aksrt_rebate_log',
'coupon_usage' => 'aksrt_coupon_usage',
'user_restriction' => 'aksrt_user_restriction',
'identity_application' => 'aksrt_identity_application',
'identity_event' => 'aksrt_identity_event',
];
if (!isset($allowed[$name])) {
return '';
}
return $wpdb->prefix . $allowed[$name];
}
}
+216
View File
@@ -0,0 +1,216 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class DocsPresenter {
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get the document root page ID for a given page.
*
* @param int $page_id Current page ID.
* @return int Root page ID (self if top-level, parent otherwise).
*/
public function get_root_id(int $page_id): int {
$parent_id = wp_get_post_parent_id($page_id);
return $parent_id ?: $page_id;
}
/**
* Build a flat ordered list of all pages under a document root.
*
* @param int $root_id Root page ID.
* @param int $max_depth Maximum depth (default 4).
* @return array Array of page objects with added '_depth' property.
*/
public function get_flat_tree(int $root_id, int $max_depth = 4): array {
$result = [];
$this->collect_children($root_id, $result, 0, $max_depth);
return $result;
}
/**
* Recursive helper to collect pages in order.
*/
private function collect_children(int $parent_id, array &$result, int $depth, int $max_depth): void {
if ($depth >= $max_depth) {
return;
}
$children = get_pages([
'parent' => $parent_id,
'sort_column' => 'menu_order, post_title',
'sort_order' => 'ASC',
'post_status' => 'publish',
]);
foreach ($children as $child) {
$child->_depth = $depth;
$result[] = $child;
$this->collect_children($child->ID, $result, $depth + 1, $max_depth);
}
}
/**
* Get sibling pages for chapter navigation (prev/next).
*
* @param int $page_id Current page ID.
* @return array{prev: object|null, next: object|null}
*/
public function get_chapter_nav(int $page_id): array {
$parent_id = wp_get_post_parent_id($page_id);
$siblings = get_pages([
'parent' => $parent_id ?: 0,
'sort_column' => 'menu_order, post_title',
'sort_order' => 'ASC',
'post_status' => 'publish',
]);
$current_index = -1;
foreach ($siblings as $i => $sibling) {
if ((int) $sibling->ID === $page_id) {
$current_index = $i;
break;
}
}
$prev = $current_index > 0 ? $siblings[$current_index - 1] : null;
$next = ($current_index >= 0 && $current_index < count($siblings) - 1)
? $siblings[$current_index + 1]
: null;
return ['prev' => $prev, 'next' => $next];
}
/**
* Get root-level document pages for the document navigation index.
*
* @param string $order Sort order: 'menu_order', 'date', 'title'.
* @return array
*/
public function get_root_pages(string $order = 'menu_order'): array {
$sort_column = 'menu_order, post_title';
if ($order === 'title') {
$sort_column = 'post_title';
} elseif ($order === 'date') {
$sort_column = 'post_date';
}
return get_pages([
'sort_column' => $sort_column,
'sort_order' => 'ASC',
'parent' => 0,
'hierarchical' => 0,
'post_status' => 'publish',
]);
}
/**
* Get recently modified pages for the sidebar.
*
* @param int $count Number of pages to return.
* @return array
*/
public function get_recent_pages(int $count = 8): array {
return get_pages([
'sort_column' => 'post_modified',
'sort_order' => 'DESC',
'number' => $count,
'post_status' => 'publish',
]);
}
/**
* Build breadcrumb trail for a document page.
*
* @param int $page_id Current page ID.
* @return array Array of {id, title, url} objects.
*/
public function get_breadcrumbs(int $page_id): array {
$trail = [];
$current = get_post($page_id);
while ($current) {
array_unshift($trail, [
'id' => $current->ID,
'title' => $current->post_title,
'url' => get_permalink($current->ID),
]);
$parent_id = wp_get_post_parent_id($current->ID);
$current = $parent_id ? get_post($parent_id) : null;
}
return $trail;
}
/**
* Search document pages by keyword.
*
* @param string $keyword Search keyword.
* @param int $root_id Optional root page ID to scope search.
* @param int $limit Max results.
* @return array Array of page objects with 'url' and 'excerpt' added.
*/
public function search_pages(string $keyword, int $root_id = 0, int $limit = 20): array {
$args = [
'post_type' => 'page',
'post_status' => 'publish',
's' => $keyword,
'posts_per_page' => $limit,
'orderby' => 'relevance',
];
if ($root_id > 0) {
$tree_ids = [$root_id];
$this->collect_children($root_id, $tree_ids, 0, 4);
$ids = array_map(static function ($item) {
return is_object($item) ? $item->ID : (int) $item;
}, $tree_ids);
$ids[] = $root_id;
$args['post__in'] = array_unique($ids);
}
$query = new \WP_Query($args);
$results = [];
foreach ($query->posts as $post) {
$results[] = [
'id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink($post->ID),
'excerpt' => wp_trim_words(wp_strip_all_tags($post->post_content), 30),
'modified' => $post->post_modified,
'parent' => (int) $post->post_parent,
];
}
wp_reset_postdata();
return $results;
}
/**
* Check whether a given page belongs to the docs mode.
*
* @param int $page_id Page ID.
* @return bool
*/
public function is_docs_page(int $page_id): bool {
return false;
}
}
+316
View File
@@ -0,0 +1,316 @@
<?php
/**
* Document navigation (zibll documentnav) — category tree + AJAX posts.
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class DocumentNav {
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_action('wp_ajax_aksrt_documentnav_posts', [$this, 'ajax_posts']);
add_action('wp_ajax_nopriv_aksrt_documentnav_posts', [$this, 'ajax_posts']);
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_page', [$this, 'save_meta_box']);
}
/**
* @return array{cat:int,initial_content:string}
*/
public static function get_options(int $post_id): array {
$raw = get_post_meta($post_id, 'documentnav_options', true);
if (!is_array($raw)) {
$raw = [];
}
$cat = isset($raw['cat']) ? absint($raw['cat']) : absint(get_post_meta($post_id, 'documentnav_cat', true));
$initial = isset($raw['initial_content']) ? sanitize_key((string) $raw['initial_content']) : sanitize_key((string) get_post_meta($post_id, 'documentnav_initial_content', true));
if (!in_array($initial, ['page_content', 'date_posts', 'updated_posts', 'views_posts'], true)) {
$initial = 'updated_posts';
}
return [
'cat' => $cat,
'initial_content' => $initial,
];
}
/**
* Render posts list HTML (zibll zib_get_documentnav_posts).
*/
public static function render_posts_html(\WP_Query $query): string {
if (!$query->have_posts()) {
$empty = function_exists('aksrt_get_null')
? aksrt_get_null(__('暂无相应内容', 'aksrt'), 40)
: '<p class="muted-2-color text-center">' . esc_html__('暂无相应内容', 'aksrt') . '</p>';
return '<div class="zib-widget">' . $empty . '</div>';
}
$html = '<div class="list-group main-shadow">';
while ($query->have_posts()) {
$query->the_post();
$time_ago = human_time_diff(get_the_time('U'), current_time('timestamp')) . __('前', 'aksrt');
$subtitle = (string) get_post_meta(get_the_ID(), 'subtitle', true);
if ($subtitle === '') {
$subtitle = (string) get_post_meta(get_the_ID(), 'aksrt_subtitle', true);
}
$html .= '<a class="list-group-item" href="' . esc_url(get_permalink()) . '">';
$html .= '<span class="badg badg-sm pull-right" title="' . esc_attr(get_the_time('Y-m-d H:i:s')) . '">' . esc_html($time_ago) . '</span>';
$html .= esc_html(get_the_title());
if ($subtitle !== '') {
$html .= '<span class="focus-color"> ' . esc_html($subtitle) . '</span>';
}
$html .= '</a>';
}
$html .= '</div>';
wp_reset_postdata();
return $html;
}
/**
* Simple paginate links for document nav container.
*/
public static function render_paginate(int $total, int $current, int $page_size, array $args = []): string {
$max = (int) ceil(max(0, $total) / max(1, $page_size));
if ($max <= 1) {
return '';
}
$current = max(1, min($current, $max));
$html = '<div class="pagenav notop text-center mt15">';
for ($i = 1; $i <= $max; $i++) {
$url = add_query_arg(array_merge($args, ['paged' => $i]));
$cls = $i === $current ? 'current' : '';
$html .= '<a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '" data-paged="' . (int) $i . '">' . (int) $i . '</a> ';
}
$html .= '</div>';
return $html;
}
/**
* Recursive category accordion HTML.
*/
public static function render_panels(int $cat_id, string $data_parent = 'accordion', string $class = 'panel', bool &$is_on_open = true): string {
if ($cat_id <= 0) {
return '';
}
$terms = _get_term_hierarchy('category');
if (empty($terms[$cat_id]) || !is_array($terms[$cat_id])) {
return '';
}
$html = '<div class="panel-group posts-panel-group" id="' . esc_attr($data_parent) . '">';
foreach ($terms[$cat_id] as $child) {
$child = (int) $child;
if ($child === $cat_id) {
continue;
}
$cat_obj = get_category($child);
if (!$cat_obj || is_wp_error($cat_obj)) {
continue;
}
$has_kids = !empty($terms[$child]);
if ($has_kids) {
$open = $is_on_open;
$is_on_open = false;
$html .= '<div class="' . esc_attr($class) . '">';
$html .= '<h4 class="panel-title">';
$html .= '<a class="panel-toggle' . ($open ? '' : ' collapsed') . ' text-ellipsis" href="javascript:;" data-toggle="collapse" data-target="#dosc-nav-catid_' . $child . '" data-parent="#' . esc_attr($data_parent) . '">';
$html .= '<i class="fa fa-angle-right mr10" aria-hidden="true"></i>' . esc_html($cat_obj->name);
$html .= '</a></h4>';
$html .= '<div id="dosc-nav-catid_' . $child . '" class="panel-collapse' . ($open ? ' collapse in is-open' : ' collapse') . '">';
$html .= '<div class="category">';
$html .= self::render_panels($child, 'dosc-cat-nav-panel-' . $child, 'panel panel-child', $is_on_open);
$html .= '</div></div></div>';
} else {
$html .= '<a class="cat-load text-ellipsis" data-cat-id="' . $child . '" href="javascript:;" title="' . esc_attr($cat_obj->name) . '">';
$html .= '<i class="loading" aria-hidden="true"></i>' . esc_html($cat_obj->name);
$html .= '</a>';
}
}
$html .= '</div>';
return $html;
}
public function ajax_posts(): void {
$search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash((string) $_REQUEST['search'])) : '';
$cat_id = isset($_REQUEST['cat_id']) ? absint($_REQUEST['cat_id']) : 0;
$one_cat = isset($_REQUEST['one_cat']) ? absint($_REQUEST['one_cat']) : 0;
$paged = isset($_REQUEST['paged']) ? max(1, absint($_REQUEST['paged'])) : 1;
$page_size = 12;
if ($search === '' && $cat_id <= 0) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('参数传入错误!', 'aksrt')]);
}
$args = [
'ignore_sticky_posts' => true,
'paged' => $paged,
'posts_per_page' => $page_size,
'post_status' => 'publish',
'post_type' => 'post',
];
if ($search !== '') {
$args['s'] = $search;
$args['cat'] = $one_cat > 0 ? $one_cat : 0;
// Persist simple history (user meta or cookie-less option list is optional).
$this->remember_search($search);
} else {
$args['cat'] = $cat_id;
}
$query = new \WP_Query($args);
$html = self::render_posts_html($query);
$html .= self::render_paginate(
(int) $query->found_posts,
$paged,
$page_size,
[
'action' => 'aksrt_documentnav_posts',
'cat_id' => $cat_id,
'search' => $search,
'one_cat' => $one_cat,
]
);
wp_send_json([
'error' => 0,
'html' => $html,
'total' => (int) $query->found_posts,
'history_search' => $this->history_buttons_html(),
]);
}
private function remember_search(string $term): void {
$term = trim($term);
if ($term === '' || mb_strlen($term) < 2) {
return;
}
$key = is_user_logged_in() ? 'aksrt_doc_search_history_' . get_current_user_id() : '';
$list = [];
if ($key !== '') {
$saved = get_user_meta(get_current_user_id(), 'aksrt_doc_search_history', true);
$list = is_array($saved) ? $saved : [];
} else {
$cookie = isset($_COOKIE['aksrt_doc_search_history']) ? wp_unslash((string) $_COOKIE['aksrt_doc_search_history']) : '';
$decoded = json_decode($cookie, true);
$list = is_array($decoded) ? $decoded : [];
}
$list = array_values(array_unique(array_filter(array_map('strval', $list))));
array_unshift($list, $term);
$list = array_slice(array_values(array_unique($list)), 0, 8);
if ($key !== '') {
update_user_meta(get_current_user_id(), 'aksrt_doc_search_history', $list);
} elseif (!headers_sent()) {
setcookie('aksrt_doc_search_history', wp_json_encode($list), time() + WEEK_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
}
}
/**
* @return list<string>
*/
private function get_history(): array {
if (is_user_logged_in()) {
$saved = get_user_meta(get_current_user_id(), 'aksrt_doc_search_history', true);
return is_array($saved) ? array_values(array_filter(array_map('strval', $saved))) : [];
}
$cookie = isset($_COOKIE['aksrt_doc_search_history']) ? wp_unslash((string) $_COOKIE['aksrt_doc_search_history']) : '';
$decoded = json_decode($cookie, true);
return is_array($decoded) ? array_values(array_filter(array_map('strval', $decoded))) : [];
}
public function history_buttons_html(): string {
$items = $this->get_history();
if (!$items) {
return '';
}
$html = '';
foreach ($items as $term) {
$html .= '<a href="javascript:;" class="search_keywords but badg mr6 mb6">' . esc_html($term) . '</a>';
}
return $html;
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_documentnav_meta',
__('文档导航设置', 'aksrt'),
[$this, 'render_meta_box'],
'page',
'side',
'default'
);
}
public function render_meta_box(\WP_Post $post): void {
$tpl = (string) get_post_meta($post->ID, '_wp_page_template', true);
if ($tpl !== 'templates/pages/page-documentnav.php') {
echo '<p class="description">' . esc_html__('将模板设为「AKSRT - 文档导航」后配置。', 'aksrt') . '</p>';
return;
}
$opt = self::get_options((int) $post->ID);
wp_nonce_field('aksrt_documentnav_meta', 'aksrt_documentnav_meta_nonce');
$cats = get_categories(['hide_empty' => false, 'parent' => 0]);
?>
<p>
<label for="documentnav_cat"><strong><?php esc_html_e('一级分类', 'aksrt'); ?></strong></label>
<select name="documentnav_cat" id="documentnav_cat" class="widefat">
<option value="0"><?php esc_html_e('请选择', 'aksrt'); ?></option>
<?php foreach ($cats as $c) : ?>
<option value="<?php echo (int) $c->term_id; ?>" <?php selected($opt['cat'], (int) $c->term_id); ?>>
<?php echo esc_html($c->name); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="documentnav_initial_content"><strong><?php esc_html_e('初始内容', 'aksrt'); ?></strong></label>
<select name="documentnav_initial_content" id="documentnav_initial_content" class="widefat">
<?php
$opts = [
'page_content' => __('显示页面内容', 'aksrt'),
'date_posts' => __('最近发布文章', 'aksrt'),
'updated_posts' => __('最近更新文章', 'aksrt'),
'views_posts' => __('查看最多文章', 'aksrt'),
];
foreach ($opts as $k => $label) :
?>
<option value="<?php echo esc_attr($k); ?>" <?php selected($opt['initial_content'], $k); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</p>
<?php
}
public function save_meta_box(int $post_id): void {
if (!isset($_POST['aksrt_documentnav_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_documentnav_meta_nonce'])), 'aksrt_documentnav_meta')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$cat = isset($_POST['documentnav_cat']) ? absint($_POST['documentnav_cat']) : 0;
$initial = isset($_POST['documentnav_initial_content']) ? sanitize_key((string) wp_unslash($_POST['documentnav_initial_content'])) : 'updated_posts';
if (!in_array($initial, ['page_content', 'date_posts', 'updated_posts', 'views_posts'], true)) {
$initial = 'updated_posts';
}
update_post_meta($post_id, 'documentnav_options', [
'cat' => $cat,
'initial_content' => $initial,
]);
}
}
+461
View File
@@ -0,0 +1,461 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class DomainModel {
public const POST_META_KEY = 'aksrt_post_meta';
public const TERM_META_KEY = 'aksrt_term_meta';
public const PAY_META_KEY = '_aksrt_pay';
public const PAY_TYPE_KEY = '_aksrt_pay_type';
public const PAY_PRICE_KEY = '_aksrt_pay_price';
public const RESOURCE_MARKER_KEY = '_aksrt_is_resource';
public const RESOURCE_DOWNLOAD_COUNT_KEY = '_aksrt_res_download_count';
public const RESOURCE_VIEW_COUNT_KEY = '_aksrt_res_view_count';
public const RESOURCE_SALES_VOLUME_KEY = '_aksrt_sales_volume';
public const POST_VIEW_COUNT_KEY = 'aksrt_post_views';
public const ORDER_MIGRATED_OPTION = 'aksrt_order_migrated_to_table';
public const RESOURCE_PAY_META_MIGRATED = 'aksrt_resource_pay_meta_migrated';
public const ORDER_PRODUCT_RESOURCE = 'resource';
public const ORDER_PRODUCT_VIP = 'vip';
public const ORDER_PRODUCT_RECHARGE = 'recharge';
/** Cash purchase of points (zibll order_type=9). */
public const ORDER_PRODUCT_POINTS_BUY = 'points_buy';
/**
* Human labels for aksrt_order.product_type (front-end order center).
*
* @return array<string, string>
*/
public static function order_product_type_labels(): array {
return [
self::ORDER_PRODUCT_RESOURCE => __('付费内容', 'aksrt'),
self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'),
self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'),
self::ORDER_PRODUCT_POINTS_BUY => __('购买积分', 'aksrt'),
];
}
public const PAY_TYPE_NO = 'no';
public const PAY_TYPE_READING = 'reading';
public const PAY_TYPE_DOWNLOAD = 'download';
public const PAY_TYPE_GALLERY = 'gallery';
public const PAY_TYPE_VIDEO = 'video';
public const PAY_TYPE_VIP = 'vip';
public const ORDER_STATUS_CREATED = 'created';
public const ORDER_STATUS_PENDING = 'pending';
public const ORDER_STATUS_PAID = 'paid';
public const ORDER_STATUS_DELIVERED = 'delivered';
public const ORDER_STATUS_COMPLETED = 'completed';
public const ORDER_STATUS_CANCELLED = 'cancelled';
public const ORDER_STATUS_EXPIRED = 'expired';
public const ORDER_STATUS_REFUNDING = 'refunding';
public const ORDER_STATUS_REFUNDED = 'refunded';
public const ORDER_STATUS_PENDING_REVIEW = 'pending_review';
public const SHIPPING_STATUS_PENDING = 0;
public const SHIPPING_STATUS_SHIPPED = 1;
public const SHIPPING_STATUS_COMPLETED = 2;
/**
* @return array<string, mixed>
*/
public static function default_post_meta(): array {
return [
'cover_images' => '',
'custom_excerpt' => '',
'layout_override' => '',
'header_image_mode' => '',
'show_toc' => '',
'show_reading_progress' => '',
'enable_lightbox' => '',
'show_related' => '',
'related_count' => 0,
'enable_like' => '',
'enable_favorite' => '',
'enable_share' => '',
'enable_print' => '',
'enable_pdf' => '',
'copyright_text' => '',
'is_featured' => false,
// §3.8 文章扩展字段
'thumbnail_url' => '',
'featured_slide' => '',
'featured_video' => '',
// Episode list: [ ['url' => string, 'title' => string], ... ]
'featured_video_episode' => [],
'subtitle' => '',
'views' => 0,
'likes' => 0,
'maxheight' => 0,
];
}
/**
* @return array<string, mixed>
*/
public static function default_term_meta(): array {
return [
'cover_url' => '',
'description' => '',
'layout_mode' => '',
'show_sidebar' => '',
'show_header' => true,
'show_featured' => false,
];
}
/**
* @return array<string, mixed>
*/
public static function default_pay_meta(): array {
return [
'pay_type' => self::PAY_TYPE_NO,
'pay_modo' => '0', // '0' cash | 'points' credits (Zibll)
'price' => 0.0,
'points_price' => 0,
'original_price' => 0.0,
'vip1_price' => 0.0,
'vip2_price' => 0.0,
'vip1_points' => 0,
'vip2_points' => 0,
'pay_limit' => 0, // 0 all | 1 VIP1+ | 2 VIP2 only (Zibll)
'version' => '',
'size' => '',
'license' => '',
'compat' => '',
'demo_url' => '',
'video_url' => '',
'downloads' => [],
'attributes' => [],
'gallery' => [],
'require_login' => false,
'require_captcha' => false,
// §3.8 电商增强字段
'virtual_sales' => 0,
'stock' => -1,
'card_keys' => [],
'purchase_limit' => 0,
'free_preview' => '',
'show_sales' => true,
// Zibll: free daily quota overbuy price (cash or points by pay_modo).
'download_limit_over_price' => 0.0,
// Content cashier: coupon + buyer-facing 推广折扣 (zibll posts_zibpay).
'coupon_s' => false,
'coupon_desc' => '',
'pay_rebate_discount' => 0.0,
];
}
/**
* @return array<string, string>
*/
public static function pay_type_labels(): array {
return [
self::PAY_TYPE_NO => '不付费',
self::PAY_TYPE_READING => '付费阅读',
self::PAY_TYPE_DOWNLOAD => '付费下载',
self::PAY_TYPE_GALLERY => '付费图库',
self::PAY_TYPE_VIDEO => '付费视频',
self::PAY_TYPE_VIP => 'VIP 专属',
];
}
public static function normalize_pay_type(string $pay_type): string {
$pay_type = sanitize_key($pay_type);
return array_key_exists($pay_type, self::pay_type_labels()) ? $pay_type : self::PAY_TYPE_NO;
}
public static function is_resource_pay_type(string $pay_type): bool {
return self::normalize_pay_type($pay_type) === self::PAY_TYPE_DOWNLOAD;
}
/**
* @param mixed $value
* @return array<int, array<string, string>>
*/
public static function sanitize_downloads($value): array {
if (!is_array($value)) {
return [];
}
$allowed_types = ['direct', 'net_disk', 'external'];
$downloads = [];
foreach ($value as $row) {
if (!is_array($row)) {
continue;
}
$url = isset($row['url']) ? esc_url_raw((string) $row['url']) : '';
if ($url === '') {
continue;
}
$type = isset($row['type']) && in_array($row['type'], $allowed_types, true)
? (string) $row['type']
: 'direct';
$extract_code = '';
foreach ([
'extract_code',
'extractCode',
'extract',
'extract_key',
'extraction_code',
'extractionCode',
'code',
'password',
'pwd',
'pan_code',
'panCode',
'pan_password',
'panPassword',
'access_code',
'accessCode',
] as $extract_key) {
if (isset($row[$extract_key]) && !is_array($row[$extract_key])) {
$candidate = trim((string) $row[$extract_key]);
if ($candidate !== '') {
$extract_code = sanitize_text_field($candidate);
break;
}
}
}
$downloads[] = [
'label' => isset($row['label']) ? sanitize_text_field((string) $row['label']) : '',
'url' => $url,
'type' => $type,
'extract_code' => $extract_code,
];
}
return array_values($downloads);
}
/**
* @param mixed $value
* @return array<int, array<string, string>>
*/
public static function sanitize_attributes($value): array {
if (!is_array($value)) {
return [];
}
$attributes = [];
foreach ($value as $row) {
if (!is_array($row)) {
continue;
}
$key = sanitize_text_field((string) ($row['key'] ?? ''));
$item_value = sanitize_text_field((string) ($row['value'] ?? ''));
if ($key === '' && $item_value === '') {
continue;
}
$attributes[] = [
'key' => $key,
'value' => $item_value,
];
}
return array_values($attributes);
}
/**
* @param mixed $value
* @return string[]
*/
public static function sanitize_gallery($value): array {
if (!is_array($value)) {
return [];
}
$gallery = [];
foreach ($value as $url) {
$url = esc_url_raw((string) $url);
if ($url !== '') {
$gallery[] = $url;
}
}
return array_values(array_unique($gallery));
}
/**
* Sanitize card/key codes for auto-delivery.
* Each entry is a non-empty sanitized string (one code per line).
*/
public static function sanitize_card_keys($value): array {
if (!is_array($value)) {
return [];
}
$keys = [];
foreach ($value as $key) {
$key = sanitize_text_field((string) $key);
if ($key !== '') {
$keys[] = $key;
}
}
return $keys;
}
/**
* @param array<string, mixed> $raw
* @return array<string, mixed>
*/
public static function sanitize_pay_meta(array $raw): array {
$meta = self::default_pay_meta();
$meta['pay_type'] = self::normalize_pay_type((string) ($raw['pay_type'] ?? $meta['pay_type']));
$pay_modo = sanitize_key((string) ($raw['pay_modo'] ?? '0'));
$meta['pay_modo'] = $pay_modo === 'points' ? 'points' : '0';
$meta['price'] = max(0, (float) ($raw['price'] ?? 0));
$meta['points_price'] = max(0, (int) ($raw['points_price'] ?? $raw['price'] ?? 0));
$meta['original_price'] = max(0, (float) ($raw['original_price'] ?? 0));
$meta['vip1_price'] = max(0, (float) ($raw['vip1_price'] ?? 0));
$meta['vip2_price'] = max(0, (float) ($raw['vip2_price'] ?? 0));
$meta['vip1_points'] = max(0, (int) ($raw['vip1_points'] ?? $raw['vip1_price'] ?? 0));
$meta['vip2_points'] = max(0, (int) ($raw['vip2_points'] ?? $raw['vip2_price'] ?? 0));
$pay_limit = (int) ($raw['pay_limit'] ?? 0);
$meta['pay_limit'] = in_array($pay_limit, [0, 1, 2], true) ? $pay_limit : 0;
$meta['version'] = sanitize_text_field((string) ($raw['version'] ?? ''));
$meta['size'] = sanitize_text_field((string) ($raw['size'] ?? ''));
$meta['license'] = sanitize_text_field((string) ($raw['license'] ?? ''));
$meta['compat'] = sanitize_text_field((string) ($raw['compat'] ?? ''));
$meta['demo_url'] = esc_url_raw((string) ($raw['demo_url'] ?? ''));
$meta['video_url'] = esc_url_raw((string) ($raw['video_url'] ?? ''));
$meta['downloads'] = self::sanitize_downloads($raw['downloads'] ?? []);
$meta['attributes'] = self::sanitize_attributes($raw['attributes'] ?? []);
$meta['gallery'] = self::sanitize_gallery($raw['gallery'] ?? []);
$meta['require_login'] = !empty($raw['require_login']);
$meta['require_captcha'] = !empty($raw['require_captcha']);
// §3.8 电商增强字段
$meta['virtual_sales'] = max(0, (int) ($raw['virtual_sales'] ?? 0));
$meta['stock'] = (int) ($raw['stock'] ?? -1);
$meta['card_keys'] = self::sanitize_card_keys($raw['card_keys'] ?? []);
$meta['purchase_limit'] = max(0, (int) ($raw['purchase_limit'] ?? 0));
$meta['free_preview'] = wp_kses_post((string) ($raw['free_preview'] ?? ''));
$meta['show_sales'] = !isset($raw['show_sales']) || !empty($raw['show_sales']);
$meta['download_limit_over_price'] = max(0, (float) ($raw['download_limit_over_price'] ?? 0));
$meta['coupon_s'] = !empty($raw['coupon_s']);
$meta['coupon_desc'] = sanitize_textarea_field((string) ($raw['coupon_desc'] ?? ''));
$meta['pay_rebate_discount'] = max(0, (float) ($raw['pay_rebate_discount'] ?? 0));
return $meta;
}
/**
* @return array<string, string>
*/
public static function order_status_labels(): array {
return [
self::ORDER_STATUS_CREATED => __('已创建', 'aksrt'),
self::ORDER_STATUS_PENDING => __('待支付', 'aksrt'),
self::ORDER_STATUS_PAID => __('已支付', 'aksrt'),
self::ORDER_STATUS_DELIVERED => __('已发货', 'aksrt'),
self::ORDER_STATUS_COMPLETED => __('已完成', 'aksrt'),
self::ORDER_STATUS_CANCELLED => __('已取消', 'aksrt'),
self::ORDER_STATUS_EXPIRED => __('已过期', 'aksrt'),
self::ORDER_STATUS_REFUNDING => __('退款中', 'aksrt'),
self::ORDER_STATUS_REFUNDED => __('已退款', 'aksrt'),
self::ORDER_STATUS_PENDING_REVIEW => __('待评价', 'aksrt'),
];
}
/**
* @return array<string, string[]>
*/
public static function order_transitions(): array {
return [
self::ORDER_STATUS_CREATED => [
self::ORDER_STATUS_PENDING,
self::ORDER_STATUS_CANCELLED,
],
self::ORDER_STATUS_PENDING => [
self::ORDER_STATUS_PAID,
self::ORDER_STATUS_CANCELLED,
self::ORDER_STATUS_EXPIRED,
],
self::ORDER_STATUS_PAID => [
self::ORDER_STATUS_DELIVERED,
self::ORDER_STATUS_COMPLETED,
self::ORDER_STATUS_REFUNDING,
],
self::ORDER_STATUS_DELIVERED => [
self::ORDER_STATUS_COMPLETED,
self::ORDER_STATUS_PENDING_REVIEW,
self::ORDER_STATUS_REFUNDING,
],
self::ORDER_STATUS_COMPLETED => [
self::ORDER_STATUS_PENDING_REVIEW,
self::ORDER_STATUS_REFUNDING,
],
self::ORDER_STATUS_CANCELLED => [],
self::ORDER_STATUS_EXPIRED => [
self::ORDER_STATUS_PENDING,
],
self::ORDER_STATUS_REFUNDING => [
self::ORDER_STATUS_REFUNDED,
self::ORDER_STATUS_PAID,
],
self::ORDER_STATUS_REFUNDED => [],
self::ORDER_STATUS_PENDING_REVIEW => [
self::ORDER_STATUS_COMPLETED,
self::ORDER_STATUS_REFUNDING,
],
];
}
public static function normalize_order_status(string $status, string $default = self::ORDER_STATUS_PENDING): string {
$status = sanitize_key($status);
return array_key_exists($status, self::order_status_labels()) ? $status : $default;
}
public static function can_transition_order_status(string $from, string $to): bool {
$from = self::normalize_order_status($from, self::ORDER_STATUS_CREATED);
$to = self::normalize_order_status($to);
$transitions = self::order_transitions();
return isset($transitions[$from]) && in_array($to, $transitions[$from], true);
}
/**
* @return string[]
*/
public static function paid_order_statuses(): array {
return [
self::ORDER_STATUS_PAID,
self::ORDER_STATUS_DELIVERED,
self::ORDER_STATUS_COMPLETED,
self::ORDER_STATUS_PENDING_REVIEW,
];
}
public static function is_paid_order_status(string $status): bool {
return in_array(self::normalize_order_status($status), self::paid_order_statuses(), true);
}
/**
* @return string[]
*/
public static function vip_active_order_statuses(): array {
return self::paid_order_statuses();
}
}
+366
View File
@@ -0,0 +1,366 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
/**
* Front-end article submit (写文章 / 投稿).
*
* AJAX authority for create / draft / update / delete; capability-gated.
*/
class FrontSubmit {
use FrontSubmitSave;
use FrontSubmitPay;
use FrontSubmitFeatured;
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_action('wp_ajax_aksrt_submit_post', [$this, 'ajax_submit_post']);
add_action('wp_ajax_aksrt_save_draft', [$this, 'ajax_save_draft']);
add_action('wp_ajax_aksrt_update_post', [$this, 'ajax_update_post']);
add_action('wp_ajax_aksrt_delete_draft', [$this, 'ajax_delete_draft']);
add_action('wp_ajax_aksrt_delete_post', [$this, 'ajax_delete_post']);
// Guest path (when enabled).
add_action('wp_ajax_nopriv_aksrt_submit_post', [$this, 'ajax_submit_post']);
add_filter('body_class', [$this, 'body_class']);
add_filter('wp_robots', [$this, 'robots_noindex']);
add_filter('aksrt_floating_buttons_enabled', [$this, 'disable_chrome_on_submit']);
add_filter('aksrt_footer_tabbar_enabled', [$this, 'disable_chrome_on_submit']);
}
/**
* @return array<string, mixed>
*/
public function settings(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_submit_settings();
}
return [
'enabled' => true,
'title_min' => 5,
'title_max' => 30,
'allowed_cat_ids' => [],
'default_user_id' => 0,
'guest_enabled' => false,
'allow_category' => true,
'allow_tags' => true,
'allow_cover' => true,
'topics_s' => false,
'custom_filter_s' => true,
'review_mode' => 'pending',
'pay_free_s' => true,
'pay_price_min' => 0,
'pay_price_max' => 100,
'pay_vip_price_s' => true,
'pay_vip_1_discount' => 80,
'pay_vip_2_discount' => 50,
];
}
/**
* Whether the user (or guest) may open / create a submission.
*/
public function can_submit(?int $user_id = null): bool {
$settings = $this->settings();
if (empty($settings['enabled'])) {
return false;
}
if ($user_id === null) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return !empty($settings['guest_enabled']) && (int) ($settings['default_user_id'] ?? 0) > 0;
}
return $this->user_can_cap('new_post_add', $user_id);
}
public function can_edit_post(int $post_id, ?int $user_id = null): bool {
if ($post_id <= 0) {
return false;
}
if ($user_id === null) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return false;
}
if (user_can($user_id, 'manage_options') || user_can($user_id, 'edit_others_posts')) {
return true;
}
$post = get_post($post_id);
if (!$post || $post->post_type !== 'post') {
return false;
}
if ((int) $post->post_author !== $user_id) {
return false;
}
// Own drafts always editable when user can add posts.
if ($post->post_status === 'draft' && $this->user_can_cap('new_post_add', $user_id)) {
return true;
}
return $this->user_can_cap('new_post_edit', $user_id, $post_id);
}
public function can_delete_post(int $post_id, ?int $user_id = null): bool {
if ($user_id === null) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return false;
}
if (user_can($user_id, 'manage_options')) {
return true;
}
$post = get_post($post_id);
if (!$post || $post->post_type !== 'post') {
return false;
}
if ((int) $post->post_author !== $user_id) {
return false;
}
if ($post->post_status === 'draft' && $this->user_can_cap('new_post_add', $user_id)) {
return true;
}
return $this->user_can_cap('new_post_delete', $user_id, $post_id);
}
public function user_can_cap(string $cap, int $user_id = 0, ?int $context = null): bool {
if ($user_id > 0 && user_can($user_id, 'manage_options')) {
return true;
}
if (function_exists('aksrt_user_can')) {
return aksrt_user_can($cap, $user_id, $context);
}
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'user_can')) {
return \AKSRT\Settings\Settings::user_can($cap, $user_id, $context);
}
return $user_id > 0;
}
/**
* @param array<string, mixed> $data
* @return array{success:bool,message:string,post_id?:int}
*/
public function submit(array $data): array {
$action = sanitize_key((string) ($data['action_type'] ?? $data['submit_action'] ?? 'posts_save'));
if ($action === 'aksrt_save_draft' || ($data['status'] ?? '') === 'draft') {
$action = 'posts_draft';
} else {
$action = 'posts_save';
}
return $this->persist($data, $action);
}
/**
* @param array<string, mixed> $data
* @return array{success:bool,message:string,post_id?:int}
*/
public function update(int $post_id, array $data): array {
$data['posts_id'] = $post_id;
$action = (($data['status'] ?? '') === 'draft') ? 'posts_draft' : 'posts_save';
return $this->persist($data, $action);
}
public function ajax_submit_post(): void {
$this->verify_nonce();
$this->maybe_verify_captcha();
$data = wp_unslash($_POST);
$result = $this->persist(is_array($data) ? $data : [], 'posts_save');
$this->send_result($result);
}
public function ajax_save_draft(): void {
$this->verify_nonce();
if (!is_user_logged_in()) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$data = wp_unslash($_POST);
$result = $this->persist(is_array($data) ? $data : [], 'posts_draft');
$this->send_result($result);
}
public function ajax_update_post(): void {
$this->verify_nonce();
$data = wp_unslash($_POST);
if (!is_array($data)) {
$data = [];
}
$post_id = absint($data['posts_id'] ?? $data['post_id'] ?? $data['edit_id'] ?? 0);
if ($post_id <= 0) {
wp_send_json_error(['message' => __('文章 ID 无效', 'aksrt')]);
}
$data['posts_id'] = $post_id;
$action = (($data['status'] ?? '') === 'draft') ? 'posts_draft' : 'posts_save';
$result = $this->persist($data, $action);
$this->send_result($result);
}
public function ajax_delete_draft(): void {
$this->verify_nonce();
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$post_id = absint($_POST['draft_id'] ?? $_POST['post_id'] ?? $_POST['id'] ?? 0);
if ($post_id <= 0) {
wp_send_json_error(['message' => __('草稿 ID 无效', 'aksrt')]);
}
$post = get_post($post_id);
if (!$post || (int) $post->post_author !== $user_id) {
wp_send_json_error(['message' => __('没有删除权限', 'aksrt')]);
}
if ($post->post_status !== 'draft' && !$this->can_delete_post($post_id, $user_id)) {
wp_send_json_error(['message' => __('只能删除草稿', 'aksrt')]);
}
if ($post->post_status === 'draft') {
$ok = wp_delete_post($post_id, true);
} else {
$ok = wp_trash_post($post_id);
}
if ($ok) {
wp_send_json_success(['message' => __('已删除', 'aksrt')]);
}
wp_send_json_error(['message' => __('删除失败', 'aksrt')]);
}
public function ajax_delete_post(): void {
$this->verify_nonce();
$user_id = get_current_user_id();
$post_id = absint($_POST['id'] ?? $_POST['post_id'] ?? 0);
if (!$this->can_delete_post($post_id, $user_id)) {
wp_send_json_error(['message' => __('您没有删除此内容的权限', 'aksrt')]);
}
$post = get_post($post_id);
if (!$post || $post->post_status === 'trash') {
wp_send_json_success(['message' => __('文章已经删除', 'aksrt'), 'reload' => true]);
}
wp_trash_post($post_id);
wp_send_json_success([
'message' => __('文章已经删除', 'aksrt'),
'reload' => true,
'goto' => home_url('/'),
]);
}
private function verify_nonce(): void {
$nonce = (string) ($_REQUEST['_nonce'] ?? $_REQUEST['nonce'] ?? $_REQUEST['_wpnonce'] ?? '');
if ($nonce === '' || (
!wp_verify_nonce($nonce, 'aksrt_submit')
&& !wp_verify_nonce($nonce, 'aksrt_submit_nonce')
)) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')], 403);
}
}
private function maybe_verify_captcha(): void {
$settings = $this->settings();
if (empty($settings['captcha'])) {
return;
}
if (!function_exists('aksrt_require_captcha')) {
return;
}
$payload = [];
if (isset($_POST['captcha']) && is_array($_POST['captcha'])) {
$payload = wp_unslash($_POST['captcha']);
}
$verify = aksrt_require_captcha('submit', is_array($payload) ? $payload : []);
if (is_wp_error($verify)) {
wp_send_json_error(['message' => $verify->get_error_message()]);
}
}
/**
* @param array{success:bool,message?:string} $result
*/
private function send_result(array $result): void {
if (!empty($result['success'])) {
wp_send_json_success($result);
}
wp_send_json_error($result);
}
/**
* Downloads image URL into media library as featured image.
*/
private function set_thumbnail_from_url(int $post_id, string $url): void {
if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
return;
}
if (!function_exists('media_sideload_image')) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
}
$ext = strtolower(pathinfo((string) (parse_url($url, PHP_URL_PATH) ?? ''), PATHINFO_EXTENSION));
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if ($ext !== '' && !in_array($ext, $allowed, true)) {
return;
}
$attachment_id = media_sideload_image($url, $post_id, '', 'id');
if (!is_wp_error($attachment_id)) {
set_post_thumbnail($post_id, (int) $attachment_id);
}
}
/**
* @param string[] $classes
* @return string[]
*/
public function body_class(array $classes): array {
if ($this->is_submit_template()) {
$classes[] = 'aksrt-page-submit';
$classes[] = 'aksrt-hide-float';
$classes[] = 'aksrt-hide-tabbar';
}
return $classes;
}
/**
* @param array<string,bool> $robots
* @return array<string,bool>
*/
public function robots_noindex(array $robots): array {
if ($this->is_submit_template()) {
$robots['noindex'] = true;
$robots['nofollow'] = true;
}
return $robots;
}
/**
* @param mixed $enabled
* @return mixed
*/
public function disable_chrome_on_submit($enabled) {
if ($this->is_submit_template()) {
return false;
}
return $enabled;
}
public function is_submit_template(): bool {
return is_page_template('templates/pages/page-submit.php');
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
/**
* Featured cover (image / slide / video) for front-end submit.
*/
trait FrontSubmitFeatured {
/**
* @param array<string, mixed> $data
*/
private function save_featured_meta(int $post_id, array $data, int $user_id): void {
if (empty($data['featured_data']) && empty($data['featured_type'])) {
return;
}
$featured = $data['featured_data'] ?? null;
if (is_string($featured)) {
$decoded = json_decode(wp_unslash($featured), true);
$featured = is_array($decoded) ? $decoded : null;
}
if (!is_array($featured)) {
// Flat payload: featured_type + fields.
$featured = [
'type' => sanitize_key((string) ($data['featured_type'] ?? '')),
'data' => $data['featured_payload'] ?? $data,
];
}
$type = sanitize_key((string) ($featured['type'] ?? ''));
$payload = is_array($featured['data'] ?? null) ? $featured['data'] : $featured;
$post_meta = get_post_meta($post_id, DomainModel::POST_META_KEY, true);
if (!is_array($post_meta)) {
$post_meta = DomainModel::default_post_meta();
}
switch ($type) {
case 'video':
if (!$this->user_can_cap('new_post_video_cover', $user_id) && !$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$video = esc_url_raw((string) ($payload['url'] ?? $payload['video'] ?? ''));
$pic = esc_url_raw((string) ($payload['pic'] ?? $payload['poster'] ?? $payload['cover'] ?? ''));
$post_meta['featured_video'] = $video;
$post_meta['featured_slide'] = '';
if ($pic !== '') {
$post_meta['thumbnail_url'] = $pic;
$post_meta['cover_images'] = $pic;
$this->maybe_set_thumbnail_url($post_id, $pic);
}
break;
case 'slide':
if (!$this->user_can_cap('new_post_slide_cover', $user_id) && !$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$ids = $payload['ids'] ?? $payload['slides'] ?? $payload['images'] ?? [];
$urls = [];
if (is_string($ids)) {
$ids = array_filter(array_map('trim', explode(',', $ids)));
}
if (is_array($ids)) {
foreach ($ids as $item) {
if (is_array($item)) {
$u = esc_url_raw((string) ($item['url'] ?? ''));
if ($u !== '') {
$urls[] = $u;
} elseif (!empty($item['id'])) {
$src = wp_get_attachment_image_url((int) $item['id'], 'full');
if ($src) {
$urls[] = $src;
}
}
} elseif (is_numeric($item)) {
$src = wp_get_attachment_image_url((int) $item, 'full');
if ($src) {
$urls[] = $src;
}
} else {
$u = esc_url_raw((string) $item);
if ($u !== '') {
$urls[] = $u;
}
}
}
}
$post_meta['featured_slide'] = implode(',', $urls);
$post_meta['featured_video'] = '';
if (!empty($urls[0])) {
$post_meta['thumbnail_url'] = $urls[0];
$post_meta['cover_images'] = $urls[0];
$this->maybe_set_thumbnail_url($post_id, $urls[0]);
}
break;
case 'image':
if (!$this->user_can_cap('new_post_image_cover', $user_id)) {
return;
}
$url = esc_url_raw((string) ($payload['url'] ?? $payload['image'] ?? ''));
$id = absint($payload['id'] ?? 0);
if ($url === '' && $id > 0) {
$src = wp_get_attachment_image_url($id, 'full');
if ($src) {
$url = $src;
}
}
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
if ($url !== '') {
$post_meta['thumbnail_url'] = $url;
$post_meta['cover_images'] = $url;
if ($id > 0) {
set_post_thumbnail($post_id, $id);
} else {
$this->maybe_set_thumbnail_url($post_id, $url);
}
}
break;
case 'close':
case 'none':
case 'clear':
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
$post_meta['thumbnail_url'] = '';
$post_meta['cover_images'] = '';
delete_post_thumbnail($post_id);
break;
default:
return;
}
update_post_meta($post_id, DomainModel::POST_META_KEY, $post_meta);
}
private function maybe_set_thumbnail_url(int $post_id, string $url): void {
if ($url === '') {
return;
}
// Prefer attachment if URL is local media.
$attach_id = attachment_url_to_postid($url);
if ($attach_id > 0) {
set_post_thumbnail($post_id, $attach_id);
return;
}
$this->set_thumbnail_from_url($post_id, $url);
}
}
+205
View File
@@ -0,0 +1,205 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
/**
* Front-end pay meta save for article submit.
*/
trait FrontSubmitPay {
/**
* @param array<string, mixed> $data
* @param array<string, mixed> $settings
* @return array{success:bool,message?:string}
*/
private function save_pay_meta(int $post_id, array $data, int $user_id, array $settings): array {
if (!$this->user_can_cap('new_post_pay', $user_id)) {
return ['success' => true];
}
// Only touch pay meta when the form sent pay fields.
$has_pay_payload = isset($data['aksrt_pay']) || isset($data['pay_enabled']) || isset($data['zibpay_s']) || isset($data['pay_type']);
if (!$has_pay_payload) {
return ['success' => true];
}
$raw = is_array($data['aksrt_pay'] ?? null) ? $data['aksrt_pay'] : [];
$enabled = !empty($data['pay_enabled']) || !empty($data['zibpay_s']);
$existing = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post_id) : DomainModel::default_pay_meta();
$meta = is_array($existing) ? $existing : DomainModel::default_pay_meta();
if (!$enabled) {
$meta['pay_type'] = DomainModel::PAY_TYPE_NO;
$this->write_pay_meta($post_id, $meta);
return ['success' => true];
}
$pay_modo = sanitize_key((string) ($raw['pay_modo'] ?? $data['pay_modo'] ?? '0'));
$pay_modo = $pay_modo === 'points' ? 'points' : '0';
$pay_type = sanitize_key((string) ($raw['pay_type'] ?? $data['pay_type'] ?? DomainModel::PAY_TYPE_READING));
if ($pay_type === '1' || $pay_type === 'reading' || $pay_type === 'payshow') {
$pay_type = DomainModel::PAY_TYPE_READING;
} elseif ($pay_type === '2' || $pay_type === 'download') {
$pay_type = DomainModel::PAY_TYPE_DOWNLOAD;
} else {
$pay_type = DomainModel::normalize_pay_type($pay_type);
if ($pay_type === DomainModel::PAY_TYPE_NO) {
$pay_type = DomainModel::PAY_TYPE_READING;
}
}
$downloads = $this->normalize_front_downloads($raw['downloads'] ?? $data['pay_download'] ?? []);
if (!empty($downloads) && $this->user_can_cap('new_post_pay_download', $user_id)) {
$pay_type = DomainModel::PAY_TYPE_DOWNLOAD;
$meta['downloads'] = $downloads;
} elseif ($pay_type === DomainModel::PAY_TYPE_DOWNLOAD && !$this->user_can_cap('new_post_pay_download', $user_id)) {
$pay_type = DomainModel::PAY_TYPE_READING;
$meta['downloads'] = [];
}
$meta['pay_type'] = $pay_type;
$meta['pay_modo'] = $pay_modo;
$min = (float) ($settings['pay_price_min'] ?? 0);
$max = (float) ($settings['pay_price_max'] ?? 0);
$allow_free = !empty($settings['pay_free_s']);
$vip_manual = !empty($settings['pay_vip_price_s']);
$vip1_disc = (int) ($settings['pay_vip_1_discount'] ?? 100);
$vip2_disc = (int) ($settings['pay_vip_2_discount'] ?? 100);
if ($pay_modo === 'points') {
$points = isset($raw['points_price']) ? (int) $raw['points_price'] : (int) ($data['points_price'] ?? 0);
if ($points < 0) {
return ['success' => false, 'message' => __('请设置正确的积分价格', 'aksrt')];
}
if (!$allow_free && $points <= 0) {
return ['success' => false, 'message' => __('请设置付费内容的积分价格', 'aksrt')];
}
if ($min > 0 && $points < $min) {
return ['success' => false, 'message' => sprintf(__('积分价格不能低于:%s', 'aksrt'), (string) $min)];
}
if ($max > 0 && $points > $max) {
return ['success' => false, 'message' => sprintf(__('积分价格不能高于:%s', 'aksrt'), (string) $max)];
}
if ($vip_manual) {
$vip1 = isset($raw['vip1_points']) ? (int) $raw['vip1_points'] : (int) ($raw['vip_1_points'] ?? 0);
$vip2 = isset($raw['vip2_points']) ? (int) $raw['vip2_points'] : (int) ($raw['vip_2_points'] ?? 0);
} else {
$vip1 = (int) floor($points * $vip1_disc / 100);
$vip2 = (int) floor($points * $vip2_disc / 100);
}
$vip1 = max(0, min($points, $vip1));
$vip2 = max(0, min($vip1, $vip2));
$meta['points_price'] = $points;
$meta['price'] = 0.0;
$meta['vip1_points'] = $vip1;
$meta['vip2_points'] = $vip2;
$meta['vip1_price'] = 0.0;
$meta['vip2_price'] = 0.0;
} else {
$price = isset($raw['price']) ? (float) $raw['price'] : (float) ($raw['pay_price'] ?? $data['pay_price'] ?? 0);
$price = round($price, 2);
if ($price < 0) {
return ['success' => false, 'message' => __('请设置正确的价格', 'aksrt')];
}
if (!$allow_free && $price <= 0) {
return ['success' => false, 'message' => __('请设置付费内容的价格', 'aksrt')];
}
if ($min > 0 && $price < $min) {
return ['success' => false, 'message' => sprintf(__('价格不能低于:%s', 'aksrt'), (string) $min)];
}
if ($max > 0 && $price > $max) {
return ['success' => false, 'message' => sprintf(__('价格不能高于:%s', 'aksrt'), (string) $max)];
}
if ($vip_manual) {
$vip1 = isset($raw['vip1_price']) ? (float) $raw['vip1_price'] : (float) ($raw['vip_1_price'] ?? 0);
$vip2 = isset($raw['vip2_price']) ? (float) $raw['vip2_price'] : (float) ($raw['vip_2_price'] ?? 0);
$vip1 = round($vip1, 2);
$vip2 = round($vip2, 2);
if ($vip1 > $price) {
return ['success' => false, 'message' => __('会员价不能高于普通价', 'aksrt')];
}
if ($vip2 > $vip1) {
return ['success' => false, 'message' => __('更高等级会员价不能高于较低等级会员价', 'aksrt')];
}
} else {
$vip1 = round($price * $vip1_disc / 100, 2);
$vip2 = round($price * $vip2_disc / 100, 2);
}
$vip1 = max(0, min($price, $vip1));
$vip2 = max(0, min($vip1, $vip2));
$meta['price'] = $price;
$meta['points_price'] = 0;
$meta['vip1_price'] = $vip1;
$meta['vip2_price'] = $vip2;
$meta['vip1_points'] = 0;
$meta['vip2_points'] = 0;
}
$pay_limit = (int) ($raw['pay_limit'] ?? $data['pay_limit'] ?? 0);
$meta['pay_limit'] = in_array($pay_limit, [0, 1, 2], true) ? $pay_limit : 0;
$meta['require_login'] = !empty($raw['require_login']) || !empty($data['require_login']);
$meta['coupon_s'] = !empty($raw['coupon_s']) || !empty($data['coupon_s']);
if (isset($raw['coupon_desc']) || isset($data['coupon_desc'])) {
$meta['coupon_desc'] = sanitize_textarea_field((string) ($raw['coupon_desc'] ?? $data['coupon_desc'] ?? ''));
}
$this->write_pay_meta($post_id, $meta);
return ['success' => true];
}
/**
* @param mixed $raw
* @return array<int, array<string, string>>
*/
private function normalize_front_downloads($raw): array {
if (!is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $row) {
if (!is_array($row)) {
continue;
}
// Accept zibll-ish keys + AKSRT keys.
$url = (string) ($row['url'] ?? $row['link'] ?? '');
$url = esc_url_raw(trim($url));
if ($url === '') {
continue;
}
$out[] = [
'label' => sanitize_text_field((string) ($row['label'] ?? $row['name'] ?? '')),
'url' => $url,
'type' => 'external',
'extract_code' => sanitize_text_field((string) ($row['extract_code'] ?? $row['copy_val'] ?? $row['more'] ?? '')),
];
}
return DomainModel::sanitize_downloads($out);
}
/**
* @param array<string, mixed> $meta
*/
private function write_pay_meta(int $post_id, array $meta): void {
$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));
update_post_meta(
$post_id,
DomainModel::RESOURCE_MARKER_KEY,
DomainModel::is_resource_pay_type((string) ($meta['pay_type'] ?? 'no')) ? 1 : 0
);
}
}
+354
View File
@@ -0,0 +1,354 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
/**
* Create / update / status machine for front-end article submit.
*/
trait FrontSubmitSave {
/**
* Persist a front-end submission (new or edit).
*
* @param array<string, mixed> $data Request payload.
* @param string $action posts_save|posts_draft
* @return array{success:bool,message:string,post_id?:int,url?:string,goto?:string,reload?:bool,time?:string,posts_id?:int}
*/
public function persist(array $data, string $action = 'posts_save'): array {
$user_id = get_current_user_id();
$settings = $this->settings();
$posts_id = absint($data['posts_id'] ?? $data['edit_id'] ?? $data['post_id'] ?? 0);
$is_guest = $user_id <= 0;
$is_edit = $posts_id > 0;
$is_draft = $action === 'posts_draft';
if (empty($settings['enabled'])) {
return ['success' => false, 'message' => __('投稿功能已关闭', 'aksrt')];
}
if ($is_guest) {
if (empty($settings['guest_enabled']) || (int) $settings['default_user_id'] <= 0) {
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
}
if ($is_draft) {
return ['success' => false, 'message' => __('未登录无法保存草稿', 'aksrt')];
}
if ($is_edit) {
return ['success' => false, 'message' => __('未登录无法编辑文章', 'aksrt')];
}
}
if ($is_edit) {
if (!$this->can_edit_post($posts_id, $user_id)) {
return ['success' => false, 'message' => __('没有编辑权限', 'aksrt')];
}
} elseif (!$this->can_submit($is_guest ? 0 : $user_id)) {
return ['success' => false, 'message' => __('没有投稿权限', 'aksrt')];
}
$title = sanitize_text_field((string) ($data['post_title'] ?? $data['title'] ?? ''));
$content = (string) ($data['post_content'] ?? $data['content'] ?? '');
$excerpt = sanitize_textarea_field((string) ($data['excerpt'] ?? $data['post_excerpt'] ?? ''));
if ($title === '') {
return ['success' => false, 'message' => __('请填写文章标题', 'aksrt')];
}
if (trim(wp_strip_all_tags($content)) === '') {
return ['success' => false, 'message' => __('还未填写任何内容', 'aksrt')];
}
$categories = $this->normalize_category_ids($data);
$tags_raw = (string) ($data['tags'] ?? '');
if (!$is_draft) {
$title_len = $this->text_length($title);
if ($title_len < (int) $settings['title_min']) {
return ['success' => false, 'message' => __('标题太短!', 'aksrt')];
}
if ($title_len > (int) $settings['title_max']) {
return [
'success' => false,
'message' => sprintf(
/* translators: %d: max title length */
__('标题太长了,不能超过%d个字', 'aksrt'),
(int) $settings['title_max']
),
];
}
if ($this->text_length(wp_strip_all_tags($content)) < 10) {
return ['success' => false, 'message' => __('文章内容过少', 'aksrt')];
}
if (!empty($settings['allow_category']) && empty($categories)) {
return ['success' => false, 'message' => __('请选择文章分类', 'aksrt')];
}
if (!empty($settings['cover_required']) && empty($data['featured_data']) && empty($data['thumbnail'])) {
return ['success' => false, 'message' => __('请设置封面图', 'aksrt')];
}
}
// Allowed category whitelist.
$allowed = $settings['allowed_cat_ids'] ?? [];
if (!empty($allowed) && !empty($categories)) {
$categories = array_values(array_intersect($categories, $allowed));
if (!$is_draft && empty($categories) && !empty($settings['allow_category'])) {
return ['success' => false, 'message' => __('所选分类不可用', 'aksrt')];
}
}
$author_id = $user_id;
if ($is_guest) {
$guest_name = sanitize_text_field((string) ($data['user_name'] ?? ''));
if ($guest_name === '') {
return ['success' => false, 'message' => __('请输入昵称', 'aksrt')];
}
$contact = sanitize_text_field((string) ($data['contact_details'] ?? ''));
$suffix = $contact !== ''
? sprintf(__('[投稿-姓名:%1$s,联系:%2$s]', 'aksrt'), $guest_name, $contact)
: sprintf(__('[投稿-姓名:%s]', 'aksrt'), $guest_name);
$title = $title . $suffix;
$author_id = (int) $settings['default_user_id'];
}
$status = 'draft';
if (!$is_draft) {
$status = $this->resolve_publish_status($user_id, $settings);
}
$can_iframe = $this->user_can_cap('new_post_iframe_video', $user_id);
if ($can_iframe) {
add_filter('wp_kses_allowed_html', [$this, 'allow_iframe_kses'], 99, 2);
}
$content = wp_kses_post($content);
if ($can_iframe) {
remove_filter('wp_kses_allowed_html', [$this, 'allow_iframe_kses'], 99);
}
$postarr = [
'post_type' => 'post',
'post_title' => $title,
'post_content' => $content,
'post_status' => $status,
'comment_status' => 'open',
];
if ($excerpt !== '' || array_key_exists('excerpt', $data) || array_key_exists('post_excerpt', $data)) {
$postarr['post_excerpt'] = $excerpt;
}
if (!empty($settings['allow_tags']) && $tags_raw !== '') {
$tag_names = preg_split('/[,,\n]+/u', $tags_raw) ?: [];
$tag_names = array_values(array_filter(array_map('trim', $tag_names)));
$postarr['tags_input'] = $tag_names;
}
if (!empty($categories)) {
$postarr['post_category'] = $categories;
}
if ($is_edit) {
$postarr['ID'] = $posts_id;
$in_id = wp_update_post($postarr, true);
} else {
$postarr['post_author'] = $author_id;
$in_id = wp_insert_post($postarr, true);
}
if (is_wp_error($in_id)) {
return ['success' => false, 'message' => $in_id->get_error_message()];
}
if (!$in_id) {
return ['success' => false, 'message' => __('文章保存失败,请稍后再试', 'aksrt')];
}
// Re-apply categories when empty update should clear (logged-in only).
if (!empty($settings['allow_category']) && isset($data['category'])) {
wp_set_post_categories((int) $in_id, $categories);
}
if (!empty($settings['allow_tags']) && array_key_exists('tags', $data) && $tags_raw === '') {
wp_set_post_tags((int) $in_id, []);
}
$this->apply_topics_and_filters((int) $in_id, $data, $settings);
$pay_result = $this->save_pay_meta((int) $in_id, $data, $user_id, $settings);
if (!$pay_result['success']) {
$pay_result['post_id'] = (int) $in_id;
$pay_result['posts_id'] = (int) $in_id;
return $pay_result;
}
$this->save_featured_meta((int) $in_id, $data, $user_id);
// Legacy thumbnail URL fallback.
$thumbnail_url = esc_url_raw((string) ($data['thumbnail'] ?? ''));
if ($thumbnail_url !== '' && empty($data['featured_data'])) {
$this->set_thumbnail_from_url((int) $in_id, $thumbnail_url);
}
$post = get_post((int) $in_id);
if ($post instanceof \WP_Post) {
do_action($is_edit ? 'aksrt_new_edit_posts' : 'aksrt_new_add_posts', $post);
if ($status === 'pending') {
do_action('aksrt_new_posts_pending', $post);
}
}
$url = get_permalink((int) $in_id) ?: '';
$send = [
'success' => true,
'post_id' => (int) $in_id,
'posts_id' => (int) $in_id,
'url' => $url,
];
switch ($status) {
case 'pending':
$send['message'] = __('内容已提交,正在等待审核', 'aksrt');
if ($user_id > 0 && function_exists('aksrt_get_user_home_url')) {
$send['reload'] = true;
$send['goto'] = add_query_arg(
['tab' => 'post', 'post_status' => 'pending'],
aksrt_get_user_home_url($user_id)
);
}
break;
case 'draft':
$send['message'] = __('草稿已保存', 'aksrt');
$send['time'] = current_time('mysql');
break;
default:
$send['message'] = __('文章已发布', 'aksrt');
$send['reload'] = true;
$send['goto'] = $url;
break;
}
return $send;
}
/**
* @param array<string, mixed> $settings
*/
private function resolve_publish_status(int $user_id, array $settings): string {
$mode = (string) ($settings['review_mode'] ?? 'pending');
if ($mode === 'draft') {
return 'draft';
}
if ($mode === 'publish') {
return 'publish';
}
// pending mode: skip audit when capability grants it.
if ($this->user_can_cap('new_post_audit_no', $user_id)) {
return 'publish';
}
return 'pending';
}
/**
* @param array<string, mixed> $data
* @return int[]
*/
private function normalize_category_ids(array $data): array {
$raw = $data['category'] ?? $data['categories'] ?? [];
if (!is_array($raw)) {
$raw = $raw === '' || $raw === null ? [] : [$raw];
}
$ids = array_values(array_unique(array_filter(array_map('absint', $raw))));
return $ids;
}
private function text_length(string $text): int {
$text = trim(wp_strip_all_tags($text));
if (function_exists('mb_strlen')) {
return (int) mb_strlen($text);
}
return strlen($text);
}
/**
* @param array<string,mixed> $data
* @param array<string,mixed> $settings
*/
private function apply_topics_and_filters(int $post_id, array $data, array $settings): void {
if (!empty($settings['topics_s']) && taxonomy_exists('topics')) {
$topics = $data['topics'] ?? [];
if (!is_array($topics)) {
$topics = $topics === '' || $topics === null ? [] : [$topics];
}
$topic_ids = array_values(array_filter(array_map('absint', $topics)));
wp_set_object_terms($post_id, $topic_ids, 'topics');
}
if (empty($settings['custom_filter_s'])) {
return;
}
$dims = [];
if (class_exists('\AKSRT\Settings\Settings')) {
$filter_all = \AKSRT\Settings\Settings::get_post_filter_settings();
$dims = is_array($filter_all['custom_filter'] ?? null) ? $filter_all['custom_filter'] : [];
}
$allowed_keys = [];
foreach ($dims as $dim) {
if (!empty($dim['key'])) {
$allowed_keys[] = sanitize_key((string) $dim['key']);
}
}
foreach ($data as $key => $val) {
$dim_key = '';
if (strpos((string) $key, 'cf_') === 0) {
$dim_key = sanitize_key(substr((string) $key, 3));
} elseif (strpos((string) $key, '_aksrt_cf_') === 0) {
$dim_key = sanitize_key(substr((string) $key, 10));
} elseif (in_array(sanitize_key((string) $key), $allowed_keys, true)) {
$dim_key = sanitize_key((string) $key);
}
if ($dim_key === '' || (!empty($allowed_keys) && !in_array($dim_key, $allowed_keys, true))) {
continue;
}
$meta_key = '_aksrt_cf_' . $dim_key;
if (is_array($val)) {
$vals = array_values(array_filter(array_map('sanitize_key', $val)));
if ($vals) {
update_post_meta($post_id, $meta_key, $vals);
} else {
delete_post_meta($post_id, $meta_key);
}
} else {
$cf_val = sanitize_key((string) $val);
if ($cf_val !== '') {
update_post_meta($post_id, $meta_key, $cf_val);
} else {
delete_post_meta($post_id, $meta_key);
}
}
}
}
/**
* @param array<string,string> $tags
* @return array<string,string>
*/
public function allow_iframe_kses(array $tags, $context = ''): array {
if ($context !== 'post' && $context !== '') {
return $tags;
}
$tags['iframe'] = [
'src' => true,
'width' => true,
'height' => true,
'frameborder' => true,
'allowfullscreen' => true,
'allow' => true,
'style' => true,
'class' => true,
'id' => true,
'scrolling' => true,
];
return $tags;
}
}
+246
View File
@@ -0,0 +1,246 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class GoRedirect {
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('init', [$this, 'register_rewrite']);
add_action('template_redirect', [$this, 'handle_go_page']);
add_filter('the_content', [$this, 'replace_external_links'], 99);
}
/**
* Register rewrite rule for /go/ endpoint
*/
public function register_rewrite(): void {
add_rewrite_rule('^go/?$', 'index.php?aksrt_go_redirect=1', 'top');
add_rewrite_tag('%aksrt_go_redirect%', '([0-1])');
}
/**
* Check if go redirect feature is enabled
*/
private function is_enabled(): bool {
if (!class_exists('\AKSRT\Settings\Settings')) return false;
// schema key = seo_go_link; runtime alias = go_redirect_enabled
return (bool) (
\AKSRT\Settings\Settings::get_value('go_redirect_enabled', false)
?: \AKSRT\Settings\Settings::get_value('seo_go_link', false)
);
}
/**
* Get excluded domains list
*/
private function get_excluded_domains(): array {
if (!class_exists('\AKSRT\Settings\Settings')) return [];
// schema key = seo_go_link_exclude; runtime alias = go_redirect_exclude_domains
$domains = \AKSRT\Settings\Settings::get_value('go_redirect_exclude_domains', '')
?: \AKSRT\Settings\Settings::get_value('seo_go_link_exclude', '');
if (empty($domains)) return [];
return array_filter(array_map('trim', explode("\n", $domains)));
}
/**
* Check if URL is external
*/
private function is_external(string $url): bool {
$site_host = wp_parse_url(home_url('/'), PHP_URL_HOST);
$url_host = wp_parse_url($url, PHP_URL_HOST);
if (!$url_host || !$site_host) return false;
// Same domain
if ($url_host === $site_host) return false;
// Check excluded domains
$excluded = $this->get_excluded_domains();
foreach ($excluded as $domain) {
$domain = strtolower(trim($domain));
if ($domain && (strtolower($url_host) === $domain || substr(strtolower($url_host), -strlen($domain) - 1) === '.' . $domain)) {
return false;
}
}
return true;
}
/**
* Generate signed go link with time-limited nonce
*/
public function create_go_link(string $url): string {
$token = wp_create_nonce('aksrt_go_' . $url);
return add_query_arg([
'url' => rawurlencode($url),
'token' => $token,
], home_url('/go/'));
}
/**
* Handle the /go/ confirmation page
*/
public function handle_go_page(): void {
if (!get_query_var('aksrt_go_redirect')) return;
$url = isset($_GET['url']) ? wp_unslash(rawurldecode($_GET['url'])) : '';
$token = isset($_GET['token']) ? sanitize_text_field(wp_unslash($_GET['token'])) : '';
if (empty($url) || empty($token)) {
wp_die(
esc_html__('无效的跳转链接。', 'aksrt'),
esc_html__('跳转错误', 'aksrt'),
['response' => 400]
);
}
// Validate nonce (gated by seo_go_link_nonce schema setting)
$require_nonce = class_exists('\AKSRT\Settings\Settings')
? !empty(\AKSRT\Settings\Settings::get_value('seo_go_link_nonce', true))
: true;
if ($require_nonce && !wp_verify_nonce($token, 'aksrt_go_' . $url)) {
wp_die(
esc_html__('链接已过期或无效,请返回原页面重新点击。', 'aksrt'),
esc_html__('链接过期', 'aksrt'),
['response' => 403]
);
}
// Validate URL
$url = esc_url_raw($url);
if (empty($url) || !wp_http_validate_url($url)) {
wp_die(
esc_html__('无效的目标地址。', 'aksrt'),
esc_html__('跳转错误', 'aksrt'),
['response' => 400]
);
}
// Security: must be external
if (!$this->is_external($url)) {
wp_redirect($url);
exit;
}
// Render confirmation page
$this->render_go_page($url);
}
/**
* Render the go confirmation page
*/
private function render_go_page(string $url): void {
$domain = wp_parse_url($url, PHP_URL_HOST) ?: $url;
$protocol = wp_parse_url($url, PHP_URL_SCHEME) ?: 'https';
set_query_var('aksrt_go_target_url', esc_url($url));
set_query_var('aksrt_go_target_domain', (string) $domain);
set_query_var('aksrt_go_target_protocol', (string) $protocol);
$template = locate_template('go.php', false, false);
if ($template) {
status_header(200);
nocache_headers();
include $template;
exit;
}
$safe_url = esc_url($url);
$safe_domain = esc_html($domain);
$safe_protocol = esc_html($protocol);
wp_head();
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php esc_html_e('外部链接跳转', 'aksrt'); ?></title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f6f7; margin: 0; padding: 0; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.go-card { background: #fff; border-radius: 12px; padding: 40px; max-width: 500px; width: 90%; box-shadow: 0 4px 20px rgba(0,0,0,0.08); text-align: center; }
.go-card h1 { font-size: 20px; margin: 0 0 12px; color: #333; }
.go-card .go-domain { font-size: 16px; font-weight: 600; color: #f04494; margin-bottom: 8px; }
.go-card .go-url { font-size: 13px; color: #999; word-break: break-all; margin-bottom: 24px; }
.go-card .go-warn { font-size: 13px; color: #999; margin-bottom: 20px; }
.go-card .go-btns { display: flex; gap: 12px; justify-content: center; }
.go-card .go-btn { padding: 10px 28px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; text-decoration: none; }
.go-card .go-btn--primary { background: #f04494; color: #fff; }
.go-card .go-btn--primary:hover { background: #e0307e; }
.go-card .go-btn--ghost { background: #f5f6f7; color: #333; border: 1px solid #ddd; }
.go-card .go-btn--ghost:hover { background: #eee; }
</style>
</head>
<body>
<div class="go-card">
<h1><?php esc_html_e('即将离开本站', 'aksrt'); ?></h1>
<div class="go-domain"><?php echo $safe_protocol; ?>://<?php echo $safe_domain; ?></div>
<div class="go-url"><?php echo $safe_url; ?></div>
<div class="go-warn"><?php esc_html_e('请注意:外部链接内容不受本站控制,请谨慎访问。', 'aksrt'); ?></div>
<div class="go-btns">
<a href="<?php echo $safe_url; ?>" class="go-btn go-btn--primary" rel="nofollow noopener noreferrer" target="_blank"><?php esc_html_e('继续访问', 'aksrt'); ?></a>
<a href="javascript:history.back()" class="go-btn go-btn--ghost"><?php esc_html_e('返回', 'aksrt'); ?></a>
</div>
</div>
</body>
</html>
<?php
exit;
}
/**
* Replace external links in post content with go links
*/
public function replace_external_links(string $content): string {
if (!$this->is_enabled()) return $content;
if (is_admin()) return $content;
if (!is_singular()) return $content;
// seo_go_link_post: whether to replace links in post content (default true)
if (class_exists('\AKSRT\Settings\Settings') && empty(\AKSRT\Settings\Settings::get_value('seo_go_link_post', true))) {
return $content;
}
return preg_replace_callback(
'/<a\s+([^>]*?)href=["\']([^"\']+)["\']([^>]*?)>/i',
function ($matches) {
$before = $matches[1];
$url = $matches[2];
$after = $matches[3];
// Skip if already a go link, anchor, or relative
if (strpos($url, '/go/') !== false || strpos($url, '#') === 0 || strpos($url, '/') === 0) {
return $matches[0];
}
// Skip if already has nofollow or is javascript
if (stripos($before . $after, 'nofollow') !== false || strpos($url, 'javascript:') === 0) {
return $matches[0];
}
if (!$this->is_external($url)) {
return $matches[0];
}
$go_url = $this->create_go_link($url);
return '<a ' . $before . 'href="' . esc_url($go_url) . '"' . $after . ' rel="nofollow noopener noreferrer">';
},
$content
);
}
}
+222
View File
@@ -0,0 +1,222 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class HomeModules {
const PREVIEW_QUERY_VAR = 'aksrt_preview';
const PREVIEW_TRANSIENT_PREFIX = 'aksrt_home_preview_';
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function init(): void {
add_filter('query_vars', [$this, 'add_query_var']);
add_action('rest_api_init', [$this, 'register_routes']);
}
public function add_query_var(array $vars): array {
$vars[] = self::PREVIEW_QUERY_VAR;
return $vars;
}
public function register_routes(): void {
register_rest_route('aksrt/v1', '/preview/homepage', [
'methods' => 'POST',
'callback' => [$this, 'rest_create_preview'],
'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
}
/**
* 获取并校验模块列表(含预览/默认回退逻辑)。
*/
private static function resolve_modules(): array {
$modules = [];
// 预览模式
$preview_id = get_query_var(self::PREVIEW_QUERY_VAR);
if ($preview_id && current_user_can('manage_options')) {
$cached = get_transient(self::PREVIEW_TRANSIENT_PREFIX . sanitize_key((string) $preview_id));
if (is_array($cached)) {
$modules = $cached;
}
}
if (empty($modules) && class_exists('\AKSRT\Settings\Settings')) {
$modules = \AKSRT\Settings\Settings::get_homepage_modules();
}
// 校验:过滤无效条目,若全部无效则清空以触发默认模板回退
if (!empty($modules)) {
$allowed = self::allowed_types();
$valid = array_filter($modules, function ($row) use ($allowed) {
return is_array($row) && !empty($row['type']) && in_array($row['type'], $allowed, true);
});
$modules = empty($valid) ? [] : array_values($valid);
}
if (empty($modules)) {
$modules = self::get_default_template('resource_standard');
}
return $modules;
}
/**
* 渲染入口(首页 CMS 形态调用)。
*
* 优先级:
* 1. ?aksrt_preview={id} 且管理员 → 读 transient 渲染
* 2. option homepage_modules
* 3. 若空 → 渲染默认模板(resource_standard)
*/
public static function render(): void {
$modules = self::resolve_modules();
echo '<div class="home-modules">';
foreach ($modules as $idx => $row) {
if (!is_array($row) || empty($row['type'])) continue;
$type = sanitize_key((string) $row['type']);
$config = isset($row['config']) && is_array($row['config']) ? $row['config'] : [];
set_query_var('aksrt_module_config', $config);
set_query_var('aksrt_module_index', $idx);
get_template_part('templates/parts/home-modules/module', $type);
}
// 清理
set_query_var('aksrt_module_config', null);
set_query_var('aksrt_module_index', null);
echo '</div>';
}
/**
* 全屏模块类型(不受侧栏约束)。
*/
public static function fullbleed_types(): array {
return ['hero'];
}
/**
* 分区渲染:仅渲染指定区域的模块。
*
* @param string $zone 'fullbleed' | 'body'
*/
public static function render_zone(string $zone = 'body'): void {
$modules = self::resolve_modules();
$fullbleed = self::fullbleed_types();
$filtered = array_filter($modules, function ($row) use ($fullbleed, $zone) {
$type = $row['type'] ?? '';
$is_fullbleed = in_array($type, $fullbleed, true);
return $zone === 'fullbleed' ? $is_fullbleed : !$is_fullbleed;
});
if (empty($filtered)) return;
$wrapper_class = $zone === 'fullbleed' ? 'home-modules home-modules--fullbleed' : 'home-modules';
echo '<div class="' . esc_attr($wrapper_class) . '">';
foreach ($filtered as $idx => $row) {
if (!is_array($row) || empty($row['type'])) continue;
$type = sanitize_key((string) $row['type']);
$config = isset($row['config']) && is_array($row['config']) ? $row['config'] : [];
set_query_var('aksrt_module_config', $config);
set_query_var('aksrt_module_index', $idx);
get_template_part('templates/parts/home-modules/module', $type);
}
set_query_var('aksrt_module_config', null);
set_query_var('aksrt_module_index', null);
echo '</div>';
}
/**
* 创建预览会话。
*/
public function rest_create_preview(\WP_REST_Request $request) {
$params = $request->get_json_params();
if (!is_array($params)) $params = $request->get_params();
$modules = isset($params['modules']) && is_array($params['modules']) ? $params['modules'] : [];
$clean = [];
$allowed_types = self::allowed_types();
foreach ($modules as $row) {
if (!is_array($row)) continue;
$type = isset($row['type']) ? sanitize_key((string) $row['type']) : '';
if (!in_array($type, $allowed_types, true)) continue;
$clean[] = [
'type' => $type,
'config' => isset($row['config']) && is_array($row['config']) ? $row['config'] : [],
];
}
$id = wp_generate_uuid4();
set_transient(self::PREVIEW_TRANSIENT_PREFIX . $id, $clean, 30 * MINUTE_IN_SECONDS);
return new \WP_REST_Response([
'preview_id' => $id,
'preview_url' => add_query_arg(self::PREVIEW_QUERY_VAR, $id, home_url('/')),
'expires_in' => 30 * MINUTE_IN_SECONDS,
], 200);
}
public static function allowed_types(): array {
return [
'hero', 'banner', 'category', 'tabs', 'ranking',
'recent', 'tag_cloud', 'friend_links', 'html', 'hotspot',
'category_recommend_cards',
// Phase 5: 运营型模块
'text_title', 'button_group', 'spacing', 'faq',
'data_grid', 'highlight_cards', 'partner_marquee', 'product_showcase',
// Widget-区等价模块(补齐 zibll widget-user / widget-more 能力)
'user', 'more',
];
}
/**
* 默认模板(3 套预设)。
*/
public static function get_default_template(string $name): array {
switch ($name) {
case 'news':
return [
['type' => 'banner', 'config' => []],
['type' => 'category', 'config' => ['count' => 8, 'columns' => 4]],
['type' => 'category', 'config' => ['count' => 8, 'columns' => 4]],
['type' => 'tag_cloud', 'config' => ['limit' => 30]],
];
case 'mixed':
return [
['type' => 'hero', 'config' => []],
['type' => 'recent', 'config' => ['count' => 6, 'layout' => 'card']],
['type' => 'category', 'config' => ['count' => 6, 'columns' => 3]],
['type' => 'category', 'config' => ['count' => 6, 'columns' => 3]],
];
case 'resource_standard':
default:
return [
['type' => 'hero', 'config' => []],
['type' => 'tabs', 'config' => ['count' => 8]],
['type' => 'recent', 'config' => ['count' => 8, 'layout' => 'grid-cms']],
['type' => 'ranking', 'config' => ['metric' => 'downloads', 'count' => 6]],
];
}
}
}
+302
View File
@@ -0,0 +1,302 @@
<?php
/**
* Page meta for 网址导航 template (page_links_* keys, zibll metabox parity).
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class LinksPageMeta {
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_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_page', [$this, 'save_meta_box']);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_page_links_meta',
__('网址导航设置', 'aksrt'),
[$this, 'render_meta_box'],
'page',
'normal',
'high'
);
}
/**
* Whether current page uses the links template.
*/
private function is_links_template(int $post_id): bool {
$tpl = (string) get_post_meta($post_id, '_wp_page_template', true);
return $tpl === 'templates/pages/page-links.php';
}
public function render_meta_box(\WP_Post $post): void {
$post_id = (int) $post->ID;
if (!$this->is_links_template($post_id)) {
echo '<p class="description">' . esc_html__('将页面模板设为「AKSRT - 网址导航」后在此配置分类、样式与提交模块。', 'aksrt') . '</p>';
return;
}
$s = function_exists('aksrt_get_page_links_settings')
? aksrt_get_page_links_settings($post_id)
: [];
wp_nonce_field('aksrt_page_links_meta', 'aksrt_page_links_meta_nonce');
$terms = get_terms([
'taxonomy' => 'link_category',
'hide_empty' => false,
]);
if (is_wp_error($terms)) {
$terms = [];
}
$selected_cats = array_map('intval', (array) ($s['category'] ?? []));
$submit_cats = array_map('intval', (array) ($s['submit_cats'] ?? []));
$search_types = (array) ($s['search_types'] ?? []);
$engine_options = [
'self' => __('站内', 'aksrt'),
'baidu' => __('百度', 'aksrt'),
'google' => __('谷歌', 'aksrt'),
'bing' => __('必应', 'aksrt'),
'sogou' => __('搜狗', 'aksrt'),
'360' => '360',
];
?>
<table class="form-table aksrt-page-links-meta" role="presentation">
<tr>
<th scope="row"><?php esc_html_e('显示页面内容', 'aksrt'); ?></th>
<td>
<label>
<input type="checkbox" name="page_links_content_s" value="1" <?php checked(!empty($s['content_s'])); ?>>
<?php esc_html_e('在链接列表上下方展示页面正文', 'aksrt'); ?>
</label>
<p>
<label>
<input type="radio" name="page_links_content_position" value="top" <?php checked(($s['content_position'] ?? 'top'), 'top'); ?>>
<?php esc_html_e('链接列表上面', 'aksrt'); ?>
</label>
&nbsp;
<label>
<input type="radio" name="page_links_content_position" value="bottom" <?php checked(($s['content_position'] ?? ''), 'bottom'); ?>>
<?php esc_html_e('链接列表下面', 'aksrt'); ?>
</label>
</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('搜索引擎模块', 'aksrt'); ?></th>
<td>
<label>
<input type="checkbox" name="page_links_search_s" value="1" <?php checked(!empty($s['search_s'])); ?>>
<?php esc_html_e('显示搜索封面(使用特色图作背景)', 'aksrt'); ?>
</label>
<p class="description"><?php esc_html_e('选择引擎(可多选,按住 Ctrl)', 'aksrt'); ?></p>
<select name="page_links_search_types[]" multiple class="widefat" size="6">
<?php foreach ($engine_options as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected(in_array($key, $search_types, true)); ?>>
<?php echo esc_html($label); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('链接分类(必选)', 'aksrt'); ?></th>
<td>
<?php if (empty($terms)) : ?>
<p class="description">
<?php
printf(
/* translators: %s: link manager URL */
wp_kses_post(__('暂无链接分类。请先在 <a href="%s">链接管理</a> 中创建分类与链接。', 'aksrt')),
esc_url(admin_url('link-manager.php'))
);
?>
</p>
<?php else : ?>
<select name="page_links_category[]" multiple class="widefat" size="8">
<?php foreach ($terms as $term) : ?>
<option value="<?php echo (int) $term->term_id; ?>" <?php selected(in_array((int) $term->term_id, $selected_cats, true)); ?>>
<?php echo esc_html($term->name . ' (' . (int) $term->count . ')'); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e('多选时按导航目录展示;一个都不选则不显示链接。', 'aksrt'); ?></p>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('显示样式', 'aksrt'); ?></th>
<td>
<?php
$style = (string) ($s['style'] ?? 'card');
$styles = [
'card' => __('图文列表', 'aksrt'),
'bigcard' => __('大卡片', 'aksrt'),
'image' => __('纯图片', 'aksrt'),
];
foreach ($styles as $key => $label) :
?>
<label style="margin-right:12px;">
<input type="radio" name="page_links_style" value="<?php echo esc_attr($key); ?>" <?php checked($style, $key); ?>>
<?php echo esc_html($label); ?>
</label>
<?php endforeach; ?>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('数量与排序', 'aksrt'); ?></th>
<td>
<label>
<?php esc_html_e('每类最多', 'aksrt'); ?>
<input type="number" name="page_links_limit" value="<?php echo esc_attr((string) (int) ($s['limit'] ?? 0)); ?>" min="0" step="1" class="small-text">
<?php esc_html_e('个(0=不限)', 'aksrt'); ?>
</label>
<p>
<select name="page_links_orderby">
<?php
$orderbys = [
'name' => __('名称', 'aksrt'),
'updated' => __('更新时间', 'aksrt'),
'rating' => __('评分', 'aksrt'),
'rand' => __('随机', 'aksrt'),
];
$orderby = (string) ($s['orderby'] ?? 'name');
foreach ($orderbys as $key => $label) :
?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($orderby, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
<select name="page_links_order">
<option value="ASC" <?php selected(($s['order'] ?? 'ASC'), 'ASC'); ?>><?php esc_html_e('升序', 'aksrt'); ?></option>
<option value="DESC" <?php selected(($s['order'] ?? ''), 'DESC'); ?>><?php esc_html_e('降序', 'aksrt'); ?></option>
</select>
</p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('链接行为', 'aksrt'); ?></th>
<td>
<label><input type="checkbox" name="page_links_go_s" value="1" <?php checked(!empty($s['go_s'])); ?>> <?php esc_html_e('外链重定向 /go/', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="page_links_blank_s" value="1" <?php checked(!empty($s['blank_s'])); ?>> <?php esc_html_e('新标签打开', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="page_links_nofollow_s" value="1" <?php checked(!empty($s['nofollow_s'])); ?>> <?php esc_html_e('添加 nofollow', 'aksrt'); ?></label>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('提交链接', 'aksrt'); ?></th>
<td>
<label><input type="checkbox" name="page_links_submit_s" value="1" <?php checked(!empty($s['submit_s'])); ?>> <?php esc_html_e('显示申请入驻', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="page_links_submit_sign_s" value="1" <?php checked(!empty($s['submit_sign_s'])); ?>> <?php esc_html_e('登录后才能提交', 'aksrt'); ?></label>
<p>
<label><?php esc_html_e('按钮标题', 'aksrt'); ?>
<input type="text" class="regular-text" name="page_links_submit_title" value="<?php echo esc_attr((string) ($s['submit_title'] ?? '')); ?>">
</label>
</p>
<p>
<label for="page_links_submit_dec"><?php esc_html_e('提交说明(HTML)', 'aksrt'); ?></label><br>
<textarea class="large-text" rows="5" id="page_links_submit_dec" name="page_links_submit_dec"><?php echo esc_textarea((string) ($s['submit_dec'] ?? '')); ?></textarea>
</p>
<?php if (!empty($terms)) : ?>
<p class="description"><?php esc_html_e('提交时可选分类(空=全部分类)', 'aksrt'); ?></p>
<select name="page_links_submit_cats[]" multiple class="widefat" size="6">
<?php foreach ($terms as $term) : ?>
<option value="<?php echo (int) $term->term_id; ?>" <?php selected(in_array((int) $term->term_id, $submit_cats, true)); ?>>
<?php echo esc_html($term->name); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</td>
</tr>
</table>
<?php
}
public function save_meta_box(int $post_id): void {
if (!isset($_POST['aksrt_page_links_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_page_links_meta_nonce'])), 'aksrt_page_links_meta')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (!$this->is_links_template($post_id) && empty($_POST['page_links_category']) && empty($_POST['page_links_submit_s'])) {
// Still allow save if user just switched template in same request.
$tpl = isset($_POST['page_template']) ? (string) wp_unslash($_POST['page_template']) : '';
if ($tpl !== 'templates/pages/page-links.php') {
return;
}
}
$bool_keys = [
'page_links_content_s',
'page_links_search_s',
'page_links_go_s',
'page_links_blank_s',
'page_links_nofollow_s',
'page_links_submit_s',
'page_links_submit_sign_s',
];
foreach ($bool_keys as $key) {
update_post_meta($post_id, $key, isset($_POST[$key]) ? 1 : 0);
}
$position = isset($_POST['page_links_content_position']) ? sanitize_key((string) wp_unslash($_POST['page_links_content_position'])) : 'top';
if (!in_array($position, ['top', 'bottom'], true)) {
$position = 'top';
}
update_post_meta($post_id, 'page_links_content_position', $position);
$style = isset($_POST['page_links_style']) ? sanitize_key((string) wp_unslash($_POST['page_links_style'])) : 'card';
if (!in_array($style, ['card', 'bigcard', 'image', 'simple'], true)) {
$style = 'card';
}
update_post_meta($post_id, 'page_links_style', $style);
$orderby = isset($_POST['page_links_orderby']) ? sanitize_key((string) wp_unslash($_POST['page_links_orderby'])) : 'name';
if (!in_array($orderby, ['name', 'updated', 'rating', 'rand', 'id', 'length'], true)) {
$orderby = 'name';
}
update_post_meta($post_id, 'page_links_orderby', $orderby);
$order = isset($_POST['page_links_order']) ? strtoupper(sanitize_text_field(wp_unslash((string) $_POST['page_links_order']))) : 'ASC';
update_post_meta($post_id, 'page_links_order', $order === 'DESC' ? 'DESC' : 'ASC');
$limit = isset($_POST['page_links_limit']) ? max(0, (int) $_POST['page_links_limit']) : 0;
update_post_meta($post_id, 'page_links_limit', $limit);
$cats = isset($_POST['page_links_category']) ? array_values(array_filter(array_map('intval', (array) wp_unslash($_POST['page_links_category'])))) : [];
update_post_meta($post_id, 'page_links_category', $cats);
$submit_cats = isset($_POST['page_links_submit_cats']) ? array_values(array_filter(array_map('intval', (array) wp_unslash($_POST['page_links_submit_cats'])))) : [];
update_post_meta($post_id, 'page_links_submit_cats', $submit_cats);
$engines = isset($_POST['page_links_search_types']) ? array_values(array_filter(array_map('sanitize_key', (array) wp_unslash($_POST['page_links_search_types'])))) : [];
$allowed = ['self', 'baidu', 'google', 'bing', 'sogou', '360'];
$engines = array_values(array_intersect($engines, $allowed));
update_post_meta($post_id, 'page_links_search_types', $engines);
$title = isset($_POST['page_links_submit_title']) ? sanitize_text_field(wp_unslash((string) $_POST['page_links_submit_title'])) : '';
update_post_meta($post_id, 'page_links_submit_title', $title);
$dec = isset($_POST['page_links_submit_dec']) ? wp_kses_post(wp_unslash((string) $_POST['page_links_submit_dec'])) : '';
update_post_meta($post_id, 'page_links_submit_dec', $dec);
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
/**
* Frontend links (bookmarks) submit — zibll frontend_links_submit parity.
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class LinksSubmit {
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_action('wp_ajax_aksrt_frontend_links_submit', [$this, 'ajax_submit']);
add_action('wp_ajax_nopriv_aksrt_frontend_links_submit', [$this, 'ajax_submit']);
}
/**
* AJAX: create a pending WordPress bookmark (link).
*/
public function ajax_submit(): void {
if (!check_ajax_referer('aksrt_frontend_links_submit', '_wpnonce', false)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('安全校验失败,请刷新后重试', 'aksrt')]);
}
if (isset($_COOKIE['aksrt_links_submit_time'])) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('操作过于频繁,请稍候再试', 'aksrt')]);
}
$link_name = isset($_POST['link_name']) ? sanitize_text_field(wp_unslash((string) $_POST['link_name'])) : '';
$link_url = isset($_POST['link_url']) ? esc_url_raw(wp_unslash((string) $_POST['link_url'])) : '';
$link_desc = isset($_POST['link_description']) ? sanitize_text_field(wp_unslash((string) $_POST['link_description'])) : '';
$cat_raw = isset($_POST['link_category']) ? wp_unslash($_POST['link_category']) : [];
if ($link_name === '') {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('请填写网站名称', 'aksrt')]);
}
if ($link_url === '' || !wp_http_validate_url($link_url)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('网站地址格式错误', 'aksrt')]);
}
// Optional logo upload.
$logo_url = '';
if (!empty($_FILES['image']['name'])) {
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_id = media_handle_upload('image', 0);
if (is_wp_error($attachment_id)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => $attachment_id->get_error_message()]);
}
$src = wp_get_attachment_image_src((int) $attachment_id, 'medium');
$logo_url = !empty($src[0]) ? (string) $src[0] : (string) wp_get_attachment_url((int) $attachment_id);
}
$categories = [];
if (is_array($cat_raw)) {
$categories = array_values(array_filter(array_map('intval', $cat_raw)));
} elseif ($cat_raw !== '' && $cat_raw !== null) {
$categories = [absint($cat_raw)];
}
// Duplicate URL guard.
global $wpdb;
$exists = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(1) FROM {$wpdb->links} WHERE link_url = %s",
$link_url
)
);
if ($exists > 0) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('您的链接已提交,请勿重复提交', 'aksrt')]);
}
if (!function_exists('wp_insert_link')) {
require_once ABSPATH . 'wp-admin/includes/bookmark.php';
}
$linkdata = [
'link_name' => $link_name,
'link_url' => $link_url,
'link_description' => $link_desc,
'link_image' => $logo_url,
// Non-admins create invisible links pending review (zibll parity).
'link_visible' => current_user_can('manage_links') ? 'Y' : 'N',
'link_category' => $categories,
];
$links_id = wp_insert_link($linkdata, true);
if (is_wp_error($links_id)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => $links_id->get_error_message()]);
}
// 30s client throttle cookie.
if (!headers_sent()) {
setcookie('aksrt_links_submit_time', (string) time(), time() + 30, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
}
$msg = $linkdata['link_visible'] === 'Y'
? __('提交成功', 'aksrt')
: __('提交成功,等待管理员处理', 'aksrt');
/**
* Fires after a frontend link submission is stored.
*
* @param array $linkdata Sanitized bookmark data.
* @param int $links_id New link ID.
*/
do_action('aksrt_frontend_links_submit_success', $linkdata, (int) $links_id);
wp_send_json(['error' => 0, 'msg' => $msg, 'reload' => true]);
}
}
+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
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* 文章导航页设置 AJAX(zibll action/post_navs.php parity).
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class PostsNavs {
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_action('wp_ajax_aksrt_post_navs_settings', [$this, 'ajax_save_settings']);
}
/**
* Super-admin only: save navs_page_name / desc / ordered cat IDs.
*/
public function ajax_save_settings(): void {
if (!current_user_can('manage_options')) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('权限不足', 'aksrt')]);
}
if (!check_ajax_referer('aksrt_post_navs_settings', '_wpnonce', false)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('安全校验失败', 'aksrt')]);
}
$page_id = isset($_POST['page_id']) ? absint($_POST['page_id']) : 0;
if ($page_id <= 0 || get_post_type($page_id) !== 'page') {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('无效页面', 'aksrt')]);
}
$cat_ids = [];
if (isset($_POST['navs_show_cat_id'])) {
$raw = wp_unslash($_POST['navs_show_cat_id']);
if (is_array($raw)) {
$cat_ids = array_values(array_filter(array_map('intval', $raw)));
} elseif (is_string($raw) && $raw !== '') {
$decoded = json_decode($raw, true);
if (is_array($decoded)) {
$cat_ids = array_values(array_filter(array_map('intval', $decoded)));
} else {
$cat_ids = array_values(array_filter(array_map('intval', explode(',', $raw))));
}
}
}
if (empty($cat_ids)) {
wp_send_json(['error' => 1, 'ys' => 'danger', 'msg' => __('请选择需要显示的分类', 'aksrt')]);
}
if (isset($_POST['navs_page_name'])) {
update_post_meta($page_id, 'navs_page_name', sanitize_text_field(wp_unslash((string) $_POST['navs_page_name'])));
}
if (isset($_POST['navs_page_desc'])) {
update_post_meta($page_id, 'navs_page_desc', sanitize_text_field(wp_unslash((string) $_POST['navs_page_desc'])));
}
update_post_meta($page_id, 'navs_show_cat_id', $cat_ids);
wp_send_json([
'error' => 0,
'msg' => __('保存成功', 'aksrt'),
'reload'=> true,
'ids' => $cat_ids,
]);
}
}
+558
View File
@@ -0,0 +1,558 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class ResourceMetaBox {
private static $instance = null;
private function __construct() {}
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_post', [$this, 'save'], 10, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_pay_meta',
__('付费内容设置', 'aksrt'),
[$this, 'render'],
'post',
'normal',
'high'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_pay_meta_save', 'aksrt_pay_meta_nonce');
$meta = function_exists('aksrt_get_pay_meta')
? aksrt_get_pay_meta($post)
: DomainModel::default_pay_meta();
$pay_types = aksrt_get_pay_types();
$is_paid = ($meta['pay_type'] ?? 'no') !== 'no';
$downloads_json = !empty($meta['downloads'])
? wp_json_encode($meta['downloads'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: '';
$attributes_json = !empty($meta['attributes'])
? wp_json_encode($meta['attributes'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: '';
$gallery_json = !empty($meta['gallery'])
? wp_json_encode($meta['gallery'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: '';
?>
<style>
.aksrt-pmb__row { display: grid; grid-template-columns: 140px 1fr; gap: 12px 16px; align-items: start; margin-bottom: 12px; }
.aksrt-pmb__row label { font-weight: 600; color: #1f2937; padding-top: 6px; font-size: 13px; }
.aksrt-pmb__row input[type="text"], .aksrt-pmb__row input[type="number"], .aksrt-pmb__row input[type="url"], .aksrt-pmb__row textarea, .aksrt-pmb__row select { width: 100%; max-width: 560px; }
.aksrt-pmb__row textarea { min-height: 120px; font-family: Consolas, "Liberation Mono", monospace; font-size: 12px; }
.aksrt-pmb__hint { color: #6b7280; font-size: 12px; margin-top: 4px; line-height: 1.5; }
.aksrt-pmb__section { margin-top: 16px; padding-top: 16px; border-top: 1px solid #e5e7eb; }
.aksrt-pmb__section-title { font-size: 14px; font-weight: 700; color: #111827; margin-bottom: 12px; }
.aksrt-pmb__toggle { display: flex; align-items: center; gap: 8px; padding: 12px; background: #f3f4f6; border-radius: 6px; margin-bottom: 16px; }
.aksrt-pmb__toggle label { font-weight: 600; }
.aksrt-pmb__vip-row { display: flex; gap: 12px; flex-wrap: wrap; }
.aksrt-pmb__vip-row > div { flex: 1; min-width: 120px; }
.aksrt-pmb__vip-row label { font-weight: 400; font-size: 12px; color: #6b7280; }
.aksrt-pmb__tools { margin-top: 6px; display: flex; gap: 6px; flex-wrap: wrap; }
.aksrt-pmb__tools button { font-size: 12px; }
.aksrt-pmb__error { color: #dc2626; font-size: 12px; display: none; margin-top: 4px; }
.aksrt-pmb__fields[data-disabled="1"] { opacity: 0.45; pointer-events: none; }
</style>
<div class="aksrt-pmb__toggle">
<label for="aksrt_pay_type"><?php esc_html_e('付费类型', 'aksrt'); ?></label>
<select id="aksrt_pay_type" name="aksrt_pay_type" style="min-width:160px;">
<?php foreach ($pay_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($meta['pay_type'], $key); ?>>
<?php echo esc_html($label); ?>
</option>
<?php endforeach; ?>
</select>
<span class="aksrt-pmb__hint">
<?php esc_html_e('选择「不付费」则为普通文章;选择其他类型将启用对应的付费功能。', 'aksrt'); ?>
</span>
</div>
<div class="aksrt-pmb__fields" id="aksrt-pmb-fields" data-disabled="<?php echo $is_paid ? '0' : '1'; ?>">
<!-- 基础价格 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('价格设置', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_modo"><?php esc_html_e('支付方式', 'aksrt'); ?></label>
<div>
<select id="aksrt_pay_modo" name="aksrt_pay_modo" style="min-width:160px;">
<option value="0" <?php selected((string) ($meta['pay_modo'] ?? '0'), '0'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected((string) ($meta['pay_modo'] ?? '0'), 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
<p class="aksrt-pmb__hint"><?php esc_html_e('对齐 zibll pay_modo:现金价或积分价结算。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_price"><?php esc_html_e('现金价格', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_price" name="aksrt_pay_price" value="<?php echo esc_attr((string) $meta['price']); ?>" min="0" step="0.01">
<p class="aksrt-pmb__hint"><?php esc_html_e('支付方式为「现金」时使用。0 = 免费。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_points_price"><?php esc_html_e('积分价格', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_points_price" name="aksrt_pay_points_price" value="<?php echo esc_attr((string) ($meta['points_price'] ?? 0)); ?>" min="0" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('支付方式为「积分」时使用。0 = 免费。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_original_price"><?php esc_html_e('原价(划线价)', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_original_price" name="aksrt_pay_original_price" value="<?php echo esc_attr((string) ($meta['original_price'] ?? '')); ?>" min="0" step="0.01" placeholder="<?php esc_attr_e('可选,显示划线效果', 'aksrt'); ?>">
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_limit"><?php esc_html_e('购买限制', 'aksrt'); ?></label>
<div>
<select id="aksrt_pay_limit" name="aksrt_pay_limit" style="min-width:200px;">
<option value="0" <?php selected((int) ($meta['pay_limit'] ?? 0), 0); ?>><?php esc_html_e('所有用户可购买', 'aksrt'); ?></option>
<option value="1" <?php selected((int) ($meta['pay_limit'] ?? 0), 1); ?>><?php esc_html_e('仅 VIP1 及以上', 'aksrt'); ?></option>
<option value="2" <?php selected((int) ($meta['pay_limit'] ?? 0), 2); ?>><?php esc_html_e('仅 VIP2', 'aksrt'); ?></option>
</select>
<p class="aksrt-pmb__hint"><?php esc_html_e('对齐 zibll pay_limit:限制购买人群(会员专属资源)。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label><?php esc_html_e('VIP 专属价', 'aksrt'); ?></label>
<div>
<div class="aksrt-pmb__vip-row">
<div>
<label for="aksrt_pay_vip1_price"><?php esc_html_e('VIP1 现金价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip1_price" name="aksrt_pay_vip1_price" value="<?php echo esc_attr((string) ($meta['vip1_price'] ?? '')); ?>" min="0" step="0.01" placeholder="0">
</div>
<div>
<label for="aksrt_pay_vip2_price"><?php esc_html_e('VIP2 现金价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip2_price" name="aksrt_pay_vip2_price" value="<?php echo esc_attr((string) ($meta['vip2_price'] ?? '')); ?>" min="0" step="0.01" placeholder="0">
</div>
<div>
<label for="aksrt_pay_vip1_points"><?php esc_html_e('VIP1 积分价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip1_points" name="aksrt_pay_vip1_points" value="<?php echo esc_attr((string) ($meta['vip1_points'] ?? '')); ?>" min="0" step="1" placeholder="0">
</div>
<div>
<label for="aksrt_pay_vip2_points"><?php esc_html_e('VIP2 积分价', 'aksrt'); ?></label>
<input type="number" id="aksrt_pay_vip2_points" name="aksrt_pay_vip2_points" value="<?php echo esc_attr((string) ($meta['vip2_points'] ?? '')); ?>" min="0" step="1" placeholder="0">
</div>
</div>
<p class="aksrt-pmb__hint"><?php esc_html_e('VIP 用户专属价。留空或 0 表示不启用该档位优惠。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 资源信息(download/gallery/video 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-resource-fields">
<div class="aksrt-pmb__section-title"><?php esc_html_e('资源信息', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_version"><?php esc_html_e('版本', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_version" name="aksrt_pay_version" value="<?php echo esc_attr((string) $meta['version']); ?>" placeholder="v1.0.0">
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_size"><?php esc_html_e('文件大小', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_size" name="aksrt_pay_size" value="<?php echo esc_attr((string) $meta['size']); ?>" placeholder="210 MB">
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_compat"><?php esc_html_e('兼容性', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_compat" name="aksrt_pay_compat" value="<?php echo esc_attr((string) $meta['compat']); ?>" placeholder="WordPress 6.4+">
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_license"><?php esc_html_e('授权', 'aksrt'); ?></label>
<input type="text" id="aksrt_pay_license" name="aksrt_pay_license" value="<?php echo esc_attr((string) $meta['license']); ?>" placeholder="MIT / GPL / 自用">
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_demo_url"><?php esc_html_e('演示地址', 'aksrt'); ?></label>
<input type="url" id="aksrt_pay_demo_url" name="aksrt_pay_demo_url" value="<?php echo esc_attr((string) $meta['demo_url']); ?>" placeholder="https://">
</div>
</div>
<!-- 自定义属性 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('自定义属性', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_attributes"><?php esc_html_e('属性列表', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_attributes" name="aksrt_pay_attributes" placeholder='[
{"key": "格式", "value": "PSD"},
{"key": "分辨率", "value": "4K"}
]'><?php echo esc_textarea($attributes_json); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('JSON 数组。每项:key(属性名)/ value(属性值)。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 下载源(download 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-download-fields">
<div class="aksrt-pmb__section-title"><?php esc_html_e('下载源', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_downloads"><?php esc_html_e('下载链接', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_downloads" name="aksrt_pay_downloads" placeholder='[
{
"label": "百度网盘",
"url": "https://pan.baidu.com/s/xxx",
"type": "net_disk",
"extract_code": "abcd"
}
]'><?php echo esc_textarea($downloads_json); ?></textarea>
<div class="aksrt-pmb__error" id="aksrt-pmb-dl-err"></div>
<div class="aksrt-pmb__tools">
<button type="button" class="button button-small" id="aksrt-pmb-add-dl"><?php esc_html_e('追加示例条目', 'aksrt'); ?></button>
<button type="button" class="button button-small" id="aksrt-pmb-fmt-dl"><?php esc_html_e('格式化 JSON', 'aksrt'); ?></button>
</div>
<p class="aksrt-pmb__hint"><?php esc_html_e('JSON 数组。每项:label / url / type(direct | net_disk | external)/ extract_code。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 图库 URL(gallery 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-gallery-fields" style="display:none;">
<div class="aksrt-pmb__section-title"><?php esc_html_e('付费图库', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_gallery"><?php esc_html_e('图片 URL 列表', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_gallery" name="aksrt_pay_gallery" placeholder='[
"https://example.com/img1.jpg",
"https://example.com/img2.jpg"
]'><?php echo esc_textarea($gallery_json); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('JSON 数组。每项为图片 URL。付费前图片将被模糊遮罩。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 视频 URL(video 类型显示) -->
<div class="aksrt-pmb__section" id="aksrt-pmb-video-fields" style="display:none;">
<div class="aksrt-pmb__section-title"><?php esc_html_e('付费视频', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_video_url"><?php esc_html_e('视频 URL', 'aksrt'); ?></label>
<div>
<input type="url" id="aksrt_pay_video_url" name="aksrt_pay_video_url" value="<?php echo esc_attr((string) ($meta['video_url'] ?? '')); ?>" placeholder="https://example.com/video.mp4">
<p class="aksrt-pmb__hint"><?php esc_html_e('支持 MP4 直链或视频平台嵌入链接。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- 选项覆盖 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('选项覆盖', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label><?php esc_html_e('单条覆盖', 'aksrt'); ?></label>
<div>
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
<input type="checkbox" id="aksrt_pay_require_login" name="aksrt_pay_require_login" value="1" <?php checked($meta['require_login']); ?>>
<label for="aksrt_pay_require_login" style="font-weight:400;padding:0;"><?php esc_html_e('强制登录后才可访问', 'aksrt'); ?></label>
</div>
<div style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" id="aksrt_pay_require_captcha" name="aksrt_pay_require_captcha" value="1" <?php checked($meta['require_captcha']); ?>>
<label for="aksrt_pay_require_captcha" style="font-weight:400;padding:0;"><?php esc_html_e('强制需要人机验证', 'aksrt'); ?></label>
</div>
<p class="aksrt-pmb__hint"><?php esc_html_e('全局开关在后台统一控制,这里仅作为单条强制覆盖。', 'aksrt'); ?></p>
</div>
</div>
</div>
<!-- §3.8 电商增强 -->
<div class="aksrt-pmb__section">
<div class="aksrt-pmb__section-title"><?php esc_html_e('电商增强', 'aksrt'); ?></div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_virtual_sales"><?php esc_html_e('虚拟销量', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_virtual_sales" name="aksrt_pay_virtual_sales" value="<?php echo esc_attr((string) ($meta['virtual_sales'] ?? 0)); ?>" min="0" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('在前端展示的销量基础上叠加此数值,用于展示效果。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_limit_over_price"><?php esc_html_e('超额下载补价', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_limit_over_price" name="aksrt_pay_limit_over_price" value="<?php echo esc_attr((string) ($meta['download_limit_over_price'] ?? 0)); ?>" min="0" step="0.01">
<p class="aksrt-pmb__hint"><?php esc_html_e('免费日限额用尽后允许购买的补价(现金/积分随支付方式)。0 = 不允许超额购买。对齐 zibll download_limit_over_price。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label><?php esc_html_e('优惠码', 'aksrt'); ?></label>
<div>
<div style="display:flex; align-items:center; gap:6px; margin-bottom:8px;">
<input type="checkbox" id="aksrt_pay_coupon_s" name="aksrt_pay_coupon_s" value="1" <?php checked(!empty($meta['coupon_s'])); ?>>
<label for="aksrt_pay_coupon_s" style="font-weight:400;padding:0;"><?php esc_html_e('允许使用优惠码(全站开关关闭时仍可对本篇启用)', 'aksrt'); ?></label>
</div>
<textarea id="aksrt_pay_coupon_desc" name="aksrt_pay_coupon_desc" style="min-height:60px;" placeholder="<?php esc_attr_e('本篇优惠码说明,可留空', 'aksrt'); ?>"><?php echo esc_textarea((string) ($meta['coupon_desc'] ?? '')); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('对齐 zibll coupon_s / coupon_desc。积分支付不使用优惠码。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_rebate_discount"><?php esc_html_e('推广优惠金额', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_rebate_discount" name="aksrt_pay_rebate_discount" value="<?php echo esc_attr((string) ($meta['pay_rebate_discount'] ?? 0)); ?>" min="0" step="0.01">
<p class="aksrt-pmb__hint"><?php esc_html_e('有有效推荐人且开启推广返佣时,现金购买可先减此金额(买家侧推广折扣,对齐 zibll pay_rebate_discount)。0 = 不启用。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_stock"><?php esc_html_e('库存', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_stock" name="aksrt_pay_stock" value="<?php echo esc_attr((string) ($meta['stock'] ?? -1)); ?>" min="-1" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('-1 表示无限库存;>0 表示限量,售完即止。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_purchase_limit"><?php esc_html_e('限购数量', 'aksrt'); ?></label>
<div>
<input type="number" id="aksrt_pay_purchase_limit" name="aksrt_pay_purchase_limit" value="<?php echo esc_attr((string) ($meta['purchase_limit'] ?? 0)); ?>" min="0" step="1">
<p class="aksrt-pmb__hint"><?php esc_html_e('每用户最多可购买次数,0 表示不限购。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_card_keys"><?php esc_html_e('自动发货卡密', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_card_keys" name="aksrt_pay_card_keys" placeholder="<?php esc_attr_e('每行一个卡密/激活码,购买后自动发放一个', 'aksrt'); ?>"><?php echo esc_textarea(implode("\n", $meta['card_keys'] ?? [])); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('每行一个卡密,购买后自动消耗一个并发给用户。留空则不自动发货。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label for="aksrt_pay_free_preview"><?php esc_html_e('免费试读段落', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_pay_free_preview" name="aksrt_pay_free_preview" style="min-height:80px;" placeholder="<?php esc_attr_e('付费内容的免费预览段落(支持 HTML)', 'aksrt'); ?>"><?php echo esc_textarea($meta['free_preview'] ?? ''); ?></textarea>
<p class="aksrt-pmb__hint"><?php esc_html_e('未付费用户可见的预览内容,支持基础 HTML。留空则不显示预览。', 'aksrt'); ?></p>
</div>
</div>
<div class="aksrt-pmb__row">
<label><?php esc_html_e('销量显示', 'aksrt'); ?></label>
<div style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" id="aksrt_pay_show_sales" name="aksrt_pay_show_sales" value="1" <?php checked($meta['show_sales'] ?? true); ?>>
<label for="aksrt_pay_show_sales" style="font-weight:400;padding:0;"><?php esc_html_e('在前端显示销量', 'aksrt'); ?></label>
</div>
</div>
</div>
</div>
<script>
(function () {
var typeSelect = document.getElementById('aksrt_pay_type');
var fields = document.getElementById('aksrt-pmb-fields');
var dlFields = document.getElementById('aksrt-pmb-download-fields');
var galleryFields = document.getElementById('aksrt-pmb-gallery-fields');
var videoFields = document.getElementById('aksrt-pmb-video-fields');
var resourceFields = document.getElementById('aksrt-pmb-resource-fields');
function updateVisibility() {
var t = typeSelect.value;
var isPaid = t !== 'no';
fields.setAttribute('data-disabled', isPaid ? '0' : '1');
// 根据付费类型显示/隐藏相关字段
if (dlFields) dlFields.style.display = (t === 'download') ? '' : 'none';
if (galleryFields) galleryFields.style.display = (t === 'gallery') ? '' : 'none';
if (videoFields) videoFields.style.display = (t === 'video') ? '' : 'none';
if (resourceFields) resourceFields.style.display = (t === 'download' || t === 'gallery' || t === 'video') ? '' : 'none';
}
if (typeSelect) {
typeSelect.addEventListener('change', updateVisibility);
updateVisibility();
}
// 下载源 JSON 工具
var dlTa = document.getElementById('aksrt_pay_downloads');
var dlErr = document.getElementById('aksrt-pmb-dl-err');
var addDlBtn = document.getElementById('aksrt-pmb-add-dl');
var fmtDlBtn = document.getElementById('aksrt-pmb-fmt-dl');
if (addDlBtn && dlTa) {
addDlBtn.addEventListener('click', function () {
var sample = {label: '百度网盘', url: 'https://pan.baidu.com/s/xxx', type: 'net_disk', extract_code: ''};
try {
var arr = dlTa.value.trim() ? JSON.parse(dlTa.value) : [];
arr.push(sample);
dlTa.value = JSON.stringify(arr, null, 2);
if (dlErr) dlErr.style.display = 'none';
} catch (e) {
if (dlErr) { dlErr.textContent = 'JSON 解析错误: ' + e.message; dlErr.style.display = 'block'; }
}
});
}
if (fmtDlBtn && dlTa) {
fmtDlBtn.addEventListener('click', function () {
try {
var arr = JSON.parse(dlTa.value);
dlTa.value = JSON.stringify(arr, null, 2);
if (dlErr) dlErr.style.display = 'none';
} catch (e) {
if (dlErr) { dlErr.textContent = 'JSON 解析错误: ' + e.message; dlErr.style.display = 'block'; }
}
});
}
})();
</script>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (wp_is_post_revision($post_id)) return;
if ($post->post_type !== 'post') return;
if (empty($_POST['aksrt_pay_meta_nonce'])
|| !wp_verify_nonce((string) $_POST['aksrt_pay_meta_nonce'], 'aksrt_pay_meta_save')) {
return;
}
if (!current_user_can('edit_post', $post_id)) return;
// 付费类型
$allowed_types = array_keys(aksrt_get_pay_types());
$pay_type = sanitize_key((string) ($_POST['aksrt_pay_type'] ?? 'no'));
if (!in_array($pay_type, $allowed_types, true)) {
$pay_type = 'no';
}
// 价格字段
$pay_modo = sanitize_key((string) ($_POST['aksrt_pay_modo'] ?? '0'));
if ($pay_modo !== 'points') {
$pay_modo = '0';
}
$price = max(0, (float) ($_POST['aksrt_pay_price'] ?? 0));
$points_price = max(0, (int) ($_POST['aksrt_pay_points_price'] ?? 0));
// Keep legacy single-price editors usable: if points field empty and modo points, mirror price.
if ($pay_modo === 'points' && $points_price <= 0 && $price > 0) {
$points_price = (int) ceil($price);
}
if ($pay_modo !== 'points' && $price <= 0 && $points_price > 0) {
$price = (float) $points_price;
}
$original_price = max(0, (float) ($_POST['aksrt_pay_original_price'] ?? 0));
$vip1_price = max(0, (float) ($_POST['aksrt_pay_vip1_price'] ?? 0));
$vip2_price = max(0, (float) ($_POST['aksrt_pay_vip2_price'] ?? 0));
$vip1_points = max(0, (int) ($_POST['aksrt_pay_vip1_points'] ?? 0));
$vip2_points = max(0, (int) ($_POST['aksrt_pay_vip2_points'] ?? 0));
$pay_limit = (int) ($_POST['aksrt_pay_limit'] ?? 0);
if (!in_array($pay_limit, [0, 1, 2], true)) {
$pay_limit = 0;
}
// 资源信息
$version = sanitize_text_field((string) ($_POST['aksrt_pay_version'] ?? ''));
$size = sanitize_text_field((string) ($_POST['aksrt_pay_size'] ?? ''));
$compat = sanitize_text_field((string) ($_POST['aksrt_pay_compat'] ?? ''));
$license = sanitize_text_field((string) ($_POST['aksrt_pay_license'] ?? ''));
$demo_url = esc_url_raw((string) ($_POST['aksrt_pay_demo_url'] ?? ''));
$video_url = esc_url_raw((string) ($_POST['aksrt_pay_video_url'] ?? ''));
// 布尔字段
$require_login = !empty($_POST['aksrt_pay_require_login']);
$require_captcha = !empty($_POST['aksrt_pay_require_captcha']);
// JSON 字段
$downloads = $this->parse_json_array($_POST['aksrt_pay_downloads'] ?? '');
$attributes = $this->parse_json_array($_POST['aksrt_pay_attributes'] ?? '');
$gallery = $this->parse_json_array($_POST['aksrt_pay_gallery'] ?? '');
// 清洗下载源
if ($downloads !== null && class_exists('AKSRT\\Core\\Setup') && method_exists('AKSRT\\Core\\Setup', 'sanitize_downloads_meta')) {
$downloads = Setup::sanitize_downloads_meta($downloads);
}
// 保存统一元数据
$pay_meta = DomainModel::sanitize_pay_meta([
'pay_type' => $pay_type,
'pay_modo' => $pay_modo,
'price' => $price,
'points_price' => $points_price,
'original_price' => $original_price,
'vip1_price' => $vip1_price,
'vip2_price' => $vip2_price,
'vip1_points' => $vip1_points,
'vip2_points' => $vip2_points,
'pay_limit' => $pay_limit,
'version' => $version,
'size' => $size,
'compat' => $compat,
'license' => $license,
'demo_url' => $demo_url,
'video_url' => $video_url,
'downloads' => $downloads ?? [],
'attributes' => $attributes ?? [],
'gallery' => $gallery ?? [],
'require_login' => $require_login,
'require_captcha' => $require_captcha,
// §3.8 电商增强字段
'virtual_sales' => $_POST['aksrt_pay_virtual_sales'] ?? 0,
'stock' => $_POST['aksrt_pay_stock'] ?? -1,
'purchase_limit' => $_POST['aksrt_pay_purchase_limit'] ?? 0,
'card_keys' => isset($_POST['aksrt_pay_card_keys']) ? array_filter(array_map('trim', explode("\n", (string) $_POST['aksrt_pay_card_keys']))) : [],
'free_preview' => $_POST['aksrt_pay_free_preview'] ?? '',
'show_sales' => !empty($_POST['aksrt_pay_show_sales']),
'download_limit_over_price' => max(0, (float) ($_POST['aksrt_pay_limit_over_price'] ?? 0)),
'coupon_s' => !empty($_POST['aksrt_pay_coupon_s']),
'coupon_desc' => (string) ($_POST['aksrt_pay_coupon_desc'] ?? ''),
'pay_rebate_discount' => max(0, (float) ($_POST['aksrt_pay_rebate_discount'] ?? 0)),
]);
update_post_meta($post_id, DomainModel::PAY_META_KEY, $pay_meta);
// 同步标量字段用于查询(模仿 zibll 的 zibpay_type 同步)
update_post_meta($post_id, DomainModel::PAY_TYPE_KEY, (string) $pay_meta['pay_type']);
$query_price = ($pay_meta['pay_modo'] === 'points')
? (float) $pay_meta['points_price']
: (float) $pay_meta['price'];
update_post_meta($post_id, DomainModel::PAY_PRICE_KEY, $query_price);
update_post_meta($post_id, '_aksrt_pay_modo', (string) $pay_meta['pay_modo']);
update_post_meta($post_id, '_aksrt_pay_limit', (int) $pay_meta['pay_limit']);
// 清理旧的 _aksrt_is_resource 标记
update_post_meta(
$post_id,
DomainModel::RESOURCE_MARKER_KEY,
DomainModel::is_resource_pay_type((string) $pay_meta['pay_type']) ? 1 : 0
);
}
/**
* 安全解析 JSON 数组。
*/
private function parse_json_array(string $raw): ?array {
$raw = trim($raw);
if ($raw === '') return [];
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
}
+405
View File
@@ -0,0 +1,405 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class SEOHead {
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 {
// seo_enabled 主开关:关闭时不挂载任何 wp_head 输出
if (class_exists('\AKSRT\Settings\Settings')) {
$seo = \AKSRT\Settings\Settings::get_seo_settings();
if (empty($seo['seo_enabled'])) return;
}
// 单一出口:所有 SEO/OG/JSON-LD 输出集中在 priority 2。
add_action('wp_head', [$this, 'render'], 2);
// robots:dl token 页面 noindex(独立、最早输出)。
add_action('wp_head', [$this, 'render_robots'], 1);
}
/**
* 站点级 SEO 配置(带 per-request memo)。
*
* @return array<string,mixed>
*/
private function config(): array {
static $cache = null;
if ($cache !== null) {
return $cache;
}
if (class_exists('\AKSRT\Settings\Settings')) {
$s = \AKSRT\Settings\Settings::get_all();
} else {
$s = [];
}
$cache = [
'connector' => trim((string) ($s['seo_connector'] ?? '-')) ?: '-',
'site_title' => trim((string) ($s['seo_title'] ?? '')),
'description' => trim((string) ($s['seo_description'] ?? ($s['site_description'] ?? ''))),
'keywords' => trim((string) ($s['seo_keywords'] ?? '')),
'canonical' => !isset($s['canonical_tag']) || !empty($s['canonical_tag']),
'og_enabled' => empty($s['disable_og_meta']),
'jsonld' => !isset($s['seo_jsonld']) || !empty($s['seo_jsonld']),
'jsonld_single' => !isset($s['seo_jsonld_single']) || !empty($s['seo_jsonld_single']),
'jsonld_page' => !isset($s['seo_jsonld_page']) || !empty($s['seo_jsonld_page']),
'default_image' => trim((string) ($s['seo_image_default'] ?? '')),
'always_default' => !empty($s['seo_image_always_default']),
];
return $cache;
}
/**
* 解析当前请求的规范 SEO 负载。
*
* @return array{title:string,description:string,keywords:string,canonical:string,image:string,og_type:string,object:?\WP_Post}
*/
private function resolve_payload(): array {
$cfg = $this->config();
$site_name = get_bloginfo('name');
$payload = [
'title' => wp_get_document_title(),
'description' => $cfg['description'],
'keywords' => $cfg['keywords'],
'canonical' => home_url(isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '/'),
'image' => $cfg['default_image'],
'og_type' => 'website',
'object' => null,
];
if (is_singular()) {
$post = get_queried_object();
if ($post instanceof \WP_Post) {
$payload['object'] = $post;
$payload['canonical'] = (string) get_permalink($post);
$payload['og_type'] = ($post->post_type === 'post') ? 'article' : 'website';
// 单篇 SEO 覆盖:per-post meta → 文章字段 → 站点默认。
$meta = $this->post_seo_meta($post->ID);
$payload['title'] = $meta['title'] !== '' ? $meta['title'] : wp_strip_all_tags(get_the_title($post));
if ($meta['description'] !== '') {
$payload['description'] = $meta['description'];
} else {
$excerpt = has_excerpt($post)
? wp_strip_all_tags(get_the_excerpt($post))
: wp_trim_words(wp_strip_all_tags((string) $post->post_content), 40, '…');
$payload['description'] = trim((string) $excerpt);
}
if ($meta['keywords'] !== '') {
$payload['keywords'] = $meta['keywords'];
} elseif ($payload['keywords'] === '') {
$payload['keywords'] = $this->terms_to_keywords($post);
}
$payload['image'] = $this->resolve_image($post, $cfg);
}
} elseif (is_home() || is_front_page()) {
if ($cfg['site_title'] !== '') {
$payload['title'] = $cfg['site_title'];
}
$payload['canonical'] = home_url('/');
} elseif (is_category() || is_tag() || is_tax()) {
$term = get_queried_object();
if ($term instanceof \WP_Term) {
$payload['canonical'] = (string) get_term_link($term);
$meta = $this->term_seo_meta((int) $term->term_id);
if ($meta['title'] !== '') {
$payload['title'] = $meta['title'];
}
if ($meta['description'] !== '') {
$payload['description'] = $meta['description'];
} else {
$term_desc = trim(wp_strip_all_tags((string) term_description($term)));
if ($term_desc !== '') {
$payload['description'] = $term_desc;
}
}
if ($meta['keywords'] !== '') {
$payload['keywords'] = $meta['keywords'];
}
}
}
if ($cfg['always_default'] && $cfg['default_image'] !== '') {
$payload['image'] = $cfg['default_image'];
}
/**
* 允许扩展(如 AI SEO)在最终输出前调整 head 负载。
*
* @param array $payload 规范 SEO 负载。
*/
return apply_filters('aksrt_seo_head_payload', $payload);
}
/**
* 读取单篇 SEO 覆盖(per-post meta)。
*
* @param int $post_id Post ID.
* @return array{title:string,description:string,keywords:string}
*/
private function post_seo_meta(int $post_id): array {
$get = static function (string $key) use ($post_id): string {
return trim((string) get_post_meta($post_id, $key, true));
};
return [
'title' => $get('_aksrt_seo_title'),
'description' => $get('_aksrt_seo_description'),
'keywords' => $get('_aksrt_seo_keywords'),
];
}
/**
* 读取 term SEO 覆盖。
*
* @return array{title:string,description:string,keywords:string}
*/
private function term_seo_meta(int $term_id): array {
$get = static function (string $key) use ($term_id): string {
return trim((string) get_term_meta($term_id, $key, true));
};
return [
'title' => $get('_aksrt_seo_title'),
'description' => $get('_aksrt_seo_description'),
'keywords' => $get('_aksrt_seo_keywords'),
];
}
/**
* 由分类/标签生成关键词回退。
*/
private function terms_to_keywords(\WP_Post $post): string {
$names = [];
foreach ((array) get_the_category($post->ID) as $cat) {
if ($cat instanceof \WP_Term) {
$names[] = $cat->name;
}
}
$tags = get_the_terms($post, 'post_tag');
if (is_array($tags)) {
foreach ($tags as $tag) {
$names[] = $tag->name;
}
}
return implode(',', array_slice(array_values(array_unique(array_filter($names))), 0, 10));
}
/**
* 解析单篇缩略图:特色图 → 内容首图 → 站点默认 → logo。
*/
private function resolve_image(\WP_Post $post, array $cfg): string {
if (has_post_thumbnail($post)) {
$image = (string) get_the_post_thumbnail_url($post, 'large');
if ($image !== '') {
return $image;
}
}
if (function_exists('aksrt_get_first_content_image_url')) {
$image = (string) aksrt_get_first_content_image_url($post->ID);
if ($image !== '') {
return $image;
}
}
if ($cfg['default_image'] !== '') {
return $cfg['default_image'];
}
if (class_exists('\AKSRT\Settings\Settings')) {
return (string) \AKSRT\Settings\Settings::logo_url();
}
return '';
}
/**
* dl token 页面 noindex。
*/
public function render_robots(): void {
if (function_exists('get_query_var') && get_query_var('aksrt_dl_token')) {
echo '<meta name="robots" content="noindex,nofollow">' . "\n";
}
}
/**
* 输出 head(favicon + canonical + description/keywords + OG/Twitter + JSON-LD)。
*/
public function render(): void {
$cfg = $this->config();
$payload = $this->resolve_payload();
echo "\n<!-- AKSRT SEO -->\n";
// Favicon(无站点图标时回退)。
if (function_exists('aksrt_get_favicon_url')) {
$favicon = (string) aksrt_get_favicon_url();
if ($favicon !== '' && !has_site_icon()) {
printf('<link rel="icon" href="%s">' . "\n", esc_url($favicon));
}
}
// Canonical。
if ($cfg['canonical'] && !is_404() && $payload['canonical'] !== '') {
printf('<link rel="canonical" href="%s">' . "\n", esc_url($payload['canonical']));
}
// Description / Keywords。
if ($payload['description'] !== '') {
printf('<meta name="description" content="%s">' . "\n", esc_attr($payload['description']));
}
if ($payload['keywords'] !== '') {
printf('<meta name="keywords" content="%s">' . "\n", esc_attr($payload['keywords']));
}
// OpenGraph + Twitter。
if ($cfg['og_enabled']) {
$this->render_open_graph($payload);
}
// JSON-LD。
if ($cfg['jsonld']) {
$this->render_jsonld($payload, $cfg);
}
echo "<!-- /AKSRT SEO -->\n";
}
/**
* 输出 OpenGraph + Twitter Card。
*/
private function render_open_graph(array $payload): void {
$site_name = get_bloginfo('name');
$locale = str_replace('-', '_', get_locale());
$og = [
['property' => 'og:type', 'content' => $payload['og_type']],
['property' => 'og:locale', 'content' => $locale],
['property' => 'og:site_name', 'content' => $site_name],
['property' => 'og:title', 'content' => $payload['title']],
['property' => 'og:url', 'content' => $payload['canonical']],
];
if ($payload['description'] !== '') {
$og[] = ['property' => 'og:description', 'content' => $payload['description']];
}
if ($payload['image'] !== '') {
$og[] = ['property' => 'og:image', 'content' => $payload['image']];
}
if ($payload['og_type'] === 'article' && $payload['object'] instanceof \WP_Post) {
$post = $payload['object'];
$published = get_the_date('c', $post);
$modified = get_the_modified_date('c', $post);
$author = get_the_author_meta('display_name', (int) $post->post_author);
if ($published) $og[] = ['property' => 'article:published_time', 'content' => $published];
if ($modified) $og[] = ['property' => 'article:modified_time', 'content' => $modified];
if ($author) $og[] = ['property' => 'article:author', 'content' => $author];
$cats = get_the_category($post->ID);
if (!empty($cats)) {
$og[] = ['property' => 'article:section', 'content' => $cats[0]->name];
}
$tags = get_the_terms($post, 'post_tag');
if (is_array($tags)) {
foreach ($tags as $tag) {
$og[] = ['property' => 'article:tag', 'content' => $tag->name];
}
}
}
foreach ($og as $tag) {
printf('<meta property="%s" content="%s">' . "\n", esc_attr($tag['property']), esc_attr($tag['content']));
}
$twitter = [
['name' => 'twitter:card', 'content' => $payload['image'] !== '' ? 'summary_large_image' : 'summary'],
['name' => 'twitter:title', 'content' => $payload['title']],
];
if ($payload['description'] !== '') {
$twitter[] = ['name' => 'twitter:description', 'content' => $payload['description']];
}
if ($payload['image'] !== '') {
$twitter[] = ['name' => 'twitter:image', 'content' => $payload['image']];
}
foreach ($twitter as $tag) {
printf('<meta name="%s" content="%s">' . "\n", esc_attr($tag['name']), esc_attr($tag['content']));
}
}
/**
* 输出 JSON-LD 结构化数据。
*/
private function render_jsonld(array $payload, array $cfg): void {
$graph = [];
// WebSite(全站)。
$graph[] = [
'@type' => 'WebSite',
'@id' => home_url('/#website'),
'url' => home_url('/'),
'name' => get_bloginfo('name'),
'inLanguage' => get_locale(),
];
if (is_singular('post') && $cfg['jsonld_single'] && $payload['object'] instanceof \WP_Post) {
$post = $payload['object'];
$node = [
'@type' => 'Article',
'headline' => $payload['title'],
'url' => $payload['canonical'],
'datePublished' => get_the_date('c', $post),
'dateModified' => get_the_modified_date('c', $post),
'author' => [
'@type' => 'Person',
'name' => get_the_author_meta('display_name', (int) $post->post_author),
],
'publisher' => [
'@type' => 'Organization',
'name' => get_bloginfo('name'),
],
];
if ($payload['description'] !== '') {
$node['description'] = $payload['description'];
}
if ($payload['image'] !== '') {
$node['image'] = $payload['image'];
}
$graph[] = $node;
} elseif (is_page() && $cfg['jsonld_page'] && $payload['object'] instanceof \WP_Post) {
$graph[] = [
'@type' => 'WebPage',
'url' => $payload['canonical'],
'name' => $payload['title'],
];
}
if (empty($graph)) {
return;
}
$doc = [
'@context' => 'https://schema.org',
'@graph' => $graph,
];
echo '<script type="application/ld+json">'
. wp_json_encode($doc, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
. '</script>' . "\n";
}
}
+278
View File
@@ -0,0 +1,278 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class SEOMetaBox {
private static $instance = null;
public const META_TITLE = '_aksrt_seo_title';
public const META_KEYWORDS = '_aksrt_seo_keywords';
public const META_DESCRIPTION = '_aksrt_seo_description';
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'], 10, 2);
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
// Term SEO (category / post_tag)
add_action('category_edit_form_fields', [$this, 'render_term_fields'], 20);
add_action('post_tag_edit_form_fields', [$this, 'render_term_fields'], 20);
add_action('edited_category', [$this, 'save_term_fields']);
add_action('edited_post_tag', [$this, 'save_term_fields']);
}
public function register_meta_box(): void {
foreach (['post', 'page'] as $screen) {
add_meta_box(
'aksrt_seo_meta',
__('独立 SEO', 'aksrt'),
[$this, 'render_meta_box'],
$screen,
'normal',
'high'
);
}
}
public function render_meta_box(\WP_Post $post): void {
wp_nonce_field('aksrt_seo_meta_save', 'aksrt_seo_meta_nonce');
$title = (string) get_post_meta($post->ID, self::META_TITLE, true);
$keywords = (string) get_post_meta($post->ID, self::META_KEYWORDS, true);
$description = (string) get_post_meta($post->ID, self::META_DESCRIPTION, true);
$ai_btn = $this->render_ai_button('post', (int) $post->ID);
$summary_btn = $this->render_summary_button((int) $post->ID);
?>
<div class="aksrt-seo-meta-box">
<p>
<label for="aksrt_seo_title"><strong><?php esc_html_e('SEO 标题', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_title" name="aksrt_seo_title" value="<?php echo esc_attr($title); ?>" />
</p>
<p>
<label for="aksrt_seo_keywords"><strong><?php esc_html_e('SEO 关键词', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_keywords" name="aksrt_seo_keywords" value="<?php echo esc_attr($keywords); ?>" placeholder="<?php esc_attr_e('多个关键词用英文逗号分隔', 'aksrt'); ?>" />
</p>
<p>
<label for="aksrt_seo_description"><strong><?php esc_html_e('SEO 描述', 'aksrt'); ?></strong></label>
<textarea class="widefat" rows="3" id="aksrt_seo_description" name="aksrt_seo_description"><?php echo esc_textarea($description); ?></textarea>
<span class="description"><?php esc_html_e('建议 50–150 字。', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
</p>
<?php if ($summary_btn !== '') : ?>
<p class="aksrt-ai-summary-row">
<span class="description"><?php esc_html_e('写作辅助:根据正文生成摘要并填入「摘要」字段(不自动保存)。', 'aksrt'); ?></span>
<?php echo $summary_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</p>
<?php endif; ?>
</div>
<?php
}
public function save_meta_box(int $post_id, $post = null): void {
if (!isset($_POST['aksrt_seo_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['aksrt_seo_meta_nonce'])), 'aksrt_seo_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (wp_is_post_revision($post_id)) {
return;
}
$map = [
self::META_TITLE => 'aksrt_seo_title',
self::META_KEYWORDS => 'aksrt_seo_keywords',
self::META_DESCRIPTION => 'aksrt_seo_description',
];
foreach ($map as $meta_key => $field) {
if (!isset($_POST[$field])) {
continue;
}
$value = sanitize_text_field(wp_unslash((string) $_POST[$field]));
if ($value === '') {
delete_post_meta($post_id, $meta_key);
} else {
update_post_meta($post_id, $meta_key, $value);
}
}
}
/**
* @param \WP_Term $term
*/
public function render_term_fields($term): void {
if (!$term instanceof \WP_Term) {
return;
}
$title = (string) get_term_meta($term->term_id, self::META_TITLE, true);
$keywords = (string) get_term_meta($term->term_id, self::META_KEYWORDS, true);
$description = (string) get_term_meta($term->term_id, self::META_DESCRIPTION, true);
$ai_btn = $this->render_ai_button('term', (int) $term->term_id);
?>
<tr class="form-field">
<th scope="row"><label for="aksrt_seo_title"><?php esc_html_e('SEO 标题', 'aksrt'); ?></label></th>
<td>
<input type="text" name="aksrt_seo_title" id="aksrt_seo_title" class="aksrt-term-seo-title" value="<?php echo esc_attr($title); ?>" />
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_seo_keywords"><?php esc_html_e('SEO 关键词', 'aksrt'); ?></label></th>
<td>
<input type="text" name="aksrt_seo_keywords" id="aksrt_seo_keywords" class="aksrt-term-seo-keywords" value="<?php echo esc_attr($keywords); ?>" />
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_seo_description"><?php esc_html_e('SEO 描述', 'aksrt'); ?></label></th>
<td>
<textarea name="aksrt_seo_description" id="aksrt_seo_description" class="aksrt-term-seo-description" rows="4"><?php echo esc_textarea($description); ?></textarea>
<p class="description"><?php esc_html_e('主题默认会自动设置 SEO 内容,如需单独设置请在此输入。', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
</td>
</tr>
<?php
}
public function save_term_fields(int $term_id): void {
if (!current_user_can('edit_term', $term_id)) {
return;
}
$map = [
self::META_TITLE => 'aksrt_seo_title',
self::META_KEYWORDS => 'aksrt_seo_keywords',
self::META_DESCRIPTION => 'aksrt_seo_description',
];
foreach ($map as $meta_key => $field) {
if (!isset($_POST[$field])) {
continue;
}
$value = sanitize_text_field(wp_unslash((string) $_POST[$field]));
if ($value === '') {
delete_term_meta($term_id, $meta_key);
} else {
update_term_meta($term_id, $meta_key, $value);
}
}
}
public function enqueue_assets(string $hook): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen) {
return;
}
$ok = in_array($screen->base, ['post', 'term', 'edit-tags'], true);
if (!$ok) {
return;
}
$path = AKSRT_DIR . '/assets/js/admin-ai-seo.js';
if (!is_file($path)) {
return;
}
wp_enqueue_script(
'aksrt-admin-ai-seo',
AKSRT_URI . '/assets/js/admin-ai-seo.js',
[],
(string) filemtime($path),
true
);
$status = class_exists('\AKSRT\Ai\AI') ? \AKSRT\Ai\AI::get_instance()->get_status() : [];
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
'nonce' => wp_create_nonce('wp_rest'),
'ai' => [
'available' => !empty($status['available']),
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
'write_assist' => !empty($status['available']),
],
'i18n' => [
'generating' => __('生成中…', 'aksrt'),
'generate' => __('AI 生成 SEO', 'aksrt'),
'generateSummary' => __('AI 生成摘要', 'aksrt'),
'success' => __('已填入 SEO 字段,请检查后更新保存', 'aksrt'),
'summarySuccess' => __('已填入摘要字段,请检查后更新保存', 'aksrt'),
'failed' => __('生成失败', 'aksrt'),
'needSave' => __('请先保存后再生成 SEO', 'aksrt'),
'needContent' => __('请先填写正文内容', 'aksrt'),
'notAvailable' => __('AI 未配置或未启用', 'aksrt'),
'noExcerptField' => __('未找到摘要字段,请展开「摘要」面板', 'aksrt'),
],
]);
wp_add_inline_style('wp-admin', '
.aksrt-ai-seo-button{display:inline-block;margin-left:8px;vertical-align:middle}
.aksrt-ai-seo-generate,.aksrt-ai-summary-generate{cursor:pointer}
.aksrt-ai-seo-generate.is-loading,.aksrt-ai-summary-generate.is-loading{opacity:.6;pointer-events:none}
.aksrt-ai-seo-msg{display:block;margin-top:6px;font-size:12px}
.aksrt-ai-seo-msg.is-error{color:#b32d2e}
.aksrt-ai-seo-msg.is-ok{color:#007017}
.aksrt-ai-summary-row{margin-top:8px;padding-top:8px;border-top:1px solid #eee}
');
}
private function render_ai_button(string $type, int $id): string {
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
if (empty($seo['seo_enabled']) || empty($seo['seo_ai_enabled'])) {
return '';
}
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
$title = !$available
? esc_attr__('请先配置 AI 连接(扩展 → AI 连接配置 或 WP Connectors)', 'aksrt')
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
return sprintf(
'<span class="aksrt-ai-seo-button"><button type="button" class="button aksrt-ai-seo-generate"%s data-type="%s" data-id="%d" title="%s">%s</button></span>',
$disabled,
esc_attr($type),
$id,
$title,
esc_html__('AI 生成 SEO', 'aksrt')
);
}
/**
* 后台「AI 生成摘要」→ 写入 WP 摘要字段(#excerpt / 块编辑器)。
*/
private function render_summary_button(int $post_id): string {
$available = class_exists('\AKSRT\Ai\AI') && \AKSRT\Ai\AI::get_instance()->is_available();
if (!$available) {
return '';
}
$disabled = $post_id <= 0 ? ' disabled' : '';
$title = $post_id <= 0
? esc_attr__('请先保存后再生成', 'aksrt')
: esc_attr__('根据正文生成摘要,填入摘要框', 'aksrt');
return sprintf(
'<span class="aksrt-ai-seo-button"><button type="button" class="button aksrt-ai-summary-generate"%s data-post-id="%d" title="%s">%s</button></span>',
$disabled,
$post_id,
$title,
esc_html__('AI 生成摘要', 'aksrt')
);
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class Scripts {
use ScriptsPart1b;
use ScriptsPart1c;
use ScriptsPart2;
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('wp_enqueue_scripts', [$this, 'enqueue_public']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_passive_polyfill'], 1);
add_action('wp_head', [$this, 'inline_css_vars']);
add_action('wp_head', [$this, 'output_custom_codes']);
add_action('wp_body_open', [$this, 'output_loading_overlay'], 1);
add_action('wp_footer', [$this, 'output_custom_footer_codes'], 99);
add_action('wp_ajax_aksrt_frontend_debug', [$this, 'handle_frontend_debug']);
add_action('wp_ajax_nopriv_aksrt_frontend_debug', [$this, 'handle_frontend_debug']);
add_action('wp_ajax_aksrt_search_history', [$this, 'handle_search_history']);
add_action('wp_ajax_aksrt_search_suggestions', [$this, 'handle_search_suggestions']);
add_action('wp_ajax_nopriv_aksrt_search_suggestions', [$this, 'handle_search_suggestions']);
add_filter('body_class', [$this, 'body_classes']);
add_filter('wp_resource_hints', [$this, 'resource_hints'], 10, 2);
}
/**
* 字体 CDN 的 preconnect 提示(提前 DNS + TLS 握手,加速首屏字体加载)
*/
public function resource_hints(array $hints, string $relation): array {
if ('preconnect' === $relation) {
$hints[] = [
'href' => 'https://fonts.googleapis.com',
'crossorigin',
];
$hints[] = [
'href' => 'https://fonts.gstatic.com',
'crossorigin',
];
$hints[] = [
'href' => 'https://cdn.jsdelivr.net',
'crossorigin',
];
}
return $hints;
}
}
+932
View File
@@ -0,0 +1,932 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
trait ScriptsPart1b {
public function enqueue_public(): void {
$base_uri = get_template_directory_uri();
$version = AKSRT_VERSION;
wp_enqueue_script('jquery');
wp_enqueue_script(
'aksrt-passive-events',
$base_uri . '/assets/js/passive-events.js',
[],
$version,
false
);
wp_enqueue_style(
'bootstrap5',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css',
[],
'5.3.3'
);
wp_enqueue_script(
'bootstrap5',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js',
['jquery'],
'5.3.3',
true
);
wp_enqueue_style('aksrt-style', get_stylesheet_uri(), ['bootstrap5'], $version);
$core_bundle = get_template_directory() . '/assets/css/build/core.min.css';
if (file_exists($core_bundle)) {
wp_enqueue_style(
'aksrt-core-bundle',
$base_uri . '/assets/css/build/core.min.css',
['aksrt-style'],
filemtime($core_bundle) ?: $version
);
$core_dep = 'aksrt-core-bundle';
} else {
$tokens_path = get_template_directory() . '/assets/css/core/tokens.css';
wp_enqueue_style(
'aksrt-tokens',
$base_uri . '/assets/css/core/tokens.css',
['aksrt-style'],
file_exists($tokens_path) ? (string) filemtime($tokens_path) : $version
);
foreach (['reset', 'typography', 'buttons', 'forms', 'alerts'] as $slug) {
$core_path = get_template_directory() . '/assets/css/core/' . $slug . '.css';
wp_enqueue_style(
'aksrt-core-' . $slug,
$base_uri . '/assets/css/core/' . $slug . '.css',
['aksrt-tokens'],
file_exists($core_path) ? (string) filemtime($core_path) : $version
);
}
$core_dep = 'aksrt-tokens';
}
$ext_css_files = ['animations', 'components-ext', 'color-utilities'];
foreach ($ext_css_files as $slug) {
$ext_path = get_template_directory() . '/assets/css/core/' . $slug . '.css';
if (file_exists($ext_path)) {
wp_enqueue_style(
'aksrt-ext-' . $slug,
$base_uri . '/assets/css/core/' . $slug . '.css',
[$core_dep],
filemtime($ext_path) ?: $version
);
}
}
$deps = [$core_dep];
$enqueue_module = function ($slug) use ($base_uri, $version, $deps) {
$path = get_template_directory() . '/assets/css/frontend/' . $slug . '.css';
wp_enqueue_style(
'aksrt-' . $slug,
$base_uri . '/assets/css/frontend/' . $slug . '.css',
$deps,
(file_exists($path) ? (string) filemtime($path) : $version)
);
};
// 4a. 全站必加 — 优先使用合并文件
$frontend_bundle = get_template_directory() . '/assets/css/build/frontend-base.min.css';
if (file_exists($frontend_bundle)) {
wp_enqueue_style(
'aksrt-frontend-bundle',
$base_uri . '/assets/css/build/frontend-base.min.css',
[$core_dep],
filemtime($frontend_bundle) ?: $version
);
// Keep shell-level source overrides live during the refactor.
foreach (['base', 'nav', 'footer'] as $shell_slug) {
$shell_css_path = get_template_directory() . '/assets/css/frontend/' . $shell_slug . '.css';
if (file_exists($shell_css_path)) {
wp_enqueue_style(
'aksrt-shell-' . $shell_slug,
$base_uri . '/assets/css/frontend/' . $shell_slug . '.css',
['aksrt-frontend-bundle'],
filemtime($shell_css_path) ?: $version
);
}
}
} else {
$enqueue_module('base');
$enqueue_module('zibll-compat-vars');
$enqueue_module('card-primitives');
$enqueue_module('overlay-primitives');
$enqueue_module('nav');
$enqueue_module('layout');
$enqueue_module('footer');
$enqueue_module('floating-buttons');
$enqueue_module('theme-background');
$enqueue_module('menu-decorations');
$enqueue_module('enhancements');
}
// 4a-b. 全局补充样式(骨架屏、空状态、分页、加载指示器)
foreach (['skeleton', 'empty-state', 'pagination', 'ajax-spinner', 'remote-modal'] as $slug) {
$css_path = get_template_directory() . '/assets/css/frontend/' . $slug . '.css';
if (file_exists($css_path)) {
wp_enqueue_style(
'aksrt-' . $slug,
$base_uri . '/assets/css/frontend/' . $slug . '.css',
[$core_dep],
filemtime($css_path) ?: $version
);
}
}
// Header user-box density (check-in / wallet / signout modal)
$hub_css = get_template_directory() . '/assets/css/frontend/nav/header-userbox.css';
if (file_exists($hub_css)) {
wp_enqueue_style(
'aksrt-header-userbox',
$base_uri . '/assets/css/frontend/nav/header-userbox.css',
[$core_dep],
(string) filemtime($hub_css)
);
}
$hub_js = get_template_directory() . '/assets/js/header-userbox.js';
if (file_exists($hub_js)) {
wp_enqueue_script(
'aksrt-header-userbox',
$base_uri . '/assets/js/header-userbox.js',
[],
(string) filemtime($hub_js),
true
);
}
// Header multi-function publish/VIP cluster
$hmb_css = get_template_directory() . '/assets/css/frontend/nav/header-menu-buttons.css';
if (file_exists($hmb_css)) {
wp_enqueue_style(
'aksrt-header-menu-buttons',
$base_uri . '/assets/css/frontend/nav/header-menu-buttons.css',
[$core_dep],
(string) filemtime($hmb_css)
);
}
// Final nav alignment overrides AFTER header-menu-buttons (zibll text/icon baseline).
// Re-enqueue shell nav.css last so inline critical rules win the cascade.
$nav_css = get_template_directory() . '/assets/css/frontend/nav.css';
if (file_exists($nav_css)) {
wp_enqueue_style(
'aksrt-nav-align',
$base_uri . '/assets/css/frontend/nav.css',
['aksrt-header-menu-buttons'],
(string) filemtime($nav_css)
);
}
$inline_search_js = get_template_directory() . '/assets/js/header-inline-search.js';
if (file_exists($inline_search_js)) {
wp_enqueue_script(
'aksrt-header-inline-search',
$base_uri . '/assets/js/header-inline-search.js',
[],
(string) filemtime($inline_search_js),
true
);
}
$hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
if (file_exists($hmb_js)) {
wp_enqueue_script(
'aksrt-header-newadd',
$base_uri . '/assets/js/header-newadd.js',
[],
(string) filemtime($hmb_js),
true
);
}
// VIP modal (zibll .pay-vip)
$vip_on = class_exists('\AKSRT\Settings\Settings')
&& !empty(\AKSRT\Settings\Settings::get_payment_vip_settings()['enabled']);
if ($vip_on) {
$vip_css = get_template_directory() . '/assets/css/frontend/vip-modal.css';
if (file_exists($vip_css)) {
wp_enqueue_style(
'aksrt-vip-modal',
$base_uri . '/assets/css/frontend/vip-modal.css',
[$core_dep],
(string) filemtime($vip_css)
);
}
$vip_js = get_template_directory() . '/assets/js/pay-vip.js';
if (file_exists($vip_js)) {
wp_enqueue_script(
'aksrt-pay-vip',
$base_uri . '/assets/js/pay-vip.js',
['aksrt-script'],
(string) filemtime($vip_js),
true
);
}
}
// Print stylesheet — only load on singular pages
if (is_singular()) {
wp_enqueue_style(
'aksrt-print',
$base_uri . '/assets/css/frontend/print.css',
[],
$version,
'print'
);
}
// 4b. 路由判定
$is_home_or_archive = is_home() || is_front_page() || is_archive() || is_search();
// 资源单页(post + _aksrt_is_resource)
$is_single_resource = (function_exists('aksrt_is_resource_post') && is_singular() && aksrt_is_resource_post(get_queried_object_id()));
// 普通博客 single(排除资源型 post)
$is_single_post = is_singular('post') && !$is_single_resource;
// 付费下载/付费墙:任意 singular 需收银台/下载脚本
$is_paid_content_single = false;
if (is_singular()) {
$qid = (int) get_queried_object_id();
if ($qid > 0) {
if (function_exists('aksrt_is_downloadable_content') && aksrt_is_downloadable_content($qid)) {
$is_paid_content_single = true;
} elseif (function_exists('aksrt_is_paid_post') && aksrt_is_paid_post($qid)) {
$is_paid_content_single = true;
} elseif (function_exists('aksrt_get_pay_meta')) {
$pm = aksrt_get_pay_meta($qid);
$pt = (string) ($pm['pay_type'] ?? 'no');
$is_paid_content_single = ($pt !== '' && $pt !== 'no');
}
}
}
// 独立下载页模板
$is_download_file_page = is_page_template('templates/pages/page-download-file.php');
// 资源虚拟列表 /resources/
$is_resource_archive = function_exists('aksrt_is_resource_archive') && aksrt_is_resource_archive();
// /user/{nicename}/ 自定义重写路由(class-aksrt-user.php 注册)
// 此路径下 is_page() / is_page_template() 均为 false,需手动识别以正确入队 CSS
$is_profile_route = (bool) get_query_var('user_nicename');
$is_user_center_route = (bool) get_query_var('aksrt_user_center');
$is_static_page = is_page() || is_singular('page') || $is_profile_route || $is_user_center_route;
$is_auth_route = (bool) get_query_var('aksrt_sign');
$is_auth_page = $is_auth_route
|| (function_exists('aksrt_is_auth_page_request') && aksrt_is_auth_page_request());
$is_profile_page = $is_profile_route
|| (function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page())
|| (function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page())
|| is_page_template('templates/pages/page-profile.php')
|| is_page_template('templates/pages/page-user-home.php');
$is_fixed_homepage = function_exists('aksrt_is_fixed_homepage_request') && aksrt_is_fixed_homepage_request();
// CMS 形态判定(可能在博客流页面也需要 grid-cms 卡片样式)
$is_cms_mode = function_exists('aksrt_is_cms_mode') ? aksrt_is_cms_mode() : false;
// 首页/归档/搜索:卡片 + 侧栏
if ($is_home_or_archive) {
$enqueue_module('post-card');
$enqueue_module('widgets');
$enqueue_module('sidebar');
$enqueue_module('breadcrumb');
}
// 顶部多功能组件:按 page-type 启用结果入队(对齐 zibll,不绑死首页)
if (function_exists('aksrt_header_slide_is_show') && aksrt_header_slide_is_show()) {
// flex/gutters used by icon cards (same helpers single-post loads via zibll-utilities)
$zu_path = get_template_directory() . '/assets/css/frontend/zibll-utilities.css';
if (file_exists($zu_path)) {
wp_enqueue_style(
'aksrt-zibll-utilities',
$base_uri . '/assets/css/frontend/zibll-utilities.css',
is_string($deps[0] ?? null) ? [$deps[0]] : (array) $deps,
filemtime($zu_path) ?: $version
);
}
$enqueue_module('header-components');
}
// 单篇文章:所有 single 相关样式
if ($is_single_post) {
// Prefer discrete enqueues over CSS @import (cache-bust + parallel load).
$single_parts = [
'zibll-utilities',
'single-post/shell',
'single-post/content',
'single-post/code-blocks',
'single-post/navigation',
'single-post/cover',
'single-post/author-actions',
'single-post/related-toc',
'single-post/hidden-content',
];
$single_dep = $deps[0] ?? $core_dep;
foreach ($single_parts as $part) {
$path = get_template_directory() . '/assets/css/frontend/' . $part . '.css';
if (!file_exists($path)) {
continue;
}
wp_enqueue_style(
'aksrt-' . str_replace('/', '-', $part),
$base_uri . '/assets/css/frontend/' . $part . '.css',
is_string($single_dep) ? [$single_dep] : (array) $deps,
filemtime($path) ?: $version
);
}
// Keep aggregator handle for any third-party deps that target it.
$enqueue_module('single-post');
$enqueue_module('post-card'); // 相关推荐使用 post-card 局部样式
// Comments pack (zibll 1:1 discrete files).
$comment_parts = ['comments/shell', 'comments/mobile-item'];
$comment_dep = $deps[0] ?? $core_dep;
foreach ($comment_parts as $part) {
$path = get_template_directory() . '/assets/css/frontend/' . $part . '.css';
if (!file_exists($path)) {
continue;
}
wp_enqueue_style(
'aksrt-' . str_replace('/', '-', $part),
$base_uri . '/assets/css/frontend/' . $part . '.css',
is_string($comment_dep) ? [$comment_dep] : (array) $deps,
filemtime($path) ?: $version
);
}
$enqueue_module('comments'); // aggregator handle for third-party deps
$enqueue_module('widgets');
$enqueue_module('sidebar');
$enqueue_module('breadcrumb');
}
// 静态页面 + 用户中心 + 友链等
if ($is_static_page) {
$enqueue_module('pages');
$enqueue_module('single-post'); // 复用 entry-content 排版
$enqueue_module('widgets'); // widget-card / widget-title
$enqueue_module('post-card'); // post-grid / post-card(用户中心文章列表)
$enqueue_module('breadcrumb');
// 启用评论的页面
if (comments_open() || get_comments_number()) {
$enqueue_module('comments');
}
}
// 用户中心 / 公开主页:路由专属 CSS
if ($is_profile_page) {
// 举报脚本(用户主页和个人中心均需要)
$report_js_path = get_template_directory() . '/assets/js/report.js';
if (file_exists($report_js_path)) {
wp_enqueue_script(
'aksrt-report',
$base_uri . '/assets/js/report.js',
[],
filemtime($report_js_path) ?: $version,
true
);
}
// 关注脚本
$follow_js_path = get_template_directory() . '/assets/js/follow.js';
if (file_exists($follow_js_path)) {
wp_enqueue_script(
'aksrt-follow',
$base_uri . '/assets/js/follow.js',
[],
filemtime($follow_js_path) ?: $version,
true
);
}
$author_js_path = get_template_directory() . '/assets/js/author.js';
if (file_exists($author_js_path)) {
wp_enqueue_script(
'aksrt-author',
$base_uri . '/assets/js/author.js',
['aksrt-script'],
(string) filemtime($author_js_path),
true
);
}
// 共享基础 profile 样式(兼容层)
$profile_css_path = get_template_directory() . '/assets/css/routes/profile.css';
wp_enqueue_style(
'aksrt-route-profile',
$base_uri . '/assets/css/routes/profile.css',
['aksrt-tokens'],
file_exists($profile_css_path) ? (string) filemtime($profile_css_path) : $version
);
// 本人中心 vs 公开主页拆分加载
$is_public_user_home = $is_profile_route
|| (function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page())
|| is_page_template('templates/pages/page-user-home.php');
if ($is_public_user_home) {
// /user/{nicename}/ 公开主页
$user_home_css_path = get_template_directory() . '/assets/css/routes/user-home.css';
wp_enqueue_style(
'aksrt-route-user-home',
$base_uri . '/assets/css/routes/user-home.css',
['aksrt-route-profile'],
file_exists($user_home_css_path) ? (string) filemtime($user_home_css_path) : $version
);
} else {
// 本人中心(page template)
$user_center_css_path = get_template_directory() . '/assets/css/routes/user-center.css';
wp_enqueue_style(
'aksrt-route-user-center',
$base_uri . '/assets/css/routes/user-center.css',
['aksrt-route-profile'],
file_exists($user_center_css_path) ? (string) filemtime($user_center_css_path) : $version
);
$identity_js = get_template_directory() . '/assets/js/routes/user-center-auth.js';
if (file_exists($identity_js)) {
wp_enqueue_script(
'aksrt-route-user-center-auth',
$base_uri . '/assets/js/routes/user-center-auth.js',
['aksrt-script'],
(string) filemtime($identity_js),
true
);
}
wp_enqueue_script(
'aksrt-route-user-center',
$base_uri . '/assets/js/routes/user-center.js',
file_exists($identity_js) ? ['aksrt-script', 'aksrt-route-user-center-auth'] : ['aksrt-script'],
file_exists(get_template_directory() . '/assets/js/routes/user-center.js')
? (string) filemtime(get_template_directory() . '/assets/js/routes/user-center.js')
: $version,
true
);
// 绑定手机短信发送按钮依赖 sign-register 的 data-aksrt-sms-trigger 契约
$sign_register_js = get_template_directory() . '/assets/js/sign-register.js';
if (file_exists($sign_register_js)) {
wp_enqueue_script(
'aksrt-sign-register',
$base_uri . '/assets/js/sign-register.js',
['aksrt-route-user-center'],
(string) filemtime($sign_register_js),
true
);
}
}
}
// 搜索结果页:路由专属 CSS(顶部卡片/胶囊搜索框/工具栏/空状态/关键词高亮)
if (is_search()) {
wp_enqueue_style(
'aksrt-route-search',
$base_uri . '/assets/css/routes/search.css',
['aksrt-tokens', 'aksrt-post-card'],
$version
);
}
// 分类汇总页:路由专属 CSS
if (is_page_template('templates/pages/page-categories.php')) {
wp_enqueue_style(
'aksrt-route-categories',
$base_uri . '/assets/css/routes/categories.css',
['aksrt-tokens'],
$version
);
}
// 友情链接页:路由专属 CSS
if (is_page_template('templates/pages/page-friends.php')) {
wp_enqueue_style(
'aksrt-route-friends',
$base_uri . '/assets/css/routes/friends.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/friends.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/friends.css')
: $version
);
}
// 网址导航页(zibll links.php)
if (is_page_template('templates/pages/page-links.php')) {
$links_css = get_template_directory() . '/assets/css/routes/links.css';
if (file_exists($links_css)) {
wp_enqueue_style(
'aksrt-route-links',
$base_uri . '/assets/css/routes/links.css',
['aksrt-tokens'],
(string) filemtime($links_css)
);
}
$links_js = get_template_directory() . '/assets/js/links-page.js';
if (file_exists($links_js)) {
wp_enqueue_script(
'aksrt-links-page',
$base_uri . '/assets/js/links-page.js',
['jquery'],
(string) filemtime($links_js),
true
);
}
}
// 文章导航页(zibll postsnavs.php)
if (is_page_template('templates/pages/page-posts-navs.php')) {
$page_navs_js = get_template_directory() . '/assets/js/page-navs.js';
if (file_exists($page_navs_js)) {
wp_enqueue_script(
'aksrt-page-navs',
$base_uri . '/assets/js/page-navs.js',
[],
(string) filemtime($page_navs_js),
true
);
}
$pn_css = get_template_directory() . '/assets/css/routes/posts-navs.css';
if (file_exists($pn_css)) {
wp_enqueue_style(
'aksrt-route-posts-navs',
$base_uri . '/assets/css/routes/posts-navs.css',
['aksrt-tokens'],
(string) filemtime($pn_css)
);
}
$section_navs = get_template_directory() . '/assets/js/section-navs.js';
if (file_exists($section_navs)) {
wp_enqueue_script(
'aksrt-section-navs',
$base_uri . '/assets/js/section-navs.js',
['jquery'],
(string) filemtime($section_navs),
true
);
}
$pn_js = get_template_directory() . '/assets/js/posts-navs-page.js';
if (file_exists($pn_js)) {
wp_enqueue_script(
'aksrt-posts-navs-page',
$base_uri . '/assets/js/posts-navs-page.js',
['jquery'],
(string) filemtime($pn_js),
true
);
}
}
// 文章归档页
if (is_page_template('templates/pages/page-archives.php')) {
$ar_css = get_template_directory() . '/assets/css/routes/archives.css';
if (file_exists($ar_css)) {
wp_enqueue_style(
'aksrt-route-archives',
$base_uri . '/assets/css/routes/archives.css',
['aksrt-tokens'],
(string) filemtime($ar_css)
);
}
}
// 文档导航页(zibll documentnav.php)
if (is_page_template('templates/pages/page-documentnav.php')) {
$dn_css = get_template_directory() . '/assets/css/routes/documentnav.css';
if (file_exists($dn_css)) {
wp_enqueue_style(
'aksrt-route-documentnav',
$base_uri . '/assets/css/routes/documentnav.css',
['aksrt-tokens'],
(string) filemtime($dn_css)
);
}
$dn_js = get_template_directory() . '/assets/js/documentnav-page.js';
if (file_exists($dn_js)) {
wp_enqueue_script(
'aksrt-documentnav-page',
$base_uri . '/assets/js/documentnav-page.js',
[],
(string) filemtime($dn_js),
true
);
}
}
// 禁封申诉页
if (is_page_template('templates/pages/page-ban-appeal.php')) {
$ba_css = get_template_directory() . '/assets/css/routes/ban-appeal.css';
if (file_exists($ba_css)) {
wp_enqueue_style(
'aksrt-route-ban-appeal',
$base_uri . '/assets/css/routes/ban-appeal.css',
['aksrt-tokens'],
(string) filemtime($ba_css)
);
}
$ba_js = get_template_directory() . '/assets/js/ban-appeal-page.js';
if (file_exists($ba_js)) {
wp_enqueue_script(
'aksrt-ban-appeal-page',
$base_uri . '/assets/js/ban-appeal-page.js',
[],
(string) filemtime($ba_js),
true
);
}
}
// 交易中心共享壳层(订单 / 收益)
// Virtual rewrite routes (aksrt_orders / aksrt_revenue) also count.
$is_commerce_center = is_page_template('templates/pages/page-orders.php')
|| is_page_template('templates/pages/page-revenue.php')
|| (bool) get_query_var('aksrt_orders')
|| (bool) get_query_var('aksrt_revenue');
if ($is_commerce_center) {
$commerce_css = get_template_directory() . '/assets/css/routes/commerce.css';
if (file_exists($commerce_css)) {
wp_enqueue_style(
'aksrt-commerce-shell',
$base_uri . '/assets/css/routes/commerce.css',
['aksrt-tokens'],
(string) filemtime($commerce_css)
);
}
}
// 订单中心:独立页 / 虚拟 rewrite / 用户中心 tab=orders
$is_orders_surface = is_page_template('templates/pages/page-orders.php')
|| (bool) get_query_var('aksrt_orders')
|| (
(function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page())
&& isset($_GET['tab'])
&& sanitize_key((string) wp_unslash($_GET['tab'])) === 'orders'
);
if ($is_orders_surface) {
wp_enqueue_style(
'aksrt-orders',
$base_uri . '/assets/css/routes/orders.css',
['aksrt-tokens'],
$version
);
// 微信扫码支付需要本地二维码生成器(AksrtShare.buildQrDataUrl)。
$weixin_share_orders = get_template_directory() . '/assets/js/weixin-share.js';
if (file_exists($weixin_share_orders)) {
wp_enqueue_script(
'aksrt-weixin-share',
$base_uri . '/assets/js/weixin-share.js',
['aksrt-script'],
(string) filemtime($weixin_share_orders),
true
);
}
wp_enqueue_script(
'aksrt-order-center',
$base_uri . '/assets/js/order-center.js',
['aksrt-script', 'aksrt-weixin-share', 'aksrt-core-remote-modal'],
file_exists(get_template_directory() . '/assets/js/order-center.js')
? (string) filemtime(get_template_directory() . '/assets/js/order-center.js')
: $version,
true
);
}
// 收益中心页
if (is_page_template('templates/pages/page-revenue.php') || (bool) get_query_var('aksrt_revenue')) {
wp_enqueue_style(
'aksrt-revenue',
$base_uri . '/assets/css/routes/revenue.css',
['aksrt-tokens'],
$version
);
}
// 消息中心页
if (is_page_template('templates/pages/page-messages.php')) {
$messages_css = get_template_directory() . '/assets/css/frontend/messages.css';
if (file_exists($messages_css)) {
wp_enqueue_style(
'aksrt-messages',
$base_uri . '/assets/css/frontend/messages.css',
['aksrt-tokens'],
(string) filemtime($messages_css)
);
}
$messages_js = get_template_directory() . '/assets/js/messages.js';
if (file_exists($messages_js)) {
wp_enqueue_script(
'aksrt-messages',
$base_uri . '/assets/js/messages.js',
['aksrt-script'],
(string) filemtime($messages_js),
true
);
}
$conversation_js = get_template_directory() . '/assets/js/conversation.js';
if (file_exists($conversation_js)) {
wp_enqueue_script(
'aksrt-conversation',
$base_uri . '/assets/js/conversation.js',
['aksrt-script'],
(string) filemtime($conversation_js),
true
);
}
}
// Unified share-data builder — single payload assembler for every share
// surface (dropdown, copy, WeChat, poster). Loaded on any singular view.
if (is_singular()) {
$share_data_js = get_template_directory() . '/assets/js/share-data-builder.js';
if (file_exists($share_data_js)) {
wp_enqueue_script(
'aksrt-share-data',
$base_uri . '/assets/js/share-data-builder.js',
['aksrt-script'],
(string) filemtime($share_data_js),
true
);
}
}
// 单篇文章 — poster-share.js(海报分享按钮)
if (is_singular('post')) {
$poster_js = get_template_directory() . '/assets/js/poster-share.js';
if (file_exists($poster_js)) {
wp_enqueue_script(
'aksrt-poster-share',
$base_uri . '/assets/js/poster-share.js',
['aksrt-script', 'aksrt-share-data'],
(string) filemtime($poster_js),
true
);
}
}
// 登录/注册/绑定 — pages + auth 路由专属 CSS
if ($is_auth_page) {
$enqueue_module('pages');
// Auth 路由专属样式
wp_enqueue_style(
'aksrt-route-auth',
$base_uri . '/assets/css/routes/auth.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/auth.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/auth.css')
: $version
);
$sign_css = get_template_directory() . '/assets/css/routes/sign.css';
if (file_exists($sign_css)) {
wp_enqueue_style(
'aksrt-route-sign',
$base_uri . '/assets/css/routes/sign.css',
['aksrt-tokens'],
(string) filemtime($sign_css)
);
}
// 手机号注册/登录 + SMS 倒计时 + 人机验证联动
$sign_js_path = get_template_directory() . '/assets/js/sign-register.js';
if (file_exists($sign_js_path)) {
wp_enqueue_script(
'aksrt-sign-register',
$base_uri . '/assets/js/sign-register.js',
['aksrt-script'],
(string) filemtime($sign_js_path),
true
);
}
}
// 404 页面
if (is_404()) {
$enqueue_module('pages');
}
// grid-cms 卡片样式
if ($is_resource_archive || $is_single_resource || ($is_cms_mode && ($is_home_or_archive || $is_static_page) && !$is_fixed_homepage)) {
$enqueue_module('post-card-cms');
// 确保 post-card 基础样式也加载(若上面分支未加)
if (!$is_home_or_archive && !$is_single_post && !$is_static_page) {
$enqueue_module('post-card');
}
}
// 单篇资源专属样式
if ($is_single_resource) {
$enqueue_module('single-post');
$enqueue_module('resource-meta');
$enqueue_module('comments');
$enqueue_module('widgets');
}
// 资源归档专属样式
if ($is_resource_archive) {
$enqueue_module('resource-archive');
}
// 单篇资源 / 付费内容 / 独立下载页:下载 + 收银台脚本
if ($is_single_resource || $is_paid_content_single || $is_download_file_page) {
$cms_download_js_path = get_template_directory() . '/assets/js/cms-download.js';
wp_enqueue_script(
'aksrt-cms-download',
$base_uri . '/assets/js/cms-download.js',
['aksrt-script', 'aksrt-runtime'],
file_exists($cms_download_js_path) ? (string) filemtime($cms_download_js_path) : $version,
true
);
}
// Legacy freeform CMS home modules: only for non-fixed CMS archive previews.
$is_cms_home = ($is_home_or_archive && $is_cms_mode) && !$is_fixed_homepage;
// 管理员预览模式(?aksrt_preview=...):无论站点风格都允许加载模块系统资源
$is_preview = is_user_logged_in() && current_user_can('manage_options') && isset($_GET['aksrt_preview']);
if ($is_cms_home || $is_preview) {
$enqueue_module('home-modules');
$enqueue_module('post-card'); // 模块内复用
$enqueue_module('header-components'); // header components 模块依赖
$enqueue_module('post-card-cms'); // grid-cms 卡片依赖
wp_enqueue_script(
'aksrt-cms-home-modules',
$base_uri . '/assets/js/cms-home-modules.js',
['aksrt-script'],
$version,
true
);
}
if ($is_fixed_homepage) {
$enqueue_module('header-components'); // header-slider / hero
$enqueue_module('post-card');
$homepage_css_path = get_template_directory() . '/assets/css/frontend/homepage-fixed.css';
$homepage_style_deps = file_exists($frontend_bundle)
? ['aksrt-frontend-bundle', 'aksrt-header-components', 'aksrt-post-card']
: ['aksrt-zibll-compat-vars', 'aksrt-layout', 'aksrt-widgets', 'aksrt-header-components', 'aksrt-post-card'];
$homepage_style_deps = array_values(array_filter(
$homepage_style_deps,
static fn(string $handle): bool => wp_style_is($handle, 'registered')
));
wp_enqueue_style(
'aksrt-homepage-fixed',
$base_uri . '/assets/css/frontend/homepage-fixed.css',
$homepage_style_deps,
file_exists($homepage_css_path) ? (string) filemtime($homepage_css_path) : $version
);
}
// 积分相关:已登录 + 启用积分功能时全站加载
if (is_user_logged_in() && class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled()) {
$enqueue_module('credits');
// profile 页:加载 credits.js(签到 + 邀请交互)
if ($is_profile_page) {
$credits_js_path = get_template_directory() . '/assets/js/credits.js';
wp_enqueue_script(
'aksrt-credits',
$base_uri . '/assets/js/credits.js',
['aksrt-script'],
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
true
);
$badges_js_path = get_template_directory() . '/assets/js/user-badges.js';
if (file_exists($badges_js_path)) {
wp_enqueue_script(
'aksrt-user-badges',
$base_uri . '/assets/js/user-badges.js',
['aksrt-script'],
(string) filemtime($badges_js_path),
true
);
}
}
}
// Badges UI: user center + public profile (zibll medal modal/list).
if (class_exists('\AKSRT\User\Badges') && \AKSRT\User\Badges::enabled()) {
$is_badge_surface = $is_profile_page
|| is_author()
|| (function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page())
|| (function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page());
if ($is_badge_surface) {
$badges_js_path_global = get_template_directory() . '/assets/js/user-badges.js';
if (file_exists($badges_js_path_global) && !wp_script_is('aksrt-user-badges', 'enqueued')) {
wp_enqueue_script(
'aksrt-user-badges',
$base_uri . '/assets/js/user-badges.js',
['aksrt-script', 'aksrt-core-remote-modal'],
(string) filemtime($badges_js_path_global),
true
);
}
}
}
$this->enqueue_public_fonts_js_and_data($base_uri, $core_dep, $deps, $is_fixed_homepage, $is_home_or_archive, $is_single_post, $is_single_resource, $version);
}
}
+625
View File
@@ -0,0 +1,625 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
/**
* Scripts public enqueue continuation: fonts, icons, global JS, localize.
*
* @package AKSRT
*/
trait ScriptsPart1c {
/**
* Fonts / FA / global JS stack / aksrtData (split from enqueue_public).
*/
private function enqueue_public_fonts_js_and_data($base_uri, $core_dep, $deps, $is_fixed_homepage, $is_home_or_archive, $is_single_post, $is_single_resource, $version): void {
// ===== 5. 字体 =====
// 5a. 拉丁字体 - Inter (UI) + Source Serif 4 (正文)
wp_enqueue_style(
'aksrt-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,400&display=swap',
[],
null
);
// 5b. 中文字体 - jsDelivr CDN (思源黑体 / 思源宋体)
// 仅 regular 字重,避免拖慢首屏;其它字重通过 system fallback (PingFang/微软雅黑)
// Fontsource v5 已按 unicode-range 自动分包,浏览器只会下载用到的简中子集
wp_enqueue_style(
'aksrt-fonts-cn',
'https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-sc@5.2.9/400.css',
[],
null
);
wp_enqueue_style(
'aksrt-fonts-cn-serif',
'https://cdn.jsdelivr.net/npm/@fontsource/noto-serif-sc@5.2.9/400.css',
[],
null
);
// ===== 6. 图标系统 =====
// Font Awesome 5 全站加载(评论回复、单篇互动栏、profile 联系方式等
// 多处模板使用 fa-* 类名,作为全主题图标包统一使用)
wp_enqueue_style(
'aksrt-font-awesome-5',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css',
[],
'5.15.4'
);
// FA4 class aliases (fa-diamond, fa-file-text-o, ...) used by zibll-style markup.
wp_enqueue_style(
'aksrt-font-awesome-5-v4-shims',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/v4-shims.min.css',
['aksrt-font-awesome-5'],
'5.15.4'
);
// ===== 7. JS =====
// Swiper.js(首页/归档/CMS 模块/单篇文章页加载)
$needs_swiper = $is_home_or_archive || $is_single_post || $is_single_resource;
if ($needs_swiper) {
wp_enqueue_style(
'aksrt-swiper',
$base_uri . '/assets/css/frontend/swiper-bundle.min.css',
[],
'11.0'
);
wp_enqueue_script(
'aksrt-swiper',
$base_uri . '/assets/js/swiper-bundle.min.js',
[],
'11.0',
true
);
}
wp_enqueue_script(
'aksrt-qrcode',
'https://cdn.jsdelivr.net/npm/kjua@0.9.0/dist/kjua.min.js',
[],
'0.9.0',
true
);
// Rehydrate system — must load before aksrt-script so AksrtFrontend is available
wp_enqueue_script(
'aksrt-rehydrate',
$base_uri . '/assets/js/rehydrate.js',
[],
file_exists(get_template_directory() . '/assets/js/rehydrate.js')
? (string) filemtime(get_template_directory() . '/assets/js/rehydrate.js')
: $version,
true
);
// Runtime infrastructure: tbquire / notyf / confirm / refreshModal / scrollTopTo / [data-*] declarative
$runtime_js = get_template_directory() . '/assets/js/aksrt-runtime.js';
if (file_exists($runtime_js)) {
wp_enqueue_script(
'aksrt-runtime',
$base_uri . '/assets/js/aksrt-runtime.js',
['aksrt-rehydrate'],
(string) filemtime($runtime_js),
true
);
}
// Core modules (split from main.js) — register with AksrtFrontend
$core_modules = [
'aksrt-core-theme' => 'theme.js',
'aksrt-core-nav' => 'nav.js',
'aksrt-core-search' => 'search.js',
'aksrt-core-dropdown' => 'dropdown.js',
'aksrt-core-scroll' => 'scroll.js',
'aksrt-core-slider' => 'slider.js',
'aksrt-core-article' => 'article.js',
'aksrt-core-comments' => 'comments.js',
'aksrt-core-comment-fixed' => 'comment-fixed-input.js',
'aksrt-core-comment-move' => 'comment-move-form.js',
// Shared zibll-parity list/modal engines (before pagination consumers).
'aksrt-core-ajax-list' => 'ajax-list.js',
'aksrt-core-remote-modal' => 'remote-modal.js',
'aksrt-core-pagination' => 'pagination.js',
'aksrt-core-archive' => 'archive.js',
'aksrt-core-footer-tabbar' => 'footer-tabbar.js',
'aksrt-core-misc' => 'misc.js',
];
$core_handles = [];
foreach ($core_modules as $handle => $file) {
$path = get_template_directory() . '/assets/js/core/' . $file;
if (file_exists($path)) {
// Depend on aksrt-runtime so window.aksrtData (localized below) is available.
$deps = ['aksrt-rehydrate', 'aksrt-runtime'];
if ($handle === 'aksrt-core-slider') {
$deps = array_merge($deps, $needs_swiper ? ['aksrt-swiper'] : []);
}
wp_enqueue_script(
$handle,
$base_uri . '/assets/js/core/' . $file,
$deps,
(string) filemtime($path),
true
);
$core_handles[] = $handle;
}
}
wp_enqueue_script(
'aksrt-script',
$base_uri . '/assets/js/main.js',
array_merge(
['jquery', 'bootstrap5', 'aksrt-rehydrate', 'aksrt-runtime'],
$core_handles
),
file_exists(get_template_directory() . '/assets/js/main.js')
? (string) filemtime(get_template_directory() . '/assets/js/main.js')
: $version,
true
);
// SVG icon data + helper (must load before other scripts that use icons)
$svg_icon_js = get_template_directory() . '/assets/js/svg-icon.js';
if (file_exists($svg_icon_js)) {
wp_enqueue_script(
'aksrt-svg-icon',
$base_uri . '/assets/js/svg-icon.js',
[],
(string) filemtime($svg_icon_js),
false // load in <head> so icons are available early
);
}
$svg_helper_js = get_template_directory() . '/assets/js/svg-icon-helper.js';
if (file_exists($svg_helper_js)) {
wp_enqueue_script(
'aksrt-svg-icon-helper',
$base_uri . '/assets/js/svg-icon-helper.js',
['aksrt-svg-icon'],
(string) filemtime($svg_helper_js),
true
);
}
// Page loading indicator (top progress bar)
$loader_js = get_template_directory() . '/assets/js/loader.js';
if (file_exists($loader_js)) {
wp_enqueue_script(
'aksrt-loader',
$base_uri . '/assets/js/loader.js',
[],
(string) filemtime($loader_js),
true
);
}
// Code block enhancements (copy button + language label)
$precode_js = get_template_directory() . '/assets/js/precode.js';
if (file_exists($precode_js)) {
wp_enqueue_script(
'aksrt-precode',
$base_uri . '/assets/js/precode.js',
[],
(string) filemtime($precode_js),
true
);
}
// Input field enhancements (auto-resize, counter, validation)
$input_expand_js = get_template_directory() . '/assets/js/input-expand.js';
if (file_exists($input_expand_js)) {
wp_enqueue_script(
'aksrt-input-expand',
$base_uri . '/assets/js/input-expand.js',
[],
(string) filemtime($input_expand_js),
true
);
}
// Instant page prefetch (hover to preload)
$instant_page_js = get_template_directory() . '/assets/js/instant-page.js';
if (file_exists($instant_page_js)) {
wp_enqueue_script(
'aksrt-instant-page',
$base_uri . '/assets/js/instant-page.js',
[],
(string) filemtime($instant_page_js),
false // load in <head> for early prefetch capability
);
}
// Touch gesture support (swipe, pull-to-dismiss)
$mini_touch_js = get_template_directory() . '/assets/js/mini-touch.js';
if (file_exists($mini_touch_js)) {
wp_enqueue_script(
'aksrt-mini-touch',
$base_uri . '/assets/js/mini-touch.js',
[],
(string) filemtime($mini_touch_js),
true
);
}
// Section navs: article TOC / scrollspy
$section_navs_js = get_template_directory() . '/assets/js/section-navs.js';
if (file_exists($section_navs_js)) {
wp_enqueue_script(
'aksrt-section-navs',
$base_uri . '/assets/js/section-navs.js',
['aksrt-rehydrate', 'aksrt-runtime'],
(string) filemtime($section_navs_js),
true
);
}
// Video player (MP4/HLS/FLV)
$video_player_js = get_template_directory() . '/assets/js/video-player.js';
if (file_exists($video_player_js)) {
wp_enqueue_script(
'aksrt-video-player',
$base_uri . '/assets/js/video-player.js',
[],
(string) filemtime($video_player_js),
true
);
}
// Image color extraction
$color_extract_js = get_template_directory() . '/assets/js/color-extract.js';
if (file_exists($color_extract_js)) {
wp_enqueue_script(
'aksrt-color-extract',
$base_uri . '/assets/js/color-extract.js',
[],
(string) filemtime($color_extract_js),
true
);
}
$widget_set_js = get_template_directory() . '/assets/js/widget-set.js';
if (file_exists($widget_set_js)) {
wp_enqueue_script(
'aksrt-widget-set',
$base_uri . '/assets/js/widget-set.js',
['aksrt-script', 'aksrt-qrcode'],
(string) filemtime($widget_set_js),
true
);
}
// ===== 全局 UI 增强(Toast、骨架屏、悬停动效、灯箱、滚动感知)=====
$enhancements_css = get_template_directory() . '/assets/css/frontend/enhancements.css';
if (file_exists($enhancements_css)) {
wp_enqueue_style(
'aksrt-enhancements',
$base_uri . '/assets/css/frontend/enhancements.css',
[$core_dep],
(string) filemtime($enhancements_css)
);
}
$enhancements_js = get_template_directory() . '/assets/js/enhancements.js';
if (file_exists($enhancements_js)) {
wp_enqueue_script(
'aksrt-enhancements',
$base_uri . '/assets/js/enhancements.js',
['aksrt-script'],
(string) filemtime($enhancements_js),
true
);
}
$enhancements2_js = get_template_directory() . '/assets/js/enhancements2.js';
if (file_exists($enhancements2_js)) {
wp_enqueue_script(
'aksrt-enhancements2',
$base_uri . '/assets/js/enhancements2.js',
['aksrt-script'],
(string) filemtime($enhancements2_js),
true
);
}
// Reward (打赏)
$reward_js = get_template_directory() . '/assets/js/reward.js';
if (file_exists($reward_js)) {
wp_enqueue_script(
'aksrt-reward',
$base_uri . '/assets/js/reward.js',
['aksrt-script'],
(string) filemtime($reward_js),
true
);
}
// Effects (count-up, countdown, marquee, tilt, FPS detection)
$effects_js = get_template_directory() . '/assets/js/effects.js';
if (file_exists($effects_js)) {
wp_enqueue_script(
'aksrt-effects',
$base_uri . '/assets/js/effects.js',
['aksrt-script'],
(string) filemtime($effects_js),
true
);
}
// Yiyan (一言轮播)
$yiyan_js = get_template_directory() . '/assets/js/yiyan.js';
if (file_exists($yiyan_js)) {
wp_enqueue_script(
'aksrt-yiyan',
$base_uri . '/assets/js/yiyan.js',
[],
(string) filemtime($yiyan_js),
true
);
}
// Frontend translate (默认关)
$translate_js = get_template_directory() . '/assets/js/frontend-translate.js';
if (file_exists($translate_js)) {
wp_enqueue_script(
'aksrt-frontend-translate',
$base_uri . '/assets/js/frontend-translate.js',
[],
(string) filemtime($translate_js),
true
);
}
// Content protection (copy notice / right-click / F12)
$content_protect_js = get_template_directory() . '/assets/js/content-protect.js';
if (file_exists($content_protect_js)) {
wp_enqueue_script(
'aksrt-content-protect',
$base_uri . '/assets/js/content-protect.js',
[],
(string) filemtime($content_protect_js),
false // load in <head> for early binding
);
}
// Site notice popup (global, controlled by Settings)
$site_notice_css = get_template_directory() . '/assets/css/frontend/site-notice.css';
if (file_exists($site_notice_css)) {
wp_enqueue_style(
'aksrt-site-notice',
$base_uri . '/assets/css/frontend/site-notice.css',
[$core_dep],
(string) filemtime($site_notice_css)
);
}
$site_notice_js = get_template_directory() . '/assets/js/site-notice.js';
if (file_exists($site_notice_js)) {
wp_enqueue_script(
'aksrt-site-notice',
$base_uri . '/assets/js/site-notice.js',
[],
(string) filemtime($site_notice_js),
true
);
}
// 弹窗登录/注册(仅未登录用户)
if (!is_user_logged_in()) {
$modal_auth_js = get_template_directory() . '/assets/js/modal-auth.js';
if (file_exists($modal_auth_js)) {
wp_enqueue_script(
'aksrt-modal-auth',
$base_uri . '/assets/js/modal-auth.js',
['aksrt-script'],
(string) filemtime($modal_auth_js),
true
);
}
}
if ($is_fixed_homepage) {
$homepage_js_path = get_template_directory() . '/assets/js/homepage-fixed.js';
wp_enqueue_script(
'aksrt-homepage-fixed',
$base_uri . '/assets/js/homepage-fixed.js',
['aksrt-script'],
file_exists($homepage_js_path) ? (string) filemtime($homepage_js_path) : $version,
true
);
}
// Comment toolbar (smilie/code/image/quick shortcodes) — post form.
$load_comment_tools = (is_singular() && comments_open());
if ($load_comment_tools) {
$comment_tools_path = get_template_directory() . '/assets/js/comment-tools.js';
wp_enqueue_script(
'aksrt-comment-tools',
$base_uri . '/assets/js/comment-tools.js',
['aksrt-script', 'aksrt-core-comment-fixed', 'aksrt-core-comment-move'],
file_exists($comment_tools_path) ? (string) filemtime($comment_tools_path) : $version,
true
);
$comment_tools_css = get_template_directory() . '/assets/css/frontend/comment-tools.css';
if (file_exists($comment_tools_css)) {
wp_enqueue_style(
'aksrt-comment-tools',
$base_uri . '/assets/css/frontend/comment-tools.css',
['aksrt-comments'],
(string) filemtime($comment_tools_css)
);
}
}
// Comment actions client (like / edit / delete / moderate / pin).
// Submit path lives in assets/js/core/comments.js (REST).
if (is_singular() && (comments_open() || (int) get_comments_number() > 0)) {
$comment_ajax_path = get_template_directory() . '/assets/js/comment-ajax.js';
if (file_exists($comment_ajax_path)) {
wp_enqueue_script(
'aksrt-comment-ajax',
$base_uri . '/assets/js/comment-ajax.js',
['aksrt-script', 'aksrt-core-comments'],
(string) filemtime($comment_ajax_path),
true
);
}
}
if (is_singular()) {
$page_template_path = get_template_directory() . '/assets/js/page-template.js';
if (file_exists($page_template_path)) {
wp_enqueue_script(
'aksrt-page-template',
$base_uri . '/assets/js/page-template.js',
['aksrt-script'],
(string) filemtime($page_template_path),
true
);
}
}
// Post/resource share functionality
if ($is_single_post || $is_single_resource) {
$weixin_share_path = get_template_directory() . '/assets/js/weixin-share.js';
if (file_exists($weixin_share_path)) {
wp_enqueue_script(
'aksrt-weixin-share',
$base_uri . '/assets/js/weixin-share.js',
['aksrt-script'],
(string) filemtime($weixin_share_path),
true
);
}
$post_share_path = get_template_directory() . '/assets/js/post-share.js';
if (file_exists($post_share_path)) {
wp_enqueue_script(
'aksrt-post-share',
$base_uri . '/assets/js/post-share.js',
['aksrt-script', 'aksrt-weixin-share', 'aksrt-share-data'],
(string) filemtime($post_share_path),
true
);
}
}
// Page navigation (TOC highlight, smooth scroll, doc search)
// 评论回复脚本
if (is_singular() && comments_open() && get_option('thread_comments')) {
wp_enqueue_script('comment-reply');
}
$runtime_routes = $this->resolve_frontend_routes();
// 数据注入 - 全站统一消费 window.aksrtData
// Attach to aksrt-runtime (loaded before all core modules) so ajaxUrl/nonce
// exist before pagination/search/article handlers run.
$aksrt_data_handle = wp_script_is('aksrt-runtime', 'registered') || wp_script_is('aksrt-runtime', 'enqueued')
? 'aksrt-runtime'
: 'aksrt-script';
wp_localize_script($aksrt_data_handle, 'aksrtData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('aksrt_public_nonce'),
'restNonce' => wp_create_nonce('wp_rest'),
'visibilityNonce' => wp_create_nonce('aksrt_visibility'),
'siteUrl' => home_url('/'),
'themeUrl' => AKSRT_URI,
'themeMode' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::theme_mode_js_value() : 'auto',
'debug' => defined('WP_DEBUG') && WP_DEBUG,
'debugLog' => defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG,
'postPaginationMode' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_pagination_mode() : 'load_more',
'commentPaginationMode' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_pagination_mode() : 'numbered',
'homepagePostsPerPage' => function_exists('aksrt_homepage_get_tab_posts_per_page') ? aksrt_homepage_get_tab_posts_per_page() : (int) get_option('posts_per_page'),
'homepageDefaultSort' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::homepage_default_sort() : 'modified',
'homepageDefaultView' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::homepage_default_view() : 'list',
'homepageSortAjax' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::homepage_sort_ajax_enabled() : true,
'homepageStickyTabs' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::homepage_sticky_tabs_enabled() : true,
'homePostsListEnabled' => !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::home_posts_list_enabled(),
'homeListSortEnabled' => !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::home_list_sort_enabled(),
'homeListSortDropdown' => class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::home_list_sort_dropdown(),
// AJAX 翻页文案
'ajaxTrigger' => (class_exists('\AKSRT\Settings\Settings') && ($t = \AKSRT\Settings\Settings::get_post_list_style_settings()['ajax_trigger_text'] ?? '') !== '') ? $t : __('加载更多', 'aksrt'),
'ajaxNomore' => (class_exists('\AKSRT\Settings\Settings') && ($n = \AKSRT\Settings\Settings::get_post_list_style_settings()['ajax_nomore_text'] ?? '') !== '') ? $n : __('没有更多了', 'aksrt'),
'ajaxLoading' => __('正在加载...', 'aksrt'),
'iasMax' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['ias_max'] : 3,
'commentIasAuto' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_display_settings()['paging_ajax_ias'] : true,
'commentUploadEnabled' => class_exists('\AKSRT\Settings\Settings') ? !empty((\AKSRT\Settings\Settings::get_comment_display_settings()['toolbar']['upload_img'] ?? false)) : false,
'commentUploadUrl' => esc_url_raw(rest_url('aksrt/v1/comment/upload-image')),
'commentUploadMaxSize' => 2 * 1024 * 1024,
'commentIasMax' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_comment_display_settings()['paging_ajax_ias_max'] : 3,
'commentQuickPhrases' => class_exists('\AKSRT\Settings\Settings') ? (\AKSRT\Settings\Settings::get_comment_display_settings()['quick_phrases'] ?? []) : [],
'commentSmiliesBase' => function_exists('aksrt_comment_smilies_base_uri') ? aksrt_comment_smilies_base_uri() : (trailingslashit(get_template_directory_uri()) . 'assets/img/smilies/'),
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
// Alias used by modal-auth / follow / report (same value as restUrl).
'apiUrl' => esc_url_raw(rest_url('aksrt/v1')),
'stickyEnabled' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_enabled'] : true,
'stickyPages' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_pages'] : ['home', 'cat'],
'badgeShow' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['badge_show'] : true,
'badgeTypes' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['badge_types'] : ['pay', 'tag', 'topics', 'cat'],
'search' => [
'mode' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::search_mode() : 'default',
'placeholder' => function_exists('aksrt_get_search_placeholder') ? aksrt_get_search_placeholder() : __('开启精彩搜索...', 'aksrt'),
'historyEnabled' => function_exists('aksrt_search_history_enabled') ? aksrt_search_history_enabled() : true,
'suggestionsEnabled' => function_exists('aksrt_search_suggestions_enabled') ? aksrt_search_suggestions_enabled() : false,
'hotKeywords' => function_exists('aksrt_get_search_hot_keywords') ? aksrt_get_search_hot_keywords() : [],
'popularTitle' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::search_popular_title() : __('热门搜索', 'aksrt'),
'contentTypes' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_search_content_types()['types'] : ['post', 'category', 'tag'],
'defaultType' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_search_content_types()['default'] : 'post',
],
// 用户状态
'isLoggedIn' => is_user_logged_in(),
'userId' => get_current_user_id(),
'cartUrl' => $runtime_routes['cart'],
'ordersUrl' => $runtime_routes['orders'],
'userCenterUrl' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'),
'routes' => [
'home' => home_url('/'),
'login' => function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(),
'register' => function_exists('aksrt_get_register_url') ? aksrt_get_register_url() : wp_registration_url(),
'resetPassword' => function_exists('aksrt_get_reset_password_url') ? aksrt_get_reset_password_url() : wp_lostpassword_url(),
'profile' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/profile/'),
'cart' => $runtime_routes['cart'],
'orders' => $runtime_routes['orders'],
'resources' => $runtime_routes['resources'],
],
'shell' => [
'wrapperId' => '',
'navId' => 'navbar-top',
'searchPanelId' => 'nav-search-panel',
'drawerId' => 'mobile-navbar',
'userMenuId' => 'sub-user-box',
'floatActionsId' => 'float-right-btns',
'tabbarId' => 'footer-tabbar',
],
'themeUri' => get_template_directory_uri(),
'codeHighlight' => class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_code_highlight_config()
: ['enabled' => true, 'lightTheme' => 'enlighter', 'darkTheme' => 'monokai', 'lineNumbers' => false, 'showButtons' => true, 'maxHeight' => 500],
'lazyload' => class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_lazyload_config()
: ['enabled' => true, 'animation' => 'blur', 'placeholder_url' => '', 'blur' => true],
'systemNotice' => class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_system_notice_config()
: ['enabled' => false],
'contentProtect' => [
'enabled' => class_exists('\AKSRT\Settings\Settings') && (bool) \AKSRT\Settings\Settings::get_value('content_protect_enabled', false),
'copyNotice' => class_exists('\AKSRT\Settings\Settings') && (bool) \AKSRT\Settings\Settings::get_value('content_protect_copy_notice', false),
'copyNoticeText' => class_exists('\AKSRT\Settings\Settings') ? (string) \AKSRT\Settings\Settings::get_value('content_protect_copy_text', '来自本站,请尊重原创版权。') : '',
'disableRightClick'=> class_exists('\AKSRT\Settings\Settings') && (bool) \AKSRT\Settings\Settings::get_value('content_protect_right_click', false),
'detectDevTools' => class_exists('\AKSRT\Settings\Settings') && (bool) \AKSRT\Settings\Settings::get_value('content_protect_f12', false),
],
'translate' => class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_i18n_config()
: ['enabled' => false, 'autoDetect' => true, 'defaultLang' => 'zh-CN'],
]);
}
}
+285
View File
@@ -0,0 +1,285 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
trait ScriptsPart2 {
/**
* 前端 debug 上报入口(仅在 WP_DEBUG 模式下写日志)
*/
public function handle_frontend_debug(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
if (!defined('WP_DEBUG') || !WP_DEBUG) {
wp_send_json_success(['ignored' => true]);
}
$ctx = [
'source' => isset($_POST['source']) ? sanitize_text_field(wp_unslash($_POST['source'])) : 'frontend',
'event' => isset($_POST['event']) ? sanitize_text_field(wp_unslash($_POST['event'])) : 'unknown',
'page' => isset($_POST['page']) ? esc_url_raw(wp_unslash($_POST['page'])) : '',
'text' => isset($_POST['text']) ? sanitize_text_field(wp_unslash($_POST['text'])) : '',
'size' => isset($_POST['size']) ? (int) wp_unslash($_POST['size']) : 0,
'error' => isset($_POST['error']) ? sanitize_text_field(wp_unslash($_POST['error'])) : '',
];
$this->debug_log('frontend debug event', $ctx);
wp_send_json_success(['logged' => true]);
}
/**
* Passive event listener polyfill — 全局加载,消除 Chrome Violation 警告
*/
public function enqueue_passive_polyfill(): void {
wp_enqueue_script(
'aksrt-passive-events',
get_template_directory_uri() . '/assets/js/passive-events.js',
[],
AKSRT_VERSION,
false
);
}
/**
* 加载后台资源 — 仅在 AKSRT 设置页面加载(与前端样式完全隔离)
*/
public function enqueue_admin($hook): void {
// 只在 AKSRT 设置页面 (SPA 容器) 加载
$screen = get_current_screen();
if (!$screen || $screen->base !== 'toplevel_page_aksrt-settings') {
return;
}
// Admin 专用面板样式 — 独立于前端,不受前端 CSS 影响
wp_enqueue_style(
'aksrt-admin-panel',
AKSRT_URI . '/assets/css/admin/admin-panel.css',
[], // 无依赖,不加载任何前端样式
AKSRT_VERSION
);
// 注意: SPA 主资源 (Vue3 build) 由 \AKSRT\Admin\Admin::enqueue_admin_assets() 负责加载
}
/**
* 输出运行时 CSS 变量覆盖
*
* 仅注入用户可在后台配置的覆盖值(logo / favicon / primary),
* 主体宽度统一使用固定 token:1280 / 1080 / 300。
*/
public function inline_css_vars(): void {
(new \AKSRT\Frontend\InlineStyleBuilder())->output();
}
/**
* 输出自定义头部/底部代码
*/
public function output_custom_codes(): void {
(new \AKSRT\Frontend\CustomCodeRenderer())->output_header();
}
public function output_loading_overlay(): void {
(new \AKSRT\Frontend\LoadingOverlayRenderer())->output();
}
/**
* 输出自定义底部代码
*/
public function output_custom_footer_codes(): void {
(new \AKSRT\Frontend\CustomCodeRenderer())->output_footer();
}
/**
* Body 类增强
*/
public function body_classes(array $classes): array {
if (is_front_page()) {
$classes[] = 'front-page';
}
if (function_exists('aksrt_is_fixed_homepage_request') && aksrt_is_fixed_homepage_request()) {
$classes[] = 'aksrt-homepage-layout';
$classes[] = 'aksrt-homepage-active';
}
if (is_singular()) {
$classes[] = 'single-post-' . get_the_ID();
}
if (is_archive()) {
$classes[] = 'archive-page';
}
if ((function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page())
|| (function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page())
|| get_query_var('user_nicename')) {
$classes[] = 'aksrt-profile';
}
// 区分本人中心与公开主页的 body class
if (function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page()) {
$classes[] = 'aksrt-user-center';
}
if (function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page()) {
$classes[] = 'aksrt-user-home';
}
if (function_exists('aksrt_is_auth_page_request') && aksrt_is_auth_page_request()) {
$classes[] = 'auth-page';
}
// Zibll site-layout-1/2/3 (no sidebar / right / left).
if (function_exists('aksrt_body_layout_classes')) {
foreach (aksrt_body_layout_classes() as $layout_class) {
$classes[] = $layout_class;
}
}
return $classes;
}
/**
* 搜索历史同步(登录用户)
*/
public function handle_search_history(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$user_id = get_current_user_id();
if (!$user_id) {
wp_send_json_error(['message' => '未登录'], 401);
}
$mode = isset($_POST['mode']) ? sanitize_key(wp_unslash($_POST['mode'])) : 'get';
$meta_key = 'aksrt_search_history';
if ($mode === 'save') {
$raw = isset($_POST['items']) ? wp_unslash($_POST['items']) : '[]';
$items = json_decode($raw, true);
if (!is_array($items)) {
$items = [];
}
$clean = [];
foreach (array_slice($items, 0, 20) as $item) {
if (!is_array($item) || empty($item['keyword'])) {
continue;
}
$clean[] = [
'keyword' => sanitize_text_field($item['keyword']),
'type' => sanitize_key($item['type'] ?? 'post'),
];
}
update_user_meta($user_id, $meta_key, $clean);
wp_send_json_success(['items' => $clean]);
}
$saved = get_user_meta($user_id, $meta_key, true);
wp_send_json_success(['items' => is_array($saved) ? $saved : []]);
}
/**
* 搜索建议(实时搜索提示)
*/
public function handle_search_suggestions(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$q = isset($_POST['q']) ? sanitize_text_field(wp_unslash($_POST['q'])) : '';
$type = isset($_POST['type']) ? sanitize_key(wp_unslash($_POST['type'])) : 'post';
if (mb_strlen($q) < 2) {
wp_send_json_success(['items' => []]);
}
$items = [];
if ($type === 'post' || $type === '') {
$posts = get_posts([
'post_type' => 'post',
'post_status' => 'publish',
's' => $q,
'posts_per_page' => 6,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
]);
foreach ($posts as $p) {
$items[] = [
'title' => $p->post_title,
'url' => get_permalink($p),
'meta' => get_the_date('', $p),
];
}
} elseif ($type === 'category') {
$terms = get_terms([
'taxonomy' => 'category',
'search' => $q,
'number' => 6,
'hide_empty' => true,
]);
if (!is_wp_error($terms)) {
foreach ($terms as $t) {
$items[] = [
'title' => $t->name,
'url' => get_term_link($t),
'meta' => sprintf(__('%d 篇文章', 'aksrt'), $t->count),
];
}
}
} elseif ($type === 'tag') {
$terms = get_terms([
'taxonomy' => 'post_tag',
'search' => $q,
'number' => 6,
'hide_empty' => true,
]);
if (!is_wp_error($terms)) {
foreach ($terms as $t) {
$items[] = [
'title' => $t->name,
'url' => get_term_link($t),
'meta' => sprintf(__('%d 篇文章', 'aksrt'), $t->count),
];
}
}
}
wp_send_json_success(['items' => $items]);
}
private function debug_log(string $message, array $context = []): void {
if (!defined('WP_DEBUG') || !WP_DEBUG) {
return;
}
$payload = '[AKSRT Scripts] ' . $message;
if (!empty($context)) {
$json = wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (false !== $json) {
$payload .= ' ' . $json;
}
}
error_log($payload);
}
/**
* Resolve public route URLs from the rebuilt mainline ownership.
*
* @return array<string, string>
*/
private function resolve_frontend_routes(): array {
return [
'orders' => $this->resolve_template_page_url('templates/pages/page-orders.php', '/orders/'),
'resources' => home_url('/resources/'),
];
}
/**
* Resolve the permalink for a template-owned page with a stable fallback.
*/
private function resolve_template_page_url(string $template_file, string $fallback): string {
if (function_exists('aksrt_find_template_page')) {
$page = aksrt_find_template_page($template_file);
if ($page instanceof \WP_Post) {
return get_permalink($page);
}
}
return home_url($fallback);
}
}
+288
View File
@@ -0,0 +1,288 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class Setup {
use SetupPart1;
use SetupPart2;
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('after_setup_theme', [$this, 'load_textdomain'], 1);
add_action('after_setup_theme', [$this, 'setup_theme']);
add_action('widgets_init', [$this, 'register_sidebars']);
add_filter('dynamic_sidebar_params', [$this, 'filter_sidebar_widget_params']);
add_action('init', [$this, 'register_nav_menus'], 0);
add_action('init', [$this, 'register_post_types'], 1);
add_action('init', [$this, 'register_meta_fields'], 5);
// 文章浏览量统计(单次访问)
add_action('wp', 'aksrt_set_post_views');
// 隐藏默认WordPress管理菜单
add_action('admin_menu', [$this, 'hide_default_menus'], 999);
}
public function load_textdomain(): void {
load_theme_textdomain('aksrt', AKSRT_DIR . '/languages');
}
public function setup_theme(): void {
// HTML5 支持
add_theme_support('html5', [
'search-form', 'comment-form', 'comment-list',
'gallery', 'caption', 'style', 'script',
'navigation-widgets',
]);
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('custom-logo', [
'height' => 120,
'width' => 300,
'flex-height' => true,
'flex-width' => true,
'header-text' => true,
]);
add_theme_support('customize-selective-refresh-widgets');
add_theme_support('align-wide');
add_theme_support('wp-block-styles');
add_theme_support('responsive-embeds');
// 文章格式
add_theme_support('post-formats', ['aside', 'image', 'video', 'quote', 'link', 'gallery']);
// 图片尺寸
add_image_size('aksrt-cover', 1200, 675, true);
add_image_size('aksrt-card', 400, 260, true);
add_image_size('aksrt-thumb', 150, 150, true);
add_image_size('aksrt-banner', 1920, 500, true);
// 内容宽度(用于区块编辑器)
if (!isset($GLOBALS['content_width'])) {
$GLOBALS['content_width'] = 900;
}
}
public function register_nav_menus(): void {
register_nav_menus([
'primary' => __('主导航', 'aksrt'),
'mobile' => __('移动端菜单', 'aksrt'),
'footer' => __('页脚菜单', 'aksrt'),
'social' => __('社交链接', 'aksrt'),
'header_actions' => __('顶部右侧入口', 'aksrt'),
]);
}
/**
* 注册自定义文章类型
*
* 资源不是 CPT:元数据挂在普通 post 上,
* 通过 _aksrt_is_resource meta 标记是否为资源。
*/
public function register_post_types(): void {
// 项目展示
register_post_type('project', [
'labels' => [
'name' => __('项目展示', 'aksrt'),
'singular_name' => __('项目', 'aksrt'),
'add_new' => __('新建项目', 'aksrt'),
'add_new_item' => __('新建项目', 'aksrt'),
'edit_item' => __('编辑项目', 'aksrt'),
'new_item' => __('新项目', 'aksrt'),
'view_item' => __('查看项目', 'aksrt'),
'search_items' => __('搜索项目', 'aksrt'),
'not_found' => __('未找到项目', 'aksrt'),
'not_found_in_trash' => __('回收站中无项目', 'aksrt'),
'menu_name' => __('项目展示', 'aksrt'),
],
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => false,
'show_in_nav_menus' => false,
'has_archive' => false,
'hierarchical' => false,
'menu_position' => 25,
'menu_icon' => 'dashicons-portfolio',
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'author', 'revisions'],
'rewrite' => ['slug' => 'project'],
'capability_type' => 'post',
'map_meta_cap' => true,
]);
}
/**
* 注册资源 post meta + 分类 term meta(aksrt_flow)
*
* 全部 show_in_rest,便于 Block Editor + Vue SPA 读写。
*
* v3.4: 资源元数据全部挂在 post 上,配合 _aksrt_is_resource 开关使用。
*/
public function register_meta_fields(): void {
$post_edit_auth = static function () {
return current_user_can('edit_posts');
};
// Core switch: mark a post as resource.
register_post_meta('post', DomainModel::RESOURCE_MARKER_KEY, [
'type' => 'boolean',
'description' => 'Whether this post is a resource post.',
'single' => true,
'default' => false,
'show_in_rest' => true,
'sanitize_callback' => static function ($v) { return (bool) $v; },
'auth_callback' => $post_edit_auth,
]);
register_post_meta('post', DomainModel::PAY_META_KEY, [
'type' => 'object',
'description' => 'Canonical paid content payload.',
'single' => true,
'default' => DomainModel::default_pay_meta(),
'show_in_rest' => [
'schema' => [
'type' => 'object',
'properties' => [
'pay_type' => [
'type' => 'string',
'enum' => array_keys(DomainModel::pay_type_labels()),
],
'price' => ['type' => 'number'],
'original_price' => ['type' => 'number'],
'vip1_price' => ['type' => 'number'],
'vip2_price' => ['type' => 'number'],
'version' => ['type' => 'string'],
'size' => ['type' => 'string'],
'license' => ['type' => 'string'],
'compat' => ['type' => 'string'],
'demo_url' => ['type' => 'string', 'format' => 'uri'],
'video_url' => ['type' => 'string', 'format' => 'uri'],
'downloads' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'label' => ['type' => 'string'],
'url' => ['type' => 'string', 'format' => 'uri'],
'type' => ['type' => 'string'],
'extract_code' => ['type' => 'string'],
],
],
],
'attributes' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'key' => ['type' => 'string'],
'value' => ['type' => 'string'],
],
],
],
'gallery' => [
'type' => 'array',
'items' => ['type' => 'string', 'format' => 'uri'],
],
'require_login' => ['type' => 'boolean'],
'require_captcha' => ['type' => 'boolean'],
],
],
],
'sanitize_callback' => [DomainModel::class, 'sanitize_pay_meta'],
'auth_callback' => $post_edit_auth,
]);
register_post_meta('post', DomainModel::PAY_TYPE_KEY, [
'type' => 'string',
'description' => 'Query-friendly paid content type.',
'single' => true,
'default' => DomainModel::PAY_TYPE_NO,
'show_in_rest' => true,
'sanitize_callback' => [DomainModel::class, 'normalize_pay_type'],
'auth_callback' => $post_edit_auth,
]);
register_post_meta('post', DomainModel::PAY_PRICE_KEY, [
'type' => 'number',
'description' => 'Query-friendly paid content price.',
'single' => true,
'default' => 0,
'show_in_rest' => true,
'sanitize_callback' => static function ($value) {
return max(0, (float) $value);
},
'auth_callback' => $post_edit_auth,
]);
foreach ([
DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY => 'Resource download count',
DomainModel::RESOURCE_VIEW_COUNT_KEY => 'Resource view count',
DomainModel::POST_VIEW_COUNT_KEY => 'Generic post view count',
] as $meta_key => $description) {
register_post_meta('post', $meta_key, [
'type' => 'integer',
'description' => $description,
'single' => true,
'default' => 0,
'show_in_rest' => true,
'sanitize_callback' => 'absint',
'auth_callback' => $post_edit_auth,
]);
}
register_term_meta('category', 'aksrt_flow', [
'type' => 'string',
'description' => 'Category flow mode: inherit/blog/cms',
'single' => true,
'default' => 'inherit',
'show_in_rest' => true,
'sanitize_callback' => function ($v) {
$v = is_string($v) ? $v : 'inherit';
return in_array($v, ['inherit', 'blog', 'cms'], true) ? $v : 'inherit';
},
'auth_callback' => function () {
return current_user_can('manage_categories');
},
]);
}
public static function sanitize_downloads_meta($value): array {
return DomainModel::sanitize_downloads($value);
}
/**
* 为 Customizer 小工具实时预览补齐编辑态包裹。
*
* @param array $params Dynamic sidebar params.
* @return array
*/
public function filter_sidebar_widget_params(array $params): array {
if (!is_customize_preview() || empty($params[0]['before_widget']) || empty($params[0]['after_widget'])) {
return $params;
}
$params[0]['before_widget'] = '<div class="customize-preview-widget-box"><div class="customize-preview-widget-edit"></div>' . $params[0]['before_widget'];
$params[0]['after_widget'] = $params[0]['after_widget'] . '</div>';
return $params;
}
/**
* 注册侧边栏
*/
}
+455
View File
@@ -0,0 +1,455 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
trait SetupPart1 {
public function register_sidebars(): void {
$fluid_wrapper_before = '<div id="%1$s" class="zib-widget %2$s">';
$card_wrapper_before = '<div id="%1$s" class="zib-widget %2$s">';
$title_before = '<h3>';
$title_after = '</h3>';
// ===== 全站 / 首页挂载位(对齐 Zibll 首页骨架) =====
register_sidebar([
'id' => 'all_top_fluid',
'name' => __('全站 - 顶部全宽', 'aksrt'),
'description' => __('显示在所有页面内容区上方的全宽挂载位。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'all_bottom_fluid',
'name' => __('全站 - 底部全宽', 'aksrt'),
'description' => __('显示在所有页面页脚上方的全宽挂载位。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'all_sidebar_top',
'name' => __('全站 - 侧栏顶部', 'aksrt'),
'description' => __('显示在全站侧栏顶部。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'all_sidebar_bottom',
'name' => __('全站 - 侧栏底部', 'aksrt'),
'description' => __('显示在全站侧栏底部。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'all_footer',
'name' => __('全站页脚小工具', 'aksrt'),
'description' => __('显示在页脚内部的统一小工具区域。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'mobile_nav_fluid',
'name' => __('移动端菜单底部', 'aksrt'),
'description' => __('显示在移动端抽屉菜单底部。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'home_top_fluid',
'name' => __('首页 - 顶部全宽', 'aksrt'),
'description' => __('显示在首页主内容上方的全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'home_top_content',
'name' => __('首页 - 内容上方', 'aksrt'),
'description' => __('显示在首页主内容区顶部。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'home_bottom_content',
'name' => __('首页 - 内容下方', 'aksrt'),
'description' => __('显示在首页主内容区底部。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'home_bottom_fluid',
'name' => __('首页 - 底部全宽', 'aksrt'),
'description' => __('显示在首页页脚上方的全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'home_sidebar',
'name' => __('首页侧栏', 'aksrt'),
'description' => __('显示在首页右侧栏。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
// 新前端使用的侧边栏 (sidebar.php)
register_sidebar([
'id' => 'sidebar-blog',
'name' => __('博客侧边栏', 'aksrt'),
'description' => __('显示在首页、归档等列表页右侧(新前端)。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
// 文章详情页侧栏
register_sidebar([
'id' => 'sidebar-article',
'name' => __('文章侧边栏', 'aksrt'),
'description' => __('显示在文章详情页右侧。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
// ===== 文章页 Widget 区域(模板zibll 多位置) =====
register_sidebar([
'id' => 'single_top_fluid',
'name' => __('文章页 · 顶部全宽', 'aksrt'),
'description' => __('文章页顶部全宽区域(在封面/面包屑之上)。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'single_top_content',
'name' => __('文章页 · 内容上方', 'aksrt'),
'description' => __('文章主体卡片上方(在内容列内)。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'single_bottom_content',
'name' => __('文章页 · 内容下方', 'aksrt'),
'description' => __('评论区下方(在内容列内)。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'single_bottom_fluid',
'name' => __('文章页 · 底部全宽', 'aksrt'),
'description' => __('文章页底部全宽区域(在页脚之上)。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'sidebar-footer-1',
'name' => __('页脚栏 1', 'aksrt'),
'description' => '',
'before_widget' => '<div class="footer-widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h4 class="footer-widget-title">',
'after_title' => '</h4>',
]);
register_sidebar([
'id' => 'sidebar-footer-2',
'name' => __('页脚栏 2', 'aksrt'),
'description' => '',
'before_widget' => '<div class="footer-widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h4 class="footer-widget-title">',
'after_title' => '</h4>',
]);
// ===== 分类页 Widget 区域 =====
register_sidebar([
'id' => 'cat_top_fluid',
'name' => __('分类页 · 顶部全宽', 'aksrt'),
'description' => __('分类归档页顶部全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'cat_top_content',
'name' => __('分类页 · 内容上方', 'aksrt'),
'description' => __('分类归档页内容区上方。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'cat_bottom_content',
'name' => __('分类页 · 内容下方', 'aksrt'),
'description' => __('分类归档页内容区下方。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'cat_bottom_fluid',
'name' => __('分类页 · 底部全宽', 'aksrt'),
'description' => __('分类归档页底部全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'cat_sidebar',
'name' => __('分类页 · 侧栏', 'aksrt'),
'description' => __('分类归档页侧栏区域。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
// ===== 标签页 Widget 区域 =====
register_sidebar([
'id' => 'tag_top_fluid',
'name' => __('标签页 · 顶部全宽', 'aksrt'),
'description' => __('标签归档页顶部全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'tag_top_content',
'name' => __('标签页 · 内容上方', 'aksrt'),
'description' => __('标签归档页内容区上方。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'tag_bottom_content',
'name' => __('标签页 · 内容下方', 'aksrt'),
'description' => __('标签归档页内容区下方。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'tag_bottom_fluid',
'name' => __('标签页 · 底部全宽', 'aksrt'),
'description' => __('标签归档页底部全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'tag_sidebar',
'name' => __('标签页 · 侧栏', 'aksrt'),
'description' => __('标签归档页侧栏区域。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
// ===== 搜索页 Widget 区域 =====
register_sidebar([
'id' => 'search_top_fluid',
'name' => __('搜索页 · 顶部全宽', 'aksrt'),
'description' => __('搜索结果页顶部全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'search_top_content',
'name' => __('搜索页 · 内容上方', 'aksrt'),
'description' => __('搜索结果页内容区上方。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'search_bottom_content',
'name' => __('搜索页 · 内容下方', 'aksrt'),
'description' => __('搜索结果页内容区下方。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'search_bottom_fluid',
'name' => __('搜索页 · 底部全宽', 'aksrt'),
'description' => __('搜索结果页底部全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'search_sidebar',
'name' => __('搜索页 · 侧栏', 'aksrt'),
'description' => __('搜索结果页侧栏区域。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'archive_bottom_fluid',
'name' => __('归档页 · 底部全宽', 'aksrt'),
'description' => __('分类/标签/归档页底部全宽区域。', 'aksrt'),
'before_widget' => $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
register_sidebar([
'id' => 'author-sidebar',
'name' => __('作者页 · 侧栏', 'aksrt'),
'description' => __('作者归档页侧栏区域。', 'aksrt'),
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
// ── Per-page dynamic sidebars (zibll zib_page_register_sidebar parity) ──
// Pages that opt in via meta `widgets_register=1` + `widgets_register_container`
// (a serialized list of area keys) get page_{key}_{ID} sidebars registered, and
// the matching `page_sidebar_{ID}` is treated as active when any of its areas
// hold widgets. page.php reads page_top_fluid_/page_top_content_/etc.
$this->register_page_sidebars();
}
/**
* Register dynamic per-page widget areas (aligns Zibll zib_page_register_sidebar).
*
* Zibll stores, per page, a `widgets_register` flag and a `widgets_register_container`
* serialized array of area keys (sidebar/top_fluid/top_content/bottom_content/bottom_fluid).
* We replicate that: for every page opted in, register page_{key}_{ID} sidebars.
*/
public function register_page_sidebars(): void {
global $wpdb;
// Cache the opt-in page list (postmeta-driven), mirroring zib_get_widgets_register_pages.
$pages = wp_cache_get('aksrt_widgets_register_pages', 'aksrt_cache_group', true);
if (false === $pages) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$rows = $wpdb->get_results(
"SELECT p.ID, p.post_title, pm.meta_value
FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
WHERE p.post_type = 'page'
AND p.post_status = 'publish'
AND pm.meta_key = 'widgets_register_container'
AND pm.meta_value LIKE '%;}'"
);
$pages = is_array($rows) ? $rows : [];
wp_cache_set('aksrt_widgets_register_pages', $pages, 'aksrt_cache_group');
}
$area_labels = [
'sidebar' => __('侧边栏', 'aksrt'),
'top_fluid' => __('顶部全宽度', 'aksrt'),
'top_content' => __('主内容上面', 'aksrt'),
'bottom_content' => __('主内容下面', 'aksrt'),
'bottom_fluid' => __('底部全宽度', 'aksrt'),
];
$fluid_wrapper_before = '<div id="%1$s" class="zib-widget %2$s">';
$card_wrapper_before = '<div id="%1$s" class="zib-widget %2$s">';
$title_before = '<h3>';
$title_after = '</h3>';
foreach ($pages as $page) {
$meta_value = maybe_unserialize($page->meta_value);
if (!is_array($meta_value)) {
continue;
}
$name = function_exists('aksrt_str_cut') ? aksrt_str_cut($page->post_title, 0, 10, '...') : mb_substr((string) $page->post_title, 0, 10);
foreach ($meta_value as $key) {
if (!isset($area_labels[$key])) {
continue;
}
$id = 'page_' . $key . '_' . (int) $page->ID;
register_sidebar([
'name' => sprintf(__('[页面:%1$s]-%2$s', 'aksrt'), $name, $area_labels[$key]),
'id' => $id,
'description' => sprintf(__('显示在[页面:%1$s]的 %2$s 位置', 'aksrt'), $name, $area_labels[$key]),
'before_widget' => $key === 'sidebar' ? $card_wrapper_before : $fluid_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
trait SetupPart2 {
/**
* 隐藏默认WordPress菜单项目 + zibll 风格运营菜单分隔线
*/
public function hide_default_menus(): void {
// 移除评论默认菜单(已移除该操作以恢复 Comments 菜单)
// 之前的实现:remove_menu_page('edit-comments.php');
// 移除链接管理(已废弃但仍可能存在)
//remove_menu_page('link-manager.php');
// 移除仪表盘中的不必要组件
remove_meta_box('dashboard_primary', 'dashboard', 'normal');
remove_meta_box('dashboard_quick_press', 'dashboard', 'side');
}
}
+305
View File
@@ -0,0 +1,305 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class Shortcodes {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 注册全部短代码。在 `init` 上调用一次。
*/
public function register(): void {
// 隐藏内容(评论/登录/密码可见)→ \AKSRT\Content\Visibility
add_shortcode('aksrt_hide', [$this, 'render_hide']);
// §3.8 用户友好短代码别名
add_shortcode('hide', [$this, 'render_hide_comment']);
add_shortcode('reply', [$this, 'render_reply']);
add_shortcode('vip', [$this, 'render_vip']);
add_shortcode('payshow', [$this, 'render_payshow']);
// Zibll-compatible hidecontent alias (type=payshow|reply|login|vip|password|…)
add_shortcode('hidecontent', [$this, 'render_hidecontent']);
add_shortcode('content', [$this, 'render_content_gate']);
// 文章归档 / 个人资料卡 → Templates 模板
add_shortcode('aksrt_archive', [$this, 'render_archive']);
add_shortcode('aksrt_profile', [$this, 'render_profile']);
// 文章列表(统一渲染器)
add_shortcode('aksrt_posts', [$this, 'render_posts']);
}
/**
* [aksrt_hide mode="comment|login|password"]...[/aksrt_hide]
*/
public function render_hide(array $atts, string $content = ''): string {
if (!class_exists('\AKSRT\Content\Visibility')) {
return '';
}
return \AKSRT\Content\Visibility::get_instance()->render_hide_shortcode($atts, $content);
}
/**
* [hide]...[/hide] — 评论后可见(默认 mode=comment)
*/
public function render_hide_comment(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'hide');
return $this->render_hide(['mode' => 'comment', 'message' => $atts['message']], $content);
}
/**
* [reply]...[/reply] — 回复(评论)后可见
*/
public function render_reply(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'reply');
$msg = $atts['message'] ?: __('回复本文后可查看隐藏内容', 'aksrt');
return $this->render_hide(['mode' => 'comment', 'message' => $msg], $content);
}
/**
* [vip]...[/vip] — VIP 用户可见
*/
public function render_vip(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'vip');
$uid = get_current_user_id();
$is_vip = $uid > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($uid);
if ($is_vip) {
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked aksrt-hidden-content--vip">'
. do_shortcode($content) . '</div>';
}
$message = $atts['message'] ?: __('此内容仅 VIP 用户可见', 'aksrt');
$html = '<div class="aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--vip">';
$html .= '<div class="aksrt-hidden-content__message">';
$html .= '<i class="fas fa-crown" aria-hidden="true"></i> ';
$html .= '<span>' . esc_html($message) . '</span>';
$html .= '</div>';
if ($uid <= 0) {
$html .= '<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="aksrt-hidden-content__login-btn btn-primary btn-sm">';
$html .= esc_html__('登录后查看', 'aksrt') . '</a>';
}
$html .= '</div>';
return $html;
}
/**
* [payshow]...[/payshow] — 付费后可见(文章 content-pay 或论坛主题解锁)
*/
public function render_payshow(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'payshow');
$pid = get_the_ID();
$can_view = $this->viewer_can_see_paid_block($pid);
if ($can_view) {
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked aksrt-hidden-content--payshow">'
. do_shortcode($content) . '</div>';
}
$message = $atts['message'] ?: __('此内容需付费后查看', 'aksrt');
$uid = get_current_user_id();
$html = '<div class="aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--payshow">';
$html .= '<div class="aksrt-hidden-content__message">';
$html .= '<i class="fas fa-lock" aria-hidden="true"></i> ';
$html .= '<span>' . esc_html($message) . '</span>';
$html .= '</div>';
if ($uid <= 0) {
$html .= '<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="aksrt-hidden-content__login-btn btn-primary btn-sm">';
$html .= esc_html__('登录后购买查看', 'aksrt') . '</a>';
}
$html .= '</div>';
return $html;
}
/**
* Zibll [hidecontent type="payshow|reply|login|…"]…[/hidecontent]
*/
public function render_hidecontent(array $atts = [], string $content = ''): string {
$atts = shortcode_atts([
'type' => 'payshow',
'message' => '',
'pass' => '',
], $atts, 'hidecontent');
$type = sanitize_key((string) $atts['type']);
$msg = (string) $atts['message'];
switch ($type) {
case 'reply':
case 'comment':
return $this->render_reply(['message' => $msg], $content);
case 'login':
case 'signin':
return $this->render_hide([
'mode' => 'login',
'message' => $msg !== '' ? $msg : __('登录后可查看隐藏内容', 'aksrt'),
], $content);
case 'password':
case 'pass':
return $this->render_hide([
'mode' => 'password',
'message' => $msg,
'pass' => (string) $atts['pass'],
], $content);
case 'vip':
return $this->render_vip(['message' => $msg], $content);
case 'payshow':
case 'pay':
case 'points':
default:
return $this->render_payshow(['message' => $msg], $content);
}
}
/**
* Whether current viewer may see a paid hide block on this post/topic.
*/
private function viewer_can_see_paid_block(int $pid): bool {
if ($pid <= 0) {
return false;
}
$post = get_post($pid);
if (!$post instanceof \WP_Post) {
return false;
}
$uid = get_current_user_id();
if ($uid > 0 && ((int) $post->post_author === $uid || user_can($uid, 'manage_options'))) {
return true;
}
if (class_exists('\AKSRT\Content\Visibility')) {
return \AKSRT\Content\Visibility::can_view_paid_content($pid);
}
return false;
}
/**
* [content type="admin|author|vip"]...[/content] — 按角色/身份控制可见性
*/
public function render_content_gate(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['type' => 'admin', 'message' => ''], $atts, 'content');
$type = sanitize_key((string) $atts['type']);
$uid = get_current_user_id();
$pid = get_the_ID();
$allowed = false;
switch ($type) {
case 'admin':
$allowed = $uid > 0 && user_can($uid, 'manage_options');
break;
case 'author':
if ($uid > 0 && $pid) {
$post = get_post($pid);
$allowed = $post && (int) $post->post_author === $uid;
}
break;
case 'vip':
$allowed = $uid > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($uid);
break;
case 'login':
$allowed = $uid > 0;
break;
}
if ($allowed) {
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked aksrt-hidden-content--role">'
. do_shortcode($content) . '</div>';
}
$default_msgs = [
'admin' => __('此内容仅管理员可见', 'aksrt'),
'author' => __('此内容仅作者可见', 'aksrt'),
'vip' => __('此内容仅 VIP 用户可见', 'aksrt'),
'login' => __('此内容仅登录用户可见', 'aksrt'),
];
$message = $atts['message'] ?: ($default_msgs[$type] ?? __('此内容已隐藏', 'aksrt'));
$html = '<div class="aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--role">';
$html .= '<div class="aksrt-hidden-content__message">';
$html .= '<i class="fas fa-eye-slash" aria-hidden="true"></i> ';
$html .= '<span>' . esc_html($message) . '</span>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* [aksrt_archive] 文章归档
*/
public function render_archive(array $atts = [], string $content = ''): string {
if (!class_exists('AKSRT\\Core\\Templates')) {
return '';
}
return Templates::get_instance()->render_archive_shortcode($atts, $content);
}
/**
* [aksrt_profile] 个人资料卡
*/
public function render_profile(array $atts = [], string $content = ''): string {
if (!class_exists('AKSRT\\Core\\Templates')) {
return '';
}
return Templates::get_instance()->render_profile_shortcode($atts, $content);
}
/**
* [aksrt_posts count="6" cat="" tag="" orderby="date" order="DESC" mode="card"]
*
* 用统一渲染器输出文章列表,避免内容里手写卡片循环。
*/
public function render_posts(array $atts = [], string $content = ''): string {
if (!function_exists('aksrt_render_post_list')) {
return '';
}
$a = shortcode_atts([
'count' => 6,
'cat' => '',
'tag' => '',
'orderby' => 'date',
'order' => 'DESC',
'mode' => 'card',
], $atts, 'aksrt_posts');
$query_args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => max(1, min(24, (int) $a['count'])),
'orderby' => sanitize_key((string) $a['orderby']) ?: 'date',
'order' => strtoupper((string) $a['order']) === 'ASC' ? 'ASC' : 'DESC',
'ignore_sticky_posts' => true,
'no_found_rows' => true,
];
$cat = sanitize_text_field((string) $a['cat']);
if ($cat !== '') {
$query_args[is_numeric($cat) ? 'cat' : 'category_name'] = $cat;
}
$tag = sanitize_text_field((string) $a['tag']);
if ($tag !== '') {
$query_args[is_numeric($tag) ? 'tag_id' : 'tag'] = $tag;
}
$mode = in_array($a['mode'], ['card', 'list', 'mini'], true) ? $a['mode'] : 'card';
$query = new \WP_Query($query_args);
$html = aksrt_render_post_list($query, [
'context' => 'widget',
'mode' => $mode,
], false);
wp_reset_postdata();
return '<div class="aksrt-shortcode-posts">' . $html . '</div>';
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class StatsTracker {
private static $instance = null;
/**
* 单例模式
*/
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 初始化:挂钩到 wp_head
*/
public static function init(): void {
add_action('wp_head', [self::get_instance(), 'track_visit'], 1);
}
/**
* 追踪访问
*/
public function track_visit(): void {
// 排除管理员、爬虫、AJAX请求
if (is_admin() || wp_doing_ajax()) {
return;
}
if (is_user_logged_in() && current_user_can('manage_options')) {
return;
}
$today = current_time('Y-m-d');
$option_key = 'aksrt_daily_stats_' . $today;
// 检查是否已统计过该访客
$visitor_cookie = 'aksrt_visitor_' . $today;
$is_unique = empty($_COOKIE[$visitor_cookie]);
// 获取或初始化今日统计
$stats = get_option($option_key, ['visitors' => 0, 'pageviews' => 0]);
if (!is_array($stats)) {
$stats = ['visitors' => 0, 'pageviews' => 0];
}
// 更新统计
$stats['pageviews']++;
if ($is_unique) {
$stats['visitors']++;
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
}
update_option($option_key, $stats, false);
// 每月1日清理旧数据
if ((int) date('j') === 1 && (int) date('H') === 0 && (int) date('i') < 5) {
$this->cleanup_old_stats();
}
}
/**
* 清理30天前的统计数据
*/
private function cleanup_old_stats(): void {
global $wpdb;
$threshold = date('Y-m-d', strtotime('-30 days'));
$wpdb->query($wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name < %s",
$wpdb->esc_like('aksrt_daily_stats_') . '%',
'aksrt_daily_stats_' . $threshold
));
}
/**
* 获取今日统计
*/
public static function get_today_stats(): array {
$today = current_time('Y-m-d');
$stats = get_option('aksrt_daily_stats_' . $today, ['visitors' => 0, 'pageviews' => 0]);
if (!is_array($stats)) {
return ['visitors' => 0, 'pageviews' => 0];
}
return [
'visitors' => absint($stats['visitors'] ?? 0),
'pageviews' => absint($stats['pageviews'] ?? 0),
];
}
/**
* 获取指定日期统计
*/
public static function get_daily_stats(string $date): array {
$stats = get_option('aksrt_daily_stats_' . $date, ['visitors' => 0, 'pageviews' => 0]);
if (!is_array($stats)) {
return ['visitors' => 0, 'pageviews' => 0];
}
return [
'visitors' => absint($stats['visitors'] ?? 0),
'pageviews' => absint($stats['pageviews'] ?? 0),
];
}
/**
* 获取最近N天统计
*/
public static function get_period_stats(int $days = 30): array {
$total = ['visitors' => 0, 'pageviews' => 0];
for ($i = 0; $i < $days; $i++) {
$date = date('Y-m-d', strtotime("-{$i} days"));
$stats = self::get_daily_stats($date);
$total['visitors'] += $stats['visitors'];
$total['pageviews'] += $stats['pageviews'];
}
return $total;
}
/**
* 获取总浏览量(所有文章)
*/
public static function get_total_views(): int {
global $wpdb;
$total = $wpdb->get_var(
"SELECT SUM(CAST(meta_value AS UNSIGNED)) FROM {$wpdb->postmeta} WHERE meta_key = 'aksrt_post_views'"
);
return max(0, (int) $total);
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class Templates {
private static $instance = null;
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_filter('theme_page_templates', [$this, 'register_page_templates']);
add_filter('template_include', [$this, 'load_page_template']);
add_filter('page_template_hierarchy', [$this, 'template_hierarchy']);
add_filter('aksrt_page_template_hierarchy', [$this, 'template_hierarchy']);
// 短代码注册统一在 Shortcodes(registry);本类只提供 archive/profile 渲染实现。
}
/**
* Selectable page templates shown in wp-admin for the rebuilt mainline.
*/
private function page_template_map(): array {
return [
'templates/pages/page-sign.php' => __('AKSRT - 登录/注册', 'aksrt'),
'templates/pages/page-profile.php' => __('AKSRT - 个人中心', 'aksrt'),
'templates/pages/page-user-home.php' => __('AKSRT - 用户主页', 'aksrt'),
'templates/pages/page-archives.php' => __('AKSRT - 文章归档', 'aksrt'),
'templates/pages/page-documentnav.php' => __('AKSRT - 文档导航', 'aksrt'),
'templates/pages/page-categories.php' => __('AKSRT - 分类汇总', 'aksrt'),
'templates/pages/page-newposts.php' => __('AKSRT - 最新文章', 'aksrt'),
// 注意:page-newposts = 最新文章列表;投稿页是 page-submit(写文章/投稿)
'templates/pages/page-submit.php' => __('AKSRT - 写文章/投稿', 'aksrt'),
'templates/pages/page-posts-navs.php' => __('AKSRT - 文章导航', 'aksrt'),
'templates/pages/page-friends.php' => __('AKSRT - 友情链接', 'aksrt'),
'templates/pages/page-links.php' => __('AKSRT - 网址导航', 'aksrt'),
'templates/pages/page-ban-appeal.php' => __('AKSRT - 禁封申诉', 'aksrt'),
'templates/pages/page-orders.php' => __('AKSRT - 订单中心', 'aksrt'),
'templates/pages/page-revenue.php' => __('AKSRT - 收益中心', 'aksrt'),
'templates/pages/page-messages.php' => __('AKSRT - 消息中心', 'aksrt'),
];
}
/**
* Register selectable page templates in wp-admin.
*/
public function register_page_templates(array $templates): array {
foreach ($this->page_template_map() as $template_file => $label) {
$templates[$template_file] = $label;
}
return $templates;
}
/**
* Resolve theme page templates through the shared page-template router.
*/
public function load_page_template(string $template): string {
global $post;
if (!$post) {
return $template;
}
$page_template_meta = get_post_meta($post->ID, '_wp_page_template', true);
$resolved_template = function_exists('aksrt_resolve_page_template_file')
? aksrt_resolve_page_template_file(
is_string($page_template_meta) ? $page_template_meta : '',
(string) $post->post_name
)
: '';
if ($resolved_template !== '' && function_exists('aksrt_locate_template_file')) {
$resolved_path = aksrt_locate_template_file($resolved_template);
if ($resolved_path) {
return $resolved_path;
}
}
return $template;
}
public function template_hierarchy(array $templates): array {
return $templates;
}
/**
* Load dedicated taxonomy templates.
*/
public function load_taxonomy_template(string $template): string {
return $template;
}
public function render_archive_shortcode(array $attrs = [], string $content = ''): string {
$template = AKSRT_DIR . '/templates/shortcodes/archive.php';
if (!file_exists($template)) {
return '';
}
ob_start();
include $template;
return (string) ob_get_clean();
}
public function render_profile_shortcode(array $attrs = [], string $content = ''): string {
$template = AKSRT_DIR . '/templates/shortcodes/profile.php';
if (!file_exists($template)) {
return '';
}
ob_start();
include $template;
return (string) ob_get_clean();
}
}
+286
View File
@@ -0,0 +1,286 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class TermMeta {
private static $instance = null;
/** Supported taxonomies */
private $taxonomies = ['category', 'post_tag'];
/** Meta key for storing term-level settings */
const META_KEY = DomainModel::TERM_META_KEY;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function init(): void {
// Register meta fields
foreach ($this->taxonomies as $tax) {
add_action("{$tax}_add_form_fields", [$this, 'render_add_fields']);
add_action("{$tax}_edit_form_fields", [$this, 'render_edit_fields']);
add_action("created_{$tax}", [$this, 'save_term_meta']);
add_action("edited_{$tax}", [$this, 'save_term_meta']);
}
add_action('pre_get_posts', [$this, 'filter_archive_query']);
}
/**
* Get term meta with defaults.
*/
public static function get_meta(int $term_id): array {
$defaults = DomainModel::default_term_meta();
$saved = get_term_meta($term_id, self::META_KEY, true);
if (!is_array($saved)) $saved = [];
return array_merge($defaults, $saved);
}
/**
* Get a single term meta value.
*/
public static function get_meta_value(int $term_id, string $key, $default = '') {
$meta = self::get_meta($term_id);
return $meta[$key] ?? $default;
}
/**
* Resolve global page-display settings with term-level overrides.
*
* @param \WP_Term $term Archive term.
* @return array<string,mixed>
*/
public static function resolve_archive_settings(\WP_Term $term): array {
$taxonomy = $term->taxonomy === 'post_tag' ? 'post_tag' : 'category';
$global = $taxonomy === 'post_tag' && class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_tag_page_settings()
: (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_category_page_settings() : [
'header_style' => 'cover',
'cover_enabled' => true,
'desc_enabled' => true,
'list_mode' => 'grid',
'filter_bar' => true,
'sidebar' => true,
'per_page' => 12,
]);
$saved = get_term_meta($term->term_id, self::META_KEY, true);
if (!is_array($saved)) {
$saved = [];
}
// Empty term layout_mode means follow global post-list settings (Zibll).
// Do not fall back to page-display list_mode=grid which forced card everywhere.
$layout_preset = isset($saved['layout_mode']) ? sanitize_key((string) $saved['layout_mode']) : '';
if ($layout_preset === '') {
$resolved_layout = ['mode' => '', 'card_style' => ''];
$layout_mode = '';
} else {
$layout_mode = $layout_preset;
$layout_map = [
'grid' => ['mode' => 'card', 'card_style' => 'thumb'],
'card' => ['mode' => 'card', 'card_style' => 'thumb'],
'list' => ['mode' => 'list', 'card_style' => 'thumb'],
'compact' => ['mode' => 'list', 'card_style' => 'compact'],
'mini' => ['mode' => 'mini', 'card_style' => 'thumb'],
'grid-cms' => ['mode' => 'grid-cms', 'card_style' => 'thumb'],
];
$resolved_layout = $layout_map[$layout_mode] ?? ['mode' => 'list', 'card_style' => 'thumb'];
}
$header_style = (string) ($global['header_style'] ?? 'simple');
if (!in_array($header_style, ['cover', 'simple', 'none'], true)) {
$header_style = 'simple';
}
if (array_key_exists('show_header', $saved)) {
$header_style = !empty($saved['show_header'])
? ($header_style === 'none' ? 'simple' : $header_style)
: 'none';
}
$sidebar_enabled = (bool) ($global['sidebar'] ?? true);
if (($saved['show_sidebar'] ?? '') === '1') {
$sidebar_enabled = true;
} elseif (($saved['show_sidebar'] ?? '') === '0') {
$sidebar_enabled = false;
}
$cover_url = isset($saved['cover_url']) ? esc_url_raw((string) $saved['cover_url']) : '';
if ($cover_url === '' && $taxonomy === 'category' && !empty($global['cover_enabled']) && class_exists('\AKSRT\Settings\Settings')) {
$cover_url = \AKSRT\Settings\Settings::get_category_cover_url($term->term_id);
}
$description = isset($saved['description']) ? sanitize_textarea_field((string) $saved['description']) : '';
if ($description === '') {
$description = (string) $term->description;
}
return [
'taxonomy' => $taxonomy,
'layout_mode' => $resolved_layout['mode'],
'card_style' => $resolved_layout['card_style'],
'layout_preset' => $layout_mode,
'header_style' => $header_style,
'show_sidebar' => $sidebar_enabled,
'show_cover' => $header_style === 'cover' && !empty($global['cover_enabled']) && $cover_url !== '',
'show_desc' => $taxonomy === 'post_tag' ? $description !== '' : (!empty($global['desc_enabled']) && $description !== ''),
'filter_bar' => $taxonomy === 'category' && !empty($global['filter_bar']),
'per_page' => max(4, min(48, (int) ($global['per_page'] ?? 12))),
'cover_url' => $cover_url,
'description' => $description,
];
}
/**
* Apply archive per-page settings to the main query.
*/
public function filter_archive_query(\WP_Query $query): void {
if (is_admin() || !$query->is_main_query()) {
return;
}
$term = null;
if ($query->is_category()) {
$cat_id = absint($query->get('cat'));
if ($cat_id > 0) {
$term = get_term($cat_id, 'category');
} else {
$slug = sanitize_title((string) $query->get('category_name'));
if ($slug !== '') {
$term = get_term_by('slug', $slug, 'category');
}
}
} elseif ($query->is_tag()) {
$tag_id = absint($query->get('tag_id'));
if ($tag_id > 0) {
$term = get_term($tag_id, 'post_tag');
} else {
$slug = sanitize_title((string) $query->get('tag'));
if ($slug !== '') {
$term = get_term_by('slug', $slug, 'post_tag');
}
}
}
if (!$term instanceof \WP_Term) {
return;
}
$settings = self::resolve_archive_settings($term);
if (!empty($settings['per_page'])) {
$query->set('posts_per_page', (int) $settings['per_page']);
}
}
/**
* Render fields for "Add new term" form.
*/
public function render_add_fields(string $taxonomy): void {
?>
<div class="form-field">
<label for="aksrt_term_cover_url"><?php esc_html_e('封面图 URL', 'aksrt'); ?></label>
<input type="url" name="aksrt_term_meta[cover_url]" id="aksrt_term_cover_url" value="" placeholder="https://...">
<p class="description"><?php esc_html_e('分类/标签页顶部封面图,留空则不显示。', 'aksrt'); ?></p>
</div>
<div class="form-field">
<label for="aksrt_term_layout"><?php esc_html_e('列表布局', 'aksrt'); ?></label>
<select name="aksrt_term_meta[layout_mode]" id="aksrt_term_layout">
<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>
</select>
</div>
<?php
}
/**
* Render fields for "Edit term" form.
*/
public function render_edit_fields(\WP_Term $term): void {
$meta = self::get_meta($term->term_id);
?>
<tr class="form-field">
<th scope="row"><label for="aksrt_term_cover_url"><?php esc_html_e('封面图 URL', 'aksrt'); ?></label></th>
<td>
<input type="url" name="aksrt_term_meta[cover_url]" id="aksrt_term_cover_url" value="<?php echo esc_url($meta['cover_url']); ?>" class="regular-text" placeholder="https://...">
<p class="description"><?php esc_html_e('分类/标签页顶部封面图,留空则不显示。', 'aksrt'); ?></p>
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_term_desc"><?php esc_html_e('自定义简介', 'aksrt'); ?></label></th>
<td>
<textarea name="aksrt_term_meta[description]" id="aksrt_term_desc" rows="3" class="large-text"><?php echo esc_textarea($meta['description']); ?></textarea>
<p class="description"><?php esc_html_e('覆盖默认的分类/标签描述。', 'aksrt'); ?></p>
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_term_layout"><?php esc_html_e('列表布局', 'aksrt'); ?></label></th>
<td>
<select name="aksrt_term_meta[layout_mode]" id="aksrt_term_layout">
<option value="" <?php selected($meta['layout_mode'], ''); ?>><?php esc_html_e('跟随全局设置', 'aksrt'); ?></option>
<option value="card" <?php selected($meta['layout_mode'], 'card'); ?>><?php esc_html_e('卡片模式', 'aksrt'); ?></option>
<option value="list" <?php selected($meta['layout_mode'], 'list'); ?>><?php esc_html_e('列表模式', 'aksrt'); ?></option>
<option value="mini" <?php selected($meta['layout_mode'], 'mini'); ?>><?php esc_html_e('迷你模式', 'aksrt'); ?></option>
</select>
</td>
</tr>
<tr class="form-field">
<th scope="row"><?php esc_html_e('显示设置', 'aksrt'); ?></th>
<td>
<label>
<input type="checkbox" name="aksrt_term_meta[show_header]" value="1" <?php checked(!empty($meta['show_header'])); ?>>
<?php esc_html_e('显示页面头部(标题、描述)', 'aksrt'); ?>
</label><br>
<label>
<input type="checkbox" name="aksrt_term_meta[show_sidebar]" value="1" <?php checked($meta['show_sidebar'] === '1'); ?>>
<?php esc_html_e('强制显示侧栏', 'aksrt'); ?>
</label><br>
<label>
<input type="checkbox" name="aksrt_term_meta[show_featured]" value="1" <?php checked(!empty($meta['show_featured'])); ?>>
<?php esc_html_e('显示推荐文章位', 'aksrt'); ?>
</label>
</td>
</tr>
<?php
}
/**
* Save term meta from form submission.
*/
public function save_term_meta(int $term_id): void {
if (!isset($_POST['aksrt_term_meta']) || !is_array($_POST['aksrt_term_meta'])) {
return;
}
if (!current_user_can('manage_categories')) {
return;
}
$raw = $_POST['aksrt_term_meta'];
$sanitized = [
'cover_url' => isset($raw['cover_url']) ? esc_url_raw($raw['cover_url']) : '',
'description' => isset($raw['description']) ? sanitize_textarea_field($raw['description']) : '',
'layout_mode' => isset($raw['layout_mode']) ? sanitize_key($raw['layout_mode']) : '',
'show_sidebar' => isset($raw['show_sidebar']) ? '1' : '',
'show_header' => isset($raw['show_header']) ? true : false,
'show_featured' => isset($raw['show_featured']) ? true : false,
];
// Only allow valid layout modes
$valid_modes = ['', 'card', 'list', 'mini', 'grid-cms'];
if (!in_array($sanitized['layout_mode'], $valid_modes, true)) {
$sanitized['layout_mode'] = '';
}
update_term_meta($term_id, self::META_KEY, $sanitized);
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class UpdateChecker {
private static $instance = null;
private const OPTION_KEY = 'aksrt_update_check';
private const CHECK_INTERVAL = DAY_IN_SECONDS * 1; // Check daily
private function __construct() {}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('admin_init', [$this, 'maybe_check']);
add_action('admin_notices', [$this, 'show_update_notice']);
}
/**
* Get current theme version
*/
private function get_current_version(): string {
$theme = wp_get_theme();
return $theme->get('Version') ?: '0.0.0';
}
/**
* Get update endpoint URL
*/
private function get_endpoint(): string {
if (class_exists('\AKSRT\Settings\Settings')) {
$endpoint = \AKSRT\Settings\Settings::get_value('update_endpoint', '');
if (!empty($endpoint)) return $endpoint;
}
return 'https://api.aksrt.com/version.json';
}
/**
* Maybe check for updates (once per day)
*/
public function maybe_check(): void {
$stored = get_option(self::OPTION_KEY, []);
$last_check = $stored['last_check'] ?? 0;
if ((time() - $last_check) < self::CHECK_INTERVAL) {
return;
}
$this->do_check();
}
/**
* Force check for updates
*/
public function do_check(): void {
$endpoint = $this->get_endpoint();
$response = wp_remote_get($endpoint, [
'timeout' => 5,
'headers' => [
'Accept' => 'application/json',
],
]);
if (is_wp_error($response)) {
update_option(self::OPTION_KEY, [
'last_check' => time(),
'error' => $response->get_error_message(),
]);
return;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (!is_array($data) || empty($data['version'])) {
return;
}
$current = $this->get_current_version();
$remote_version = $data['version'];
$update_available = version_compare($remote_version, $current, '>');
update_option(self::OPTION_KEY, [
'last_check' => time(),
'current_version' => $current,
'remote_version' => $remote_version,
'update_available' => $update_available,
'download_url' => $data['download_url'] ?? '',
'changelog' => $data['changelog'] ?? '',
'notify' => !empty($data['notify']),
]);
}
/**
* Show admin notice if update is available
*/
public function show_update_notice(): void {
if (!current_user_can('update_themes')) return;
$stored = get_option(self::OPTION_KEY, []);
if (empty($stored['update_available'])) return;
$remote = $stored['remote_version'] ?? '';
$current = $stored['current_version'] ?? '';
$download_url = $stored['download_url'] ?? '';
$changelog = $stored['changelog'] ?? '';
// User dismissed this version?
$dismissed = get_user_meta(get_current_user_id(), 'aksrt_dismissed_update', true);
if ($dismissed === $remote) return;
$dismiss_url = wp_nonce_url(
add_query_arg('aksrt_dismiss_update', $remote, admin_url()),
'aksrt_dismiss_update_' . $remote
);
echo '<div class="notice notice-info is-dismissible">';
echo '<p><strong>' . esc_html__('AKSRT 主题更新', 'aksrt') . '</strong></p>';
echo '<p>' . sprintf(
esc_html__('新版本 %s 可用(当前版本 %s)。', 'aksrt'),
'<strong>' . esc_html($remote) . '</strong>',
esc_html($current)
) . '</p>';
if (!empty($changelog)) {
echo '<p>' . esc_html($changelog) . '</p>';
}
echo '<p>';
if (!empty($download_url)) {
echo '<a href="' . esc_url($download_url) . '" class="button button-primary" target="_blank">'
. esc_html__('下载更新', 'aksrt') . '</a> ';
}
echo '<a href="' . esc_url($dismiss_url) . '" class="button">'
. esc_html__('忽略此版本', 'aksrt') . '</a>';
echo '</p>';
echo '</div>';
}
/**
* Handle dismiss action
*/
public static function handle_dismiss(): void {
if (!isset($_GET['aksrt_dismiss_update'])) return;
$version = sanitize_text_field($_GET['aksrt_dismiss_update']);
if (!wp_verify_nonce($_GET['_wp_nonce'] ?? '', 'aksrt_dismiss_update_' . $version)) return;
update_user_meta(get_current_user_id(), 'aksrt_dismissed_update', $version);
wp_safe_redirect(remove_query_arg(['aksrt_dismiss_update', '_wp_nonce']));
exit;
}
/**
* Get stored update info
*/
public function get_update_info(): array {
return get_option(self::OPTION_KEY, []);
}
}
+393
View File
@@ -0,0 +1,393 @@
<?php
/**
* Public widget list REST endpoints.
*
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class WidgetAjax {
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_action('rest_api_init', [$this, 'register_routes']);
}
public function register_routes(): void {
$routes = [
'/widgets/posts' => 'rest_main_posts',
'/widgets/tab-posts' => 'rest_tab_posts',
'/widgets/paid-posts' => 'rest_paid_posts',
'/widgets/category-tabs' => 'rest_category_tabs',
'/widgets/category-block' => 'rest_category_block',
'/widgets/category-recommend' => 'rest_category_recommend',
'/widgets/hot-posts' => 'rest_hot_posts',
'/widgets/oneline-posts' => 'rest_oneline_posts',
'/widgets/recent-comments' => 'rest_recent_comments',
'/widgets/user-lists' => 'rest_user_lists',
'/widgets/tag-cloud' => 'rest_tag_cloud',
'/widgets/categories' => 'rest_categories',
'/widgets/term-cards' => 'rest_term_cards',
'/widgets/term-aggregation' => 'rest_term_aggregation',
];
foreach ($routes as $path => $cb) {
register_rest_route('aksrt/v1', $path, [
'methods' => 'GET',
'callback' => [$this, $cb],
'permission_callback' => '__return_true',
]);
}
}
public function rest_main_posts(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetMainPosts')) {
return $this->fail();
}
$instance = [
'count' => max(1, min(30, (int) ($request->get_param('count') ?? 6))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC',
'cat' => max(0, (int) ($request->get_param('cat') ?? 0)),
'limit_day' => max(0, (int) ($request->get_param('limit_day') ?? 0)),
'topics' => sanitize_text_field((string) ($request->get_param('topics') ?? '')),
'pay_type' => sanitize_key((string) ($request->get_param('pay_type') ?? '')),
'style' => sanitize_key((string) ($request->get_param('style') ?? 'list')),
'show_thumb' => $request->get_param('show_thumb') === null ? 1 : (int) $request->get_param('show_thumb'),
'show_meta' => $request->get_param('show_meta') === null ? 1 : (int) $request->get_param('show_meta'),
'show_number' => (int) ($request->get_param('show_number') ?? 0),
'paginate' => in_array(sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), ['none','ajax','number'], true) ? sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')) : 'ajax',
];
$page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1));
return $this->payload(\AKSRT\Widgets\WidgetMainPosts::query_payload($instance, $page), $page);
}
public function rest_tab_posts(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\TabPostsWidget')) {
return $this->fail();
}
$default_count = max(1, min(20, (int) ($request->get_param('count') ?? 5)));
$style = sanitize_key((string) ($request->get_param('style') ?? 'mini'));
if (!in_array($style, ['list', 'card', 'mini'], true)) {
$style = 'mini';
}
$paginate = sanitize_key((string) ($request->get_param('paginate') ?? 'none'));
if (!in_array($paginate, ['none', 'ajax', 'number'], true)) {
$paginate = 'none';
}
$page = max(1, (int) ($request->get_param('page') ?? 1));
$tab = [
'title' => '',
'category_id' => max(0, (int) ($request->get_param('cat') ?? $request->get_param('category_id') ?? 0)),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC',
'count' => max(0, min(20, (int) ($request->get_param('tab_count') ?? 0))),
'enabled' => true,
'topics' => sanitize_text_field((string) ($request->get_param('topics') ?? '')),
'limit_day' => max(0, (int) ($request->get_param('limit_day') ?? 0)),
'pay_type' => sanitize_key((string) ($request->get_param('pay_type') ?? '')),];
$opts = [
'style' => $style,
'paginate' => $paginate,
'show_thumb' => $request->get_param('show_thumb') === null ? 1 : (int) $request->get_param('show_thumb'),
'show_meta' => $request->get_param('show_meta') === null ? 1 : (int) $request->get_param('show_meta'),
'show_number' => (int) ($request->get_param('show_number') ?? 0),
];
$payload = \AKSRT\Widgets\TabPostsWidget::render_tab_payload($tab, $default_count, $opts, $page);
$html = (string) ($payload['html'] ?? '');
if ($html === '') {
$html = function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无文章', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无文章', 'aksrt') . '</p>';
} else {
$html = '<div class="widget-tab-posts__list widget-tab-posts__list--' . esc_attr($style) . ' aksrt-main-posts style-' . esc_attr($style) . '">' . $html . '</div>';
}
return new \WP_REST_Response([
'success' => true,
'html' => $html,
'paginate_html' => (string) ($payload['paginate_html'] ?? ''),
'has_more' => !empty($payload['has_more']),
'page' => (int) ($payload['page'] ?? $page),
'max_pages' => (int) ($payload['max_pages'] ?? 1),
'total' => (int) ($payload['total'] ?? 0),
], 200);
}
public function rest_paid_posts(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetPaidPosts')) {
return $this->fail();
}
$paginate = sanitize_key((string) ($request->get_param('paginate') ?? 'ajax'));
if (!in_array($paginate, ['none', 'ajax', 'number'], true)) {
$paginate = 'ajax';
}
$instance = [
'count' => max(1, min(20, (int) ($request->get_param('count') ?? 6))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'cat' => max(0, (int) ($request->get_param('cat') ?? 0)),
'limit_day' => max(0, (int) ($request->get_param('limit_day') ?? 0)),
'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')),
'paginate' => $paginate,
];
$page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1));
return $this->payload(\AKSRT\Widgets\WidgetPaidPosts::query_payload($instance, $page), $page);
}
public function rest_category_tabs(\WP_REST_Request $request) {
$cat = max(0, (int) ($request->get_param('cat') ?? 0));
$count = max(1, min(12, (int) ($request->get_param('count') ?? 6)));
$layout = sanitize_key((string) ($request->get_param('layout') ?? 'card'));
$columns = max(1, min(4, (int) ($request->get_param('columns') ?? 3)));
$context = sanitize_key((string) ($request->get_param('context') ?? 'homepage'));
if ($cat <= 0) {
return new \WP_REST_Response(['success' => true, 'html' => ''], 200);
}
$post_types = function_exists('aksrt_home_widget_post_types') ? aksrt_home_widget_post_types() : ['post'];
$query = new \WP_Query([
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => $count,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'cat' => $cat,
]);
ob_start();
if (function_exists('aksrt_home_widget_render_post_grid')) {
aksrt_home_widget_render_post_grid($query, $layout, $columns, $context);
} elseif ($query->have_posts()) {
if (function_exists('aksrt_render_mini_list')) {
echo '<div class="widget-tab-posts__list widget-tab-posts__list--mini">';
aksrt_render_mini_list($query, ['context' => 'widget', 'show_thumb' => true]);
echo '</div>';
} else {
echo '<ul class="widget-tab-posts__list">';
while ($query->have_posts()) {
$query->the_post();
echo '<li><a href="' . esc_url(get_permalink()) . '">' . esc_html(get_the_title()) . '</a></li>';
}
echo '</ul>';
}
} else {
echo function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无文章', 'aksrt')) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
: '<p class="widget-empty ajax-item">' . esc_html__('暂无文章', 'aksrt') . '</p>';
}
wp_reset_postdata();
$html = (string) ob_get_clean();
if ($html === '') {
$html = function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无文章', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无文章', 'aksrt') . '</p>';
}
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_hot_posts(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\HotPostsWidget')) {
return $this->fail();
}
$paginate = sanitize_key((string) ($request->get_param('paginate') ?? 'ajax'));
if (!in_array($paginate, ['none', 'ajax', 'number'], true)) {
$paginate = 'ajax';
}
$instance = [
'count' => max(1, min(20, (int) ($request->get_param('count') ?? 5))),
'order_by' => sanitize_key((string) ($request->get_param('order_by') ?? 'views')),
'days' => max(0, (int) ($request->get_param('days') ?? 30)),
'show_thumb' => $request->get_param('show_thumb') === null ? 1 : (int) $request->get_param('show_thumb'),
'target_blank'=> (int) ($request->get_param('target_blank') ?? 0),
'paginate' => $paginate,
];
$page = max(1, (int) ($request->get_param('page') ?? 1));
return $this->payload(\AKSRT\Widgets\HotPostsWidget::query_payload($instance, $page), $page);
}
public function rest_oneline_posts(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetOnelinePosts')) {
return $this->fail();
}
$instance = [
'count' => max(1, min(20, (int) ($request->get_param('count') ?? 8))),
'style' => sanitize_key((string) ($request->get_param('style') ?? 'card')),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC',
'cat' => max(0, (int) ($request->get_param('cat') ?? 0)),
'limit_day' => max(0, (int) ($request->get_param('limit_day') ?? 0)),
'topics' => sanitize_text_field((string) ($request->get_param('topics') ?? '')),
'pay_type' => sanitize_key((string) ($request->get_param('pay_type') ?? '')),
];
$html = \AKSRT\Widgets\WidgetOnelinePosts::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_recent_comments(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\RecentCommentsWidget')) {
return $this->fail();
}
$instance = [
'count' => max(1, min(20, (int) ($request->get_param('count') ?? 5))),
'show_avatar' => $request->get_param('show_avatar') === null ? 1 : (int) $request->get_param('show_avatar'),
'outpost' => sanitize_text_field((string) ($request->get_param('outpost') ?? '')),
'outer' => sanitize_text_field((string) ($request->get_param('outer') ?? '')),
];
$html = \AKSRT\Widgets\RecentCommentsWidget::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_user_lists(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetUserLists')) {
return $this->fail();
}
$instance = [
'number' => max(1, min(50, (int) ($request->get_param('number') ?? 8))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'user_registered')),
'order' => sanitize_key((string) ($request->get_param('order') ?? 'DESC')),
'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')),
'exclude' => sanitize_text_field((string) ($request->get_param('exclude') ?? '')),
'hide_box' => (int) ($request->get_param('hide_box') ?? 0),
'blur_bg' => (int) ($request->get_param('blur_bg') ?? 0),
];
$html = \AKSRT\Widgets\WidgetUserLists::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_category_block(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\CategoryBlockWidget')) {
return $this->fail();
}
$instance = [
'category_id' => max(0, (int) ($request->get_param('cat') ?? $request->get_param('category_id') ?? 0)),
'count' => max(1, min(12, (int) ($request->get_param('count') ?? 6))),
'columns' => max(1, min(4, (int) ($request->get_param('columns') ?? 3))),
'layout' => sanitize_key((string) ($request->get_param('layout') ?? 'card')),
'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')),
];
$context = sanitize_key((string) ($request->get_param('context') ?? 'homepage'));
$page = max(1, (int) ($request->get_param('page') ?? 1));
return $this->payload(\AKSRT\Widgets\CategoryBlockWidget::query_payload($instance, $page, $context), $page);
}
public function rest_category_recommend(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\CategoryRecommendWidget')) {
return $this->fail();
}
$raw_ids = $request->get_param('category_ids');
if (is_string($raw_ids)) {
$raw_ids = array_filter(array_map('intval', preg_split('/[\s,]+/', $raw_ids) ?: []));
}
$instance = [
'category_ids' => is_array($raw_ids) ? $raw_ids : [],
'posts_per_category' => max(1, min(8, (int) ($request->get_param('posts_per_category') ?? 5))),
'columns' => max(1, min(4, (int) ($request->get_param('columns') ?? 2))),
'show_views' => (int) ($request->get_param('show_views') ?? 0),
'show_description' => $request->get_param('show_description') === null ? 1 : (int) $request->get_param('show_description'),
'show_dates' => $request->get_param('show_dates') === null ? 1 : (int) $request->get_param('show_dates'),
'show_more' => $request->get_param('show_more') === null ? 1 : (int) $request->get_param('show_more'),
];
$html = \AKSRT\Widgets\CategoryRecommendWidget::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_tag_cloud(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\TagCloudWidget')) {
return $this->fail();
}
$instance = [
'taxonomy' => sanitize_key((string) ($request->get_param('taxonomy') ?? 'post_tag')),
'count' => max(1, min(80, (int) ($request->get_param('count') ?? 15))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')),
'color' => sanitize_html_class((string) ($request->get_param('color') ?? 'rand')),
'show_count' => (int) ($request->get_param('show_count') ?? 0),
'blank' => (int) ($request->get_param('blank') ?? 0),
'fixed_width' => (int) ($request->get_param('fixed_width') ?? 0),
'hide_empty' => (int) ($request->get_param('hide_empty') ?? 0),
];
$html = \AKSRT\Widgets\TagCloudWidget::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_categories(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\CategoriesWidget')) {
return $this->fail();
}
$instance = [
'count' => max(1, min(50, (int) ($request->get_param('count') ?? 8))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')),
'order' => sanitize_key((string) ($request->get_param('order') ?? 'DESC')),
'parent' => max(0, (int) ($request->get_param('parent') ?? 0)),
'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')),
'exclude' => sanitize_text_field((string) ($request->get_param('exclude') ?? '')),
'show_count' => $request->get_param('show_count') === null ? 1 : (int) $request->get_param('show_count'),
'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'),
'hierarchical' => (int) ($request->get_param('hierarchical') ?? 0),
];
$html = \AKSRT\Widgets\CategoriesWidget::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_term_cards(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetTermCards')) {
return $this->fail();
}
$instance = [
'taxonomy' => sanitize_key((string) ($request->get_param('taxonomy') ?? 'category')),
'number' => max(1, min(30, (int) ($request->get_param('number') ?? 8))),
'pc_row' => max(1, min(12, (int) ($request->get_param('pc_row') ?? $request->get_param('columns') ?? 2))),
'm_row' => max(1, min(12, (int) ($request->get_param('m_row') ?? 1))),
'columns' => max(1, min(12, (int) ($request->get_param('columns') ?? $request->get_param('pc_row') ?? 2))),
'type' => sanitize_key((string) ($request->get_param('type') ?? 'style-1')),
'height_scale' => max(20, min(300, (int) ($request->get_param('height_scale') ?? 60))),
'mask_opacity' => max(0, min(90, (int) ($request->get_param('mask_opacity') ?? 10))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')),
'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')),
'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'),
'target_blank' => (int) ($request->get_param('target_blank') ?? 0),
];
$html = \AKSRT\Widgets\WidgetTermCards::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_term_aggregation(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetTermAggregation')) {
return $this->fail();
}
$instance = [
'taxonomy' => sanitize_key((string) ($request->get_param('taxonomy') ?? 'category')),
'term_ids' => sanitize_text_field((string) ($request->get_param('term_ids') ?? '')),
'count' => max(1, min(20, (int) ($request->get_param('count') ?? 4))),
'columns' => max(1, min(3, (int) ($request->get_param('columns') ?? 1))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'order' => strtoupper(sanitize_key((string) ($request->get_param('order') ?? 'desc'))) === 'ASC' ? 'ASC' : 'DESC',
'auto_terms' => max(1, min(4, (int) ($request->get_param('auto_terms') ?? 2))),
'target_blank' => (int) ($request->get_param('target_blank') ?? 0),
];
$html = \AKSRT\Widgets\WidgetTermAggregation::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
private function fail(): \WP_REST_Response {
return new \WP_REST_Response(['success' => false, 'message' => 'unavailable'], 500);
}
private function payload(array $payload, int $page): \WP_REST_Response {
return new \WP_REST_Response([
'success' => true,
'html' => (string) ($payload['html'] ?? ''),
'paginate_html' => (string) ($payload['paginate_html'] ?? ''),
'has_more' => !empty($payload['has_more']),
'page' => (int) ($payload['page'] ?? $page),
'max_pages' => (int) ($payload['max_pages'] ?? 1),
'total' => (int) ($payload['total'] ?? 0),
], 200);
}
}