refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories (core, auth, user, credits, content, payment/gateways, forum, shop, sms, ai, messages, extensions, widgets, settings, admin). Update bootstrap require paths, co-locate traits with owners, fix forum/user relative requires, drop duplicate shop controller requires, and wire home-modules load. Path-only; class names and init order unchanged.
This commit is contained in:
1 parent
2a8fe68048
commit
ce1c13b947
115 files changed
+171
-224
No files matched your search
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Category Rewrite — Remove /category/ from URLs
|
||||
*
|
||||
* Rewrites category URLs from /category/slug/ to /slug/.
|
||||
* Adds 301 redirect for old /category/ URLs.
|
||||
*
|
||||
* WARNING: May conflict with other "no-category" plugins.
|
||||
* Enable via Settings → SEO → Remove category base.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Category_Rewrite {
|
||||
|
||||
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')) return false;
|
||||
// schema key = seo_no_category; runtime alias = seo_remove_category
|
||||
return (bool) (
|
||||
AKSRT_Settings::get_value('seo_remove_category', false)
|
||||
?: AKSRT_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,543 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Comment Actions
|
||||
*
|
||||
* REST endpoints for comment edit / delete / moderate.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Comment_Actions {
|
||||
|
||||
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'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
}
|
||||
|
||||
$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' => '',
|
||||
'comment_author_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') ? AKSRT_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,52 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT DB Migrator
|
||||
*
|
||||
* Maintains runtime tables (credits / download tokens / shop / forum support, etc.).
|
||||
* Schema only via dbDelta — no option/data migration, no legacy option dual-write.
|
||||
*
|
||||
* Trigger:
|
||||
* 1. after_switch_theme
|
||||
* 2. admin_init when aksrt_db_schema_version !== SCHEMA_VERSION
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_DB_Migrator {
|
||||
use AKSRT_DB_Migrator_Part_2;
|
||||
use AKSRT_DB_Migrator_Part_3;
|
||||
|
||||
/** Current schema version; changing it re-runs dbDelta. */
|
||||
const SCHEMA_VERSION = '1.15.0';
|
||||
|
||||
const OPT_KEY = 'aksrt_db_schema_version';
|
||||
|
||||
/**
|
||||
* Historical flag still readable by older code paths.
|
||||
* Plate CPT is now native when registered; prefer post_type_exists().
|
||||
*/
|
||||
const OPT_PLATE_CPT_MIGRATED = 'aksrt_plate_cpt_migrated';
|
||||
|
||||
/**
|
||||
* 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,219 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Docs Presenter
|
||||
*
|
||||
* Builds document tree structures, breadcrumb chains,
|
||||
* and chapter navigation for the docs mode.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Docs_Presenter {
|
||||
|
||||
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,391 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Front Submit — 前台投稿系统骨架
|
||||
*
|
||||
* 提供前台投稿、草稿保存、编辑回退能力。
|
||||
* 当前为骨架实现,后续 Phase 补充完整投稿产品线。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Front_Submit {
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否可以投稿
|
||||
*
|
||||
* @param int|null $user_id User ID (null = current user).
|
||||
* @return bool
|
||||
*/
|
||||
public function can_submit(?int $user_id = null): bool {
|
||||
if ($user_id === null) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
if ($user_id <= 0) return false;
|
||||
|
||||
// Read submission settings
|
||||
$submit_settings = [];
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$submit_settings = AKSRT_Settings::get_value('post_list.submit', []);
|
||||
}
|
||||
|
||||
// Check if front submission is enabled (fix: was checking !empty && empty which is always false)
|
||||
if (isset($submit_settings['submit_enabled']) && !$submit_settings['submit_enabled']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Role whitelist: if configured, user must have one of the allowed roles
|
||||
$allowed_roles = $submit_settings['submit_roles'] ?? [];
|
||||
if (!empty($allowed_roles)) {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) return false;
|
||||
$user_roles = $user->roles;
|
||||
if (empty(array_intersect($user_roles, (array) $allowed_roles))) {
|
||||
// Also allow administrators
|
||||
if (!in_array('administrator', $user_roles, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Credits threshold: if configured, user must have enough credits
|
||||
$min_credits = (int) ($submit_settings['submit_min_credits'] ?? 0);
|
||||
if ($min_credits > 0 && class_exists('AKSRT_Credits')) {
|
||||
$balance = AKSRT_Credits::balance($user_id);
|
||||
if ($balance < $min_credits) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: allow users with edit_posts capability (or any logged-in user if submission is enabled)
|
||||
return user_can($user_id, 'edit_posts') || !empty($submit_settings['submit_enabled']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交文章
|
||||
*
|
||||
* @param array $data 投稿数据
|
||||
* @return array{success: bool, post_id?: int, message: string}
|
||||
*/
|
||||
public function submit(array $data): array {
|
||||
$user_id = get_current_user_id();
|
||||
if (!$this->can_submit($user_id)) {
|
||||
return ['success' => false, 'message' => __('没有投稿权限', 'aksrt')];
|
||||
}
|
||||
|
||||
$title = sanitize_text_field((string) ($data['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($data['content'] ?? ''));
|
||||
$category = (int) ($data['category'] ?? 0);
|
||||
$tags = sanitize_text_field((string) ($data['tags'] ?? ''));
|
||||
$status = sanitize_key((string) ($data['status'] ?? 'pending'));
|
||||
|
||||
if ($title === '') {
|
||||
return ['success' => false, 'message' => __('标题不能为空', 'aksrt')];
|
||||
}
|
||||
|
||||
$valid_statuses = ['pending', 'draft', 'publish'];
|
||||
if (!in_array($status, $valid_statuses, true)) {
|
||||
$status = 'pending';
|
||||
}
|
||||
|
||||
// Determine review policy: if review is not required and user can publish, auto-publish
|
||||
if ($status === 'pending') {
|
||||
$submit_settings = [];
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$submit_settings = AKSRT_Settings::get_value('post_list.submit', []);
|
||||
}
|
||||
$review_required = $submit_settings['review_required'] ?? true;
|
||||
$skip_moderation = false;
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$all = AKSRT_Settings::get_all();
|
||||
$skip_moderation = !empty($all['skip_submit_moderation']);
|
||||
}
|
||||
|
||||
if (!$review_required || $skip_moderation) {
|
||||
// Auto-publish if user has publish_posts capability
|
||||
if (user_can($user_id, 'publish_posts')) {
|
||||
$status = 'publish';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$post_data = [
|
||||
'post_title' => $title,
|
||||
'post_content' => $content,
|
||||
'post_status' => $status,
|
||||
'post_author' => $user_id,
|
||||
'post_type' => 'post',
|
||||
];
|
||||
|
||||
$post_id = wp_insert_post($post_data, true);
|
||||
|
||||
if (is_wp_error($post_id)) {
|
||||
return ['success' => false, 'message' => $post_id->get_error_message()];
|
||||
}
|
||||
|
||||
// 设置分类
|
||||
if ($category > 0) {
|
||||
wp_set_post_categories($post_id, [$category]);
|
||||
}
|
||||
|
||||
// 设置标签
|
||||
if ($tags !== '') {
|
||||
$tag_names = array_map('trim', explode(',', $tags));
|
||||
wp_set_post_tags($post_id, $tag_names);
|
||||
}
|
||||
|
||||
// 设置封面图
|
||||
$thumbnail_url = sanitize_url((string) ($data['thumbnail'] ?? ''));
|
||||
if ($thumbnail_url !== '') {
|
||||
$this->set_thumbnail_from_url($post_id, $thumbnail_url);
|
||||
}
|
||||
|
||||
// 设置专题 (topics taxonomy)
|
||||
$this->apply_topics_and_filters($post_id, $data);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'post_id' => $post_id,
|
||||
'message' => $status === 'draft' ? __('草稿已保存', 'aksrt') : __('投稿已提交,等待审核', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文章
|
||||
*/
|
||||
public function update(int $post_id, array $data): array {
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
|
||||
}
|
||||
|
||||
$post = get_post($post_id);
|
||||
if (!$post || (int) $post->post_author !== $user_id) {
|
||||
return ['success' => false, 'message' => __('没有编辑权限', 'aksrt')];
|
||||
}
|
||||
|
||||
$update_data = ['ID' => $post_id];
|
||||
|
||||
if (isset($data['title'])) {
|
||||
$update_data['post_title'] = sanitize_text_field($data['title']);
|
||||
}
|
||||
if (isset($data['content'])) {
|
||||
$update_data['post_content'] = wp_kses_post($data['content']);
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$update_data['post_status'] = sanitize_key($data['status']);
|
||||
}
|
||||
|
||||
$result = wp_update_post($update_data, true);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return ['success' => false, 'message' => $result->get_error_message()];
|
||||
}
|
||||
|
||||
// Update category
|
||||
$category = (int) ($data['category'] ?? 0);
|
||||
if ($category > 0) {
|
||||
wp_set_post_categories($post_id, [$category]);
|
||||
}
|
||||
|
||||
// Update tags
|
||||
if (isset($data['tags'])) {
|
||||
$tags = sanitize_text_field((string) $data['tags']);
|
||||
if ($tags !== '') {
|
||||
$tag_names = array_map('trim', explode(',', $tags));
|
||||
wp_set_post_tags($post_id, $tag_names);
|
||||
} else {
|
||||
wp_set_post_tags($post_id, []);
|
||||
}
|
||||
}
|
||||
|
||||
// Update thumbnail
|
||||
if (isset($data['thumbnail'])) {
|
||||
$thumbnail_url = sanitize_url((string) $data['thumbnail']);
|
||||
if ($thumbnail_url !== '') {
|
||||
$this->set_thumbnail_from_url($post_id, $thumbnail_url);
|
||||
}
|
||||
}
|
||||
|
||||
// Update topics + custom filter meta
|
||||
$this->apply_topics_and_filters($post_id, $data);
|
||||
|
||||
return ['success' => true, 'post_id' => $post_id, 'message' => __('已更新', 'aksrt')];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 设置封面图
|
||||
*
|
||||
* Downloads the image from the given URL, sideloads it into the media library,
|
||||
* and sets it as the post's featured image.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $url Image URL.
|
||||
*/
|
||||
private function set_thumbnail_from_url(int $post_id, string $url): void {
|
||||
if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Require media handling functions
|
||||
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';
|
||||
}
|
||||
|
||||
// Check if URL is an image
|
||||
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION));
|
||||
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
||||
if ($ext !== '' && !in_array($ext, $allowed_exts, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sideload the image
|
||||
$attachment_id = media_sideload_image($url, $post_id, '', 'id');
|
||||
|
||||
if (!is_wp_error($attachment_id)) {
|
||||
set_post_thumbnail($post_id, $attachment_id);
|
||||
} elseif (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log(sprintf('[AKSRT Front Submit] Failed to set thumbnail for post %d: %s — %s', $post_id, $url, $attachment_id->get_error_message()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 提交投稿
|
||||
*/
|
||||
public function ajax_submit_post(): void {
|
||||
check_ajax_referer('aksrt_submit', '_nonce');
|
||||
|
||||
$data = $_POST;
|
||||
$result = $this->submit($data);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result);
|
||||
}
|
||||
|
||||
wp_send_json_error($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 保存草稿
|
||||
*/
|
||||
public function ajax_save_draft(): void {
|
||||
check_ajax_referer('aksrt_submit', '_nonce');
|
||||
|
||||
$_POST['status'] = 'draft';
|
||||
$data = $_POST;
|
||||
$result = $this->submit($data);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result);
|
||||
}
|
||||
|
||||
wp_send_json_error($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 更新文章
|
||||
*/
|
||||
public function ajax_update_post(): void {
|
||||
check_ajax_referer('aksrt_submit', '_nonce');
|
||||
|
||||
$post_id = (int) ($_POST['post_id'] ?? 0);
|
||||
if ($post_id <= 0) {
|
||||
wp_send_json_error(['message' => __('文章 ID 无效', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = $this->update($post_id, $_POST);
|
||||
|
||||
if ($result['success']) {
|
||||
wp_send_json_success($result);
|
||||
}
|
||||
|
||||
wp_send_json_error($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: delete draft
|
||||
*/
|
||||
public function ajax_delete_draft(): void {
|
||||
check_ajax_referer('aksrt_submit', '_nonce');
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
// Fix: JS sends 'draft_id', not 'post_id'
|
||||
$post_id = (int) (($_POST['draft_id'] ?? $_POST['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') {
|
||||
wp_send_json_error(['message' => __('只能删除草稿', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = wp_delete_post($post_id, true);
|
||||
if ($result) {
|
||||
wp_send_json_success(['message' => __('草稿已删除', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('删除失败', 'aksrt')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply topics taxonomy terms and custom filter meta values to a post.
|
||||
*
|
||||
* Reads 'topics' array and 'cf_{dim_key}' scalar values from $data.
|
||||
* Custom filter keys are validated against the configured dimension definitions.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $data Submission data ($_POST).
|
||||
*/
|
||||
private function apply_topics_and_filters(int $post_id, array $data): void {
|
||||
// Topics taxonomy
|
||||
if (isset($data['topics']) && is_array($data['topics']) && taxonomy_exists('topics')) {
|
||||
$topic_ids = array_map('absint', $data['topics']);
|
||||
$topic_ids = array_filter($topic_ids);
|
||||
wp_set_object_terms($post_id, array_values($topic_ids), 'topics');
|
||||
}
|
||||
|
||||
// Custom filter meta values (_aksrt_cf_{dim_key})
|
||||
foreach ($data as $key => $val) {
|
||||
if (strpos($key, 'cf_') !== 0) {
|
||||
continue;
|
||||
}
|
||||
$dim_key = sanitize_key(substr($key, 3));
|
||||
if ($dim_key === '') {
|
||||
continue;
|
||||
}
|
||||
$cf_val = sanitize_key((string) $val);
|
||||
$meta_key = '_aksrt_cf_' . $dim_key;
|
||||
if ($cf_val !== '') {
|
||||
update_post_meta($post_id, $meta_key, $cf_val);
|
||||
} else {
|
||||
delete_post_meta($post_id, $meta_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Go Redirect — External link redirect with auth confirmation
|
||||
*
|
||||
* Replaces external links in post content with /go/?url=... redirect links.
|
||||
* Shows a confirmation page before redirecting (anti-phishing).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Go_Redirect {
|
||||
|
||||
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')) return false;
|
||||
// schema key = seo_go_link; runtime alias = go_redirect_enabled
|
||||
return (bool) (
|
||||
AKSRT_Settings::get_value('go_redirect_enabled', false)
|
||||
?: AKSRT_Settings::get_value('seo_go_link', false)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get excluded domains list
|
||||
*/
|
||||
private function get_excluded_domains(): array {
|
||||
if (!class_exists('AKSRT_Settings')) return [];
|
||||
// schema key = seo_go_link_exclude; runtime alias = go_redirect_exclude_domains
|
||||
$domains = AKSRT_Settings::get_value('go_redirect_exclude_domains', '')
|
||||
?: AKSRT_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')
|
||||
? !empty(AKSRT_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') && empty(AKSRT_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,233 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Home Modules
|
||||
*
|
||||
* CMS 形态首页模块系统:
|
||||
* - 注册可用模块清单(9 类)
|
||||
* - 渲染调度:按用户编排顺序依次 get_template_part
|
||||
* - 预览会话:?aksrt_preview={id} → 读 transient 中的草稿模块
|
||||
*
|
||||
* 模块类型:
|
||||
* hero / banner / category / tabs / ranking
|
||||
* recent / tag_cloud / friend_links / html
|
||||
*
|
||||
* 默认模板:3 套预设(resource_standard / news / mixed)。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Home_Modules {
|
||||
|
||||
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')) {
|
||||
$modules = AKSRT_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,866 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Post Meta Box
|
||||
*
|
||||
* 为文章(post)添加局部配置字段:
|
||||
* - 附加封面图组(用于列表多图模式)
|
||||
* - 自定义摘要
|
||||
* - 列表布局覆盖
|
||||
* - 文章头图模式
|
||||
* - 目录树开关
|
||||
* - 版权声明覆盖
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Post_Meta_Box {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const META_KEY = AKSRT_Domain_Model::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 = AKSRT_Domain_Model::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')
|
||||
? AKSRT_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')
|
||||
? AKSRT_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')
|
||||
? AKSRT_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_Domain_Model')) {
|
||||
echo '—';
|
||||
return;
|
||||
}
|
||||
$pay = aksrt_get_pay_meta($post_id);
|
||||
$pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($pay['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
|
||||
echo '<span class="description">' . esc_html__('免费', 'aksrt') . '</span>';
|
||||
return;
|
||||
}
|
||||
$labels = AKSRT_Domain_Model::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, AKSRT_Domain_Model::RESOURCE_SALES_VOLUME_KEY, true);
|
||||
$db_sales = 0;
|
||||
$revenue = 0.0;
|
||||
if (class_exists('AKSRT_Order_Repository')) {
|
||||
$map = AKSRT_Order_Repository::get_instance()->get_content_sales_map(
|
||||
[$post_id],
|
||||
AKSRT_Domain_Model::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 === AKSRT_Domain_Model::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_Domain_Model')) {
|
||||
return;
|
||||
}
|
||||
$current = isset($_GET['aksrt_pay_type']) ? sanitize_key((string) wp_unslash($_GET['aksrt_pay_type'])) : '';
|
||||
$labels = AKSRT_Domain_Model::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_Domain_Model')) {
|
||||
return;
|
||||
}
|
||||
$pay_type = AKSRT_Domain_Model::normalize_pay_type($pay_type);
|
||||
$meta_query = (array) $query->get('meta_query');
|
||||
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
|
||||
$meta_query[] = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
|
||||
'value' => AKSRT_Domain_Model::PAY_TYPE_NO,
|
||||
'compare' => '=',
|
||||
],
|
||||
[
|
||||
'key' => AKSRT_Domain_Model::PAY_TYPE_KEY,
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$meta_query[] = [
|
||||
'key' => AKSRT_Domain_Model::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_Domain_Model') ? AKSRT_Domain_Model::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_Domain_Model') || !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'] = AKSRT_Domain_Model::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 = AKSRT_Domain_Model::sanitize_pay_meta($meta);
|
||||
update_post_meta($post_id, AKSRT_Domain_Model::PAY_META_KEY, $meta);
|
||||
update_post_meta($post_id, AKSRT_Domain_Model::PAY_TYPE_KEY, (string) ($meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
|
||||
update_post_meta($post_id, AKSRT_Domain_Model::PAY_PRICE_KEY, (float) ($meta['price'] ?? 0));
|
||||
if (class_exists('AKSRT_Domain_Model') && method_exists('AKSRT_Domain_Model', 'is_resource_pay_type')) {
|
||||
update_post_meta(
|
||||
$post_id,
|
||||
AKSRT_Domain_Model::RESOURCE_MARKER_KEY,
|
||||
AKSRT_Domain_Model::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,563 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Pay Meta Box — 统一付费内容设置
|
||||
*
|
||||
* 模仿 zibll 的 posts_zibpay 机制:
|
||||
* - 任何 post 都可以通过设置付费类型成为付费文章
|
||||
* - pay_type 驱动行为(reading/download/gallery/video/vip/no)
|
||||
* - 统一存储为 _aksrt_pay 序列化数组
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Resource_Meta_Box {
|
||||
|
||||
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)
|
||||
: AKSRT_Domain_Model::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_Setup') && method_exists('AKSRT_Setup', 'sanitize_downloads_meta')) {
|
||||
$downloads = AKSRT_Setup::sanitize_downloads_meta($downloads);
|
||||
}
|
||||
|
||||
// 保存统一元数据
|
||||
$pay_meta = AKSRT_Domain_Model::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, AKSRT_Domain_Model::PAY_META_KEY, $pay_meta);
|
||||
|
||||
// 同步标量字段用于查询(模仿 zibll 的 zibpay_type 同步)
|
||||
update_post_meta($post_id, AKSRT_Domain_Model::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, AKSRT_Domain_Model::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,
|
||||
AKSRT_Domain_Model::RESOURCE_MARKER_KEY,
|
||||
AKSRT_Domain_Model::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,68 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Scripts & Styles
|
||||
*
|
||||
* 前端资源加载:CSS、JS、内联样式变量
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Scripts {
|
||||
use AKSRT_Scripts_Part_1b;
|
||||
use AKSRT_Scripts_Part_2;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return AKSRT_Scripts::get_instance();
|
||||
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT SEO Head — 单一 head 输出装配器
|
||||
*
|
||||
* Phase 7 收口:站点 SEO、单篇 SEO、OpenGraph/Twitter、JSON-LD、canonical、
|
||||
* favicon、robots 全部从这里一次性装配输出,替代此前散落在
|
||||
* inc/helpers/aksrt-template-tags.php 的多个 wp_head 闭包。
|
||||
*
|
||||
* 所有取值只经过 AKSRT_Settings + 模板 helper,不直接 raw get_option()。
|
||||
* 唯一 head 来源,便于追踪。详见 seo-output-matrix.md。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_SEO_Head {
|
||||
|
||||
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')) {
|
||||
$seo = AKSRT_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')) {
|
||||
$s = AKSRT_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')) {
|
||||
return (string) AKSRT_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,281 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT SEO Meta Box — 文章/页面独立 SEO + AI 生成按钮
|
||||
*
|
||||
* Meta keys 与 AKSRT_SEO_Head 读取一致:
|
||||
* _aksrt_seo_title / _aksrt_seo_keywords / _aksrt_seo_description
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_SEO_Meta_Box {
|
||||
|
||||
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') ? AKSRT_AI::get_instance()->get_status() : [];
|
||||
$seo = class_exists('AKSRT_Settings') ? AKSRT_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') ? AKSRT_Settings::get_seo_settings() : [];
|
||||
if (empty($seo['seo_enabled']) || empty($seo['seo_ai_enabled'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$available = class_exists('AKSRT_AI') && AKSRT_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') && AKSRT_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,397 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Theme Setup
|
||||
*
|
||||
* 主题初始化:注册菜单、侧边栏、图片尺寸、自定义文章类型等
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Setup {
|
||||
use AKSRT_Setup_Part_1;
|
||||
use AKSRT_Setup_Part_2;
|
||||
|
||||
|
||||
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('init', [$this, 'register_shop_rewrite_rules'], 20);
|
||||
add_filter('query_vars', [$this, 'register_shop_query_vars']);
|
||||
add_action('template_redirect', [$this, 'shop_template_redirect']);
|
||||
// Shop fulfillment hook (§6.5 R68)
|
||||
if (class_exists('AKSRT_Shop')) {
|
||||
add_action('aksrt_order_paid', [AKSRT_Shop::get_instance(), 'handle_order_paid'], 10, 2);
|
||||
}
|
||||
// Auto-confirm receipt cron (§6.5 R68)
|
||||
if (!wp_next_scheduled('aksrt_auto_confirm_receipt')) {
|
||||
wp_schedule_event(time(), 'hourly', 'aksrt_auto_confirm_receipt');
|
||||
}
|
||||
if (class_exists('AKSRT_Shop')) {
|
||||
add_action('aksrt_auto_confirm_receipt', [AKSRT_Shop::get_instance(), 'run_auto_confirm_cron']);
|
||||
}
|
||||
// 文章浏览量统计(单次访问)
|
||||
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 {
|
||||
// Shop rewrite settings (safe fallback if settings not yet loaded)
|
||||
$shop_rewrite = [];
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$shop = AKSRT_Settings::get_shop_settings();
|
||||
$shop_rewrite = $shop['rewrite'] ?? [];
|
||||
}
|
||||
$product_slug = sanitize_key((string) ($shop_rewrite['product_slug'] ?? 'product'));
|
||||
$suffix_html = !empty($shop_rewrite['suffix_html']);
|
||||
|
||||
// Shop admin visibility: align with zibll native sidebar when shop is enabled.
|
||||
$shop_enabled = true;
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$shop_settings = AKSRT_Settings::get_shop_settings();
|
||||
$shop_enabled = !empty($shop_settings['enabled']);
|
||||
}
|
||||
// Always register UI in admin so settings/tools remain reachable; hide menu when disabled.
|
||||
$shop_show_ui = true;
|
||||
$shop_show_menu = is_admin() ? $shop_enabled : false;
|
||||
|
||||
// 商品 CPT — native WP sidebar (zibll: shop_product @ pos 35)
|
||||
register_post_type('aksrt_product', [
|
||||
'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'),
|
||||
'all_items' => __('所有商品', 'aksrt'),
|
||||
'menu_name' => __('商城', 'aksrt'),
|
||||
],
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => $shop_show_ui,
|
||||
'show_in_menu' => $shop_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => 35,
|
||||
'menu_icon' => 'dashicons-store',
|
||||
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'author', 'revisions'],
|
||||
'rewrite' => ['slug' => $product_slug, 'with_front' => false],
|
||||
'capability_type' => 'post',
|
||||
'map_meta_cap' => true,
|
||||
]);
|
||||
|
||||
// 商品分类 taxonomy(独立于文章 category)
|
||||
register_taxonomy('aksrt_product_cat', 'aksrt_product', [
|
||||
'labels' => [
|
||||
'name' => __('商品分类', 'aksrt'),
|
||||
'singular_name' => __('商品分类', 'aksrt'),
|
||||
'search_items' => __('搜索商品分类', 'aksrt'),
|
||||
'all_items' => __('所有商品分类', 'aksrt'),
|
||||
'parent_item' => __('父级分类', 'aksrt'),
|
||||
'edit_item' => __('编辑分类', 'aksrt'),
|
||||
'add_new_item' => __('新增分类', 'aksrt'),
|
||||
'menu_name' => __('商品分类', 'aksrt'),
|
||||
],
|
||||
'public' => true,
|
||||
'hierarchical' => true,
|
||||
'show_ui' => $shop_show_ui,
|
||||
'show_in_menu' => $shop_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => ['slug' => 'product-cat', 'with_front' => false],
|
||||
]);
|
||||
|
||||
// 商品标签 taxonomy(独立于文章 post_tag)
|
||||
register_taxonomy('aksrt_product_tag', 'aksrt_product', [
|
||||
'labels' => [
|
||||
'name' => __('商品标签', 'aksrt'),
|
||||
'singular_name' => __('商品标签', 'aksrt'),
|
||||
'search_items' => __('搜索商品标签', 'aksrt'),
|
||||
'all_items' => __('所有商品标签', 'aksrt'),
|
||||
'edit_item' => __('编辑标签', 'aksrt'),
|
||||
'add_new_item' => __('新增标签', 'aksrt'),
|
||||
'menu_name' => __('特色标签', 'aksrt'),
|
||||
],
|
||||
'public' => true,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => $shop_show_ui,
|
||||
'show_in_menu' => $shop_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => ['slug' => 'product-tag', 'with_front' => false],
|
||||
]);
|
||||
|
||||
// 项目展示
|
||||
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', AKSRT_Domain_Model::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', AKSRT_Domain_Model::PAY_META_KEY, [
|
||||
'type' => 'object',
|
||||
'description' => 'Canonical paid content payload.',
|
||||
'single' => true,
|
||||
'default' => AKSRT_Domain_Model::default_pay_meta(),
|
||||
'show_in_rest' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'pay_type' => [
|
||||
'type' => 'string',
|
||||
'enum' => array_keys(AKSRT_Domain_Model::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' => [AKSRT_Domain_Model::class, 'sanitize_pay_meta'],
|
||||
'auth_callback' => $post_edit_auth,
|
||||
]);
|
||||
|
||||
register_post_meta('post', AKSRT_Domain_Model::PAY_TYPE_KEY, [
|
||||
'type' => 'string',
|
||||
'description' => 'Query-friendly paid content type.',
|
||||
'single' => true,
|
||||
'default' => AKSRT_Domain_Model::PAY_TYPE_NO,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => [AKSRT_Domain_Model::class, 'normalize_pay_type'],
|
||||
'auth_callback' => $post_edit_auth,
|
||||
]);
|
||||
|
||||
register_post_meta('post', AKSRT_Domain_Model::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 ([
|
||||
AKSRT_Domain_Model::RESOURCE_DOWNLOAD_COUNT_KEY => 'Resource download count',
|
||||
AKSRT_Domain_Model::RESOURCE_VIEW_COUNT_KEY => 'Resource view count',
|
||||
AKSRT_Domain_Model::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 AKSRT_Domain_Model::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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册侧边栏
|
||||
*/
|
||||
}
|
||||
|
||||
return AKSRT_Setup::get_instance();
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT 访问量统计系统
|
||||
*
|
||||
* 追踪每日访客数和浏览量,为网站统计小工具提供数据支持。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Stats_Tracker {
|
||||
|
||||
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,144 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Page Templates
|
||||
*
|
||||
* @package AKSRT
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_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('template_include', [$this, 'load_taxonomy_template']);
|
||||
add_filter('page_template_hierarchy', [$this, 'template_hierarchy']);
|
||||
add_filter('aksrt_page_template_hierarchy', [$this, 'template_hierarchy']);
|
||||
|
||||
// 短代码注册统一在 AKSRT_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-categories.php' => __('AKSRT - 分类汇总', 'aksrt'),
|
||||
'templates/pages/page-newposts.php' => __('AKSRT - 最新文章', 'aksrt'),
|
||||
'templates/pages/page-posts-navs.php' => __('AKSRT - 文章导航', 'aksrt'),
|
||||
'templates/pages/page-friends.php' => __('AKSRT - 友情链接', 'aksrt'),
|
||||
'templates/pages/page-orders.php' => __('AKSRT - 订单中心', 'aksrt'),
|
||||
'templates/pages/page-revenue.php' => __('AKSRT - 收益中心', 'aksrt'),
|
||||
'templates/pages/page-cart.php' => __('AKSRT - 购物车', 'aksrt'),
|
||||
'templates/pages/page-messages.php' => __('AKSRT - 消息中心', 'aksrt'),
|
||||
'templates/pages/page-product.php' => __('AKSRT - 商品详情', 'aksrt'),
|
||||
'templates/pages/page-product-edit.php' => __('AKSRT - 商品编辑', 'aksrt'),
|
||||
'templates/pages/page-forums.php' => __('AKSRT - 论坛', 'aksrt'),
|
||||
'templates/forum/forum-compose.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 product taxonomy templates.
|
||||
*/
|
||||
public function load_taxonomy_template(string $template): string {
|
||||
if (is_tax('aksrt_product_cat')) {
|
||||
$cat_template = AKSRT_DIR . '/templates/shop/product-cat.php';
|
||||
if (file_exists($cat_template)) {
|
||||
return $cat_template;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_tax('aksrt_product_tag')) {
|
||||
$tag_template = AKSRT_DIR . '/templates/shop/product-tag.php';
|
||||
if (file_exists($tag_template)) {
|
||||
return $tag_template;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
return AKSRT_Templates::get_instance();
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Term Meta
|
||||
*
|
||||
* 为分类(category)和标签(post_tag)添加局部配置字段:
|
||||
* - 头图/封面 URL
|
||||
* - 自定义简介
|
||||
* - 列表布局模式覆盖
|
||||
* - 侧栏开关
|
||||
* - 推荐位开关
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Term_Meta {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** Supported taxonomies */
|
||||
private $taxonomies = ['category', 'post_tag'];
|
||||
|
||||
/** Meta key for storing term-level settings */
|
||||
const META_KEY = AKSRT_Domain_Model::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 = AKSRT_Domain_Model::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')
|
||||
? AKSRT_Settings::get_tag_page_settings()
|
||||
: (class_exists('AKSRT_Settings') ? AKSRT_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')) {
|
||||
$cover_url = AKSRT_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,182 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Update Checker — 版本检测与更新提示
|
||||
*
|
||||
* Checks for new theme versions from a remote endpoint.
|
||||
* Shows admin notice when update is available.
|
||||
* Does NOT implement license verification or anti-piracy (per §0.1).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Update_Checker {
|
||||
|
||||
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')) {
|
||||
$endpoint = AKSRT_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, []);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle dismiss on admin_init
|
||||
add_action('admin_init', function () {
|
||||
if (isset($_GET['aksrt_dismiss_update'])) {
|
||||
AKSRT_Update_Checker::handle_dismiss();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,463 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_DB_Migrator_Part_2 {
|
||||
|
||||
|
||||
/**
|
||||
* 立即运行(供激活钩子直接调用)。
|
||||
*/
|
||||
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);
|
||||
|
||||
// ---------- 13. 商品变体/SKU ----------
|
||||
$sql_variant = "CREATE TABLE {$prefix}aksrt_product_variant (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
product_id BIGINT UNSIGNED NOT NULL,
|
||||
sku VARCHAR(64) NOT NULL DEFAULT '',
|
||||
attributes TEXT DEFAULT NULL,
|
||||
price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
stock INT NOT NULL DEFAULT 0,
|
||||
image VARCHAR(512) NOT NULL DEFAULT '',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_product (product_id),
|
||||
KEY idx_sku (sku),
|
||||
KEY idx_sort (sort_order)
|
||||
) {$charset_collate};";
|
||||
dbDelta($sql_variant);
|
||||
|
||||
// ── 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);
|
||||
|
||||
// ── after_sale ──
|
||||
$sql_after_sale = "CREATE TABLE {$prefix}aksrt_after_sale (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_number VARCHAR(32) NOT NULL DEFAULT '',
|
||||
user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'refund',
|
||||
reason TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
note TEXT DEFAULT NULL,
|
||||
operator_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
operator_note TEXT DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
updated_at DATETIME DEFAULT NULL,
|
||||
resolved_at DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_order (order_number),
|
||||
KEY idx_user (user_id),
|
||||
KEY idx_status (status),
|
||||
KEY idx_type (type),
|
||||
KEY idx_created (created_at)
|
||||
) {$charset_collate};";
|
||||
dbDelta($sql_after_sale);
|
||||
|
||||
// ── order_shipping ──
|
||||
$sql_shipping = "CREATE TABLE {$prefix}aksrt_order_shipping (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
order_number VARCHAR(32) NOT NULL DEFAULT '',
|
||||
carrier VARCHAR(60) NOT NULL DEFAULT '',
|
||||
tracking_no VARCHAR(128) NOT NULL DEFAULT '',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'shipped',
|
||||
note TEXT DEFAULT NULL,
|
||||
operator_id BIGINT UNSIGNED DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
updated_at DATETIME DEFAULT NULL,
|
||||
delivered_at DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_order (order_number),
|
||||
KEY idx_status (status),
|
||||
KEY idx_tracking (tracking_no),
|
||||
KEY idx_created (created_at)
|
||||
) {$charset_collate};";
|
||||
dbDelta($sql_shipping);
|
||||
|
||||
// ── 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);
|
||||
|
||||
// ── card_key 卡密表 (§6.2 自动发货) ──
|
||||
$sql_card_key = "CREATE TABLE {$prefix}aksrt_card_key (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
options_key VARCHAR(255) NOT NULL DEFAULT '',
|
||||
card_code TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'available',
|
||||
order_number VARCHAR(32) NOT NULL DEFAULT '',
|
||||
used_by BIGINT UNSIGNED DEFAULT NULL,
|
||||
used_at DATETIME DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_product (product_id),
|
||||
KEY idx_options (options_key(50)),
|
||||
KEY idx_status (status),
|
||||
KEY idx_order (order_number)
|
||||
) {$charset_collate};";
|
||||
dbDelta($sql_card_key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_DB_Migrator_Part_3 {
|
||||
|
||||
|
||||
/**
|
||||
* 表名访问器(其它模块通过此函数取,避免散落硬编码)。
|
||||
*/
|
||||
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',
|
||||
'after_sale' => 'aksrt_after_sale',
|
||||
'order_shipping' => 'aksrt_order_shipping',
|
||||
'charge_card' => 'aksrt_charge_card',
|
||||
'rebate_log' => 'aksrt_rebate_log',
|
||||
'coupon_usage' => 'aksrt_coupon_usage',
|
||||
'card_key' => 'aksrt_card_key',
|
||||
];
|
||||
if (!isset($allowed[$name])) {
|
||||
return '';
|
||||
}
|
||||
return $wpdb->prefix . $allowed[$name];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Scripts_Part_2 {
|
||||
|
||||
|
||||
/**
|
||||
* 前端 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::enqueue_admin_assets() 负责加载
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出运行时 CSS 变量覆盖
|
||||
*
|
||||
* 仅注入用户可在后台配置的覆盖值(logo / favicon / primary),
|
||||
* 主体宽度统一使用固定 token:1280 / 1080 / 300。
|
||||
*/
|
||||
public function inline_css_vars(): void {
|
||||
(new AKSRT_Inline_Style_Builder())->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出自定义头部/底部代码
|
||||
*/
|
||||
public function output_custom_codes(): void {
|
||||
(new AKSRT_Custom_Code_Renderer())->output_header();
|
||||
}
|
||||
|
||||
public function output_loading_overlay(): void {
|
||||
(new AKSRT_Loading_Overlay_Renderer())->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出自定义底部代码
|
||||
*/
|
||||
public function output_custom_footer_codes(): void {
|
||||
(new AKSRT_Custom_Code_Renderer())->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 [
|
||||
'cart' => $this->resolve_template_page_url('templates/pages/page-cart.php', '/cart/'),
|
||||
'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,399 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Setup_Part_1 {
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
// ── Shop widget areas (§6.9) ──
|
||||
$shop_widgets = [
|
||||
'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'],
|
||||
'shop_product_sidebar' => ['商品详情页 · 侧栏', '商品详情页侧栏区域(PC 端)。'],
|
||||
'shop_product_bottom_fluid' => ['商品详情页 · 底部通栏', '商品详情页底部全宽区域。'],
|
||||
'shop_cart_top_fluid' => ['购物车页 · 顶部通栏', '购物车页顶部全宽区域。'],
|
||||
'shop_cart_bottom_fluid' => ['购物车页 · 底部通栏', '购物车页底部全宽区域。'],
|
||||
];
|
||||
foreach ($shop_widgets as $sid => $meta) {
|
||||
register_sidebar([
|
||||
'id' => $sid,
|
||||
'name' => $meta[0],
|
||||
'description' => $meta[1],
|
||||
'before_widget' => $card_wrapper_before,
|
||||
'after_widget' => '</div>',
|
||||
'before_title' => $title_before,
|
||||
'after_title' => $title_after,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Setup_Part_2 {
|
||||
|
||||
|
||||
/**
|
||||
* 隐藏默认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');
|
||||
|
||||
// Separators around shop (35) and forum (40) — match zibll admin_menu_separator.
|
||||
global $menu;
|
||||
if (!is_array($menu)) {
|
||||
return;
|
||||
}
|
||||
if (!isset($menu[34])) {
|
||||
$menu[34] = ['', 'read', 'separator-aksrt-shop', '', 'wp-menu-separator'];
|
||||
}
|
||||
if (!isset($menu[39])) {
|
||||
$menu[39] = ['', 'read', 'separator-aksrt-forum', '', 'wp-menu-separator'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register shop rewrite rules for:
|
||||
* - /shop/{ID}.html — product detail (ID-based, .html suffix)
|
||||
* - /shop/{ID}.html/comment-{page} — product comment pagination
|
||||
* - /{cart_slug}/ — cart page (fixed alias)
|
||||
*
|
||||
* Runs at init priority 20 (after CPT registration at priority 1).
|
||||
*/
|
||||
public function register_shop_rewrite_rules(): void {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return;
|
||||
}
|
||||
$shop = AKSRT_Settings::get_shop_settings();
|
||||
if (empty($shop['enabled'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rw = $shop['rewrite'] ?? [];
|
||||
$product_slug = sanitize_key((string) ($rw['product_slug'] ?? 'product'));
|
||||
$cart_slug = sanitize_key((string) ($rw['cart_slug'] ?? 'cart'));
|
||||
$suffix_html = !empty($rw['suffix_html']);
|
||||
|
||||
// Product ID-based .html routes: /shop/{ID}.html
|
||||
if ($suffix_html) {
|
||||
add_rewrite_rule(
|
||||
'^shop/(\d+)\.html/comment-page-([0-9]{1,})/?$',
|
||||
'index.php?post_type=aksrt_product&p=$matches[1]&cpage=$matches[2]',
|
||||
'top'
|
||||
);
|
||||
add_rewrite_rule(
|
||||
'^shop/(\d+)\.html/?$',
|
||||
'index.php?post_type=aksrt_product&p=$matches[1]',
|
||||
'top'
|
||||
);
|
||||
// Slug-based .html: /{product_slug}/{slug}.html
|
||||
add_rewrite_rule(
|
||||
'^' . preg_quote($product_slug) . '/([^/]+)\.html/comment-page-([0-9]{1,})/?$',
|
||||
'index.php?post_type=aksrt_product&aksrt_product=$matches[1]&cpage=$matches[3]',
|
||||
'top'
|
||||
);
|
||||
add_rewrite_rule(
|
||||
'^' . preg_quote($product_slug) . '/([^/]+)\.html/?$',
|
||||
'index.php?post_type=aksrt_product&aksrt_product=$matches[1]',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
// Cart page: /{cart_slug}/
|
||||
add_rewrite_rule(
|
||||
'^' . preg_quote($cart_slug) . '/?$',
|
||||
'index.php?pagename=' . $cart_slug,
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register shop-specific query vars.
|
||||
*/
|
||||
public function register_shop_query_vars(array $vars): array {
|
||||
$vars[] = 'product_id';
|
||||
$vars[] = 'shop_action';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template redirect for shop pages.
|
||||
*
|
||||
* - Single aksrt_product: serve page-product.php template
|
||||
* - Cart page: serve page-cart.php template
|
||||
*/
|
||||
public function shop_template_redirect(): void {
|
||||
// Single product: if the main query resolves to aksrt_product, use our template
|
||||
if (is_singular('aksrt_product')) {
|
||||
$tpl = AKSRT_DIR . '/templates/pages/page-product.php';
|
||||
if (file_exists($tpl)) {
|
||||
// Set product_id query var for template consumption
|
||||
set_query_var('product_id', get_queried_object_id());
|
||||
include $tpl;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user