From 3cf0c089691c315065d951e4d98ffb4cad60f8c3 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 17 Jul 2026 14:44:36 +0800 Subject: [PATCH] fix(autoload): one PSR-4 type per payment/widget file PSR-4 split left GatewayInterface/Gateway duplicated across files; loading BalanceGateway redeclared the interface. Strip sibling types from polluted files (payment + widgets) so each path owns one type. --- src/AKSRT/Payment/BalanceGateway.php | 339 +--------- src/AKSRT/Payment/Gateway.php | 65 +- src/AKSRT/Payment/GatewayInterface.php | 11 - src/AKSRT/Widgets/CategoryRecommendWidget.php | 610 ------------------ src/AKSRT/Widgets/CategoryTabsWidget.php | 431 ------------- src/AKSRT/Widgets/WidgetForumBoardList.php | 175 ----- src/AKSRT/Widgets/WidgetForumRecentTopics.php | 103 --- src/AKSRT/Widgets/WidgetProductOneline.php | 224 ------- src/AKSRT/Widgets/WidgetProductTab.php | 248 ------- 9 files changed, 3 insertions(+), 2203 deletions(-) diff --git a/src/AKSRT/Payment/BalanceGateway.php b/src/AKSRT/Payment/BalanceGateway.php index 5abf2bb0..e0ae4cc6 100644 --- a/src/AKSRT/Payment/BalanceGateway.php +++ b/src/AKSRT/Payment/BalanceGateway.php @@ -8,346 +8,11 @@ namespace AKSRT\Payment; defined('ABSPATH') || exit; /** - * AKSRT Payment Gateway — 支付网关适配器层 - * - * 定义支付网关统一接口和基础实现。 - * 具体网关(支付宝、微信支付等)通过子类实现。 - * - * @package AKSRT - */ - -defined('ABSPATH') || exit; - -/** - * 支付网关统一接口 - */ -interface GatewayInterface { - /** - * 创建支付单 - * - * @param array $order 订单数据 - * @return array{success: bool, payment_url?: string, payment_data?: array, message?: string} - */ - public function create_payment(array $order): array; - - /** - * 验证回调 - * - * @param \WP_REST_Request $request 回调请求 - * @return array{success: bool, order_number?: string, amount?: float, transaction_id?: string, message?: string} - */ - public function verify_callback(\WP_REST_Request $request): array; - - /** - * 查询支付状态 - * - * @param string $order_number 订单号 - * @return array{success: bool, status?: string, transaction_id?: string, message?: string} - */ - public function query_status(string $order_number): array; - - /** - * 退款 - * - * @param string $order_number 订单号 - * @param float $amount 退款金额 - * @return array{success: bool, refund_id?: string, message?: string} - */ - public function refund(string $order_number, float $amount): array; - - /** - * 获取网关显示名称 - */ - public function get_name(): string; - - /** - * 获取网关标识 - */ - public function get_id(): string; - - /** - * 网关是否已配置 - */ - public function is_configured(): bool; -} - -/** - * 支付网关管理器 - */ -class Gateway { - - private static $instance = null; - private $gateways = []; - - public static function get_instance(): self { - if (null === self::$instance) self::$instance = new self(); - return self::$instance; - } - - /** - * 注册网关 - */ - public function register(string $id, GatewayInterface $gateway): void { - $this->gateways[$id] = $gateway; - } - - /** - * 获取网关 - */ - public function get_gateway(string $id): ?GatewayInterface { - return $this->gateways[$id] ?? null; - } - - /** - * 获取所有已注册网关 - * - * @return GatewayInterface[] - */ - public function get_all(): array { - return $this->gateways; - } - - /** - * 获取可用网关列表(已配置的) - */ - public function get_available(): array { - $available = []; - foreach ($this->gateways as $id => $gateway) { - if ($gateway->is_configured()) { - $available[$id] = $gateway; - } - } - return $available; - } - - /** - * 获取网关状态摘要(用于后台展示) - */ - public function get_status_summary(): array { - $summary = []; - foreach ($this->gateways as $id => $gateway) { - $summary[] = [ - 'id' => $id, - 'name' => $gateway->get_name(), - 'configured' => $gateway->is_configured(), - 'available' => $gateway->is_configured(), - ]; - } - return $summary; - } - - /** - * 绑定支付回调路由。必须在 functions.php 中初始化。 - * - * 此前网关 create_payment() 把 notify_url/return_url 指向 - * /wp-json/aksrt/v1/payment/{gateway}/{notify|return|cancel},但这些路由 - * 从未注册,导致所有跳转型网关(支付宝/微信/PayPal/Stripe)无法完成支付。 - */ - public function init(): void { - add_action('rest_api_init', [$this, 'register_routes']); - } - - public function register_routes(): void { - $args = [ - 'methods' => ['GET', 'POST'], - 'permission_callback' => '__return_true', // 真实性由各网关签名校验保证 - 'args' => [ - 'gateway' => ['sanitize_callback' => 'sanitize_key'], - ], - ]; - - register_rest_route('aksrt/v1', '/payment/(?P[a-z0-9_]+)/notify', array_merge($args, [ - 'callback' => [$this, 'handle_notify'], - ])); - register_rest_route('aksrt/v1', '/payment/(?P[a-z0-9_]+)/return', array_merge($args, [ - 'callback' => [$this, 'handle_return'], - ])); - register_rest_route('aksrt/v1', '/payment/(?P[a-z0-9_]+)/cancel', array_merge($args, [ - 'callback' => [$this, 'handle_cancel'], - ])); - } - - /** - * 异步通知(服务器到服务器)。验签 → 到账 → 输出网关期望的 ACK。 - */ - public function handle_notify(\WP_REST_Request $request) { - $gateway_id = sanitize_key((string) $request->get_param('gateway')); - $gateway = $this->get_gateway($gateway_id); - if (!$gateway) { - return new \WP_REST_Response('gateway not found', 404); - } - - $verify = $gateway->verify_callback($request); - $verified = !empty($verify['success']); - $paid = $verified && ($verify['status'] ?? '') === 'paid'; - - if ($paid) { - $this->complete_order($gateway_id, $verify); - } - - // 网关各自要求的 ACK 格式 - if ($gateway_id === 'wechatpay' && method_exists($gateway, 'output_callback_response')) { - $gateway->output_callback_response(['success' => $verified]); // exit - } - if ($gateway_id === 'alipay') { - // 支付宝异步通知要求返回纯文本 success,否则会重复通知 - header('Content-Type: text/plain; charset=utf-8'); - echo $verified ? 'success' : 'fail'; - exit; - } - - // Stripe / PayPal webhook:200 表示已接收;验签失败返回 400 触发重试 - return new \WP_REST_Response(['received' => $verified], $verified ? 200 : 400); - } - - /** - * 用户浏览器同步返回。尽力确认到账(兜底异步通知延迟),再跳回订单中心。 - */ - public function handle_return(\WP_REST_Request $request) { - $gateway_id = sanitize_key((string) $request->get_param('gateway')); - $order_number = sanitize_text_field((string) ($request->get_param('order') - ?: $request->get_param('out_trade_no') - ?: '')); - $gateway = $this->get_gateway($gateway_id); - $status = 'pending'; - - if ($gateway) { - if ($gateway_id === 'alipay') { - // 支付宝同步返回带签名参数,可直接验签作为到账兜底 - $verify = $gateway->verify_callback($request); - if (!empty($verify['success'])) { - $order_number = $order_number ?: sanitize_text_field((string) ($verify['order_number'] ?? '')); - if (($verify['status'] ?? '') === 'paid' && $this->complete_order($gateway_id, $verify)) { - $status = 'success'; - } - } - } elseif ($gateway_id === 'paypal' && method_exists($gateway, 'capture_order')) { - // PayPal v2:用户批准后在 return 时捕获(token = PayPal 订单号) - $paypal_order_id = sanitize_text_field((string) ($request->get_param('token') ?: '')); - if ($paypal_order_id !== '') { - $cap = $gateway->capture_order($paypal_order_id); - if (!empty($cap['success']) && ($cap['status'] ?? '') === 'paid') { - $order_number = $order_number ?: sanitize_text_field((string) ($cap['order_number'] ?? '')); - $cap['order_number'] = $order_number; - if ($this->complete_order($gateway_id, $cap)) { - $status = 'success'; - } - } - } - } elseif ($order_number !== '') { - // 其余网关:主动查询订单状态兜底 - $q = $gateway->query_status($order_number); - if (!empty($q['success']) && ($q['status'] ?? '') === 'paid') { - $done = $this->complete_order($gateway_id, [ - 'order_number' => $order_number, - 'amount' => 0, // 由网关 API 已确认到账,金额校验交给 notify 路径 - 'transaction_id' => $q['transaction_id'] ?? '', - 'status' => 'paid', - ]); - if ($done) { - $status = 'success'; - } - } - } - } - - $this->redirect_to_orders($order_number, $status); - return null; - } - - /** - * 用户取消支付,跳回订单中心。 - */ - public function handle_cancel(\WP_REST_Request $request) { - $order_number = sanitize_text_field((string) ($request->get_param('order') - ?: $request->get_param('out_trade_no') - ?: '')); - $this->redirect_to_orders($order_number, 'cancelled'); - return null; - } - - /** - * 幂等、带金额校验的订单完成。仅由验签通过的回调调用。 - * - * @param string $gateway_id 网关 ID - * @param array $verify 网关 verify_callback / query 结果 - * @return bool - */ - public function complete_order(string $gateway_id, array $verify): bool { - $order_number = sanitize_text_field((string) ($verify['order_number'] ?? '')); - if ($order_number === '' || !class_exists('OrderRepository')) { - return false; - } - - $repo = OrderRepository::get_instance(); - $order = $repo->get($order_number); - if (!$order) { - return false; - } - - $current = (string) ($order['status'] ?? ''); - - // 幂等:已支付/已交付/已完成视为成功,不重复处理(回调可能多次到达) - if (in_array($current, ['paid', 'delivered', 'completed'], true)) { - return true; - } - // 只允许从未支付态完成 - if (!in_array($current, ['created', 'pending'], true)) { - return false; - } - - // 金额校验,防止前端/回调金额被篡改 - $expected = (float) ($order['amount'] ?? 0); - $paid_amount = (float) ($verify['amount'] ?? 0); - if ($expected > 0 && $paid_amount > 0 && abs($expected - $paid_amount) > 0.01) { - if (defined('WP_DEBUG') && WP_DEBUG) { - error_log(sprintf('[AKSRT Payment] amount mismatch order=%s expected=%.2f paid=%.2f gateway=%s', $order_number, $expected, $paid_amount, $gateway_id)); - } - return false; - } - - return $repo->update_status($order_number, Order::STATUS_PAID, [ - 'payment_method' => $gateway_id, - 'payment_transaction_id' => sanitize_text_field((string) ($verify['transaction_id'] ?? '')), - 'note' => __('支付网关回调确认到账。', 'aksrt'), - ]); - } - - private function redirect_to_orders(string $order_number, string $status): void { - $url = add_query_arg(array_filter([ - 'payment' => sanitize_key($status), - 'order' => $order_number, - ]), $this->orders_page_url()); - - nocache_headers(); - wp_safe_redirect($url); - exit; - } - - private function orders_page_url(): string { - $pages = get_posts([ - 'post_type' => 'page', - 'post_status' => 'publish', - 'posts_per_page' => 1, - 'meta_key' => '_wp_page_template', - 'meta_value' => 'templates/pages/page-orders.php', - 'fields' => 'ids', - 'no_found_rows' => true, - ]); - - if (!empty($pages)) { - return (string) get_permalink((int) $pages[0]); - } - - return home_url('/orders/'); - } -} - -/** - * 站内余额支付网关(内置实现) + * 站内余额支付网关(内置实现)。 * * 优先扣现金钱包(\AKSRT\Credits\Balance / pay_balance);未启用时回退到积分。 */ + class BalanceGateway implements GatewayInterface { public function get_id(): string { diff --git a/src/AKSRT/Payment/Gateway.php b/src/AKSRT/Payment/Gateway.php index 08b9a3f9..a95eb20a 100644 --- a/src/AKSRT/Payment/Gateway.php +++ b/src/AKSRT/Payment/Gateway.php @@ -8,72 +8,9 @@ namespace AKSRT\Payment; defined('ABSPATH') || exit; /** - * AKSRT Payment Gateway — 支付网关适配器层 - * - * 定义支付网关统一接口和基础实现。 - * 具体网关(支付宝、微信支付等)通过子类实现。 - * - * @package AKSRT + * 支付网关管理器(注册/回调路由/到账)。 */ -defined('ABSPATH') || exit; - -/** - * 支付网关统一接口 - */ -interface GatewayInterface { - /** - * 创建支付单 - * - * @param array $order 订单数据 - * @return array{success: bool, payment_url?: string, payment_data?: array, message?: string} - */ - public function create_payment(array $order): array; - - /** - * 验证回调 - * - * @param \WP_REST_Request $request 回调请求 - * @return array{success: bool, order_number?: string, amount?: float, transaction_id?: string, message?: string} - */ - public function verify_callback(\WP_REST_Request $request): array; - - /** - * 查询支付状态 - * - * @param string $order_number 订单号 - * @return array{success: bool, status?: string, transaction_id?: string, message?: string} - */ - public function query_status(string $order_number): array; - - /** - * 退款 - * - * @param string $order_number 订单号 - * @param float $amount 退款金额 - * @return array{success: bool, refund_id?: string, message?: string} - */ - public function refund(string $order_number, float $amount): array; - - /** - * 获取网关显示名称 - */ - public function get_name(): string; - - /** - * 获取网关标识 - */ - public function get_id(): string; - - /** - * 网关是否已配置 - */ - public function is_configured(): bool; -} - -/** - * 支付网关管理器 - */ class Gateway { private static $instance = null; diff --git a/src/AKSRT/Payment/GatewayInterface.php b/src/AKSRT/Payment/GatewayInterface.php index 8f3e1eed..3aa0d627 100644 --- a/src/AKSRT/Payment/GatewayInterface.php +++ b/src/AKSRT/Payment/GatewayInterface.php @@ -7,17 +7,6 @@ namespace AKSRT\Payment; defined('ABSPATH') || exit; -/** - * AKSRT Payment Gateway — 支付网关适配器层 - * - * 定义支付网关统一接口和基础实现。 - * 具体网关(支付宝、微信支付等)通过子类实现。 - * - * @package AKSRT - */ - -defined('ABSPATH') || exit; - /** * 支付网关统一接口 */ diff --git a/src/AKSRT/Widgets/CategoryRecommendWidget.php b/src/AKSRT/Widgets/CategoryRecommendWidget.php index e511e618..49db1d85 100644 --- a/src/AKSRT/Widgets/CategoryRecommendWidget.php +++ b/src/AKSRT/Widgets/CategoryRecommendWidget.php @@ -7,616 +7,6 @@ namespace AKSRT\Widgets; defined('ABSPATH') || exit; -/** - * Homepage content widgets aligned to the fixed Zibll-like layout. - * - * @package AKSRT - */ - -defined('ABSPATH') || exit; - -if (!function_exists('aksrt_home_widget_allowed_layouts')) { - /** - * Allowed post-card layouts for homepage content widgets. - * - * @return array - */ - function aksrt_home_widget_allowed_layouts(): array { - return [ - 'card' => __('卡片', 'aksrt'), - 'list' => __('列表', 'aksrt'), - 'grid-cms' => __('资源网格', 'aksrt'), - ]; - } -} - -if (!function_exists('aksrt_home_widget_post_types')) { - /** - * Post types used by homepage widgets. - * - * @return array - */ - function aksrt_home_widget_post_types(): array { - return ['post']; - } -} - -if (!function_exists('aksrt_home_widget_available_categories')) { - /** - * Return categories for widget admin forms. - * - * @return array - */ - function aksrt_home_widget_available_categories(): array { - $categories = get_categories([ - 'taxonomy' => 'category', - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC', - ]); - - return is_array($categories) ? $categories : []; - } -} - -if (!function_exists('aksrt_home_widget_fallback_category_ids')) { - /** - * Pick top categories as fallback when widget instance has no selection. - * - * @param int $limit Maximum category ids to return. - * @return array - */ - function aksrt_home_widget_fallback_category_ids(int $limit = 1): array { - $limit = max(1, min(8, $limit)); - $terms = get_terms([ - 'taxonomy' => 'category', - 'orderby' => 'count', - 'order' => 'DESC', - 'number' => $limit, - 'hide_empty' => true, - 'fields' => 'ids', - ]); - - if (is_wp_error($terms) || !is_array($terms)) { - return []; - } - - return array_values(array_filter(array_map('absint', $terms))); - } -} - -if (!function_exists('aksrt_home_widget_normalize_category_ids')) { - /** - * Normalize selected category ids. - * - * @param mixed $raw_ids Raw ids from widget instance. - * @param int $limit Maximum ids to keep. - * @return array - */ - function aksrt_home_widget_normalize_category_ids($raw_ids, int $limit = 4): array { - $ids = array_map('absint', (array) $raw_ids); - $ids = array_values(array_filter($ids, static fn($id) => $id > 0)); - - if ($limit > 0) { - $ids = array_slice($ids, 0, $limit); - } - - return $ids; - } -} - -if (!function_exists('aksrt_home_widget_resolve_layout')) { - /** - * Sanitize requested widget post layout. - * - * @param mixed $layout Layout value. - * @param string $default Default layout. - * @return string - */ - function aksrt_home_widget_resolve_layout($layout, string $default = 'card'): string { - $allowed = array_keys(aksrt_home_widget_allowed_layouts()); - $layout = is_string($layout) ? $layout : $default; - - return in_array($layout, $allowed, true) ? $layout : $default; - } -} - -if (!function_exists('aksrt_home_widget_resolve_context')) { - /** - * Resolve widget area context from sidebar args. - */ - function aksrt_home_widget_resolve_context(array $args): string { - if (function_exists('aksrt_widget_get_area_context')) { - return aksrt_widget_get_area_context($args); - } - - return 'homepage'; - } -} - -if (!function_exists('aksrt_home_widget_effective_layout')) { - /** - * Compact rich homepage widgets when placed into a sidebar area. - */ - function aksrt_home_widget_effective_layout(string $layout, string $context): string { - if ($context === 'sidebar' && $layout !== 'list') { - return 'list'; - } - - return $layout; - } -} - -if (!function_exists('aksrt_home_widget_effective_columns')) { - /** - * Sidebar widgets should stay single-column. - */ - function aksrt_home_widget_effective_columns(int $columns, string $context): int { - if ($context === 'sidebar') { - return 1; - } - - return max(1, min(4, $columns)); - } -} - -if (!function_exists('aksrt_home_widget_render_header')) { - /** - * Render a compact Zibll-like section header. - * - * @param string $title Section title. - * @param string $more_link Optional more link. - * @param string $more_text Optional more text. - * @return void - */ - function aksrt_home_widget_render_header(string $title, string $more_link = '', string $more_text = ''): void { - ?> -
-

- - - - - -
- have_posts()) { - echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无内容', 'aksrt')) : '

' . esc_html__('暂无内容', 'aksrt') . '

'; - wp_reset_postdata(); - return; - } - - if ($context === 'sidebar') { - $use_homepage_mini = function_exists('aksrt_is_fixed_homepage_request') - && aksrt_is_fixed_homepage_request() - && function_exists('aksrt_render_mini_list') - && (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::homepage_sidebar_mini_enabled()); - - if ($use_homepage_mini && function_exists('aksrt_render_mini_list')) { - echo '
'; - aksrt_render_mini_list($query, ['context' => 'sidebar', 'show_thumb' => true]); - echo '
'; - return; - } - - // Fallback: simple sidebar list - echo '
    '; - while ($query->have_posts()) { - $query->the_post(); - ?> -
  • - - - 'lazy', 'class' => 'fit-cover']); ?> - - -
    - - - - -
    -
  • - '; - wp_reset_postdata(); - return; - } - - // Homepage / content area: delegate to shared renderer - $layout = aksrt_home_widget_resolve_layout($layout, 'card'); - $columns = max(1, min(4, $columns)); - - if (function_exists('aksrt_render_post_list')) { - aksrt_render_post_list($query, [ - 'mode' => $layout === 'grid-cms' ? 'grid-cms' : $layout, - 'context' => 'widget', - 'columns' => $columns, - ], true); - } else { - // Fallback: use template part - $previous_layout = get_query_var('aksrt_post_card_layout', ''); - $previous_scene = get_query_var('aksrt_post_card_scene', ''); - - set_query_var('aksrt_post_card_layout', $layout); - set_query_var('aksrt_post_card_scene', 'home'); - - $classes = [ - 'aksrt-home-widget__post-grid', - 'aksrt-home-widget__post-grid--' . $layout, - 'post-grid', - 'post-grid--' . $layout, - ]; - - printf( - '
    ', - esc_attr(implode(' ', $classes)), - (int) $columns - ); - - while ($query->have_posts()) { - $query->the_post(); - get_template_part('templates/parts/content', 'post-card'); - } - - echo '
    '; - - set_query_var('aksrt_post_card_layout', $previous_layout); - set_query_var('aksrt_post_card_scene', $previous_scene); - } - - wp_reset_postdata(); - } -} - -if (!function_exists('aksrt_home_widget_get_lazyload_args')) { - /** - * Lazyload settings reused by category recommend widget. - * - * @return array{enabled: bool, placeholder_url: string} - */ - function aksrt_home_widget_get_lazyload_args(): array { - $config = class_exists('\AKSRT\Settings\Settings') - ? \AKSRT\Settings\Settings::get_lazyload_config() - : ['enabled' => false, 'placeholder_url' => '']; - - return [ - 'enabled' => ! empty($config['enabled']), - 'placeholder_url' => ! empty($config['placeholder_url']) - ? (string) $config['placeholder_url'] - : get_template_directory_uri() . '/assets/img/placeholder.svg', - ]; - } -} - -/** - * Homepage category block widget. - */ -class CategoryBlockWidget extends \WP_Widget { - - public function __construct() { - parent::__construct( - 'aksrt_category_block', - esc_html__('AKSRT 分类块', 'aksrt'), - [ - 'classname' => 'widget_aksrt_category_block', - 'description' => esc_html__('指定分类的最新内容区块,可挂载到首页内容挂载区。', 'aksrt'), - 'customize_selective_refresh' => true, - ] - ); - } - - public function widget($args, $instance): void { - $context = aksrt_home_widget_resolve_context($args); - $category_id = $this->resolve_category_id($instance); - $term = $category_id > 0 ? get_term($category_id, 'category') : null; - $title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base); - $title = $title !== '' ? $title : (($term && ! is_wp_error($term)) ? $term->name : __('精选分类', 'aksrt')); - $count = max(1, min(12, (int) ($instance['count'] ?? 6))); - $columns = aksrt_home_widget_effective_columns((int) ($instance['columns'] ?? 3), $context); - $layout = aksrt_home_widget_effective_layout( - aksrt_home_widget_resolve_layout($instance['layout'] ?? 'card', 'card'), - $context - ); - $show_more = ! empty($instance['show_more']); - - $query_args = [ - 'post_type' => aksrt_home_widget_post_types(), - 'post_status' => 'publish', - 'posts_per_page' => $count, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - ]; - - if ($category_id > 0) { - $query_args['cat'] = $category_id; - } - - $query = new \WP_Query($query_args); - - echo $args['before_widget']; - echo '
    '; - - aksrt_home_widget_render_header( - (string) $title, - ($show_more && $term && ! is_wp_error($term)) ? (string) get_category_link($category_id) : '', - __('查看分类', 'aksrt') - ); - - aksrt_home_widget_render_post_grid($query, $layout, $columns, $context); - - echo '
    '; - echo $args['after_widget']; - } - - public function form($instance): void { - $title = (string) ($instance['title'] ?? ''); - $category_id = (int) ($instance['category_id'] ?? 0); - $count = (int) ($instance['count'] ?? 6); - $columns = (int) ($instance['columns'] ?? 3); - $layout = aksrt_home_widget_resolve_layout($instance['layout'] ?? 'card', 'card'); - $show_more = ! empty($instance['show_more']); - $categories = aksrt_home_widget_available_categories(); - ?> -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - id="get_field_id('show_more')); ?>" name="get_field_name('show_more')); ?>" value="1"> - -

    - - sanitize_text_field($new_instance['title'] ?? ''), - 'category_id' => absint($new_instance['category_id'] ?? 0), - 'count' => max(1, min(12, absint($new_instance['count'] ?? 6))), - 'columns' => max(1, min(4, absint($new_instance['columns'] ?? 3))), - 'layout' => aksrt_home_widget_resolve_layout($new_instance['layout'] ?? 'card', 'card'), - 'show_more' => ! empty($new_instance['show_more']) ? 1 : 0, - ]; - } - - private function resolve_category_id(array $instance): int { - $category_id = absint($instance['category_id'] ?? 0); - if ($category_id > 0) { - return $category_id; - } - - $fallback = aksrt_home_widget_fallback_category_ids(1); - return $fallback[0] ?? 0; - } -} - -/** - * Homepage category tabs widget. - */ -class CategoryTabsWidget extends \WP_Widget { - - public function __construct() { - parent::__construct( - 'aksrt_category_tabs', - esc_html__('AKSRT 分类 Tabs', 'aksrt'), - [ - 'classname' => 'widget_aksrt_category_tabs', - 'description' => esc_html__('多分类切换内容流,可挂载到首页内容挂载区。', 'aksrt'), - 'customize_selective_refresh' => true, - ] - ); - } - - public function widget($args, $instance): void { - $context = aksrt_home_widget_resolve_context($args); - $title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base); - $title = $title !== '' ? $title : __('热门分类', 'aksrt'); - $count = max(1, min(12, (int) ($instance['count'] ?? 6))); - $columns = aksrt_home_widget_effective_columns((int) ($instance['columns'] ?? 3), $context); - $layout = aksrt_home_widget_effective_layout( - aksrt_home_widget_resolve_layout($instance['layout'] ?? 'card', 'card'), - $context - ); - $show_more = ! empty($instance['show_more']); - $tabs = $this->resolve_tabs($instance); - - echo $args['before_widget']; - echo '
    '; - - if (empty($tabs)) { - echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无可用分类', 'aksrt')) : '

    ' . esc_html__('暂无可用分类', 'aksrt') . '

    '; - echo '
    '; - echo $args['after_widget']; - return; - } - - ?> -
    -

    -
      - $tab) : ?> -
    • - -
    • - -
    -
    - $tab) { - $query = new \WP_Query([ - 'post_type' => aksrt_home_widget_post_types(), - 'post_status' => 'publish', - 'posts_per_page' => $count, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - 'cat' => (int) $tab['id'], - ]); - ?> -
    > - - - - -
    - '; - echo $args['after_widget']; - } - - public function form($instance): void { - $title = (string) ($instance['title'] ?? ''); - $category_ids = aksrt_home_widget_normalize_category_ids($instance['category_ids'] ?? [], 6); - $count = (int) ($instance['count'] ?? 6); - $columns = (int) ($instance['columns'] ?? 3); - $layout = aksrt_home_widget_resolve_layout($instance['layout'] ?? 'card', 'card'); - $show_more = ! empty($instance['show_more']); - $categories = aksrt_home_widget_available_categories(); - ?> -

    - - -

    -

    - - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - id="get_field_id('show_more')); ?>" name="get_field_name('show_more')); ?>" value="1"> - -

    - - sanitize_text_field($new_instance['title'] ?? ''), - 'category_ids' => aksrt_home_widget_normalize_category_ids($new_instance['category_ids'] ?? [], 6), - 'count' => max(1, min(12, absint($new_instance['count'] ?? 6))), - 'columns' => max(1, min(4, absint($new_instance['columns'] ?? 3))), - 'layout' => aksrt_home_widget_resolve_layout($new_instance['layout'] ?? 'card', 'card'), - 'show_more' => ! empty($new_instance['show_more']) ? 1 : 0, - ]; - } - - /** - * Resolve configured category tabs. - * - * @param array $instance Widget instance. - * @return array - */ - private function resolve_tabs(array $instance): array { - $category_ids = aksrt_home_widget_normalize_category_ids($instance['category_ids'] ?? [], 6); - if (empty($category_ids)) { - $category_ids = aksrt_home_widget_fallback_category_ids(4); - } - - $tabs = []; - foreach ($category_ids as $category_id) { - $term = get_term($category_id, 'category'); - if (! $term || is_wp_error($term)) { - continue; - } - - $tabs[] = [ - 'id' => (int) $category_id, - 'name' => $term->name, - 'link' => get_category_link($category_id), - ]; - } - - return $tabs; - } -} - /** * Homepage category recommend cards widget. */ diff --git a/src/AKSRT/Widgets/CategoryTabsWidget.php b/src/AKSRT/Widgets/CategoryTabsWidget.php index e396481f..a996a157 100644 --- a/src/AKSRT/Widgets/CategoryTabsWidget.php +++ b/src/AKSRT/Widgets/CategoryTabsWidget.php @@ -7,437 +7,6 @@ namespace AKSRT\Widgets; defined('ABSPATH') || exit; -/** - * Homepage content widgets aligned to the fixed Zibll-like layout. - * - * @package AKSRT - */ - -defined('ABSPATH') || exit; - -if (!function_exists('aksrt_home_widget_allowed_layouts')) { - /** - * Allowed post-card layouts for homepage content widgets. - * - * @return array - */ - function aksrt_home_widget_allowed_layouts(): array { - return [ - 'card' => __('卡片', 'aksrt'), - 'list' => __('列表', 'aksrt'), - 'grid-cms' => __('资源网格', 'aksrt'), - ]; - } -} - -if (!function_exists('aksrt_home_widget_post_types')) { - /** - * Post types used by homepage widgets. - * - * @return array - */ - function aksrt_home_widget_post_types(): array { - return ['post']; - } -} - -if (!function_exists('aksrt_home_widget_available_categories')) { - /** - * Return categories for widget admin forms. - * - * @return array - */ - function aksrt_home_widget_available_categories(): array { - $categories = get_categories([ - 'taxonomy' => 'category', - 'hide_empty' => false, - 'orderby' => 'name', - 'order' => 'ASC', - ]); - - return is_array($categories) ? $categories : []; - } -} - -if (!function_exists('aksrt_home_widget_fallback_category_ids')) { - /** - * Pick top categories as fallback when widget instance has no selection. - * - * @param int $limit Maximum category ids to return. - * @return array - */ - function aksrt_home_widget_fallback_category_ids(int $limit = 1): array { - $limit = max(1, min(8, $limit)); - $terms = get_terms([ - 'taxonomy' => 'category', - 'orderby' => 'count', - 'order' => 'DESC', - 'number' => $limit, - 'hide_empty' => true, - 'fields' => 'ids', - ]); - - if (is_wp_error($terms) || !is_array($terms)) { - return []; - } - - return array_values(array_filter(array_map('absint', $terms))); - } -} - -if (!function_exists('aksrt_home_widget_normalize_category_ids')) { - /** - * Normalize selected category ids. - * - * @param mixed $raw_ids Raw ids from widget instance. - * @param int $limit Maximum ids to keep. - * @return array - */ - function aksrt_home_widget_normalize_category_ids($raw_ids, int $limit = 4): array { - $ids = array_map('absint', (array) $raw_ids); - $ids = array_values(array_filter($ids, static fn($id) => $id > 0)); - - if ($limit > 0) { - $ids = array_slice($ids, 0, $limit); - } - - return $ids; - } -} - -if (!function_exists('aksrt_home_widget_resolve_layout')) { - /** - * Sanitize requested widget post layout. - * - * @param mixed $layout Layout value. - * @param string $default Default layout. - * @return string - */ - function aksrt_home_widget_resolve_layout($layout, string $default = 'card'): string { - $allowed = array_keys(aksrt_home_widget_allowed_layouts()); - $layout = is_string($layout) ? $layout : $default; - - return in_array($layout, $allowed, true) ? $layout : $default; - } -} - -if (!function_exists('aksrt_home_widget_resolve_context')) { - /** - * Resolve widget area context from sidebar args. - */ - function aksrt_home_widget_resolve_context(array $args): string { - if (function_exists('aksrt_widget_get_area_context')) { - return aksrt_widget_get_area_context($args); - } - - return 'homepage'; - } -} - -if (!function_exists('aksrt_home_widget_effective_layout')) { - /** - * Compact rich homepage widgets when placed into a sidebar area. - */ - function aksrt_home_widget_effective_layout(string $layout, string $context): string { - if ($context === 'sidebar' && $layout !== 'list') { - return 'list'; - } - - return $layout; - } -} - -if (!function_exists('aksrt_home_widget_effective_columns')) { - /** - * Sidebar widgets should stay single-column. - */ - function aksrt_home_widget_effective_columns(int $columns, string $context): int { - if ($context === 'sidebar') { - return 1; - } - - return max(1, min(4, $columns)); - } -} - -if (!function_exists('aksrt_home_widget_render_header')) { - /** - * Render a compact Zibll-like section header. - * - * @param string $title Section title. - * @param string $more_link Optional more link. - * @param string $more_text Optional more text. - * @return void - */ - function aksrt_home_widget_render_header(string $title, string $more_link = '', string $more_text = ''): void { - ?> -
    -

    - - - - - -
    - have_posts()) { - echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无内容', 'aksrt')) : '

    ' . esc_html__('暂无内容', 'aksrt') . '

    '; - wp_reset_postdata(); - return; - } - - if ($context === 'sidebar') { - $use_homepage_mini = function_exists('aksrt_is_fixed_homepage_request') - && aksrt_is_fixed_homepage_request() - && function_exists('aksrt_render_mini_list') - && (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::homepage_sidebar_mini_enabled()); - - if ($use_homepage_mini && function_exists('aksrt_render_mini_list')) { - echo '
    '; - aksrt_render_mini_list($query, ['context' => 'sidebar', 'show_thumb' => true]); - echo '
    '; - return; - } - - // Fallback: simple sidebar list - echo '
      '; - while ($query->have_posts()) { - $query->the_post(); - ?> -
    • - - - 'lazy', 'class' => 'fit-cover']); ?> - - -
      - - - - -
      -
    • - '; - wp_reset_postdata(); - return; - } - - // Homepage / content area: delegate to shared renderer - $layout = aksrt_home_widget_resolve_layout($layout, 'card'); - $columns = max(1, min(4, $columns)); - - if (function_exists('aksrt_render_post_list')) { - aksrt_render_post_list($query, [ - 'mode' => $layout === 'grid-cms' ? 'grid-cms' : $layout, - 'context' => 'widget', - 'columns' => $columns, - ], true); - } else { - // Fallback: use template part - $previous_layout = get_query_var('aksrt_post_card_layout', ''); - $previous_scene = get_query_var('aksrt_post_card_scene', ''); - - set_query_var('aksrt_post_card_layout', $layout); - set_query_var('aksrt_post_card_scene', 'home'); - - $classes = [ - 'aksrt-home-widget__post-grid', - 'aksrt-home-widget__post-grid--' . $layout, - 'post-grid', - 'post-grid--' . $layout, - ]; - - printf( - '
      ', - esc_attr(implode(' ', $classes)), - (int) $columns - ); - - while ($query->have_posts()) { - $query->the_post(); - get_template_part('templates/parts/content', 'post-card'); - } - - echo '
      '; - - set_query_var('aksrt_post_card_layout', $previous_layout); - set_query_var('aksrt_post_card_scene', $previous_scene); - } - - wp_reset_postdata(); - } -} - -if (!function_exists('aksrt_home_widget_get_lazyload_args')) { - /** - * Lazyload settings reused by category recommend widget. - * - * @return array{enabled: bool, placeholder_url: string} - */ - function aksrt_home_widget_get_lazyload_args(): array { - $config = class_exists('\AKSRT\Settings\Settings') - ? \AKSRT\Settings\Settings::get_lazyload_config() - : ['enabled' => false, 'placeholder_url' => '']; - - return [ - 'enabled' => ! empty($config['enabled']), - 'placeholder_url' => ! empty($config['placeholder_url']) - ? (string) $config['placeholder_url'] - : get_template_directory_uri() . '/assets/img/placeholder.svg', - ]; - } -} - -/** - * Homepage category block widget. - */ -class CategoryBlockWidget extends \WP_Widget { - - public function __construct() { - parent::__construct( - 'aksrt_category_block', - esc_html__('AKSRT 分类块', 'aksrt'), - [ - 'classname' => 'widget_aksrt_category_block', - 'description' => esc_html__('指定分类的最新内容区块,可挂载到首页内容挂载区。', 'aksrt'), - 'customize_selective_refresh' => true, - ] - ); - } - - public function widget($args, $instance): void { - $context = aksrt_home_widget_resolve_context($args); - $category_id = $this->resolve_category_id($instance); - $term = $category_id > 0 ? get_term($category_id, 'category') : null; - $title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base); - $title = $title !== '' ? $title : (($term && ! is_wp_error($term)) ? $term->name : __('精选分类', 'aksrt')); - $count = max(1, min(12, (int) ($instance['count'] ?? 6))); - $columns = aksrt_home_widget_effective_columns((int) ($instance['columns'] ?? 3), $context); - $layout = aksrt_home_widget_effective_layout( - aksrt_home_widget_resolve_layout($instance['layout'] ?? 'card', 'card'), - $context - ); - $show_more = ! empty($instance['show_more']); - - $query_args = [ - 'post_type' => aksrt_home_widget_post_types(), - 'post_status' => 'publish', - 'posts_per_page' => $count, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - ]; - - if ($category_id > 0) { - $query_args['cat'] = $category_id; - } - - $query = new \WP_Query($query_args); - - echo $args['before_widget']; - echo '
      '; - - aksrt_home_widget_render_header( - (string) $title, - ($show_more && $term && ! is_wp_error($term)) ? (string) get_category_link($category_id) : '', - __('查看分类', 'aksrt') - ); - - aksrt_home_widget_render_post_grid($query, $layout, $columns, $context); - - echo '
      '; - echo $args['after_widget']; - } - - public function form($instance): void { - $title = (string) ($instance['title'] ?? ''); - $category_id = (int) ($instance['category_id'] ?? 0); - $count = (int) ($instance['count'] ?? 6); - $columns = (int) ($instance['columns'] ?? 3); - $layout = aksrt_home_widget_resolve_layout($instance['layout'] ?? 'card', 'card'); - $show_more = ! empty($instance['show_more']); - $categories = aksrt_home_widget_available_categories(); - ?> -

      - - -

      -

      - - -

      -

      - - -

      -

      - - -

      -

      - - -

      -

      - id="get_field_id('show_more')); ?>" name="get_field_name('show_more')); ?>" value="1"> - -

      - - sanitize_text_field($new_instance['title'] ?? ''), - 'category_id' => absint($new_instance['category_id'] ?? 0), - 'count' => max(1, min(12, absint($new_instance['count'] ?? 6))), - 'columns' => max(1, min(4, absint($new_instance['columns'] ?? 3))), - 'layout' => aksrt_home_widget_resolve_layout($new_instance['layout'] ?? 'card', 'card'), - 'show_more' => ! empty($new_instance['show_more']) ? 1 : 0, - ]; - } - - private function resolve_category_id(array $instance): int { - $category_id = absint($instance['category_id'] ?? 0); - if ($category_id > 0) { - return $category_id; - } - - $fallback = aksrt_home_widget_fallback_category_ids(1); - return $fallback[0] ?? 0; - } -} - /** * Homepage category tabs widget. */ diff --git a/src/AKSRT/Widgets/WidgetForumBoardList.php b/src/AKSRT/Widgets/WidgetForumBoardList.php index ba14e3ff..5ee4dc9d 100644 --- a/src/AKSRT/Widgets/WidgetForumBoardList.php +++ b/src/AKSRT/Widgets/WidgetForumBoardList.php @@ -7,181 +7,6 @@ namespace AKSRT\Widgets; defined('ABSPATH') || exit; -/** - * Forum-specific widgets. - * - * @package AKSRT - */ - -defined('ABSPATH') || exit; - -/** - * Forum Hot Topics Widget - */ -class WidgetForumHotTopics extends \WP_Widget { - - public function __construct() { - parent::__construct( - 'aksrt_forum_hot_topics', - __('论坛 - 热门话题', 'aksrt'), - ['description' => __('显示论坛热门话题列表。', 'aksrt')] - ); - } - - public function widget($args, $instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('热门话题', 'aksrt'); - $count = !empty($instance['count']) ? max(1, min(20, (int) $instance['count'])) : 5; - $period = !empty($instance['period']) ? sanitize_key($instance['period']) : 'week'; - - echo $args['before_widget']; - echo $args['before_title'] . esc_html($title) . $args['after_title']; - - $date_query = []; - if ($period === 'week') { - $date_query = [['after' => '7 days ago']]; - } elseif ($period === 'month') { - $date_query = [['after' => '30 days ago']]; - } - - $query = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'posts_per_page' => $count, - 'post_status' => 'publish', - 'meta_key' => '_aksrt_views', - 'orderby' => 'meta_value_num', - 'order' => 'DESC', - 'date_query' => $date_query, - ]); - - if ($query->have_posts()) { - echo '
        '; - $i = 0; - while ($query->have_posts()) { - $query->the_post(); - $i++; - $views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true); - echo '
      • '; - echo '' . $i . ''; - echo ''; - echo esc_html(get_the_title()); - echo ''; - echo ' ' . $views . ''; - echo '
      • '; - } - echo '
      '; - wp_reset_postdata(); - } else { - echo '

      ' . esc_html__('暂无话题', 'aksrt') . '

      '; - } - - echo $args['after_widget']; - } - - public function form($instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('热门话题', 'aksrt'); - $count = !empty($instance['count']) ? (int) $instance['count'] : 5; - $period = !empty($instance['period']) ? $instance['period'] : 'week'; - ?> -

      - - -

      -

      - - -

      -

      - - -

      - __('显示论坛最新话题列表。', 'aksrt')] - ); - } - - public function widget($args, $instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('最新话题', 'aksrt'); - $count = !empty($instance['count']) ? max(1, min(20, (int) $instance['count'])) : 5; - - echo $args['before_widget']; - echo $args['before_title'] . esc_html($title) . $args['after_title']; - - $query = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'posts_per_page' => $count, - 'post_status' => 'publish', - 'orderby' => 'date', - 'order' => 'DESC', - ]); - - if ($query->have_posts()) { - echo '
        '; - while ($query->have_posts()) { - $query->the_post(); - $author = get_the_author_meta('display_name'); - echo '
      • '; - echo ''; - echo esc_html(get_the_title()); - echo ''; - echo '' . esc_html($author) . ''; - echo '
      • '; - } - echo '
      '; - wp_reset_postdata(); - } else { - echo '

      ' . esc_html__('暂无话题', 'aksrt') . '

      '; - } - - echo $args['after_widget']; - } - - public function form($instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('最新话题', 'aksrt'); - $count = !empty($instance['count']) ? (int) $instance['count'] : 5; - ?> -

      - - -

      -

      - - -

      - __('显示论坛热门话题列表。', 'aksrt')] - ); - } - - public function widget($args, $instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('热门话题', 'aksrt'); - $count = !empty($instance['count']) ? max(1, min(20, (int) $instance['count'])) : 5; - $period = !empty($instance['period']) ? sanitize_key($instance['period']) : 'week'; - - echo $args['before_widget']; - echo $args['before_title'] . esc_html($title) . $args['after_title']; - - $date_query = []; - if ($period === 'week') { - $date_query = [['after' => '7 days ago']]; - } elseif ($period === 'month') { - $date_query = [['after' => '30 days ago']]; - } - - $query = new \WP_Query([ - 'post_type' => 'aksrt_forum_topic', - 'posts_per_page' => $count, - 'post_status' => 'publish', - 'meta_key' => '_aksrt_views', - 'orderby' => 'meta_value_num', - 'order' => 'DESC', - 'date_query' => $date_query, - ]); - - if ($query->have_posts()) { - echo '
        '; - $i = 0; - while ($query->have_posts()) { - $query->the_post(); - $i++; - $views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true); - echo '
      • '; - echo '' . $i . ''; - echo ''; - echo esc_html(get_the_title()); - echo ''; - echo ' ' . $views . ''; - echo '
      • '; - } - echo '
      '; - wp_reset_postdata(); - } else { - echo '

      ' . esc_html__('暂无话题', 'aksrt') . '

      '; - } - - echo $args['after_widget']; - } - - public function form($instance): void { - $title = !empty($instance['title']) ? $instance['title'] : __('热门话题', 'aksrt'); - $count = !empty($instance['count']) ? (int) $instance['count'] : 5; - $period = !empty($instance['period']) ? $instance['period'] : 'week'; - ?> -

      - - -

      -

      - - -

      -

      - - -

      - __('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')] - ); - } - - public function widget($args, $instance): void { - $title = apply_filters('widget_title', $instance['title'] ?? ''); - $per_page = max(1, min(50, (int) ($instance['per_page'] ?? 8))); - $orderby = sanitize_key($instance['orderby'] ?? 'date'); - $order = in_array(strtoupper($instance['order'] ?? ''), ['ASC', 'DESC'], true) ? strtoupper($instance['order']) : 'DESC'; - $cat_ids = array_filter(array_map('intval', explode(',', (string) ($instance['cat_ids'] ?? '')))); - $tag_ids = array_filter(array_map('intval', explode(',', (string) ($instance['tag_ids'] ?? '')))); - $discount_ids = array_filter(array_map('intval', explode(',', (string) ($instance['discount_ids'] ?? '')))); - $exclude_cat = !empty($instance['exclude_cat']); - $style = sanitize_key($instance['style'] ?? 'default'); - $paged = max(1, (int) ($_GET['aksrt_product_page'] ?? 1)); - - $query_args = [ - 'post_type' => 'aksrt_product', - 'post_status' => 'publish', - 'posts_per_page' => $per_page, - 'paged' => $paged, - 'orderby' => $orderby === 'sales' ? 'meta_value_num' : $orderby, - 'order' => $order, - ]; - - if ($orderby === 'sales') { - $query_args['meta_key'] = '_aksrt_sales'; - } - - // Taxonomy filters - $tax_query = []; - if (!empty($cat_ids)) { - $tax_query[] = [ - 'taxonomy' => 'aksrt_product_cat', - 'field' => 'term_id', - 'terms' => $cat_ids, - 'operator' => $exclude_cat ? 'NOT IN' : 'IN', - ]; - } - if (!empty($tag_ids)) { - $tax_query[] = [ - 'taxonomy' => 'aksrt_product_tag', - 'field' => 'term_id', - 'terms' => $tag_ids, - ]; - } - if (!empty($discount_ids)) { - $tax_query[] = [ - 'taxonomy' => 'shop_discount', - 'field' => 'term_id', - 'terms' => $discount_ids, - ]; - } - if (!empty($tax_query)) { - $query_args['tax_query'] = $tax_query; - } - - $products = new \WP_Query($query_args); - - echo $args['before_widget']; - if ($title) { - echo $args['before_title'] . esc_html($title) . $args['after_title']; - } - - $style_class = 'aksrt-product-widget--' . $style; - echo '
      '; - - if ($products->have_posts()) { - echo '
      '; - while ($products->have_posts()) { - $products->the_post(); - $pid = get_the_ID(); - $card = class_exists('\AKSRT\Shop\Shop') - ? \AKSRT\Shop\Shop::get_instance()->build_product_card_view($pid) - : []; - $price = (float) ($card['price'] ?? 0); - $image = $card['image'] ?? ''; - echo '
      '; - if ($image) { - echo ''; - echo '' . esc_attr(get_the_title($pid)) . ''; - echo ''; - } - echo '
      '; - echo '' . esc_html(get_the_title($pid)) . ''; - echo '
      '; - echo '¥' . esc_html(number_format_i18n($price, 2)) . ''; - if (!empty($card['sales'])) { - echo '' . esc_html($card['sales']) . __(' 已售', 'aksrt') . ''; - } - echo '
      '; - } - echo '
      '; - - // Pagination - if ($products->max_num_pages > 1) { - echo '
      '; - echo paginate_links([ - 'base' => add_query_arg('aksrt_product_page', '%#%'), - 'format' => '', - 'current' => $paged, - 'total' => $products->max_num_pages, - 'prev_text' => '«', - 'next_text' => '»', - ]); - echo '
      '; - } - } else { - echo '

      ' . esc_html__('暂无商品', 'aksrt') . '

      '; - } - - echo '
      '; - echo $args['after_widget']; - - wp_reset_postdata(); - } - - public function form($instance): void { - $defaults = [ - 'title' => __('商品推荐', 'aksrt'), - 'per_page' => 8, - 'orderby' => 'date', - 'order' => 'DESC', - 'cat_ids' => '', - 'tag_ids' => '', - 'discount_ids' => '', - 'exclude_cat' => false, - 'style' => 'default', - ]; - $instance = wp_parse_args($instance, $defaults); - ?> -

      - - -

      -

      - - -

      -

      - - -

      -

      - - -

      -

      - - -

      -

      - -

      -

      - - -

      -

      - - -

      -

      - - -

      - sanitize_text_field($new_instance['title'] ?? ''), - 'per_page' => max(1, min(50, (int) ($new_instance['per_page'] ?? 8))), - 'orderby' => sanitize_key($new_instance['orderby'] ?? 'date'), - 'order' => in_array(strtoupper($new_instance['order'] ?? ''), ['ASC', 'DESC'], true) ? strtoupper($new_instance['order']) : 'DESC', - 'cat_ids' => sanitize_text_field($new_instance['cat_ids'] ?? ''), - 'tag_ids' => sanitize_text_field($new_instance['tag_ids'] ?? ''), - 'discount_ids' => sanitize_text_field($new_instance['discount_ids'] ?? ''), - 'exclude_cat' => !empty($new_instance['exclude_cat']), - 'style' => sanitize_key($new_instance['style'] ?? 'default'), - ]; - } -} - /** * Horizontal swiper product widget — equivalent to zibll `zib_shop_widget_ui_oneline_product_lists`. */ diff --git a/src/AKSRT/Widgets/WidgetProductTab.php b/src/AKSRT/Widgets/WidgetProductTab.php index 450cd77c..a54e4a9d 100644 --- a/src/AKSRT/Widgets/WidgetProductTab.php +++ b/src/AKSRT/Widgets/WidgetProductTab.php @@ -7,254 +7,6 @@ namespace AKSRT\Widgets; defined('ABSPATH') || exit; -/** - * Shop product widgets (§6.9) - * - * - WidgetProductList: product list widget (filter/sort/paginate/AJAX) - * - WidgetProductOneline: horizontal swiper product widget - * - WidgetProductTab: multi-tab product list widget - * - * @package AKSRT - */ - -defined('ABSPATH') || exit; - -/** - * Product list widget — equivalent to zibll `zib_shop_widget_ui_product_lists`. - * - * Supports: include/exclude category, discount, tag; sort order; list style; - * AJAX pagination; per-page count. - */ -class WidgetProductList extends \WP_Widget { - - public function __construct() { - parent::__construct( - 'aksrt_product_list', - __('商品列表', 'aksrt'), - ['description' => __('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')] - ); - } - - public function widget($args, $instance): void { - $title = apply_filters('widget_title', $instance['title'] ?? ''); - $per_page = max(1, min(50, (int) ($instance['per_page'] ?? 8))); - $orderby = sanitize_key($instance['orderby'] ?? 'date'); - $order = in_array(strtoupper($instance['order'] ?? ''), ['ASC', 'DESC'], true) ? strtoupper($instance['order']) : 'DESC'; - $cat_ids = array_filter(array_map('intval', explode(',', (string) ($instance['cat_ids'] ?? '')))); - $tag_ids = array_filter(array_map('intval', explode(',', (string) ($instance['tag_ids'] ?? '')))); - $discount_ids = array_filter(array_map('intval', explode(',', (string) ($instance['discount_ids'] ?? '')))); - $exclude_cat = !empty($instance['exclude_cat']); - $style = sanitize_key($instance['style'] ?? 'default'); - $paged = max(1, (int) ($_GET['aksrt_product_page'] ?? 1)); - - $query_args = [ - 'post_type' => 'aksrt_product', - 'post_status' => 'publish', - 'posts_per_page' => $per_page, - 'paged' => $paged, - 'orderby' => $orderby === 'sales' ? 'meta_value_num' : $orderby, - 'order' => $order, - ]; - - if ($orderby === 'sales') { - $query_args['meta_key'] = '_aksrt_sales'; - } - - // Taxonomy filters - $tax_query = []; - if (!empty($cat_ids)) { - $tax_query[] = [ - 'taxonomy' => 'aksrt_product_cat', - 'field' => 'term_id', - 'terms' => $cat_ids, - 'operator' => $exclude_cat ? 'NOT IN' : 'IN', - ]; - } - if (!empty($tag_ids)) { - $tax_query[] = [ - 'taxonomy' => 'aksrt_product_tag', - 'field' => 'term_id', - 'terms' => $tag_ids, - ]; - } - if (!empty($discount_ids)) { - $tax_query[] = [ - 'taxonomy' => 'shop_discount', - 'field' => 'term_id', - 'terms' => $discount_ids, - ]; - } - if (!empty($tax_query)) { - $query_args['tax_query'] = $tax_query; - } - - $products = new \WP_Query($query_args); - - echo $args['before_widget']; - if ($title) { - echo $args['before_title'] . esc_html($title) . $args['after_title']; - } - - $style_class = 'aksrt-product-widget--' . $style; - echo '
      '; - - if ($products->have_posts()) { - echo '
      '; - while ($products->have_posts()) { - $products->the_post(); - $pid = get_the_ID(); - $card = class_exists('\AKSRT\Shop\Shop') - ? \AKSRT\Shop\Shop::get_instance()->build_product_card_view($pid) - : []; - $price = (float) ($card['price'] ?? 0); - $image = $card['image'] ?? ''; - echo '
      '; - if ($image) { - echo ''; - echo '' . esc_attr(get_the_title($pid)) . ''; - echo ''; - } - echo '
      '; - echo '' . esc_html(get_the_title($pid)) . ''; - echo '
      '; - echo '¥' . esc_html(number_format_i18n($price, 2)) . ''; - if (!empty($card['sales'])) { - echo '' . esc_html($card['sales']) . __(' 已售', 'aksrt') . ''; - } - echo '
      '; - } - echo '
      '; - - // Pagination - if ($products->max_num_pages > 1) { - echo '
      '; - echo paginate_links([ - 'base' => add_query_arg('aksrt_product_page', '%#%'), - 'format' => '', - 'current' => $paged, - 'total' => $products->max_num_pages, - 'prev_text' => '«', - 'next_text' => '»', - ]); - echo '
      '; - } - } else { - echo '

      ' . esc_html__('暂无商品', 'aksrt') . '

      '; - } - - echo '
      '; - echo $args['after_widget']; - - wp_reset_postdata(); - } - - public function form($instance): void { - $defaults = [ - 'title' => __('商品推荐', 'aksrt'), - 'per_page' => 8, - 'orderby' => 'date', - 'order' => 'DESC', - 'cat_ids' => '', - 'tag_ids' => '', - 'discount_ids' => '', - 'exclude_cat' => false, - 'style' => 'default', - ]; - $instance = wp_parse_args($instance, $defaults); - ?> -

      - - -

      -

      - - -

      -

      - - -

      -

      - - -

      -

      - - -

      -

      - -

      -

      - - -

      -

      - - -

      -

      - - -

      - sanitize_text_field($new_instance['title'] ?? ''), - 'per_page' => max(1, min(50, (int) ($new_instance['per_page'] ?? 8))), - 'orderby' => sanitize_key($new_instance['orderby'] ?? 'date'), - 'order' => in_array(strtoupper($new_instance['order'] ?? ''), ['ASC', 'DESC'], true) ? strtoupper($new_instance['order']) : 'DESC', - 'cat_ids' => sanitize_text_field($new_instance['cat_ids'] ?? ''), - 'tag_ids' => sanitize_text_field($new_instance['tag_ids'] ?? ''), - 'discount_ids' => sanitize_text_field($new_instance['discount_ids'] ?? ''), - 'exclude_cat' => !empty($new_instance['exclude_cat']), - 'style' => sanitize_key($new_instance['style'] ?? 'default'), - ]; - } -} - -/** - * Horizontal swiper product widget — equivalent to zibll `zib_shop_widget_ui_oneline_product_lists`. - */ -class WidgetProductOneline extends WidgetProductList { - - public function __construct() { - \WP_Widget::__construct( - 'aksrt_product_oneline', - __('横滑商品', 'aksrt'), - ['description' => __('AKSRT 单行横滑商品 Swiper 组件。', 'aksrt')] - ); - } - - public function widget($args, $instance): void { - $instance['style'] = 'oneline'; - parent::widget($args, $instance); - } - - public function form($instance): void { - $instance = wp_parse_args($instance, ['style' => 'oneline']); - parent::form($instance); - } -} - /** * Multi-tab product widget — equivalent to zibll `zib_shop_widget_ui_tab_product`. * Each tab has independent category/discount filter.