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:
1 parent
56f1a8175f
commit
8c5b1555c9
285 files changed
+26
-3600
No files matched your search
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
<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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册侧边栏
|
||||
*/
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, []);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user