diff --git a/assets/css/frontend/widget-batch5.css b/assets/css/frontend/widget-batch5.css new file mode 100644 index 00000000..ccb27575 --- /dev/null +++ b/assets/css/frontend/widget-batch5.css @@ -0,0 +1,554 @@ +/** + * Batch5: user card guest/login depth, main posts, term aggregation. + */ + +.aksrt-user-card { + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); + text-align: center; + margin-bottom: 12px; +} +.aksrt-user-card__cover { + height: 90px; + background: linear-gradient(135deg, #93c5fd, #c4b5fd) center/cover no-repeat; +} +.aksrt-user-card__cover.is-empty { + background: linear-gradient(135deg, #e2e8f0, #f8fafc); +} +.aksrt-user-card__body { padding: 0 14px 16px; position: relative; } +.aksrt-user-card__checkin { + position: absolute; + right: 10px; + top: 8px; + z-index: 2; + max-width: 48%; +} +.aksrt-user-card__checkin .but { + padding: 4px 8px !important; + font-size: 12px !important; + line-height: 1.2; +} +.aksrt-user-card__checkin .aksrt-header-checkin-details { display: none; } +.aksrt-user-card__avatar { + display: inline-block; + margin-top: -36px; + width: 72px; + height: 72px; + border-radius: 50%; + overflow: hidden; + border: 3px solid var(--surface-1, #fff); + background: var(--surface-2, #eee); +} +.aksrt-user-card__avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } +.aksrt-user-card__name { + margin: 10px 0 0; + font-size: 16px; + font-weight: 700; +} +.aksrt-user-card__name a { color: inherit; text-decoration: none; } +.aksrt-user-card__bio, +.aksrt-user-card__guest-title { + margin: 8px 0 0; + font-size: 12px; + color: var(--muted-2-color, #888); + line-height: 1.5; +} +.aksrt-user-card__guest-title { font-size: 14px; font-weight: 600; color: inherit; } +.aksrt-user-card__stats { + display: flex; + justify-content: center; + gap: 18px; + margin-top: 12px; +} +.aksrt-user-card__stat-value { + display: block; + font-size: 16px; + font-weight: 700; +} +.aksrt-user-card__stat-label { + font-size: 11px; + color: var(--muted-2-color, #999); +} +.aksrt-user-card__actions { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 8px; + margin-top: 14px; +} +.aksrt-user-card__btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 96px; + padding: 7px 14px; + border-radius: 999px; + font-size: 13px; + text-decoration: none; + font-weight: 600; +} +.aksrt-user-card__btn--primary { + background: var(--color-primary, #3b82f6); + color: #fff !important; +} +.aksrt-user-card__btn--ghost { + background: var(--surface-2, #f3f4f6); + color: inherit !important; +} + +/* Main posts */ +.aksrt-main-posts { display: flex; flex-direction: column; gap: 10px; } +.aksrt-main-posts.style-card { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} +@media (max-width: 575.98px) { + .aksrt-main-posts.style-card { grid-template-columns: 1fr; } +} +.aksrt-main-posts__card { + display: block; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); + text-decoration: none; + color: inherit; +} +.aksrt-main-posts__thumb { + height: 110px; + background: var(--surface-2, #f3f4f6); + overflow: hidden; +} +.aksrt-main-posts__thumb.mini { + width: 56px; + height: 56px; + border-radius: 8px; + flex-shrink: 0; +} +.aksrt-main-posts__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } +.aksrt-main-posts__thumb.is-empty { background: linear-gradient(135deg, #e2e8f0, #f8fafc); } +.aksrt-main-posts__body { padding: 10px 12px 12px; min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; } +.aksrt-main-posts__title { + font-size: 13px; + font-weight: 600; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.aksrt-main-posts__meta { + display: flex; + gap: 10px; + font-size: 11px; + color: var(--muted-2-color, #999); +} +.aksrt-main-posts__row { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: inherit; + padding: 4px 0; + border-bottom: 1px dashed var(--color-line, #e5e7eb); +} +.aksrt-main-posts__row:last-child { border-bottom: 0; } +.aksrt-main-posts__num { + width: 20px; + flex-shrink: 0; + font-size: 12px; + font-weight: 700; + color: var(--color-primary, #3b82f6); + text-align: center; +} + +/* Term aggregation */ +.aksrt-term-agg { + display: grid; + grid-template-columns: repeat(var(--aksrt-ta-cols, 1), minmax(0, 1fr)); + gap: 12px; +} +.aksrt-term-agg__card { + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 12px; + overflow: hidden; + background: var(--surface-1, #fff); +} +.aksrt-term-agg__head { + display: block; + text-decoration: none; + color: inherit; + position: relative; +} +.aksrt-term-agg__cover { + height: 96px; + background: var(--surface-2, #f3f4f6) center/cover no-repeat; +} +.aksrt-term-agg__cover.is-empty { + background: linear-gradient(135deg, #dbeafe, #eff6ff); +} +.aksrt-term-agg__name { + position: absolute; + left: 12px; + bottom: 10px; + font-weight: 700; + color: #fff; + text-shadow: 0 1px 3px rgba(0,0,0,.45); +} +.aksrt-term-agg__count { + position: absolute; + right: 10px; + top: 10px; + font-size: 11px; + padding: 2px 8px; + border-radius: 999px; + background: rgba(0,0,0,.45); + color: #fff; +} +.aksrt-term-agg__posts { + list-style: none; + margin: 0; + padding: 8px 12px 10px; +} +.aksrt-term-agg__posts li { + padding: 6px 0; + border-bottom: 1px dashed var(--color-line, #eee); +} +.aksrt-term-agg__posts li:last-child { border-bottom: 0; } +.aksrt-term-agg__posts a { + color: inherit; + text-decoration: none; + font-size: 13px; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + + +/* Icon cover cards */ +.aksrt-icon-cover { + display: grid; + grid-template-columns: repeat(var(--aksrt-icc-pc, 2), minmax(0, 1fr)); + gap: 10px; + --aksrt-icc-icon: 25px; +} +@media (max-width: 575.98px) { + .aksrt-icon-cover { + grid-template-columns: repeat(var(--aksrt-icc-m, 1), minmax(0, 1fr)); + } +} +.aksrt-icon-cover__item { + display: flex; + align-items: center; + gap: 10px; + padding: 12px; + border-radius: 12px; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-1, #fff); + text-decoration: none; + color: inherit; +} +.aksrt-icon-cover__icon { + width: calc(var(--aksrt-icc-icon) + 18px); + height: calc(var(--aksrt-icc-icon) + 18px); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--aksrt-icc-icon); + color: var(--color-primary, #3b82f6); + background: var(--surface-2, #f3f4f6); + flex-shrink: 0; + overflow: hidden; +} +.aksrt-icon-cover.is-radius .aksrt-icon-cover__icon { border-radius: 10px; } +.aksrt-icon-cover__icon img { width: 70%; height: 70%; object-fit: contain; } +.aksrt-icon-cover__title { font-weight: 700; font-size: 14px; } +.aksrt-icon-cover__desc { font-size: 12px; color: var(--muted-2-color, #888); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +/* Bookmark links */ +.aksrt-bookmark-links { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.aksrt-bookmark-links.has-box { + padding: 10px; + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 12px; +} +.aksrt-bookmark-links.style-simple { + grid-template-columns: 1fr; +} +.aksrt-bookmark-links__item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-radius: 10px; + background: var(--surface-2, #f8fafc); + text-decoration: none; + color: inherit; + min-width: 0; +} +.aksrt-bookmark-links__img { + width: 36px; + height: 36px; + border-radius: 8px; + object-fit: cover; + flex-shrink: 0; +} +.aksrt-bookmark-links__text { min-width: 0; display: flex; flex-direction: column; } +.aksrt-bookmark-links__name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.aksrt-bookmark-links__desc { font-size: 11px; color: var(--muted-2-color, #999); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + + +/* Search keywords */ +.aksrt-search-widget__keywords { margin-top: 10px; } +.aksrt-search-widget__kw-title { + font-size: 12px; + color: var(--muted-2-color, #888); + margin-bottom: 6px; +} +.aksrt-search-widget__kw-list { display: flex; flex-wrap: wrap; gap: 6px; } +.aksrt-search-widget__kw { + display: inline-block; + padding: 3px 10px; + border-radius: 999px; + background: var(--surface-2, #f3f4f6); + color: inherit; + text-decoration: none; + font-size: 12px; +} +.aksrt-search-widget__more { margin-top: 8px; text-align: right; font-size: 12px; } +.aksrt-search-widget__more a { color: var(--color-primary, #3b82f6); text-decoration: none; } + +/* Scroll notice */ +.aksrt-scroll-notice { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 10px; + background: #eff6ff; + color: #1d4ed8; + margin-bottom: 8px; + overflow: hidden; +} +.aksrt-scroll-notice.is-radius { border-radius: 999px; } +.aksrt-scroll-notice.color-c-green { background: #ecfdf5; color: #047857; } +.aksrt-scroll-notice.color-c-yellow { background: #fffbeb; color: #b45309; } +.aksrt-scroll-notice.color-c-red { background: #fef2f2; color: #b91c1c; } +.aksrt-scroll-notice.color-c-purple { background: #f5f3ff; color: #6d28d9; } +.aksrt-scroll-notice__icon { flex-shrink: 0; } +.aksrt-scroll-notice__viewport { position: relative; flex: 1; min-width: 0; height: 1.4em; } +.aksrt-scroll-notice__item { + position: absolute; + inset: 0; + opacity: 0; + transform: translateY(8px); + transition: opacity .35s ease, transform .35s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 13px; + line-height: 1.4; +} +.aksrt-scroll-notice__item.is-active { + opacity: 1; + transform: translateY(0); +} +.aksrt-scroll-notice__item a { + color: inherit; + text-decoration: none; +} +.aksrt-scroll-notice__item i { margin-right: 6px; opacity: .85; } + +.hot-post-item { display: flex; align-items: center; gap: 10px; } +.hot-post-item__thumb { + width: 48px; height: 48px; border-radius: 8px; overflow: hidden; flex-shrink: 0; + background: var(--surface-2, #eee); +} +.hot-post-item__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } +.aksrt-cover-card__bg.is-video video { + width: 100%; height: 100%; object-fit: cover; display: block; +} + +.widget-buttons { + display: flex; + flex-wrap: wrap; + gap: var(--btn-gap, 10px); + margin-bottom: 8px; +} +.widget-buttons--vertical { flex-direction: column; } +.widget-buttons.align-center { justify-content: center; } +.widget-buttons.align-left { justify-content: flex-start; } +.widget-buttons.align-right { justify-content: flex-end; } +.widget-buttons__btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + text-decoration: none; + font-size: 13px; + font-weight: 600; + background: var(--color-primary, #3b82f6); + color: #fff !important; +} +.widget-buttons.is-radius .widget-buttons__btn { border-radius: 999px; } +.widget-buttons.is-hollow .widget-buttons__btn { + background: transparent; + color: var(--color-primary, #3b82f6) !important; + border: 1px solid currentColor; +} +.widget-buttons__btn--secondary { background: #64748b; } +.widget-buttons__btn--outline { + background: transparent; + color: inherit !important; + border: 1px solid var(--color-line, #ddd); +} +.widget-buttons__btn--text { + background: transparent; + color: var(--color-primary, #3b82f6) !important; + padding-left: 0; + padding-right: 0; +} +.widget-buttons__btn--pink { background: #ec4899; } +.widget-buttons__btn--blue { background: #3b82f6; } +.widget-buttons__btn--green { background: #22c55e; } + +.aksrt-video-widget { + position: relative; + width: 100%; + height: 0; + border-radius: 8px; + overflow: hidden; + background: #111; + margin-bottom: 10px; +} +.aksrt-video-widget iframe, +.aksrt-video-widget video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: 0; + object-fit: cover; +} +.aksrt-video-widget.controller-hide video::-webkit-media-controls { + display: none !important; +} + +.widget-recent-comments__post { + margin-top: 2px; + font-size: 11px; +} +.widget-recent-comments__post a { + color: var(--muted-2-color, #999); + text-decoration: none; +} + +.widget-icon-cards { + display: grid; + grid-template-columns: repeat(var(--ic-columns, 2), minmax(0, 1fr)); + gap: 10px; +} +@media (max-width: 575.98px) { + .widget-icon-cards { + grid-template-columns: repeat(var(--ic-m-columns, 2), minmax(0, 1fr)); + } +} +.widget-icon-cards.is-bare .widget-icon-cards__item { + background: transparent; + border: 0; + box-shadow: none; +} +.widget-icon-cards__item.is-vertical { + flex-direction: column; + text-align: center; + padding: 16px 10px; +} +.widget-icon-cards__item.is-vertical .widget-icon-cards__icon { + margin: 0 auto 10px; + width: calc(var(--ic-icon, 28px) + 20px); + height: calc(var(--ic-icon, 28px) + 20px); + font-size: var(--ic-icon, 28px); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--surface-2, #f3f4f6); + color: var(--color-primary, #3b82f6); +} +.aksrt-tag-cloud.tag-cloud { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.aksrt-tag-cloud .but { + display: inline-flex; + align-items: center; + padding: 4px 10px; + border-radius: 999px; + background: var(--surface-2, #f3f4f6); + color: inherit; + text-decoration: none; + font-size: 12px; +} +.aksrt-tag-cloud .c-blue { background: #dbeafe; color: #1d4ed8; } +.aksrt-tag-cloud .c-green { background: #dcfce7; color: #15803d; } +.aksrt-tag-cloud .c-yellow { background: #fef3c7; color: #b45309; } +.aksrt-tag-cloud .c-purple { background: #ede9fe; color: #6d28d9; } +.aksrt-tag-cloud .c-red { background: #fee2e2; color: #b91c1c; } +.aksrt-tag-cloud.fixed-width a { min-width: 72px; justify-content: center; } +.aksrt-cat-list .count { + float: right; + color: var(--muted-2-color, #999); + font-size: 12px; +} + +.aksrt-user-card__oauth { + margin-top: 14px; + padding-top: 12px; + border-top: 1px dashed var(--color-line, #e5e7eb); +} +.aksrt-user-card__oauth .social-login-divider { + font-size: 12px; + color: var(--muted-2-color, #999); + margin-bottom: 8px; +} +.aksrt-user-card__oauth .social-login-item, +.aksrt-user-card__oauth a { + margin: 0 4px; +} + +.aksrt-author-card__follow { + border: 0; + cursor: pointer; + margin-right: 6px; + background: #ef4444; + color: #fff !important; +} +.aksrt-author-card__follow.is-following { + background: var(--surface-2, #e5e7eb); + color: inherit !important; +} + +.aksrt-author-card__badge { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 8px; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + background: #fef3c7; + color: #b45309; +} +.aksrt-author-card__badge img { border-radius: 50%; } diff --git a/assets/css/frontend/widgets.css b/assets/css/frontend/widgets.css index 69e0c933..69820df2 100644 --- a/assets/css/frontend/widgets.css +++ b/assets/css/frontend/widgets.css @@ -8,3 +8,4 @@ @import url("widget-batch3d.css"); @import url("widget-batch3e.css"); @import url("widget-batch3f.css"); +@import url("widget-batch5.css"); diff --git a/functions.php b/functions.php index 5767db5d..684ca546 100644 --- a/functions.php +++ b/functions.php @@ -140,6 +140,9 @@ if (get_option('aksrt_flushed_version') !== AKSRT_VERSION) { if (class_exists('\AKSRT\Auth\WeixinGzh')) { \AKSRT\Auth\WeixinGzh::get_instance()->init(); } +if (class_exists('\AKSRT\Frontend\WeChatShare')) { + \AKSRT\Frontend\WeChatShare::get_instance()->init(); +} // 初始化代理登录(根据配置模式选择性加载) $aksrt_proxy_mode = \AKSRT\Settings\Settings::oauth_proxy_mode(); @@ -171,6 +174,12 @@ if (class_exists('\AKSRT\User\Restriction')) { if (class_exists('\AKSRT\User\Center')) { \AKSRT\User\Center::get_instance()->init(); } +if (class_exists('\AKSRT\User\Badges')) { + \AKSRT\User\Badges::get_instance()->init(); +} +if (class_exists('\AKSRT\User\BadgeAutoAward')) { + \AKSRT\User\BadgeAutoAward::get_instance()->init(); +} if (class_exists('\AKSRT\User\Home')) { \AKSRT\User\Home::get_instance()->init(); } diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php index 8ba39126..fcde9935 100644 --- a/inc/modules/widgets-register.php +++ b/inc/modules/widgets-register.php @@ -10,6 +10,16 @@ defined('ABSPATH') || exit; /** * Register custom widgets. */ +function aksrt_unregister_default_widgets(): void { + unregister_widget('WP_Widget_RSS'); + unregister_widget('WP_Widget_Recent_Posts'); + unregister_widget('WP_Widget_Recent_Comments'); + unregister_widget('WP_Widget_Pages'); + unregister_widget('WP_Widget_Search'); + unregister_widget('WP_Widget_Calendar'); +} +add_action('widgets_init', 'aksrt_unregister_default_widgets', 1); + function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\AboutWidget'); register_widget('\AKSRT\Widgets\CategoriesWidget'); @@ -74,6 +84,10 @@ function aksrt_register_widgets() { register_widget('\AKSRT\Widgets\WidgetAuthorCard'); register_widget('\AKSRT\Widgets\WidgetOnelinePosts'); register_widget('\AKSRT\Widgets\WidgetTiltCases'); + register_widget('\AKSRT\Widgets\WidgetMainPosts'); + register_widget('\AKSRT\Widgets\WidgetTermAggregation'); + register_widget('\AKSRT\Widgets\WidgetIconCoverCards'); + register_widget('\AKSRT\Widgets\WidgetBookmarkLinks'); // Forum widgets diff --git a/src/AKSRT/Core/SetupPart1.php b/src/AKSRT/Core/SetupPart1.php index 26634b99..f721a3ec 100644 --- a/src/AKSRT/Core/SetupPart1.php +++ b/src/AKSRT/Core/SetupPart1.php @@ -407,11 +407,15 @@ trait SetupPart1 { // ── Shop widget areas (§6.9) ── $shop_widgets = [ + 'shop_home_top_fluid' => ['商城首页 · 顶部通栏', '商城首页顶部全宽区域。'], + 'shop_home_sidebar' => ['商城首页 · 侧栏', '商城首页侧栏区域。'], 'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'], 'shop_product_sidebar' => ['商品详情页 · 侧栏', '商品详情页侧栏区域(PC 端)。'], 'shop_product_bottom_fluid' => ['商品详情页 · 底部通栏', '商品详情页底部全宽区域。'], 'shop_cart_top_fluid' => ['购物车页 · 顶部通栏', '购物车页顶部全宽区域。'], 'shop_cart_bottom_fluid' => ['购物车页 · 底部通栏', '购物车页底部全宽区域。'], + 'shop_order_top_fluid' => ['订单页 · 顶部通栏', '订单中心顶部全宽。'], + 'shop_order_bottom_fluid' => ['订单页 · 底部通栏', '订单中心底部全宽。'], ]; foreach ($shop_widgets as $sid => $meta) { register_sidebar([ diff --git a/src/AKSRT/Credits/Signin.php b/src/AKSRT/Credits/Signin.php index fca51f9c..ffd5684c 100644 --- a/src/AKSRT/Credits/Signin.php +++ b/src/AKSRT/Credits/Signin.php @@ -159,8 +159,16 @@ class Signin { // Keep lightweight meta for header/badge consumers (zibll checkin_all_day). update_user_meta($user_id, 'aksrt_signin_' . $today_str, 1); $all_day = (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true); - update_user_meta($user_id, 'aksrt_checkin_all_day', $all_day + 1); + if ($all_day <= 0) { + // Fallback to zibll key if migrated users only have that meta. + $all_day = (int) get_user_meta($user_id, 'checkin_all_day', true); + } + $all_day = $all_day + 1; + update_user_meta($user_id, 'aksrt_checkin_all_day', $all_day); + update_user_meta($user_id, 'checkin_all_day', $all_day); // zibll alias update_user_meta($user_id, 'aksrt_checkin_continuous_day', $new_streak); + update_user_meta($user_id, 'checkin_continuous_day', $new_streak); // zibll alias + do_action('aksrt_checkin_done', $user_id); return new \WP_REST_Response([ 'success' => true, diff --git a/src/AKSRT/Frontend/WeChatShare.php b/src/AKSRT/Frontend/WeChatShare.php new file mode 100644 index 00000000..07836449 --- /dev/null +++ b/src/AKSRT/Frontend/WeChatShare.php @@ -0,0 +1,260 @@ +config = class_exists('\AKSRT\Settings\Settings') + ? \AKSRT\Settings\Settings::get_share_settings() + : []; + + if (empty($this->config['wechat_enabled'])) { + return; + } + if (empty($this->config['wechat_appid']) || empty($this->config['wechat_app_secret'])) { + return; + } + + add_action('wp_enqueue_scripts', [$this, 'enqueue_sdk'], 20); + add_action('wp_footer', [$this, 'print_share_date'], 99); + } + + public function enqueue_sdk(): void { + // Only needed in WeChat browser, but enqueue lightly for all (script is external CDN). + wp_enqueue_script( + 'aksrt-jweixin', + 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js', + [], + '1.6.0', + true + ); + // Ensure our bridge runs after jweixin. + if (wp_script_is('aksrt-weixin-share', 'registered') || wp_script_is('aksrt-weixin-share', 'enqueued')) { + wp_script_add_data('aksrt-weixin-share', 'group', 1); + } + } + + public function print_share_date(): void { + $url = $this->current_url(); + $sign = $this->get_sign($url); + if (!$sign) { + return; + } + $img = $this->resolve_image(); + $title = wp_get_document_title(); + $desc = ''; + if (is_singular()) { + $post = get_post(); + if ($post) { + $desc = wp_strip_all_tags(get_the_excerpt($post)); + } + } + if ($desc === '') { + $desc = (string) get_bloginfo('description'); + } + $payload = [ + 'appId' => $sign['appId'], + 'timestamp' => (string) $sign['timestamp'], + 'nonceStr' => $sign['nonceStr'], + 'signature' => $sign['signature'], + 'url' => $url, + 'title' => $title, + 'img' => $img, + 'desc' => $desc, + ]; + ?> + + get_jsapi_ticket(); + if ($ticket === '') { + return null; + } + $timestamp = time(); + $nonce = $this->nonce_string(); + $raw = "jsapi_ticket={$ticket}&noncestr={$nonce}×tamp={$timestamp}&url={$url}"; + return [ + 'appId' => (string) $this->config['wechat_appid'], + 'nonceStr' => $nonce, + 'timestamp' => $timestamp, + 'signature' => sha1($raw), + ]; + } + + private function current_url(): string { + $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') + || (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) + || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'); + $protocol = $https ? 'https://' : 'http://'; + $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_HOST'])) : wp_parse_url(home_url('/'), PHP_URL_HOST); + $uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/'; + // Drop hash fragment if any (should not be present in REQUEST_URI). + return $protocol . $host . $uri; + } + + private function resolve_image(): string { + $only_logo = !empty($this->config['only_logo']); + $default = ''; + if (!empty($this->config['share_img'])) { + $default = (string) $this->config['share_img']; + } elseif (class_exists('\AKSRT\Settings\Settings')) { + $default = (string) (\AKSRT\Settings\Settings::get_value('favicon', '') ?: \AKSRT\Settings\Settings::get_value('iconpng', '')); + } + if ($only_logo) { + return $default; + } + + $pic = ''; + if (is_singular()) { + $post_id = get_queried_object_id(); + if ($post_id && has_post_thumbnail($post_id)) { + $pic = (string) get_the_post_thumbnail_url($post_id, 'full'); + } + } elseif (is_author()) { + $author = get_queried_object(); + if ($author && !empty($author->ID)) { + $pic = (string) get_user_meta((int) $author->ID, 'custom_avatar', true); + if ($pic === '') { + $pic = get_avatar_url((int) $author->ID, ['size' => 300]); + } + } + } elseif (is_category() || is_tag() || is_tax()) { + $term = get_queried_object(); + if ($term && !empty($term->term_id)) { + $img_id = (int) get_term_meta((int) $term->term_id, 'thumbnail_id', true); + if ($img_id) { + $pic = (string) (wp_get_attachment_image_url($img_id, 'full') ?: ''); + } + } + } + + return $pic !== '' ? $pic : $default; + } + + private function get_jsapi_ticket(): string { + $opt = get_option('aksrt_wechatshare_ticket', []); + $now = time() + 300; + if (is_array($opt) && !empty($opt['ticket']) && !empty($opt['expiration_time']) && (int) $opt['expiration_time'] > $now) { + return (string) $opt['ticket']; + } + + $token = $this->get_access_token(); + if ($token === '') { + return ''; + } + $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=' . rawurlencode($token) . '&type=jsapi'; + $res = $this->http_json($url); + if (empty($res['ticket'])) { + return ''; + } + $res['expiration_time'] = time() + max(60, (int) ($res['expires_in'] ?? 7200)); + update_option('aksrt_wechatshare_ticket', $res, false); + return (string) $res['ticket']; + } + + private function get_access_token(): string { + // Prefer shared GZH token cache when present. + $opt = get_option('aksrt_weixingzh_access_token', []); + $now = time() + 300; + if (is_array($opt) && !empty($opt['access_token']) && !empty($opt['expiration_time']) && (int) $opt['expiration_time'] > $now) { + return (string) $opt['access_token']; + } + + $appid = (string) $this->config['wechat_appid']; + $secret = (string) $this->config['wechat_app_secret']; + $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . rawurlencode($appid) . '&secret=' . rawurlencode($secret); + $res = $this->http_json($url); + if (empty($res['access_token'])) { + return ''; + } + $res['expiration_time'] = time() + max(60, (int) ($res['expires_in'] ?? 7200)); + update_option('aksrt_weixingzh_access_token', $res, false); + return (string) $res['access_token']; + } + + /** + * @return array + */ + private function http_json(string $url): array { + $response = wp_remote_get($url, [ + 'timeout' => 8, + 'headers' => ['Accept' => 'application/json'], + ]); + if (is_wp_error($response)) { + return []; + } + $body = (string) wp_remote_retrieve_body($response); + $data = json_decode($body, true); + return is_array($data) ? $data : []; + } + + private function nonce_string(int $length = 16): string { + $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + $str = ''; + $max = strlen($chars) - 1; + for ($i = 0; $i < $length; $i++) { + $str .= $chars[random_int(0, $max)]; + } + return $str; + } +} diff --git a/src/AKSRT/User/BadgeAutoAward.php b/src/AKSRT/User/BadgeAutoAward.php new file mode 100644 index 00000000..e04af9d7 --- /dev/null +++ b/src/AKSRT/User/BadgeAutoAward.php @@ -0,0 +1,168 @@ +evaluate((int) $user->ID); + } + } + + public function maybe_for_current_user(): void { + if (!is_user_logged_in() || is_admin()) { + return; + } + // Throttle: once per 10 minutes per user. + $uid = get_current_user_id(); + $key = 'aksrt_badge_auto_' . $uid; + if (get_transient($key)) { + return; + } + set_transient($key, 1, 10 * MINUTE_IN_SECONDS); + $this->evaluate($uid); + } + + public function on_content_change($arg = null): void { + $uid = get_current_user_id(); + if ($uid > 0) { + $this->evaluate($uid); + } + } + + public function on_follow(int $follower_id, int $following_id): void { + if ($following_id > 0) { + $this->evaluate($following_id); + } + if ($follower_id > 0) { + $this->evaluate($follower_id); + } + } + + public function on_checkin(int $user_id): void { + if ($user_id > 0) { + $this->evaluate($user_id); + } + } + + public function evaluate(int $user_id): void { + if ($user_id <= 0 || !class_exists('\AKSRT\User\Badges') || !Badges::enabled()) { + return; + } + if (!class_exists('\AKSRT\Settings\Settings')) { + return; + } + $identity = \AKSRT\Settings\Settings::get_user_identity_settings(); + $medal = is_array($identity['medal'] ?? null) ? $identity['medal'] : []; + $items = is_array($medal['items_by_slug'] ?? null) ? $medal['items_by_slug'] : []; + if (!$items) { + // Fall back to badge dictionary without thresholds. + return; + } + + foreach ($items as $slug => $item) { + $get_type = sanitize_key((string) ($item['get_type'] ?? 'manually_add')); + if ($get_type === 'manually_add' || $get_type === '') { + continue; + } + $need = max(0, (int) ($item['get_val'] ?? 0)); + if ($need <= 0) { + continue; + } + $val = $this->metric($user_id, $get_type); + if ($val >= $need) { + Badges::grant($user_id, (string) $slug, 0); + } + } + } + + private function metric(int $user_id, string $type): int { + switch ($type) { + case 'registration_time': + $user = get_userdata($user_id); + if (!$user) { + return 0; + } + $reg = strtotime((string) $user->user_registered); + if ($reg <= 0) { + return 0; + } + return max(0, (int) floor((time() - $reg) / DAY_IN_SECONDS)); + + case 'checkin_all_day': + $v = (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true); + if ($v <= 0) { + $v = (int) get_user_meta($user_id, 'checkin_all_day', true); + } + return $v; + + case 'new_post': + return (int) count_user_posts($user_id, 'post', true); + + case 'bbs_post': + return post_type_exists('aksrt_forum_topic') + ? (int) count_user_posts($user_id, 'aksrt_forum_topic', true) + : 0; + + case 'comment': + return (int) get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']); + + case 'followed': + if (class_exists('\AKSRT\User\Follow')) { + return (int) Follow::get_instance()->get_follower_count($user_id); + } + return 0; + + case 'like': + return (int) get_user_meta($user_id, 'aksrt_like_count', true) + ?: (int) get_user_meta($user_id, 'like-posts', true); + + case 'views': + return (int) get_user_meta($user_id, 'aksrt_user_views', true); + + case 'pay_income': + return (int) get_user_meta($user_id, 'aksrt_pay_income', true); + + case 'pay_rebate': + return (int) get_user_meta($user_id, 'aksrt_pay_rebate', true); + + default: + /** + * Allow extensions to provide metric values. + * + * @param int $value + * @param int $user_id + * @param string $type + */ + return (int) apply_filters('aksrt_badge_metric_value', 0, $user_id, $type); + } + } +} diff --git a/src/AKSRT/User/Badges.php b/src/AKSRT/User/Badges.php index 95c8f3d2..bee2531c 100644 --- a/src/AKSRT/User/Badges.php +++ b/src/AKSRT/User/Badges.php @@ -185,9 +185,14 @@ class Badges { } $wear = sanitize_key((string) get_user_meta($user_id, self::META_WEAR, true)); if ($wear === '') { - return ''; + // zibll alias. + $wear = sanitize_key((string) get_user_meta($user_id, 'wear_medal', true)); } $owned = self::get_owned($user_id); + if ($wear === '') { + // Default to newest/first owned badge when unset (zibll behavior). + return $owned[0] ?? ''; + } return in_array($wear, $owned, true) ? $wear : ''; } @@ -211,6 +216,7 @@ class Badges { return new \WP_Error('aksrt_badge_not_owned', __('尚未获得该徽章', 'aksrt'), ['status' => 403]); } update_user_meta($user_id, self::META_WEAR, $slug); + update_user_meta($user_id, 'wear_medal', $slug); // zibll alias return true; } diff --git a/src/AKSRT/Widgets/AboutWidget.php b/src/AKSRT/Widgets/AboutWidget.php index dd68d1a1..a1aba833 100644 --- a/src/AKSRT/Widgets/AboutWidget.php +++ b/src/AKSRT/Widgets/AboutWidget.php @@ -99,8 +99,13 @@ class AboutWidget extends \WP_Widget { $avatar = get_avatar_url(0, ['size' => 160, 'default' => 'mystery']); $oauth_html = ''; - if (function_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'get_login_buttons')) { - $oauth_html = \AKSRT\Auth\OAuth::get_instance()->get_login_buttons(); + if (class_exists('\AKSRT\Auth\OAuth')) { + $oauth = \AKSRT\Auth\OAuth::get_instance(); + if (method_exists($oauth, 'render_social_buttons')) { + $oauth_html = (string) $oauth->render_social_buttons(__('社交账号登录', 'aksrt')); + } elseif (method_exists($oauth, 'get_login_buttons')) { + $oauth_html = (string) $oauth->get_login_buttons(); + } } ?>
@@ -120,9 +125,9 @@ class AboutWidget extends \WP_Widget {
- + - +
diff --git a/src/AKSRT/Widgets/ButtonsWidget.php b/src/AKSRT/Widgets/ButtonsWidget.php index cadfde31..20eb79b2 100644 --- a/src/AKSRT/Widgets/ButtonsWidget.php +++ b/src/AKSRT/Widgets/ButtonsWidget.php @@ -1,5 +1,7 @@ esc_html__('显示一组自定义按钮', 'aksrt'), - ]); - } + public function __construct() { + parent::__construct('aksrt_buttons', esc_html__('AKSRT 按钮组', 'aksrt'), [ + 'description' => esc_html__('显示一组自定义按钮,支持外链跳转与样式。', 'aksrt'), + ]); + } - public function widget($args, $instance): void { - echo $args['before_widget']; - $title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base); - $buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : []; - $layout = sanitize_key($instance['layout'] ?? 'vertical'); + public function widget($args, $instance): void { + $buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : []; + $layout = sanitize_key((string) ($instance['layout'] ?? 'horizontal')); + if (!in_array($layout, ['vertical', 'horizontal'], true)) { + $layout = 'horizontal'; + } + $align = sanitize_key((string) ($instance['align'] ?? 'center')); + if (!in_array($align, ['left', 'center', 'right'], true)) { + $align = 'center'; + } + $radius = !empty($instance['radius']); + $hollow = !empty($instance['hollow']); + $go_link = !empty($instance['go_link']); + $gap = max(4, min(30, (int) ($instance['gap'] ?? 10))); - if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title']; + $items = []; + foreach ($buttons as $btn) { + if (!is_array($btn)) { + continue; + } + $text = sanitize_text_field((string) ($btn['text'] ?? '')); + $url = esc_url_raw(trim((string) ($btn['url'] ?? ''))); + if ($text === '') { + continue; + } + if ($url !== '' && $go_link && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + $items[] = [ + 'text' => $text, + 'url' => $url, + 'style' => sanitize_key((string) ($btn['style'] ?? 'primary')), + 'icon' => sanitize_text_field((string) ($btn['icon'] ?? $btn['icon_left'] ?? '')), + 'icon_right' => sanitize_text_field((string) ($btn['icon_right'] ?? '')), + 'new_window' => !empty($btn['new_window']) || !empty($btn['target']), + ]; + } + if (!$items) { + return; + } - if (!empty($buttons)) : ?> -
- - - - - - -
- id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - echo $args['after_widget']; - } + $wrap = 'widget-buttons widget-buttons--' . $layout . ' align-' . $align; + if ($radius) { + $wrap .= ' is-radius'; + } + if ($hollow) { + $wrap .= ' is-hollow'; + } + echo '
'; + foreach ($items as $btn) { + $style = in_array($btn['style'], ['primary', 'secondary', 'outline', 'text', 'pink', 'blue', 'green', 'yellow', 'purple'], true) + ? $btn['style'] + : 'primary'; + $cls = 'widget-buttons__btn widget-buttons__btn--' . $style; + $href = $btn['url'] !== '' ? $btn['url'] : 'javascript:void(0)'; + $tgt = $btn['new_window'] && $btn['url'] !== '' ? ' target="_blank" rel="noopener noreferrer"' : ''; + echo ''; + if ($btn['icon'] !== '') { + echo ' '; + } + echo esc_html($btn['text']); + if ($btn['icon_right'] !== '') { + echo ' '; + } + echo ''; + } + echo '
'; - public function form($instance): void { - $buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : []; - while (count($buttons) < 3) { - $buttons[] = ['text' => '', 'url' => '', 'style' => 'primary', 'icon' => '', 'new_window' => false]; - } - ?> -

- - -

-

- - -

-
- $btn) : ?> -
-

- " value="" placeholder=""> -

-

- " value="" placeholder=""> -

-

- -

-

- " value="" placeholder=""> -

-

- -

-
- sanitize_text_field($btn['text'] ?? ''), - 'url' => esc_url_raw($btn['url'] ?? ''), - 'style' => $style, - 'icon' => sanitize_text_field($btn['icon'] ?? ''), - 'new_window' => !empty($btn['new_window']), - ]; - } - } - return [ - 'title' => sanitize_text_field($new_instance['title'] ?? ''), - 'layout' => in_array($new_instance['layout'] ?? '', ['vertical', 'horizontal'], true) ? $new_instance['layout'] : 'vertical', - 'buttons' => $buttons, - ]; - } + public function form($instance): void { + $buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : []; + while (count($buttons) < 4) { + $buttons[] = ['text' => '', 'url' => '', 'style' => 'primary', 'icon' => '', 'icon_right' => '', 'new_window' => false]; + } + ?> +

+ +

+

+ + +

+

+ + + +

+ $btn) : ?> +
+ " value="" placeholder=""> + " value="" placeholder=""> + " value="" placeholder=""> + " value="" placeholder=""> + + +
+ $text, + 'url' => esc_url_raw(trim((string) ($btn['url'] ?? ''))), + 'style' => sanitize_key((string) ($btn['style'] ?? 'primary')), + 'icon' => sanitize_text_field((string) ($btn['icon'] ?? '')), + 'icon_right' => sanitize_text_field((string) ($btn['icon_right'] ?? '')), + 'new_window' => !empty($btn['new_window']), + ]; + if (count($buttons) >= 8) { + break; + } + } + } + return [ + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'layout' => ($new_instance['layout'] ?? '') === 'vertical' ? 'vertical' : 'horizontal', + 'align' => in_array(($new_instance['align'] ?? ''), ['left', 'center', 'right'], true) ? $new_instance['align'] : 'center', + 'radius' => !empty($new_instance['radius']) ? 1 : 0, + 'hollow' => !empty($new_instance['hollow']) ? 1 : 0, + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + 'gap' => max(4, min(30, (int) ($new_instance['gap'] ?? 10))), + 'buttons' => $buttons, + ]; + } } diff --git a/src/AKSRT/Widgets/CategoriesWidget.php b/src/AKSRT/Widgets/CategoriesWidget.php index df7a969e..fe2362ce 100644 --- a/src/AKSRT/Widgets/CategoriesWidget.php +++ b/src/AKSRT/Widgets/CategoriesWidget.php @@ -1,5 +1,7 @@ esc_html__('以列表形式展示文章分类', 'aksrt'), + [ + 'description' => esc_html__('以列表形式展示文章分类', 'aksrt'), 'customize_selective_refresh' => true, - ) + ] ); } - public function widget($args, $instance) { - echo $args['before_widget']; + public function widget($args, $instance): void { + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - $title = apply_filters('widget_title', $instance['title'], $instance, $this->id_base); + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - if ($title): ?> -

- parse_ids((string) ($instance['exclude'] ?? '')); + $include = $this->parse_ids((string) ($instance['include'] ?? '')); - $count = absint($instance['count'] ?: 8); - $cats = get_categories(array('number' => $count, 'orderby' => 'count', 'order' => 'DESC')); - ?> - - 'category', + 'number' => $count, + 'orderby' => $orderby, + 'order' => $order, + 'hide_empty' => $hide_empty, + 'hierarchical' => $hierarchical, + ]; + if ($parent > 0) { + $term_args['parent'] = $parent; + } + if ($include) { + $term_args['include'] = $include; + $term_args['orderby'] = 'include'; + unset($term_args['number']); + } + if ($exclude) { + $term_args['exclude'] = $exclude; + } + + $cats = get_categories($term_args); + if (!$cats) { + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } + + echo ''; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - public function form($instance) { - $title = $instance['title'] ?? __('分类目录', 'aksrt'); - $count = isset($instance['count']) ? (int)$instance['count'] : 8; + /** @return array */ + private function parse_ids(string $raw): array { + if ($raw === '') { + return []; + } + $ids = []; + foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) { + $id = (int) $p; + if ($id > 0) { + $ids[] = $id; + } + } + return array_values(array_unique($ids)); + } + + public function form($instance): void { ?>

- - +

- - + +

+

+ + +

+

+ +

+

+ +

+

+ +

+

+
+
+

sanitize_text_field($new['title']), - 'count' => absint($new['count']), + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'count' => max(1, min(50, (int) ($new_instance['count'] ?? 8))), + 'orderby' => $orderby, + 'order' => strtoupper((string) ($new_instance['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC', + 'parent' => max(0, (int) ($new_instance['parent'] ?? 0)), + 'include' => sanitize_text_field((string) ($new_instance['include'] ?? '')), + 'exclude' => sanitize_text_field((string) ($new_instance['exclude'] ?? '')), + 'show_count' => !empty($new_instance['show_count']) ? 1 : 0, + 'hide_empty' => !empty($new_instance['hide_empty']) ? 1 : 0, + 'hierarchical' => !empty($new_instance['hierarchical']) ? 1 : 0, ]; } } diff --git a/src/AKSRT/Widgets/CustomHtmlGapWidget.php b/src/AKSRT/Widgets/CustomHtmlGapWidget.php index e6d35042..6a02ef61 100644 --- a/src/AKSRT/Widgets/CustomHtmlGapWidget.php +++ b/src/AKSRT/Widgets/CustomHtmlGapWidget.php @@ -19,7 +19,7 @@ class CustomHtmlGapWidget extends \WP_Widget { $mode = sanitize_key($instance['mode'] ?? 'html'); if ($mode === 'gap') { - $height = max(0, min(200, (int) ($instance['gap_height'] ?? 20))); + $height = max(0, min(500, (int) ($instance['gap_height'] ?? 20))); echo ''; return; } @@ -51,7 +51,7 @@ class CustomHtmlGapWidget extends \WP_Widget {

- +

in_array($new_instance['mode'] ?? '', ['html', 'gap'], true) ? $new_instance['mode'] : 'html', 'title' => sanitize_text_field($new_instance['title'] ?? ''), 'content' => wp_kses_post($new_instance['content'] ?? ''), - 'gap_height' => max(0, min(200, (int) ($new_instance['gap_height'] ?? 20))), + 'gap_height' => max(0, min(500, (int) ($new_instance['gap_height'] ?? 20))), ]; } } diff --git a/src/AKSRT/Widgets/FriendLinksWidget.php b/src/AKSRT/Widgets/FriendLinksWidget.php index 3114456c..b4b9de51 100644 --- a/src/AKSRT/Widgets/FriendLinksWidget.php +++ b/src/AKSRT/Widgets/FriendLinksWidget.php @@ -1,5 +1,7 @@ esc_html__('显示友情链接列表', 'aksrt'), - ]); - } + public function __construct() { + parent::__construct('aksrt_friend_links', esc_html__('AKSRT 友情链接', 'aksrt'), [ + 'description' => esc_html__('显示友情链接(设置项或 WordPress 书签)。', 'aksrt'), + ]); + } - public function widget($args, $instance): void { - echo $args['before_widget']; - $title = apply_filters('widget_title', $instance['title'] ?? __('友情链接', 'aksrt'), $instance, $this->id_base); - $show_icon = !empty($instance['show_icon']); - $columns = max(1, min(4, (int) ($instance['columns'] ?? 2))); + public function widget($args, $instance): void { + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? __('友情链接', 'aksrt')), $instance, $this->id_base); + $show_icon = !empty($instance['show_icon']) || !isset($instance['show_icon']); + $columns = max(1, min(4, (int) ($instance['columns'] ?? 2))); + $source = sanitize_key((string) ($instance['source'] ?? 'settings')); + $go_link = !empty($instance['go_link']); + $limit = max(1, min(100, (int) ($instance['limit'] ?? 24))); - if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title']; + if ($title) { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - $links = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_friend_links() : []; + $links = $this->resolve_links($source, $instance, $limit); + if (!$links) { + echo function_exists('aksrt_get_widget_null') + ? aksrt_get_widget_null(__('暂无友情链接', 'aksrt')) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + : '

' . esc_html__('暂无友情链接', 'aksrt') . '

'; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } + ?> + + - - - ' . esc_html__('暂无友情链接', 'aksrt') . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> - $instance + * @return array> + */ + private function resolve_links(string $source, array $instance, int $limit): array { + if ($source === 'bookmarks') { + $bm_args = [ + 'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'name')) ?: 'name', + 'order' => strtoupper((string) ($instance['order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC', + 'limit' => $limit, + ]; + $cats = trim((string) ($instance['cats'] ?? '')); + if ($cats !== '') { + $bm_args['category'] = $cats; + } + $out = []; + foreach (get_bookmarks($bm_args) as $b) { + $out[] = [ + 'name' => (string) $b->link_name, + 'url' => (string) $b->link_url, + 'description' => (string) $b->link_description, + 'icon' => (string) $b->link_image, + ]; + } + return $out; + } - echo $args['after_widget']; - } + $links = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_friend_links() : []; + if (!is_array($links) || !$links) { + // Fallback to WP bookmarks when settings empty. + return $this->resolve_links('bookmarks', $instance, $limit); + } + return array_slice(array_values($links), 0, $limit); + } - public function form($instance): void { - ?> -

- - -

-

- - -

-

- -

-

- +

+ +

+

+ +

+

+ + +

+

+ +

+

+
+ +

+ sanitize_text_field($new_instance['title'] ?? ''), - 'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 2))), - 'show_icon'=> !empty($new_instance['show_icon']), - ]; - } + public function update($new_instance, $old_instance): array { + return [ + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'source' => ($new_instance['source'] ?? '') === 'bookmarks' ? 'bookmarks' : 'settings', + 'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 2))), + 'limit' => max(1, min(100, (int) ($new_instance['limit'] ?? 24))), + 'cats' => sanitize_text_field((string) ($new_instance['cats'] ?? '')), + 'show_icon' => !empty($new_instance['show_icon']) ? 1 : 0, + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + 'orderby' => sanitize_key((string) ($new_instance['orderby'] ?? 'name')), + 'order' => strtoupper((string) ($new_instance['order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC', + ]; + } } diff --git a/src/AKSRT/Widgets/HotPostsWidget.php b/src/AKSRT/Widgets/HotPostsWidget.php index 0ce3448e..0c52ba49 100644 --- a/src/AKSRT/Widgets/HotPostsWidget.php +++ b/src/AKSRT/Widgets/HotPostsWidget.php @@ -27,6 +27,7 @@ class HotPostsWidget extends \WP_Widget { $count = absint($instance['count'] ?? 5); $order_by = $instance['order_by'] ?? 'views'; $days = absint($instance['days'] ?? 30); + $show_thumb = !empty($instance['show_thumb']); if ($title) { echo $args['before_title'] . esc_html($title) . $args['after_title']; @@ -85,6 +86,13 @@ class HotPostsWidget extends \WP_Widget { ?>
+ + + + +
@@ -132,7 +140,11 @@ class HotPostsWidget extends \WP_Widget {

- + +

+ esc_html__('显示图标+文字卡片网格', 'aksrt'), - ]); - } + public function __construct() { + parent::__construct('aksrt_icon_cards', esc_html__('AKSRT 图标卡片', 'aksrt'), [ + 'description' => esc_html__('图标在上、文案在下的竖版图标卡片网格。', 'aksrt'), + ]); + } - public function widget($args, $instance): void { - echo $args['before_widget']; - $title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base); - $cards = isset($instance['cards']) && is_array($instance['cards']) ? $instance['cards'] : []; - $columns = max(1, min(4, (int) ($instance['columns'] ?? 2))); + public function widget($args, $instance): void { + $cards = isset($instance['cards']) && is_array($instance['cards']) ? $instance['cards'] : []; + $columns = max(1, min(4, (int) ($instance['columns'] ?? $instance['pc_row'] ?? 2))); + $m_cols = max(1, min(3, (int) ($instance['m_row'] ?? 2))); + $go_link = !empty($instance['go_link']); + $show_bg = !isset($instance['show_widget_bg']) || !empty($instance['show_widget_bg']); + $icon_size = max(18, min(48, (int) ($instance['icon_size'] ?? 28))); - if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title']; + $valid = []; + foreach ($cards as $card) { + if (!is_array($card)) { + continue; + } + $title = sanitize_text_field((string) ($card['title'] ?? '')); + if ($title === '') { + continue; + } + $url = esc_url_raw(trim((string) ($card['link'] ?? $card['url'] ?? ''))); + if ($url !== '' && $go_link && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + $valid[] = [ + 'title' => $title, + 'icon' => sanitize_text_field((string) ($card['icon'] ?? '')), + 'desc' => sanitize_text_field((string) ($card['description'] ?? $card['desc'] ?? '')), + 'url' => $url, + ]; + } + if (!$valid) { + return; + } - if (!empty($cards)) : ?> -
- - < class="widget-icon-cards__item"> - -
- -
-
- -
- -
- > - -
- id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - echo $args['after_widget']; - } + $style = sprintf('--ic-columns:%d;--ic-m-columns:%d;--ic-icon:%dpx;', $columns, $m_cols, $icon_size); + echo '
'; + foreach ($valid as $card) { + $tag = $card['url'] !== '' ? 'a' : 'div'; + $extra = $card['url'] !== '' ? ' href="' . esc_url($card['url']) . '" target="_blank" rel="noopener noreferrer"' : ''; + echo '<' . $tag . ' class="widget-icon-cards__item is-vertical"' . $extra . '>'; + if ($card['icon'] !== '') { + echo '
'; + } + echo '
'; + echo '
' . esc_html($card['title']) . '
'; + if ($card['desc'] !== '') { + echo '
' . esc_html($card['desc']) . '
'; + } + echo '
'; + } + echo '
'; - public function form($instance): void { - $cards = isset($instance['cards']) && is_array($instance['cards']) ? $instance['cards'] : []; - while (count($cards) < 4) { - $cards[] = ['title' => '', 'icon' => '', 'link' => '', 'description' => '']; - } - ?> -

- - -

-

- - -

-
- $card) : ?> -
-

" value="" placeholder="">

-

" value="" placeholder="">

-

" value="" placeholder="">

-

" value="" placeholder="">

-
- sanitize_text_field($card['title'] ?? ''), - 'icon' => sanitize_text_field($card['icon'] ?? ''), - 'link' => esc_url_raw($card['link'] ?? ''), - 'description' => sanitize_text_field($card['description'] ?? ''), - ]; - } - } - return [ - 'title' => sanitize_text_field($new_instance['title'] ?? ''), - 'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 2))), - 'cards' => $cards, - ]; - } + public function form($instance): void { + $cards = isset($instance['cards']) && is_array($instance['cards']) ? $instance['cards'] : []; + while (count($cards) < 4) { + $cards[] = ['title' => '', 'icon' => '', 'link' => '', 'description' => '']; + } + ?> +

+ +

+

+ + +

+

+ + +

+
+ $card) : ?> +
+ " value="" placeholder=""> + " value="" placeholder=""> + " value="" placeholder=""> + " value="" placeholder=""> +
+ $title, + 'icon' => sanitize_text_field((string) ($card['icon'] ?? '')), + 'link' => esc_url_raw(trim((string) ($card['link'] ?? ''))), + 'description' => sanitize_text_field((string) ($card['description'] ?? '')), + ]; + if (count($cards) >= 12) { + break; + } + } + } + return [ + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'columns' => max(1, min(4, (int) ($new_instance['columns'] ?? 2))), + 'm_row' => max(1, min(3, (int) ($new_instance['m_row'] ?? 2))), + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + 'show_widget_bg' => !empty($new_instance['show_widget_bg']) ? 1 : 0, + 'cards' => $cards, + ]; + } } diff --git a/src/AKSRT/Widgets/NoticeWidget.php b/src/AKSRT/Widgets/NoticeWidget.php index 08a03955..18ff6ebc 100644 --- a/src/AKSRT/Widgets/NoticeWidget.php +++ b/src/AKSRT/Widgets/NoticeWidget.php @@ -1,5 +1,7 @@ esc_html__('显示公告/通知信息', 'aksrt'), - ]); - } + public function __construct() { + parent::__construct('aksrt_notice', esc_html__('AKSRT 滚动公告', 'aksrt'), [ + 'description' => esc_html__('多条公告滚动显示。', 'aksrt'), + ]); + } - public function widget($args, $instance): void { - echo $args['before_widget']; - $title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base); - $content = wp_kses_post($instance['content'] ?? ''); - $style = sanitize_key($instance['style'] ?? 'info'); - $icon = sanitize_text_field($instance['icon'] ?? 'fas fa-bullhorn'); + public function widget($args, $instance): void { + $items = $this->normalize_items($instance); + if (!$items) { + return; + } - if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title']; - ?> -
- -
- -
-
- -

- - -

-

- - -

-

- - -

-

- - - -

- id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - public function update($new_instance, $old_instance): array { - return [ - 'title' => sanitize_text_field($new_instance['title'] ?? ''), - 'content' => wp_kses_post($new_instance['content'] ?? ''), - 'style' => in_array($new_instance['style'] ?? '', ['info', 'success', 'warning', 'danger'], true) ? $new_instance['style'] : 'info', - 'icon' => sanitize_text_field($new_instance['icon'] ?? 'fas fa-bullhorn'), - ]; - } + $uid = 'aksrt-notice-' . esc_attr($this->id); + $class = 'aksrt-scroll-notice color-' . $color . ($radius ? ' is-radius' : ''); + echo '
'; + echo '
'; + echo '
'; + foreach ($items as $i => $item) { + $active = $i === 0 ? ' is-active' : ''; + $inner = ''; + if ($item['icon'] !== '') { + $inner .= ' '; + } + $inner .= esc_html($item['title']); + echo '
'; + if ($item['href'] !== '') { + $tgt = $blank ? ' target="_blank" rel="noopener noreferrer"' : ''; + echo '' . $inner . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + echo '' . $inner . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + } + echo '
'; + + if (count($items) > 1) { + ?> + + $instance + * @return array + */ + private function normalize_items(array $instance): array { + $raw = $instance['items'] ?? $instance['img_ids'] ?? null; + if ($raw === null && !empty($instance['content'])) { + // Legacy single content. + return [[ + 'title' => wp_strip_all_tags((string) $instance['content']), + 'icon' => sanitize_text_field((string) ($instance['icon'] ?? 'fas fa-bullhorn')), + 'href' => '', + ]]; + } + if (is_string($raw)) { + $decoded = json_decode($raw, true); + $raw = is_array($decoded) ? $decoded : []; + } + if (!is_array($raw)) { + return []; + } + $out = []; + foreach ($raw as $row) { + if (!is_array($row)) { + continue; + } + $title = sanitize_text_field((string) ($row['title'] ?? $row['content'] ?? '')); + if ($title === '') { + continue; + } + $out[] = [ + 'title' => $title, + 'icon' => sanitize_text_field((string) ($row['icon'] ?? '')), + 'href' => esc_url_raw(trim((string) ($row['href'] ?? $row['url'] ?? $row['link'] ?? ''))), + ]; + if (count($out) >= 12) { + break; + } + } + return $out; + } + + public function form($instance): void { + $items = $this->normalize_items($instance); + if (!$items) { + $items = [ + ['title' => '', 'icon' => 'fas fa-bullhorn', 'href' => ''], + ['title' => '', 'icon' => 'fas fa-info-circle', 'href' => ''], + ]; + } + $json = wp_json_encode($items, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+
+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'items' => $this->normalize_items(['items' => $new_instance['items'] ?? []]), + 'interval' => max(2, min(15, (int) ($new_instance['interval'] ?? 4))), + 'color' => sanitize_html_class((string) ($new_instance['color'] ?? 'c-blue')), + 'blank' => !empty($new_instance['blank']) ? 1 : 0, + 'radius' => !empty($new_instance['radius']) ? 1 : 0, + ]; + } } diff --git a/src/AKSRT/Widgets/RecentCommentsWidget.php b/src/AKSRT/Widgets/RecentCommentsWidget.php index f24de874..5820e1aa 100644 --- a/src/AKSRT/Widgets/RecentCommentsWidget.php +++ b/src/AKSRT/Widgets/RecentCommentsWidget.php @@ -1,5 +1,7 @@ esc_html__('显示最新评论列表', 'aksrt'), - ]); - } + public function __construct() { + parent::__construct('aksrt_recent_comments', esc_html__('AKSRT 最新评论', 'aksrt'), [ + 'description' => esc_html__('显示最新评论列表', 'aksrt'), + ]); + } - public function widget($args, $instance): void { - echo $args['before_widget']; - $title = apply_filters('widget_title', $instance['title'] ?? __('最新评论', 'aksrt'), $instance, $this->id_base); - $count = max(1, min(20, (int) ($instance['count'] ?? 5))); - $show_avatar = !empty($instance['show_avatar']); + public function widget($args, $instance): void { + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? __('最新评论', 'aksrt')), $instance, $this->id_base); + $count = max(1, min(20, (int) ($instance['count'] ?? $instance['limit'] ?? 5))); + $show_avatar = !isset($instance['show_avatar']) || !empty($instance['show_avatar']); + $outpost = max(0, (int) ($instance['outpost'] ?? 0)); + $outer = sanitize_text_field((string) ($instance['outer'] ?? '')); - if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title']; + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - $comments = get_comments([ - 'number' => $count, - 'status' => 'approve', - 'post_status' => 'publish', - ]); + $query = [ + 'number' => $count, + 'status' => 'approve', + 'post_status' => 'publish', + ]; + if ($outpost > 0) { + $query['post__not_in'] = [$outpost]; + } + if ($outer !== '') { + // Exclude comments containing outer keywords (comma separated). + // Implemented post-filter below. + } + $comments = get_comments($query); + if ($outer !== '' && $comments) { + $keys = array_filter(array_map('trim', preg_split('/[,,]/', $outer) ?: [])); + if ($keys) { + $comments = array_values(array_filter($comments, static function ($c) use ($keys) { + $body = (string) $c->comment_content; + foreach ($keys as $k) { + if ($k !== '' && str_contains($body, $k)) { + return false; + } + } + return true; + })); + } + } - if ($comments) : ?> -
- comment_post_ID); - if (!$post) continue; - ?> -
- -
- -
- -
-
- comment_author); ?> - comment_date), current_time('timestamp'))); ?> -
-

- comment_content, 15, '...')); ?> -

- - ID)); ?> - -
-
- -
- - ' . esc_html__('暂无评论', 'aksrt') . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> - '; + foreach ($comments as $comment) { + $post = get_post($comment->comment_post_ID); + if (!$post) { + continue; + } + echo '
'; + if ($show_avatar) { + echo '
' . get_avatar($comment, 32) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + echo '
'; + echo '' . esc_html($comment->comment_author) . ''; + echo '' . esc_html(human_time_diff(strtotime($comment->comment_date), current_time('timestamp'))) . ''; + echo '
'; + echo '

' . esc_html(wp_trim_words($comment->comment_content, 15, '...')) . '

'; + echo '
' . esc_html(get_the_title($post)) . '
'; + echo '
'; + } + echo '
'; + } else { + echo '

' . esc_html__('暂无评论', 'aksrt') . '

'; + } - echo $args['after_widget']; - } + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - public function form($instance): void { - ?> -

- - -

-

- - -

-

- -

- +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field($new_instance['title'] ?? ''), - 'count' => max(1, min(20, (int) ($new_instance['count'] ?? 5))), - 'show_avatar' => !empty($new_instance['show_avatar']), - ]; - } + public function update($new_instance, $old_instance): array { + return [ + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'count' => max(1, min(20, (int) ($new_instance['count'] ?? 5))), + 'outpost' => max(0, (int) ($new_instance['outpost'] ?? 0)), + 'outer' => sanitize_text_field((string) ($new_instance['outer'] ?? '')), + 'show_avatar' => !empty($new_instance['show_avatar']) ? 1 : 0, + ]; + } } diff --git a/src/AKSRT/Widgets/SearchWidget.php b/src/AKSRT/Widgets/SearchWidget.php index eca6bc69..394da33f 100644 --- a/src/AKSRT/Widgets/SearchWidget.php +++ b/src/AKSRT/Widgets/SearchWidget.php @@ -1,5 +1,7 @@ esc_html__('带样式的搜索框', 'aksrt'), - ]); - } + public function __construct() { + parent::__construct('aksrt_search', esc_html__('AKSRT 搜索', 'aksrt'), [ + 'description' => esc_html__('搜索框,可显示热门关键词。', 'aksrt'), + ]); + } - public function widget($args, $instance): void { - echo $args['before_widget']; - $title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base); - if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title']; - $placeholder = sanitize_text_field($instance['placeholder'] ?? __('搜索文章...', 'aksrt')); - ?> - - id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - public function form($instance): void { - ?> -

- - -

-

- - -

- sanitize_text_field($new_instance['title'] ?? ''), - 'placeholder' => sanitize_text_field($new_instance['placeholder'] ?? ''), - ]; - } + $keywords = []; + if ($show_kw) { + if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'hot_search_keywords')) { + $keywords = \AKSRT\Settings\Settings::hot_search_keywords(); + } elseif (function_exists('aksrt_get_setting')) { + $raw = (string) aksrt_get_setting('hot_search_keywords', ''); + $keywords = array_values(array_filter(array_map('trim', preg_split('/[\r\n,,]+/', $raw) ?: []))); + } + $custom = trim((string) ($instance['keywords'] ?? '')); + if ($custom !== '') { + $keywords = array_values(array_filter(array_map('trim', preg_split('/[\r\n,,]+/', $custom) ?: []))); + } + $keywords = array_slice($keywords, 0, 12); + } + ?> + + +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'placeholder' => sanitize_text_field((string) ($new_instance['placeholder'] ?? '')), + 'show_keywords' => !empty($new_instance['show_keywords']) ? 1 : 0, + 'show_history' => !empty($new_instance['show_history']) ? 1 : 0, + 'keywords_title' => sanitize_text_field((string) ($new_instance['keywords_title'] ?? '')), + 'keywords' => sanitize_textarea_field((string) ($new_instance['keywords'] ?? '')), + 'more_but' => sanitize_text_field((string) ($new_instance['more_but'] ?? '')), + 'more_but_url' => esc_url_raw(trim((string) ($new_instance['more_but_url'] ?? ''))), + ]; + } } diff --git a/src/AKSRT/Widgets/TagCloudWidget.php b/src/AKSRT/Widgets/TagCloudWidget.php index 22eeb779..53044419 100644 --- a/src/AKSRT/Widgets/TagCloudWidget.php +++ b/src/AKSRT/Widgets/TagCloudWidget.php @@ -1,5 +1,7 @@ esc_html__('展示文章标签云', 'aksrt'), + [ + 'description' => esc_html__('展示文章标签/分类云', 'aksrt'), 'customize_selective_refresh' => true, - ) + ] ); } - public function widget($args, $instance) { - echo $args['before_widget']; + public function widget($args, $instance): void { + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - $title = apply_filters('widget_title', $instance['title'], $instance, $this->id_base); - $count = absint($instance['count'] ?: 15); + $taxonomy = sanitize_key((string) ($instance['taxonomy'] ?? 'post_tag')); + if (!taxonomy_exists($taxonomy)) { + $taxonomy = 'post_tag'; + } + $number = max(1, min(80, (int) ($instance['count'] ?? $instance['number'] ?? 15))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'count')); + if (!in_array($orderby, ['count', 'name', 'id'], true)) { + $orderby = 'count'; + } + $show_count = !empty($instance['show_count']); + $blank = !empty($instance['blank']); + $color = sanitize_html_class((string) ($instance['color'] ?? 'rand')); + $fixed_width = !empty($instance['fixed_width']); - if ($title): ?> -

- + $tags = get_terms([ + 'taxonomy' => $taxonomy, + 'orderby' => $orderby, + 'order' => 'DESC', + 'number' => $number, + 'hide_empty' => !empty($instance['hide_empty']), + ]); + if (is_wp_error($tags) || !$tags) { + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } -
- $count, 'orderby' => 'count', 'order' => 'DESC']); - foreach ($tags as $tag): - $size = 12 + min(4, (int)($tag->count / 5)); - ?> - #name); ?> - -
- '; + foreach ($tags as $tag) { + $url = get_term_link($tag); + if (is_wp_error($url)) { + continue; + } + $cls = 'but'; + if ($color === 'rand') { + if ($i > 10) { + $i = 0; + } + $cls .= ' ' . $palette[$i]; + $i++; + } elseif ($color !== '') { + $cls .= ' ' . $color; + } + $tgt = $blank ? ' target="_blank" rel="noopener noreferrer"' : ''; + $count = $show_count ? ' (' . (int) $tag->count . ')' : ''; + echo '' . esc_html($tag->name) . $count . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } - public function form($instance) { - $title = $instance['title'] ?? __('标签云', 'aksrt'); - $count = isset($instance['count']) ? (int)$instance['count'] : 15; + public function form($instance): void { + $taxs = get_taxonomies(['public' => true], 'objects'); ?>

- - +

- - + +

+

+ +

+

+ +

+

+ +

+

+
+
+
+

sanitize_text_field($new['title']), - 'count' => absint($new['count']), + 'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'taxonomy' => sanitize_key((string) ($new_instance['taxonomy'] ?? 'post_tag')), + 'count' => max(1, min(80, (int) ($new_instance['count'] ?? 15))), + 'orderby' => in_array(($new_instance['orderby'] ?? ''), ['count', 'name', 'id'], true) ? $new_instance['orderby'] : 'count', + 'color' => sanitize_html_class((string) ($new_instance['color'] ?? 'rand')), + 'show_count' => !empty($new_instance['show_count']) ? 1 : 0, + 'blank' => !empty($new_instance['blank']) ? 1 : 0, + 'fixed_width' => !empty($new_instance['fixed_width']) ? 1 : 0, + 'hide_empty' => !empty($new_instance['hide_empty']) ? 1 : 0, ]; } } diff --git a/src/AKSRT/Widgets/WidgetAuthorCard.php b/src/AKSRT/Widgets/WidgetAuthorCard.php index 70c11018..6a523eda 100644 --- a/src/AKSRT/Widgets/WidgetAuthorCard.php +++ b/src/AKSRT/Widgets/WidgetAuthorCard.php @@ -85,6 +85,28 @@ class WidgetAuthorCard extends \WP_Widget { if ($bio !== '') { echo '
' . esc_html(wp_trim_words($bio, 28)) . '
'; } + $show_badge = !isset($instance['show_badge']) || !empty($instance['show_badge']); + if ($show_badge && class_exists('\AKSRT\User\Badges') && \AKSRT\User\Badges::public_display()) { + $wear = \AKSRT\User\Badges::get_wear($author_id); + if ($wear === '') { + // zibll alias fallback + $wear = sanitize_key((string) get_user_meta($author_id, 'wear_medal', true)); + } + if ($wear !== '') { + $map = \AKSRT\User\Badges::get_dictionary_map(); + if (isset($map[$wear])) { + $badge = $map[$wear]; + echo '
'; + if (!empty($badge['icon_url'])) { + echo ' '; + } else { + echo ' '; + } + echo esc_html((string) $badge['label']); + echo '
'; + } + } + } if ($show_stats) { echo '
'; echo '' . esc_html((string) $post_count) . '' . esc_html__('文章', 'aksrt') . ''; @@ -94,6 +116,13 @@ class WidgetAuthorCard extends \WP_Widget { echo '' . esc_html((string) $followers) . '' . esc_html__('粉丝', 'aksrt') . ''; echo '
'; } + $cuid = get_current_user_id(); + $show_follow = !isset($instance['show_follow']) || !empty($instance['show_follow']); + if ($show_follow && $cuid > 0 && $cuid !== $author_id && class_exists('\AKSRT\User\Follow')) { + $following = \AKSRT\User\Follow::get_instance()->is_following($cuid, $author_id); + $label = $following ? __('已关注', 'aksrt') : __('关注', 'aksrt'); + echo ''; + } echo '' . esc_html__('查看主页', 'aksrt') . ''; echo '
'; @@ -115,7 +144,9 @@ class WidgetAuthorCard extends \WP_Widget {


- +
+
+

max(0, (int) ($new_instance['user_id'] ?? 0)), 'show_img_bg' => !empty($new_instance['show_img_bg']) ? 1 : 0, 'show_stats' => !empty($new_instance['show_stats']) ? 1 : 0, + 'show_follow' => !empty($new_instance['show_follow']) ? 1 : 0, + 'show_badge' => !empty($new_instance['show_badge']) ? 1 : 0, ]; } } diff --git a/src/AKSRT/Widgets/WidgetBookmarkLinks.php b/src/AKSRT/Widgets/WidgetBookmarkLinks.php new file mode 100644 index 00000000..e3d900e0 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetBookmarkLinks.php @@ -0,0 +1,161 @@ + esc_html__('WordPress 链接(书签)列表,适合友链/导航。', 'aksrt'), + 'classname' => 'widget_aksrt_bookmark_links', + ] + ); + } + + public function widget($args, $instance): void { + $limit = max(0, min(100, (int) ($instance['links_limit'] ?? 12))); + $orderby = sanitize_key((string) ($instance['links_orderby'] ?? 'name')); + if (!in_array($orderby, ['name', 'updated', 'rating', 'rand', 'id'], true)) { + $orderby = 'name'; + } + $order = strtoupper((string) ($instance['links_order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC'; + $type = sanitize_key((string) ($instance['type'] ?? 'card')); + if (!in_array($type, ['card', 'image', 'simple'], true)) { + $type = 'card'; + } + $go_link = !empty($instance['go_link']); + $blank = !isset($instance['blank']) || !empty($instance['blank']); + $nofollow = !empty($instance['nofollow']); + $show_box = !empty($instance['show_box']); + + $bm_args = [ + 'orderby' => $orderby === 'rand' ? 'rand' : $orderby, + 'order' => $order, + 'limit' => $limit > 0 ? $limit : -1, + ]; + $cats = trim((string) ($instance['links_cats'] ?? '')); + if ($cats !== '' && $cats !== 'all') { + $bm_args['category'] = $cats; + } + $bookmarks = get_bookmarks($bm_args); + if (!$bookmarks) { + return; + } + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + $wrap = 'aksrt-bookmark-links style-' . $type . ($show_box ? ' has-box' : ''); + echo '
'; + foreach ($bookmarks as $b) { + $url = (string) $b->link_url; + if ($url !== '' && $go_link && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + $rel = []; + if ($nofollow) { + $rel[] = 'nofollow'; + } + if ($blank) { + $rel[] = 'noopener'; + $rel[] = 'noreferrer'; + } + $rel_attr = $rel ? ' rel="' . esc_attr(implode(' ', $rel)) . '"' : ''; + $tgt = $blank ? ' target="_blank"' : ''; + $img = (string) $b->link_image; + $name = (string) $b->link_name; + $desc = (string) $b->link_description; + + echo ''; + if ($type !== 'simple' && $img !== '') { + echo ''; + } + echo ''; + echo '' . esc_html($name) . ''; + if ($type === 'card' && $desc !== '') { + echo '' . esc_html($desc) . ''; + } + echo ''; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ + +

+

+
+
+
+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'links_cats' => sanitize_text_field((string) ($new_instance['links_cats'] ?? '')), + 'links_limit' => max(0, min(100, (int) ($new_instance['links_limit'] ?? 12))), + 'links_orderby' => sanitize_key((string) ($new_instance['links_orderby'] ?? 'name')), + 'links_order' => strtoupper((string) ($new_instance['links_order'] ?? 'ASC')) === 'DESC' ? 'DESC' : 'ASC', + 'type' => in_array(($new_instance['type'] ?? ''), ['card', 'image', 'simple'], true) ? $new_instance['type'] : 'card', + 'blank' => !empty($new_instance['blank']) ? 1 : 0, + 'nofollow' => !empty($new_instance['nofollow']) ? 1 : 0, + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + 'show_box' => !empty($new_instance['show_box']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetCoverCards.php b/src/AKSRT/Widgets/WidgetCoverCards.php index 99875055..61dc1f52 100644 --- a/src/AKSRT/Widgets/WidgetCoverCards.php +++ b/src/AKSRT/Widgets/WidgetCoverCards.php @@ -35,6 +35,7 @@ class WidgetCoverCards extends \WP_Widget { foreach ($items as $item) { $image = esc_url($item['image'] ?? ''); + $video = esc_url($item['video'] ?? ''); $link = esc_url($item['link'] ?? ''); $card_title = sanitize_text_field($item['title'] ?? ''); $desc = sanitize_text_field($item['desc'] ?? ''); @@ -53,7 +54,11 @@ class WidgetCoverCards extends \WP_Widget { echo '
'; - if ($image) { + if ($video) { + echo '
'; + echo ''; + echo '
'; + } elseif ($image) { echo '
'; echo '' . esc_attr($card_title) . ''; echo '
'; diff --git a/src/AKSRT/Widgets/WidgetIconCoverCards.php b/src/AKSRT/Widgets/WidgetIconCoverCards.php new file mode 100644 index 00000000..4aca3f14 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetIconCoverCards.php @@ -0,0 +1,163 @@ + esc_html__('横向图标 + 标题/描述的导航卡片。', 'aksrt'), + 'classname' => 'widget_aksrt_icon_cover_cards', + ] + ); + } + + public function widget($args, $instance): void { + $cards = $this->normalize($instance['cards'] ?? []); + if (!$cards) { + return; + } + $cols = max(1, min(4, (int) ($instance['pc_row'] ?? 2))); + $m_cols = max(1, min(2, (int) ($instance['m_row'] ?? 1))); + $icon_size = max(16, min(48, (int) ($instance['icon_size'] ?? 25))); + $radius = !empty($instance['icon_radius4']); + $go_link = !empty($instance['go_link']); + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + $style = sprintf('--aksrt-icc-pc:%d;--aksrt-icc-m:%d;--aksrt-icc-icon:%dpx;', $cols, $m_cols, $icon_size); + echo '
'; + foreach ($cards as $card) { + $url = $card['url']; + if ($url !== '' && $go_link && function_exists('aksrt_go_link')) { + $url = aksrt_go_link($url, true); + } + $tag = $url !== '' ? 'a' : 'div'; + $href = $url !== '' ? ' href="' . esc_url($url) . '"' . ($card['target'] !== '' ? ' target="' . esc_attr($card['target']) . '" rel="noopener noreferrer"' : '') : ''; + echo '<' . $tag . ' class="aksrt-icon-cover__item"' . $href . '>'; + echo '
'; + if ($card['icon_image'] !== '') { + echo ''; + } elseif ($card['icon'] !== '') { + echo ''; + } + echo '
'; + echo '
'; + if ($card['title'] !== '') { + echo '
' . esc_html($card['title']) . '
'; + } + if ($card['desc'] !== '') { + echo '
' . esc_html($card['desc']) . '
'; + } + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * @param mixed $raw + * @return array> + */ + private function normalize($raw): array { + if (is_string($raw)) { + $decoded = json_decode($raw, true); + $raw = is_array($decoded) ? $decoded : []; + } + if (!is_array($raw)) { + return []; + } + $out = []; + foreach ($raw as $row) { + if (!is_array($row)) { + continue; + } + $title = sanitize_text_field((string) ($row['title'] ?? '')); + $icon = sanitize_text_field((string) ($row['icon'] ?? '')); + $icon_image = esc_url_raw(trim((string) ($row['icon_image'] ?? $row['customize_icon'] ?? ''))); + if ($title === '' && $icon === '' && $icon_image === '') { + continue; + } + $url = ''; + if (is_array($row['link'] ?? null)) { + $url = esc_url_raw(trim((string) ($row['link']['url'] ?? ''))); + $target = sanitize_text_field((string) ($row['link']['target'] ?? '')); + } else { + $url = esc_url_raw(trim((string) ($row['url'] ?? $row['link'] ?? ''))); + $target = sanitize_text_field((string) ($row['target'] ?? '_blank')); + } + $out[] = [ + 'title' => $title, + 'desc' => sanitize_text_field((string) ($row['desc'] ?? '')), + 'icon' => $icon, + 'icon_image' => $icon_image, + 'url' => $url, + 'target' => $target, + ]; + if (count($out) >= 16) { + break; + } + } + return $out; + } + + public function form($instance): void { + $json = is_string($instance['cards'] ?? null) + ? (string) $instance['cards'] + : wp_json_encode($this->normalize($instance['cards'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + ?> +

+ +

+

+ +

+

+ + + +

+

+
+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'cards' => $this->normalize($new_instance['cards'] ?? []), + 'pc_row' => max(1, min(4, (int) ($new_instance['pc_row'] ?? 2))), + 'm_row' => max(1, min(2, (int) ($new_instance['m_row'] ?? 1))), + 'icon_size' => max(16, min(48, (int) ($new_instance['icon_size'] ?? 25))), + 'icon_radius4' => !empty($new_instance['icon_radius4']) ? 1 : 0, + 'go_link' => !empty($new_instance['go_link']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetMainPosts.php b/src/AKSRT/Widgets/WidgetMainPosts.php new file mode 100644 index 00000000..f2ce4ff8 --- /dev/null +++ b/src/AKSRT/Widgets/WidgetMainPosts.php @@ -0,0 +1,207 @@ + esc_html__('卡片/迷你/列表样式的文章模块。', 'aksrt'), + 'classname' => 'widget_aksrt_main_posts', + ] + ); + } + + public function widget($args, $instance): void { + $count = max(1, min(30, (int) ($instance['count'] ?? 6))); + $style = sanitize_key((string) ($instance['style'] ?? 'list')); + if (!in_array($style, ['list', 'card', 'mini'], true)) { + $style = 'list'; + } + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + $cat = (int) ($instance['cat'] ?? 0); + $limit_day = max(0, (int) ($instance['limit_day'] ?? 0)); + $show_thumb = !isset($instance['show_thumb']) || !empty($instance['show_thumb']); + $show_meta = !isset($instance['show_meta']) || !empty($instance['show_meta']); + $show_number = !empty($instance['show_number']); + + $query = [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => $count, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + ]; + if ($cat > 0) { + $query['cat'] = $cat; + } + if ($limit_day > 0) { + $query['date_query'] = [['after' => $limit_day . ' days ago']]; + } + switch ($orderby) { + case 'views': + $query['meta_key'] = 'aksrt_post_views'; + $query['orderby'] = 'meta_value_num'; + $query['order'] = 'DESC'; + break; + case 'comments': + $query['orderby'] = 'comment_count'; + $query['order'] = 'DESC'; + break; + case 'rand': + $query['orderby'] = 'rand'; + break; + case 'modified': + $query['orderby'] = 'modified'; + $query['order'] = 'DESC'; + break; + default: + $query['orderby'] = 'date'; + $query['order'] = 'DESC'; + } + + $q = new \WP_Query($query); + if (!$q->have_posts()) { + wp_reset_postdata(); + return; + } + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + echo ''; + wp_reset_postdata(); + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+
+
+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'count' => max(1, min(30, (int) ($new_instance['count'] ?? 6))), + 'style' => $style, + 'orderby' => $orderby, + 'cat' => max(0, (int) ($new_instance['cat'] ?? 0)), + 'limit_day' => max(0, (int) ($new_instance['limit_day'] ?? 0)), + 'show_thumb' => !empty($new_instance['show_thumb']) ? 1 : 0, + 'show_meta' => !empty($new_instance['show_meta']) ? 1 : 0, + 'show_number' => !empty($new_instance['show_number']) ? 1 : 0, + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetTermAggregation.php b/src/AKSRT/Widgets/WidgetTermAggregation.php new file mode 100644 index 00000000..be75dcff --- /dev/null +++ b/src/AKSRT/Widgets/WidgetTermAggregation.php @@ -0,0 +1,197 @@ + esc_html__('分类封面 + 最新文章列表聚合卡。', 'aksrt'), + 'classname' => 'widget_aksrt_term_aggregation', + ] + ); + } + + public function widget($args, $instance): void { + $taxonomy = sanitize_key((string) ($instance['taxonomy'] ?? 'category')); + if (!taxonomy_exists($taxonomy)) { + $taxonomy = 'category'; + } + $term_ids = $this->parse_ids((string) ($instance['term_ids'] ?? '')); + $count = max(1, min(12, (int) ($instance['count'] ?? 4))); + $orderby = sanitize_key((string) ($instance['orderby'] ?? 'date')); + $columns = max(1, min(3, (int) ($instance['columns'] ?? 1))); + + if (!$term_ids) { + // Fallback: top terms by count. + $terms = get_terms([ + 'taxonomy' => $taxonomy, + 'number' => max(1, min(4, (int) ($instance['auto_terms'] ?? 2))), + 'orderby' => 'count', + 'order' => 'DESC', + 'hide_empty' => true, + ]); + if (is_wp_error($terms) || !$terms) { + return; + } + $term_ids = array_map(static fn($t) => (int) $t->term_id, $terms); + } + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + echo '
'; + foreach ($term_ids as $term_id) { + $term = get_term($term_id, $taxonomy); + if (!$term || is_wp_error($term)) { + continue; + } + $link = get_term_link($term); + if (is_wp_error($link)) { + continue; + } + $cover = ''; + $img_id = (int) get_term_meta($term_id, 'thumbnail_id', true); + if (!$img_id) { + $img_id = (int) get_term_meta($term_id, '_aksrt_cover', true); + } + if ($img_id) { + $cover = wp_get_attachment_image_url($img_id, 'medium') ?: ''; + } + if ($cover === '') { + $cover = (string) get_term_meta($term_id, '_aksrt_cover_url', true); + } + + $posts_q = [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'posts_per_page' => $count, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'tax_query' => [[ + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $term_id, + ]], + ]; + if ($orderby === 'views') { + $posts_q['meta_key'] = 'aksrt_post_views'; + $posts_q['orderby'] = 'meta_value_num'; + $posts_q['order'] = 'DESC'; + } elseif ($orderby === 'modified') { + $posts_q['orderby'] = 'modified'; + $posts_q['order'] = 'DESC'; + } else { + $posts_q['orderby'] = 'date'; + $posts_q['order'] = 'DESC'; + } + $q = new \WP_Query($posts_q); + + echo '
'; + echo ''; + echo '
'; + echo '
' . esc_html($term->name) . '
'; + echo '
' . esc_html(sprintf(_n('%d 篇', '%d 篇', (int) $term->count, 'aksrt'), (int) $term->count)) . '
'; + echo '
'; + if ($q->have_posts()) { + echo ''; + wp_reset_postdata(); + } + echo '
'; + } + echo '
'; + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** @return array */ + private function parse_ids(string $raw): array { + if ($raw === '') { + return []; + } + $ids = []; + foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) { + $id = (int) $p; + if ($id > 0) { + $ids[] = $id; + } + } + return array_values(array_unique($ids)); + } + + public function form($instance): void { + $taxs = get_taxonomies(['public' => true], 'objects'); + ?> +

+ +

+

+ +

+

+ +

+

+ + +

+

+ +

+ sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'taxonomy' => sanitize_key((string) ($new_instance['taxonomy'] ?? 'category')), + 'term_ids' => sanitize_text_field((string) ($new_instance['term_ids'] ?? '')), + 'count' => max(1, min(12, (int) ($new_instance['count'] ?? 4))), + 'columns' => max(1, min(3, (int) ($new_instance['columns'] ?? 1))), + 'orderby' => $orderby, + 'auto_terms' => max(1, min(4, (int) ($new_instance['auto_terms'] ?? 2))), + ]; + } +} diff --git a/src/AKSRT/Widgets/WidgetUserCard.php b/src/AKSRT/Widgets/WidgetUserCard.php index 4810a259..c47afeea 100644 --- a/src/AKSRT/Widgets/WidgetUserCard.php +++ b/src/AKSRT/Widgets/WidgetUserCard.php @@ -1,5 +1,7 @@ __('在侧栏显示当前登录用户或指定用户的信息卡片。', 'aksrt')] - ); - } + public function __construct() { + parent::__construct( + 'aksrt_user_card', + __('用户卡片', 'aksrt'), + ['description' => __('未登录显示登录入口;登录后显示当前用户卡片、签到与快捷按钮。', 'aksrt')] + ); + } - public function widget($args, $instance): void { - $title = !empty($instance['title']) ? $instance['title'] : ''; - $show_stats = !empty($instance['show_stats']); - $show_actions = !empty($instance['show_actions']); + public function widget($args, $instance): void { + $show_cover = !isset($instance['show_img_bg']) || !empty($instance['show_img_bg']); + $show_stats = !empty($instance['show_stats']) || !isset($instance['show_stats']); + $show_actions = !empty($instance['show_button']) || !isset($instance['show_button']); + $show_checkin = !empty($instance['show_checkin']) || !isset($instance['show_checkin']); + $loged_title = (string) ($instance['loged_title'] ?? __('Hi!请登录', 'aksrt')); + $btn1_text = (string) ($instance['button_1_text'] ?? __('发布文章', 'aksrt')); + $btn2_text = (string) ($instance['button_2_text'] ?? __('用户中心', 'aksrt')); - // 获取目标用户 - $user_id = !empty($instance['user_id']) ? (int) $instance['user_id'] : 0; - if ($user_id <= 0 && is_user_logged_in()) { - $user_id = get_current_user_id(); - } + // Fixed user id wins; else current user. + $user_id = !empty($instance['user_id']) ? (int) $instance['user_id'] : 0; + if ($user_id <= 0 && is_user_logged_in()) { + $user_id = get_current_user_id(); + } - if ($user_id <= 0) return; + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base); + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - $user = get_userdata($user_id); - if (!$user) return; + if ($user_id <= 0) { + $this->render_guest($show_cover, $loged_title); + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } - // 获取用户统计 - $post_count = count_user_posts($user_id, 'post', true); - $comment_count = get_comments(['user_id' => $user_id, 'count' => true]); - $follower_count = 0; - $following_count = 0; - if (class_exists('\AKSRT\User\Follow')) { - $follower_count = \AKSRT\User\Follow::get_instance()->get_follower_count($user_id); - $following_count = \AKSRT\User\Follow::get_instance()->get_following_count($user_id); - } + $user = get_userdata($user_id); + if (!$user) { + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } - // 头像 - $avatar_url = function_exists('aksrt_get_avatar_url') - ? aksrt_get_avatar_url($user_id, ['size' => 144]) - : get_avatar_url($user_id, ['size' => 144]); + $is_self = is_user_logged_in() && (int) get_current_user_id() === $user_id; + $avatar_url = function_exists('aksrt_get_avatar_url') + ? aksrt_get_avatar_url($user_id, ['size' => 144]) + : get_avatar_url($user_id, ['size' => 144]); + $profile_url = function_exists('aksrt_get_user_home_url') + ? aksrt_get_user_home_url($user_id) + : get_author_posts_url($user_id); + $center_url = function_exists('aksrt_get_user_center_url') + ? aksrt_get_user_center_url() + : $profile_url; + $compose_url = home_url('/'); + // Prefer front submit / write page when available. + if (function_exists('get_pages')) { + $pages = get_pages(['meta_key' => '_wp_page_template', 'meta_value' => 'templates/pages/page-submit.php', 'number' => 1]); + if (!empty($pages[0])) { + $compose_url = get_permalink($pages[0]); + } + } + if ($compose_url === home_url('/') && function_exists('aksrt_get_user_center_url')) { + $compose_url = aksrt_get_user_center_url('posts'); + } + $bio = get_user_meta($user_id, 'description', true); + if (!is_string($bio) || $bio === '') { + $bio = (string) $user->description; + } + $cover = (string) get_user_meta($user_id, 'aksrt_cover', true); + if ($cover === '') { + $cover = (string) get_user_meta($user_id, 'custom_avatar_cover', true); + } - // 用户主页链接 - $profile_url = function_exists('aksrt_get_user_home_url') - ? aksrt_get_user_home_url($user_id) - : get_author_posts_url($user_id); + $post_count = count_user_posts($user_id, 'post', true); + $follower_count = 0; + $following_count = 0; + if (class_exists('\AKSRT\User\Follow')) { + $follower_count = \AKSRT\User\Follow::get_instance()->get_follower_count($user_id); + $following_count = \AKSRT\User\Follow::get_instance()->get_following_count($user_id); + } - // 简介 - $bio = get_user_meta($user_id, 'description', true); - if (!$bio) $bio = $user->description ?: ''; + echo '
'; + if ($show_cover) { + $bg = $cover !== '' ? ' style="background-image:url(' . esc_url($cover) . ')"' : ''; + echo '
'; + } + echo '
'; + if ($show_checkin && $is_self && function_exists('aksrt_header_user_checkin_btn')) { + // Reuse compact check-in button markup (without details if already long). + $btn = aksrt_header_user_checkin_btn($user_id); + if ($btn !== '') { + echo '
' . $btn . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + echo '
'; + echo '' . esc_attr($user->display_name) . ''; + echo '
'; + echo '

' . esc_html($user->display_name) . '

'; + if ($bio !== '') { + echo '

' . esc_html(wp_trim_words($bio, 24)) . '

'; + } + if ($show_stats) { + echo '
'; + echo '
' . esc_html(number_format_i18n($post_count)) . '' . esc_html__('文章', 'aksrt') . '
'; + echo '
' . esc_html(number_format_i18n($follower_count)) . '' . esc_html__('粉丝', 'aksrt') . '
'; + echo '
' . esc_html(number_format_i18n($following_count)) . '' . esc_html__('关注', 'aksrt') . '
'; + echo '
'; + } + if ($show_actions && $is_self) { + echo ''; + } elseif ($show_actions) { + echo ''; + } + echo '
'; - echo $args['before_widget']; + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - if ($title) { - echo $args['before_title'] . esc_html($title) . $args['after_title']; - } + private function render_guest(bool $show_cover, string $loged_title): void { + $login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(); + $register = function_exists('aksrt_get_register_url') ? aksrt_get_register_url() : wp_registration_url(); + $default_avatar = function_exists('get_avatar_url') + ? get_avatar_url(0, ['size' => 144, 'force_default' => true]) + : includes_url('images/blank.gif'); - echo '
'; + $oauth_html = ''; + if (class_exists('\AKSRT\Auth\OAuth')) { + $oauth = \AKSRT\Auth\OAuth::get_instance(); + if (method_exists($oauth, 'render_social_buttons')) { + $oauth_html = (string) $oauth->render_social_buttons(__('社交账号登录', 'aksrt')); + } elseif (method_exists($oauth, 'get_login_buttons')) { + $oauth_html = (string) $oauth->get_login_buttons(); + } + } - // 头像 - echo '
'; - echo ''; - echo '' . esc_attr($user->display_name) . ''; - echo ''; - echo '
'; + echo '
'; + if ($show_cover) { + echo '
'; + } + echo '
'; + echo '
'; + echo '
' . esc_html($loged_title) . '
'; + echo '
'; + echo ''; + if ($register) { + echo ''; + } + echo '
'; + if ($oauth_html !== '') { + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '
'; + } - // 昵称 - echo '

'; - echo '' . esc_html($user->display_name) . ''; - echo '

'; + public function form($instance): void { + ?> +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+
+
+
+ +

+ ' . esc_html(wp_trim_words($bio, 20)) . '

'; - } - - // 统计 - if ($show_stats) { - echo '
'; - echo '
' . number_format_i18n($post_count) . '' . esc_html__('文章', 'aksrt') . '
'; - echo '
' . number_format_i18n($follower_count) . '' . esc_html__('粉丝', 'aksrt') . '
'; - echo '
' . number_format_i18n($following_count) . '' . esc_html__('关注', 'aksrt') . '
'; - echo '
'; - } - - // 操作按钮 - if ($show_actions) { - echo '
'; - if (is_user_logged_in() && (int) get_current_user_id() === $user_id) { - echo '' . esc_html__('个人主页', 'aksrt') . ''; - if (function_exists('aksrt_get_user_center_url')) { - echo '' . esc_html__('设置', 'aksrt') . ''; - } - } else { - echo '' . esc_html__('查看主页', 'aksrt') . ''; - } - echo '
'; - } - - echo '
'; - - echo $args['after_widget']; - } - - public function form($instance): void { - $title = !empty($instance['title']) ? $instance['title'] : ''; - $user_id = !empty($instance['user_id']) ? (int) $instance['user_id'] : 0; - $show_stats = !empty($instance['show_stats']); - $show_actions = !empty($instance['show_actions']); - ?> -

- - -

-

- - -

-

- -

-

- -

- sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'user_id' => max(0, (int) ($new_instance['user_id'] ?? 0)), + 'loged_title' => sanitize_text_field((string) ($new_instance['loged_title'] ?? '')), + 'button_1_text' => sanitize_text_field((string) ($new_instance['button_1_text'] ?? '')), + 'button_2_text' => sanitize_text_field((string) ($new_instance['button_2_text'] ?? '')), + 'show_img_bg' => !empty($new_instance['show_img_bg']) ? 1 : 0, + 'show_stats' => !empty($new_instance['show_stats']) ? 1 : 0, + 'show_button' => !empty($new_instance['show_button']) ? 1 : 0, + 'show_checkin' => !empty($new_instance['show_checkin']) ? 1 : 0, + ]; + } } diff --git a/src/AKSRT/Widgets/WidgetUserRanking.php b/src/AKSRT/Widgets/WidgetUserRanking.php index 4094f7a5..c64dd3c2 100644 --- a/src/AKSRT/Widgets/WidgetUserRanking.php +++ b/src/AKSRT/Widgets/WidgetUserRanking.php @@ -29,6 +29,13 @@ class WidgetUserRanking extends \WP_Widget { echo $args['before_widget']; echo $args['before_title'] . esc_html($title) . $args['after_title']; + if (!empty($instance['checkin_btn']) && is_user_logged_in() && function_exists('aksrt_header_user_checkin_btn')) { + $cbtn = aksrt_header_user_checkin_btn(get_current_user_id()); + if ($cbtn !== '') { + echo '
' . $cbtn . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + echo '
    '; $rank = 0; foreach ($users as $user) { @@ -110,6 +117,58 @@ class WidgetUserRanking extends \WP_Widget { } break; + case 'checkin_all_day': + global $wpdb; + $rows = $wpdb->get_results($wpdb->prepare( + "SELECT user_id, meta_value as val FROM {$wpdb->usermeta} + WHERE meta_key IN ('aksrt_checkin_all_day','checkin_all_day') + ORDER BY CAST(meta_value AS UNSIGNED) DESC + LIMIT %d", + $count * 2 + ), ARRAY_A); + $seen = []; + foreach ((array) $rows as $row) { + $uid = (int) $row['user_id']; + if (isset($seen[$uid])) continue; + $seen[$uid] = true; + $user = get_userdata($uid); + if ($user) { + $users[] = [ + 'id' => $uid, + 'name' => $user->display_name, + 'value' => number_format_i18n((int) $row['val']) . ' 天', + ]; + } + if (count($users) >= $count) break; + } + break; + + case 'checkin_continuous_day': + global $wpdb; + $rows = $wpdb->get_results($wpdb->prepare( + "SELECT user_id, meta_value as val FROM {$wpdb->usermeta} + WHERE meta_key IN ('aksrt_checkin_continuous_day','checkin_continuous_day') + ORDER BY CAST(meta_value AS UNSIGNED) DESC + LIMIT %d", + $count * 2 + ), ARRAY_A); + $seen = []; + foreach ((array) $rows as $row) { + $uid = (int) $row['user_id']; + if (isset($seen[$uid])) continue; + $seen[$uid] = true; + $user = get_userdata($uid); + if ($user) { + $users[] = [ + 'id' => $uid, + 'name' => $user->display_name, + 'value' => number_format_i18n((int) $row['val']) . ' 天', + ]; + } + if (count($users) >= $count) break; + } + break; + case 'followers': if (class_exists('\AKSRT\User\Follow')) { global $wpdb; @@ -157,6 +216,8 @@ class WidgetUserRanking extends \WP_Widget { + +

    @@ -169,15 +230,22 @@ class WidgetUserRanking extends \WP_Widget {

    +

    + +

    __('在侧栏嵌入视频播放器。', 'aksrt')] - ); - } + public function __construct() { + parent::__construct( + 'aksrt_video_player', + __('视频播放器', 'aksrt'), + ['description' => __('侧栏/流体区嵌入视频播放器。', 'aksrt')] + ); + } - public function widget($args, $instance): void { - $title = !empty($instance['title']) ? $instance['title'] : ''; - $video_url = !empty($instance['video_url']) ? $instance['video_url'] : ''; - $poster = !empty($instance['poster']) ? $instance['poster'] : ''; - $autoplay = !empty($instance['autoplay']); - $aspect_ratio = !empty($instance['aspect_ratio']) ? $instance['aspect_ratio'] : '16:9'; + public function widget($args, $instance): void { + $title = (string) ($instance['title'] ?? ''); + $video_url = trim((string) ($instance['video_url'] ?? $instance['url'] ?? '')); + $poster = trim((string) ($instance['poster'] ?? $instance['pic'] ?? '')); + $autoplay = !empty($instance['autoplay']); + $loop = !empty($instance['loop']); + $muted = $autoplay || !empty($instance['muted']); + $hide_controller = !empty($instance['hide_controller']); + $scale = max(20, min(100, (int) ($instance['scale_height'] ?? 56))); + $aspect = (string) ($instance['aspect_ratio'] ?? ''); - if (empty($video_url)) return; + if ($video_url === '') { + return; + } - // 解析视频类型 - $embed_url = $this->get_embed_url($video_url); - $is_embed = !empty($embed_url); + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + if ($title !== '') { + echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - echo $args['before_widget']; + if ($aspect !== '' && str_contains($aspect, ':')) { + $parts = explode(':', $aspect); + if (count($parts) === 2 && (int) $parts[0] > 0) { + $scale = (int) round(((int) $parts[1] / (int) $parts[0]) * 100); + } + } - if ($title) { - echo $args['before_title'] . esc_html($title) . $args['after_title']; - } + $embed = $this->get_embed_url($video_url); + $cls = 'aksrt-video-widget' . ($hide_controller ? ' controller-hide' : ''); + echo '
    '; + if ($embed !== '') { + $src = $embed; + if ($autoplay) { + $src = add_query_arg(['autoplay' => '1', 'mute' => '1'], $src); + } + echo ''; + } else { + $attrs = ' controls playsinline'; + if ($autoplay) { + $attrs .= ' autoplay'; + } + if ($loop) { + $attrs .= ' loop'; + } + if ($muted) { + $attrs .= ' muted'; + } + if ($poster !== '') { + $attrs .= ' poster="' . esc_url($poster) . '"'; + } + echo ''; + } + echo '
    '; - // 计算 padding-bottom - $ratio_parts = explode(':', $aspect_ratio); - if (count($ratio_parts) === 2 && (int) $ratio_parts[1] > 0) { - $padding = ((int) $ratio_parts[1] / (int) $ratio_parts[0]) * 100; - } else { - $padding = 56.25; // 16:9 - } + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } - echo '
    '; + private function get_embed_url(string $url): string { + if (preg_match('#(?:youtube\.com/watch\?v=|youtu\.be/)([A-Za-z0-9_-]+)#', $url, $m)) { + return 'https://www.youtube.com/embed/' . rawurlencode($m[1]); + } + if (preg_match('#bilibili\.com/video/(BV[\w]+)#', $url, $m)) { + return 'https://player.bilibili.com/player.html?bvid=' . rawurlencode($m[1]) . '&high_quality=1'; + } + if (preg_match('#v\.qq\.com/.+?/([A-Za-z0-9]+)\.html#', $url, $m)) { + return 'https://v.qq.com/txp/iframe/player.html?vid=' . rawurlencode($m[1]); + } + return ''; + } - if ($is_embed) { - echo ''; - } else { - echo ''; - echo ''; - echo ''; - } + public function form($instance): void { + ?> +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    +
    +
    + +

    + '; - - echo $args['after_widget']; - } - - /** - * 解析 YouTube / Bilibili 链接为嵌入 URL - */ - private function get_embed_url(string $url): string { - // YouTube - if (preg_match('/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/', $url, $m)) { - return 'https://www.youtube.com/embed/' . $m[1]; - } - - // Bilibili - if (preg_match('/bilibili\.com\/video\/(BV[a-zA-Z0-9]+)/', $url, $m)) { - return 'https://player.bilibili.com/player.html?bvid=' . $m[1] . '&high_quality=1'; - } - - // 腾讯视频 - if (preg_match('/v\.qq\.com\/x\/page\/([a-zA-Z0-9]+)/', $url, $m)) { - return 'https://v.qq.com/txp/iframe/player.html?vid=' . $m[1]; - } - - return ''; - } - - public function form($instance): void { - $title = !empty($instance['title']) ? $instance['title'] : ''; - $video_url = !empty($instance['video_url']) ? $instance['video_url'] : ''; - $poster = !empty($instance['poster']) ? $instance['poster'] : ''; - $autoplay = !empty($instance['autoplay']); - $aspect_ratio = !empty($instance['aspect_ratio']) ? $instance['aspect_ratio'] : '16:9'; - ?> -

    - - -

    -

    - - - -

    -

    - - -

    -

    - - -

    -

    - -

    - sanitize_text_field((string) ($new_instance['title'] ?? '')), + 'video_url' => esc_url_raw(trim((string) ($new_instance['video_url'] ?? ''))), + 'poster' => esc_url_raw(trim((string) ($new_instance['poster'] ?? ''))), + 'scale_height' => max(20, min(100, (int) ($new_instance['scale_height'] ?? 56))), + 'autoplay' => !empty($new_instance['autoplay']) ? 1 : 0, + 'loop' => !empty($new_instance['loop']) ? 1 : 0, + 'hide_controller' => !empty($new_instance['hide_controller']) ? 1 : 0, + ]; + } } diff --git a/templates/pages/page-cart.php b/templates/pages/page-cart.php index ae8bcb0a..d3df38b4 100644 --- a/templates/pages/page-cart.php +++ b/templates/pages/page-cart.php @@ -34,7 +34,10 @@ $address_book = is_array($cart_view['address_book'] ?? null) ? $cart_view['addre ?>
    - + +
    + +
    + +
    + diff --git a/templates/pages/page-orders.php b/templates/pages/page-orders.php index a2abd93a..b762f978 100644 --- a/templates/pages/page-orders.php +++ b/templates/pages/page-orders.php @@ -55,6 +55,9 @@ $tabs = [ ]; ?> + +
    +
    @@ -369,4 +372,7 @@ $tabs = [
    + +
    + diff --git a/templates/pages/page-product.php b/templates/pages/page-product.php index c9d70490..5be01cea 100644 --- a/templates/pages/page-product.php +++ b/templates/pages/page-product.php @@ -38,11 +38,15 @@ $author = $author_id > 0 ? get_userdata($author_id) : null; ?>
    + +
    +
    -
    + +
    0 ? get_userdata($author_id) : null;
    + + +
    + +
    +
    diff --git a/templates/shop/product-cat.php b/templates/shop/product-cat.php index d15133fa..3cbe6dcc 100644 --- a/templates/shop/product-cat.php +++ b/templates/shop/product-cat.php @@ -9,6 +9,12 @@ defined('ABSPATH') || exit; get_header(); +if (is_active_sidebar('shop_home_top_fluid')) { + echo '
    '; + dynamic_sidebar('shop_home_top_fluid'); + echo '
    '; +} + $term = get_queried_object(); $paged = max(1, (int) get_query_var('paged')); $orderby = ''; @@ -141,4 +147,9 @@ wp_enqueue_script( AKSRT_VERSION, true ); +if (is_active_sidebar('shop_product_bottom_fluid')) { + echo '
    '; + dynamic_sidebar('shop_product_bottom_fluid'); + echo '
    '; +} get_footer(); diff --git a/templates/shop/product-tag.php b/templates/shop/product-tag.php index b6ba0f89..fd4896f2 100644 --- a/templates/shop/product-tag.php +++ b/templates/shop/product-tag.php @@ -9,6 +9,12 @@ defined('ABSPATH') || exit; get_header(); +if (is_active_sidebar('shop_home_top_fluid')) { + echo '
    '; + dynamic_sidebar('shop_home_top_fluid'); + echo '
    '; +} + $term = get_queried_object(); $paged = max(1, (int) get_query_var('paged')); $orderby = ''; @@ -165,4 +171,9 @@ wp_enqueue_script( AKSRT_VERSION, true ); +if (is_active_sidebar('shop_product_bottom_fluid')) { + echo '
    '; + dynamic_sidebar('shop_product_bottom_fluid'); + echo '
    '; +} get_footer();