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.
This commit is contained in:
@@ -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<gateway>[a-z0-9_]+)/notify', array_merge($args, [
|
||||
'callback' => [$this, 'handle_notify'],
|
||||
]));
|
||||
register_rest_route('aksrt/v1', '/payment/(?P<gateway>[a-z0-9_]+)/return', array_merge($args, [
|
||||
'callback' => [$this, 'handle_return'],
|
||||
]));
|
||||
register_rest_route('aksrt/v1', '/payment/(?P<gateway>[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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,17 +7,6 @@ namespace AKSRT\Payment;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* AKSRT Payment Gateway — 支付网关适配器层
|
||||
*
|
||||
* 定义支付网关统一接口和基础实现。
|
||||
* 具体网关(支付宝、微信支付等)通过子类实现。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* 支付网关统一接口
|
||||
*/
|
||||
|
||||
@@ -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<string, string>
|
||||
*/
|
||||
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<int, string>
|
||||
*/
|
||||
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<int, \WP_Term>
|
||||
*/
|
||||
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<int, int>
|
||||
*/
|
||||
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<int, int>
|
||||
*/
|
||||
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 {
|
||||
?>
|
||||
<header class="aksrt-home-widget__header">
|
||||
<h3 class="aksrt-home-widget__title"><?php echo esc_html($title); ?></h3>
|
||||
<?php if ($more_link !== '') : ?>
|
||||
<a class="aksrt-home-widget__more" href="<?php echo esc_url($more_link); ?>">
|
||||
<?php echo esc_html($more_text !== '' ? $more_text : __('查看更多', 'aksrt')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_home_widget_render_post_grid')) {
|
||||
/**
|
||||
* Render posts for homepage widgets.
|
||||
*
|
||||
* Delegates to the unified shared renderer (aksrt_render_post_list).
|
||||
*
|
||||
* @param \WP_Query $query Posts query.
|
||||
* @param string $layout Card layout.
|
||||
* @param int $columns Desktop columns.
|
||||
* @param string $context Sidebar or homepage context.
|
||||
* @return void
|
||||
*/
|
||||
function aksrt_home_widget_render_post_grid(\WP_Query $query, string $layout = 'card', int $columns = 3, string $context = 'homepage'): void {
|
||||
if (!$query->have_posts()) {
|
||||
echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无内容', 'aksrt')) : '<p class="widget-empty">' . esc_html__('暂无内容', 'aksrt') . '</p>';
|
||||
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 '<div class="widget-tab-posts__list widget-tab-posts__list--mini aksrt-home-widget__sidebar-list">';
|
||||
aksrt_render_mini_list($query, ['context' => 'sidebar', 'show_thumb' => true]);
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: simple sidebar list
|
||||
echo '<ul class="widget-tab-posts__list aksrt-home-widget__sidebar-list">';
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
?>
|
||||
<li class="widget-tab-posts__item">
|
||||
<?php if (has_post_thumbnail()) : ?>
|
||||
<a href="<?php the_permalink(); ?>" class="widget-tab-posts__thumb">
|
||||
<?php the_post_thumbnail('thumbnail', ['loading' => 'lazy', 'class' => 'fit-cover']); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="widget-tab-posts__info">
|
||||
<a href="<?php the_permalink(); ?>" class="widget-tab-posts__title"><?php the_title(); ?></a>
|
||||
<span class="widget-tab-posts__meta">
|
||||
<?php echo esc_html(human_time_diff(get_the_time('U'), current_time('timestamp')) . __('前', 'aksrt')); ?>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<?php
|
||||
}
|
||||
echo '</ul>';
|
||||
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(
|
||||
'<div class="%1$s" style="--aksrt-widget-columns:%2$d;">',
|
||||
esc_attr(implode(' ', $classes)),
|
||||
(int) $columns
|
||||
);
|
||||
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
get_template_part('templates/parts/content', 'post-card');
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
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 '<section class="aksrt-home-widget aksrt-home-widget--category-block">';
|
||||
|
||||
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 '</section>';
|
||||
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();
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('category_id')); ?>"><?php esc_html_e('分类:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('category_id')); ?>" name="<?php echo esc_attr($this->get_field_name('category_id')); ?>">
|
||||
<option value="0"><?php esc_html_e('自动选择最热门分类', 'aksrt'); ?></option>
|
||||
<?php foreach ($categories as $category) : ?>
|
||||
<option value="<?php echo (int) $category->term_id; ?>" <?php selected($category_id, (int) $category->term_id); ?>>
|
||||
<?php echo esc_html($category->name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('文章数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" min="1" max="12" value="<?php echo (int) $count; ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('columns')); ?>"><?php esc_html_e('桌面列数:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('columns')); ?>" name="<?php echo esc_attr($this->get_field_name('columns')); ?>" type="number" min="1" max="4" value="<?php echo (int) $columns; ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('layout')); ?>"><?php esc_html_e('布局:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('layout')); ?>" name="<?php echo esc_attr($this->get_field_name('layout')); ?>">
|
||||
<?php foreach (aksrt_home_widget_allowed_layouts() as $value => $label) : ?>
|
||||
<option value="<?php echo esc_attr($value); ?>" <?php selected($layout, $value); ?>>
|
||||
<?php echo esc_html($label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" <?php checked($show_more); ?> id="<?php echo esc_attr($this->get_field_id('show_more')); ?>" name="<?php echo esc_attr($this->get_field_name('show_more')); ?>" value="1">
|
||||
<label for="<?php echo esc_attr($this->get_field_id('show_more')); ?>"><?php esc_html_e('显示分类链接', 'aksrt'); ?></label>
|
||||
</p>
|
||||
<?php if (function_exists('aksrt_widget_render_location_help')) { aksrt_widget_render_location_help('hybrid'); } ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => 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 '<section class="aksrt-home-widget aksrt-home-widget--tabs widget-tab-posts">';
|
||||
|
||||
if (empty($tabs)) {
|
||||
echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无可用分类', 'aksrt')) : '<p class="widget-empty">' . esc_html__('暂无可用分类', 'aksrt') . '</p>';
|
||||
echo '</section>';
|
||||
echo $args['after_widget'];
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<header class="aksrt-home-widget__header aksrt-home-widget__header--tabs">
|
||||
<h3 class="aksrt-home-widget__title"><?php echo esc_html($title); ?></h3>
|
||||
<ul class="widget-tab-posts__nav aksrt-home-widget__tab-nav" role="tablist">
|
||||
<?php foreach ($tabs as $index => $tab) : ?>
|
||||
<li role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="widget-tab-posts__tab<?php echo 0 === $index ? ' active' : ''; ?>"
|
||||
data-tab="cat-<?php echo (int) $tab['id']; ?>"
|
||||
aria-selected="<?php echo 0 === $index ? 'true' : 'false'; ?>"
|
||||
>
|
||||
<?php echo esc_html($tab['name']); ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</header>
|
||||
<?php
|
||||
|
||||
foreach ($tabs as $index => $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'],
|
||||
]);
|
||||
?>
|
||||
<div class="widget-tab-posts__panel<?php echo 0 === $index ? ' active' : ''; ?>" data-tab-panel="cat-<?php echo (int) $tab['id']; ?>" role="tabpanel"<?php echo 0 === $index ? '' : ' hidden'; ?>>
|
||||
<?php aksrt_home_widget_render_post_grid($query, $layout, $columns, $context); ?>
|
||||
<?php if ($show_more) : ?>
|
||||
<div class="aksrt-home-widget__footer">
|
||||
<a class="aksrt-home-widget__more" href="<?php echo esc_url($tab['link']); ?>">
|
||||
<?php esc_html_e('查看更多', 'aksrt'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
echo '</section>';
|
||||
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();
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('category_ids')); ?>"><?php esc_html_e('分类(可多选):', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('category_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('category_ids')); ?>[]" multiple size="6">
|
||||
<?php foreach ($categories as $category) : ?>
|
||||
<option value="<?php echo (int) $category->term_id; ?>" <?php selected(in_array((int) $category->term_id, $category_ids, true)); ?>>
|
||||
<?php echo esc_html($category->name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small><?php esc_html_e('留空时自动选择热门分类。', 'aksrt'); ?></small>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('每个 Tab 的文章数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" min="1" max="12" value="<?php echo (int) $count; ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('columns')); ?>"><?php esc_html_e('桌面列数:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('columns')); ?>" name="<?php echo esc_attr($this->get_field_name('columns')); ?>" type="number" min="1" max="4" value="<?php echo (int) $columns; ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('layout')); ?>"><?php esc_html_e('布局:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('layout')); ?>" name="<?php echo esc_attr($this->get_field_name('layout')); ?>">
|
||||
<?php foreach (aksrt_home_widget_allowed_layouts() as $value => $label) : ?>
|
||||
<option value="<?php echo esc_attr($value); ?>" <?php selected($layout, $value); ?>>
|
||||
<?php echo esc_html($label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" <?php checked($show_more); ?> id="<?php echo esc_attr($this->get_field_id('show_more')); ?>" name="<?php echo esc_attr($this->get_field_name('show_more')); ?>" value="1">
|
||||
<label for="<?php echo esc_attr($this->get_field_id('show_more')); ?>"><?php esc_html_e('显示“查看更多”链接', 'aksrt'); ?></label>
|
||||
</p>
|
||||
<?php if (function_exists('aksrt_widget_render_location_help')) { aksrt_widget_render_location_help('hybrid'); } ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => 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<int, array{id:int,name:string,link:string}>
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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<string, string>
|
||||
*/
|
||||
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<int, string>
|
||||
*/
|
||||
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<int, \WP_Term>
|
||||
*/
|
||||
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<int, int>
|
||||
*/
|
||||
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<int, int>
|
||||
*/
|
||||
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 {
|
||||
?>
|
||||
<header class="aksrt-home-widget__header">
|
||||
<h3 class="aksrt-home-widget__title"><?php echo esc_html($title); ?></h3>
|
||||
<?php if ($more_link !== '') : ?>
|
||||
<a class="aksrt-home-widget__more" href="<?php echo esc_url($more_link); ?>">
|
||||
<?php echo esc_html($more_text !== '' ? $more_text : __('查看更多', 'aksrt')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_home_widget_render_post_grid')) {
|
||||
/**
|
||||
* Render posts for homepage widgets.
|
||||
*
|
||||
* Delegates to the unified shared renderer (aksrt_render_post_list).
|
||||
*
|
||||
* @param \WP_Query $query Posts query.
|
||||
* @param string $layout Card layout.
|
||||
* @param int $columns Desktop columns.
|
||||
* @param string $context Sidebar or homepage context.
|
||||
* @return void
|
||||
*/
|
||||
function aksrt_home_widget_render_post_grid(\WP_Query $query, string $layout = 'card', int $columns = 3, string $context = 'homepage'): void {
|
||||
if (!$query->have_posts()) {
|
||||
echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无内容', 'aksrt')) : '<p class="widget-empty">' . esc_html__('暂无内容', 'aksrt') . '</p>';
|
||||
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 '<div class="widget-tab-posts__list widget-tab-posts__list--mini aksrt-home-widget__sidebar-list">';
|
||||
aksrt_render_mini_list($query, ['context' => 'sidebar', 'show_thumb' => true]);
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: simple sidebar list
|
||||
echo '<ul class="widget-tab-posts__list aksrt-home-widget__sidebar-list">';
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
?>
|
||||
<li class="widget-tab-posts__item">
|
||||
<?php if (has_post_thumbnail()) : ?>
|
||||
<a href="<?php the_permalink(); ?>" class="widget-tab-posts__thumb">
|
||||
<?php the_post_thumbnail('thumbnail', ['loading' => 'lazy', 'class' => 'fit-cover']); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="widget-tab-posts__info">
|
||||
<a href="<?php the_permalink(); ?>" class="widget-tab-posts__title"><?php the_title(); ?></a>
|
||||
<span class="widget-tab-posts__meta">
|
||||
<?php echo esc_html(human_time_diff(get_the_time('U'), current_time('timestamp')) . __('前', 'aksrt')); ?>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<?php
|
||||
}
|
||||
echo '</ul>';
|
||||
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(
|
||||
'<div class="%1$s" style="--aksrt-widget-columns:%2$d;">',
|
||||
esc_attr(implode(' ', $classes)),
|
||||
(int) $columns
|
||||
);
|
||||
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
get_template_part('templates/parts/content', 'post-card');
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
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 '<section class="aksrt-home-widget aksrt-home-widget--category-block">';
|
||||
|
||||
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 '</section>';
|
||||
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();
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('category_id')); ?>"><?php esc_html_e('分类:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('category_id')); ?>" name="<?php echo esc_attr($this->get_field_name('category_id')); ?>">
|
||||
<option value="0"><?php esc_html_e('自动选择最热门分类', 'aksrt'); ?></option>
|
||||
<?php foreach ($categories as $category) : ?>
|
||||
<option value="<?php echo (int) $category->term_id; ?>" <?php selected($category_id, (int) $category->term_id); ?>>
|
||||
<?php echo esc_html($category->name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('文章数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" min="1" max="12" value="<?php echo (int) $count; ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('columns')); ?>"><?php esc_html_e('桌面列数:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('columns')); ?>" name="<?php echo esc_attr($this->get_field_name('columns')); ?>" type="number" min="1" max="4" value="<?php echo (int) $columns; ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('layout')); ?>"><?php esc_html_e('布局:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('layout')); ?>" name="<?php echo esc_attr($this->get_field_name('layout')); ?>">
|
||||
<?php foreach (aksrt_home_widget_allowed_layouts() as $value => $label) : ?>
|
||||
<option value="<?php echo esc_attr($value); ?>" <?php selected($layout, $value); ?>>
|
||||
<?php echo esc_html($label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" <?php checked($show_more); ?> id="<?php echo esc_attr($this->get_field_id('show_more')); ?>" name="<?php echo esc_attr($this->get_field_name('show_more')); ?>" value="1">
|
||||
<label for="<?php echo esc_attr($this->get_field_id('show_more')); ?>"><?php esc_html_e('显示分类链接', 'aksrt'); ?></label>
|
||||
</p>
|
||||
<?php if (function_exists('aksrt_widget_render_location_help')) { aksrt_widget_render_location_help('hybrid'); } ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => 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.
|
||||
*/
|
||||
|
||||
@@ -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 '<ul class="aksrt-forum-widget__list">';
|
||||
$i = 0;
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$i++;
|
||||
$views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true);
|
||||
echo '<li class="aksrt-forum-widget__item">';
|
||||
echo '<span class="aksrt-forum-widget__rank">' . $i . '</span>';
|
||||
echo '<a href="' . esc_url(get_permalink()) . '" class="aksrt-forum-widget__link">';
|
||||
echo esc_html(get_the_title());
|
||||
echo '</a>';
|
||||
echo '<span class="aksrt-forum-widget__views"><i class="fas fa-eye"></i> ' . $views . '</span>';
|
||||
echo '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
wp_reset_postdata();
|
||||
} else {
|
||||
echo '<p class="aksrt-forum-widget__empty">' . esc_html__('暂无话题', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
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';
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" value="<?php echo esc_attr($count); ?>" min="1" max="20">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('period')); ?>"><?php esc_html_e('时间范围:', 'aksrt'); ?></label>
|
||||
<select id="<?php echo esc_attr($this->get_field_id('period')); ?>" name="<?php echo esc_attr($this->get_field_name('period')); ?>">
|
||||
<option value="all" <?php selected($period, 'all'); ?>><?php esc_html_e('全部', 'aksrt'); ?></option>
|
||||
<option value="week" <?php selected($period, 'week'); ?>><?php esc_html_e('最近一周', 'aksrt'); ?></option>
|
||||
<option value="month" <?php selected($period, 'month'); ?>><?php esc_html_e('最近一月', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$instance = [];
|
||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
||||
$instance['count'] = max(1, min(20, (int) ($new_instance['count'] ?? 5)));
|
||||
$instance['period'] = in_array($new_instance['period'] ?? '', ['all', 'week', 'month'], true) ? $new_instance['period'] : 'week';
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forum Recent Topics Widget
|
||||
*/
|
||||
class WidgetForumRecentTopics extends \WP_Widget {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'aksrt_forum_recent_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;
|
||||
|
||||
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 '<ul class="aksrt-forum-widget__list">';
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$author = get_the_author_meta('display_name');
|
||||
echo '<li class="aksrt-forum-widget__item">';
|
||||
echo '<a href="' . esc_url(get_permalink()) . '" class="aksrt-forum-widget__link">';
|
||||
echo esc_html(get_the_title());
|
||||
echo '</a>';
|
||||
echo '<span class="aksrt-forum-widget__author">' . esc_html($author) . '</span>';
|
||||
echo '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
wp_reset_postdata();
|
||||
} else {
|
||||
echo '<p class="aksrt-forum-widget__empty">' . esc_html__('暂无话题', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
echo $args['after_widget'];
|
||||
}
|
||||
|
||||
public function form($instance): void {
|
||||
$title = !empty($instance['title']) ? $instance['title'] : __('最新话题', 'aksrt');
|
||||
$count = !empty($instance['count']) ? (int) $instance['count'] : 5;
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" value="<?php echo esc_attr($count); ?>" min="1" max="20">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$instance = [];
|
||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
||||
$instance['count'] = max(1, min(20, (int) ($new_instance['count'] ?? 5)));
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forum Board List Widget
|
||||
*/
|
||||
|
||||
@@ -7,109 +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 '<ul class="aksrt-forum-widget__list">';
|
||||
$i = 0;
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$i++;
|
||||
$views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true);
|
||||
echo '<li class="aksrt-forum-widget__item">';
|
||||
echo '<span class="aksrt-forum-widget__rank">' . $i . '</span>';
|
||||
echo '<a href="' . esc_url(get_permalink()) . '" class="aksrt-forum-widget__link">';
|
||||
echo esc_html(get_the_title());
|
||||
echo '</a>';
|
||||
echo '<span class="aksrt-forum-widget__views"><i class="fas fa-eye"></i> ' . $views . '</span>';
|
||||
echo '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
wp_reset_postdata();
|
||||
} else {
|
||||
echo '<p class="aksrt-forum-widget__empty">' . esc_html__('暂无话题', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
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';
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" value="<?php echo esc_attr($count); ?>" min="1" max="20">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('period')); ?>"><?php esc_html_e('时间范围:', 'aksrt'); ?></label>
|
||||
<select id="<?php echo esc_attr($this->get_field_id('period')); ?>" name="<?php echo esc_attr($this->get_field_name('period')); ?>">
|
||||
<option value="all" <?php selected($period, 'all'); ?>><?php esc_html_e('全部', 'aksrt'); ?></option>
|
||||
<option value="week" <?php selected($period, 'week'); ?>><?php esc_html_e('最近一周', 'aksrt'); ?></option>
|
||||
<option value="month" <?php selected($period, 'month'); ?>><?php esc_html_e('最近一月', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$instance = [];
|
||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
||||
$instance['count'] = max(1, min(20, (int) ($new_instance['count'] ?? 5)));
|
||||
$instance['period'] = in_array($new_instance['period'] ?? '', ['all', 'week', 'month'], true) ? $new_instance['period'] : 'week';
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forum Recent Topics Widget
|
||||
*/
|
||||
|
||||
@@ -7,230 +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 '<div class="aksrt-product-widget ' . esc_attr($style_class) . '" data-aksrt-product-widget>';
|
||||
|
||||
if ($products->have_posts()) {
|
||||
echo '<div class="aksrt-product-widget__grid">';
|
||||
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 '<div class="aksrt-product-widget__item">';
|
||||
if ($image) {
|
||||
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__thumb">';
|
||||
echo '<img src="' . esc_url($image) . '" alt="' . esc_attr(get_the_title($pid)) . '" loading="lazy">';
|
||||
echo '</a>';
|
||||
}
|
||||
echo '<div class="aksrt-product-widget__info">';
|
||||
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__title">' . esc_html(get_the_title($pid)) . '</a>';
|
||||
echo '<div class="aksrt-product-widget__meta">';
|
||||
echo '<span class="aksrt-product-widget__price">¥' . esc_html(number_format_i18n($price, 2)) . '</span>';
|
||||
if (!empty($card['sales'])) {
|
||||
echo '<span class="aksrt-product-widget__sales">' . esc_html($card['sales']) . __(' 已售', 'aksrt') . '</span>';
|
||||
}
|
||||
echo '</div></div></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
// Pagination
|
||||
if ($products->max_num_pages > 1) {
|
||||
echo '<div class="aksrt-product-widget__pagination">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('aksrt_product_page', '%#%'),
|
||||
'format' => '',
|
||||
'current' => $paged,
|
||||
'total' => $products->max_num_pages,
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
]);
|
||||
echo '</div>';
|
||||
}
|
||||
} else {
|
||||
echo '<p class="aksrt-product-widget__empty">' . esc_html__('暂无商品', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
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);
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr($instance['title']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('per_page')); ?>"><?php esc_html_e('每页数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" type="number" min="1" max="50" id="<?php echo esc_attr($this->get_field_id('per_page')); ?>" name="<?php echo esc_attr($this->get_field_name('per_page')); ?>" value="<?php echo esc_attr((string) $instance['per_page']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('orderby')); ?>"><?php esc_html_e('排序:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('orderby')); ?>" name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
|
||||
<option value="date" <?php selected($instance['orderby'], 'date'); ?>><?php esc_html_e('最新', 'aksrt'); ?></option>
|
||||
<option value="sales" <?php selected($instance['orderby'], 'sales'); ?>><?php esc_html_e('销量', 'aksrt'); ?></option>
|
||||
<option value="title" <?php selected($instance['orderby'], 'title'); ?>><?php esc_html_e('标题', 'aksrt'); ?></option>
|
||||
<option value="rand" <?php selected($instance['orderby'], 'rand'); ?>><?php esc_html_e('随机', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('order')); ?>"><?php esc_html_e('方向:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('order')); ?>" name="<?php echo esc_attr($this->get_field_name('order')); ?>">
|
||||
<option value="DESC" <?php selected($instance['order'], 'DESC'); ?>><?php esc_html_e('降序', 'aksrt'); ?></option>
|
||||
<option value="ASC" <?php selected($instance['order'], 'ASC'); ?>><?php esc_html_e('升序', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>"><?php esc_html_e('分类ID(逗号分隔):', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('cat_ids')); ?>" value="<?php echo esc_attr($instance['cat_ids']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="<?php echo esc_attr($this->get_field_name('exclude_cat')); ?>" value="1" <?php checked($instance['exclude_cat']); ?>>
|
||||
<?php esc_html_e('排除上述分类', 'aksrt'); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>"><?php esc_html_e('标签ID(逗号分隔):', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('tag_ids')); ?>" value="<?php echo esc_attr($instance['tag_ids']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>"><?php esc_html_e('优惠活动ID(逗号分隔):', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('discount_ids')); ?>" value="<?php echo esc_attr($instance['discount_ids']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('style')); ?>"><?php esc_html_e('列表样式:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('style')); ?>" name="<?php echo esc_attr($this->get_field_name('style')); ?>">
|
||||
<option value="default" <?php selected($instance['style'], 'default'); ?>><?php esc_html_e('默认', 'aksrt'); ?></option>
|
||||
<option value="card" <?php selected($instance['style'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
|
||||
<option value="list" <?php selected($instance['style'], 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
|
||||
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => 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`.
|
||||
*/
|
||||
|
||||
@@ -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 '<div class="aksrt-product-widget ' . esc_attr($style_class) . '" data-aksrt-product-widget>';
|
||||
|
||||
if ($products->have_posts()) {
|
||||
echo '<div class="aksrt-product-widget__grid">';
|
||||
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 '<div class="aksrt-product-widget__item">';
|
||||
if ($image) {
|
||||
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__thumb">';
|
||||
echo '<img src="' . esc_url($image) . '" alt="' . esc_attr(get_the_title($pid)) . '" loading="lazy">';
|
||||
echo '</a>';
|
||||
}
|
||||
echo '<div class="aksrt-product-widget__info">';
|
||||
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__title">' . esc_html(get_the_title($pid)) . '</a>';
|
||||
echo '<div class="aksrt-product-widget__meta">';
|
||||
echo '<span class="aksrt-product-widget__price">¥' . esc_html(number_format_i18n($price, 2)) . '</span>';
|
||||
if (!empty($card['sales'])) {
|
||||
echo '<span class="aksrt-product-widget__sales">' . esc_html($card['sales']) . __(' 已售', 'aksrt') . '</span>';
|
||||
}
|
||||
echo '</div></div></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
// Pagination
|
||||
if ($products->max_num_pages > 1) {
|
||||
echo '<div class="aksrt-product-widget__pagination">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('aksrt_product_page', '%#%'),
|
||||
'format' => '',
|
||||
'current' => $paged,
|
||||
'total' => $products->max_num_pages,
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
]);
|
||||
echo '</div>';
|
||||
}
|
||||
} else {
|
||||
echo '<p class="aksrt-product-widget__empty">' . esc_html__('暂无商品', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
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);
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr($instance['title']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('per_page')); ?>"><?php esc_html_e('每页数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" type="number" min="1" max="50" id="<?php echo esc_attr($this->get_field_id('per_page')); ?>" name="<?php echo esc_attr($this->get_field_name('per_page')); ?>" value="<?php echo esc_attr((string) $instance['per_page']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('orderby')); ?>"><?php esc_html_e('排序:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('orderby')); ?>" name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
|
||||
<option value="date" <?php selected($instance['orderby'], 'date'); ?>><?php esc_html_e('最新', 'aksrt'); ?></option>
|
||||
<option value="sales" <?php selected($instance['orderby'], 'sales'); ?>><?php esc_html_e('销量', 'aksrt'); ?></option>
|
||||
<option value="title" <?php selected($instance['orderby'], 'title'); ?>><?php esc_html_e('标题', 'aksrt'); ?></option>
|
||||
<option value="rand" <?php selected($instance['orderby'], 'rand'); ?>><?php esc_html_e('随机', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('order')); ?>"><?php esc_html_e('方向:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('order')); ?>" name="<?php echo esc_attr($this->get_field_name('order')); ?>">
|
||||
<option value="DESC" <?php selected($instance['order'], 'DESC'); ?>><?php esc_html_e('降序', 'aksrt'); ?></option>
|
||||
<option value="ASC" <?php selected($instance['order'], 'ASC'); ?>><?php esc_html_e('升序', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>"><?php esc_html_e('分类ID(逗号分隔):', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('cat_ids')); ?>" value="<?php echo esc_attr($instance['cat_ids']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="<?php echo esc_attr($this->get_field_name('exclude_cat')); ?>" value="1" <?php checked($instance['exclude_cat']); ?>>
|
||||
<?php esc_html_e('排除上述分类', 'aksrt'); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>"><?php esc_html_e('标签ID(逗号分隔):', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('tag_ids')); ?>" value="<?php echo esc_attr($instance['tag_ids']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>"><?php esc_html_e('优惠活动ID(逗号分隔):', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('discount_ids')); ?>" value="<?php echo esc_attr($instance['discount_ids']); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('style')); ?>"><?php esc_html_e('列表样式:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('style')); ?>" name="<?php echo esc_attr($this->get_field_name('style')); ?>">
|
||||
<option value="default" <?php selected($instance['style'], 'default'); ?>><?php esc_html_e('默认', 'aksrt'); ?></option>
|
||||
<option value="card" <?php selected($instance['style'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
|
||||
<option value="list" <?php selected($instance['style'], 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
|
||||
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => 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.
|
||||
|
||||
Reference in New Issue
Block a user