Merge pull request 'Worktree ux depth batch2' (#219) from worktree-ux-depth-batch2 into main
Reviewed-on: CYL/wordpress-theme-aksrt#219
This commit is contained in:
commit
6a202f3eab
9 files changed
+965
-6
No files matched your search
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Widget slider (AKSRT 幻灯片).
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
.aksrt-widget-slider {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-2, #f3f4f6);
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__track {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--aksrt-slider-h, 220px);
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__slide {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .35s ease;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__slide.is-active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__slide img,
|
||||||
|
.aksrt-widget-slider__link {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__caption {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: linear-gradient(transparent, rgba(0,0,0,.55));
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__caption strong {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__prev,
|
||||||
|
.aksrt-widget-slider__next {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 2;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0,0,0,.35);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 28px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__prev { left: 8px; }
|
||||||
|
.aksrt-widget-slider__next { right: 8px; }
|
||||||
|
.aksrt-widget-slider__dots {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__dot {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: rgba(255,255,255,.45);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.aksrt-widget-slider__dot.is-active {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
@import url("widgets/part-2.css");
|
@import url("widgets/part-2.css");
|
||||||
@import url("widgets/part-3.css");
|
@import url("widgets/part-3.css");
|
||||||
@import url("widgets/part-4.css");
|
@import url("widgets/part-4.css");
|
||||||
|
@import url("widget-slider.css");
|
||||||
+48
-6
@@ -419,6 +419,37 @@
|
|||||||
|
|
||||||
// ── QR Code Login (§10.7) ──
|
// ── QR Code Login (§10.7) ──
|
||||||
|
|
||||||
|
|
||||||
|
function bindWeixingzhCodeForm(container) {
|
||||||
|
if (!container) return;
|
||||||
|
var form = container.querySelector('[data-aksrt-weixingzh-code]');
|
||||||
|
if (!form || form._bound) return;
|
||||||
|
form._bound = true;
|
||||||
|
form.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var input = form.querySelector('[name="code"]');
|
||||||
|
var code = input ? String(input.value || '').trim() : '';
|
||||||
|
if (!code) return;
|
||||||
|
var btn = form.querySelector('button[type="submit"]');
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
request('/auth/weixingzh-code', { code: code })
|
||||||
|
.then(function (result) {
|
||||||
|
var data = (result && result.data) || result || {};
|
||||||
|
if ((result && result.ok) || (result && result.success) || data.logged_in) {
|
||||||
|
onAuthSuccess(data.data || data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error((result && (result.message || result.msg)) || '验证码错误');
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
alert(err.message || '登录失败');
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initQrLogin() {
|
function initQrLogin() {
|
||||||
var qrPanel = modal.querySelector('[data-panel="qrcode"]');
|
var qrPanel = modal.querySelector('[data-panel="qrcode"]');
|
||||||
if (!qrPanel) return;
|
if (!qrPanel) return;
|
||||||
@@ -442,9 +473,16 @@
|
|||||||
|
|
||||||
request('/auth/qrcode-login', {})
|
request('/auth/qrcode-login', {})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
if (result.ok && result.data && result.data.qr_html) {
|
var payload = (result && result.data) || result || {};
|
||||||
container.innerHTML = result.data.qr_html;
|
var qrHtml = payload.qr_html || (payload.data && payload.data.qr_html) || '';
|
||||||
startQrPolling(result.data.check_url || '/auth/check-qrcode-login', result.data.token || '');
|
var checkUrl = payload.check_url || (payload.data && payload.data.check_url) || '/auth/check-qrcode-login';
|
||||||
|
var token = payload.token || (payload.data && payload.data.token) || '';
|
||||||
|
if (qrHtml) {
|
||||||
|
container.innerHTML = qrHtml;
|
||||||
|
bindWeixingzhCodeForm(container);
|
||||||
|
if (checkUrl && token) {
|
||||||
|
startQrPolling(checkUrl, token);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
container.innerHTML = '<p class="aksrt-modal-auth__qr-error">扫码登录暂不可用</p>';
|
container.innerHTML = '<p class="aksrt-modal-auth__qr-error">扫码登录暂不可用</p>';
|
||||||
}
|
}
|
||||||
@@ -459,9 +497,13 @@
|
|||||||
qrLoginTimer = setInterval(function () {
|
qrLoginTimer = setInterval(function () {
|
||||||
request(checkUrl, { token: token })
|
request(checkUrl, { token: token })
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
if (result.ok && result.data && result.data.logged_in) {
|
var data = (result && result.data) || result || {};
|
||||||
stopQrPolling();
|
var body = data.data || data;
|
||||||
onAuthSuccess(result.data);
|
if ((result && result.ok) || data.success || body.logged_in) {
|
||||||
|
if (body.logged_in || (data.data && data.data.logged_in)) {
|
||||||
|
stopQrPolling();
|
||||||
|
onAuthSuccess(body.data || body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function () { /* keep polling */ });
|
.catch(function () { /* keep polling */ });
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ if (get_option('aksrt_flushed_version') !== AKSRT_VERSION) {
|
|||||||
\AKSRT\Core\Scripts::get_instance()->init();
|
\AKSRT\Core\Scripts::get_instance()->init();
|
||||||
\AKSRT\Admin\Admin::get_instance()->init();
|
\AKSRT\Admin\Admin::get_instance()->init();
|
||||||
\AKSRT\Auth\OAuth::get_instance()->init();
|
\AKSRT\Auth\OAuth::get_instance()->init();
|
||||||
|
if (class_exists('\AKSRT\Auth\WeixinGzh')) {
|
||||||
|
\AKSRT\Auth\WeixinGzh::get_instance()->init();
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化代理登录(根据配置模式选择性加载)
|
// 初始化代理登录(根据配置模式选择性加载)
|
||||||
$aksrt_proxy_mode = \AKSRT\Settings\Settings::oauth_proxy_mode();
|
$aksrt_proxy_mode = \AKSRT\Settings\Settings::oauth_proxy_mode();
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ function aksrt_register_widgets() {
|
|||||||
// 文章目录树(zibll posts_navs)
|
// 文章目录树(zibll posts_navs)
|
||||||
register_widget('\AKSRT\Widgets\WidgetPostsNavs');
|
register_widget('\AKSRT\Widgets\WidgetPostsNavs');
|
||||||
|
|
||||||
|
// 幻灯片
|
||||||
|
register_widget('\AKSRT\Widgets\WidgetSlider');
|
||||||
|
|
||||||
// Forum widgets
|
// Forum widgets
|
||||||
if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
|
if (class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
|
||||||
register_widget('\AKSRT\Widgets\WidgetForumBoardList');
|
register_widget('\AKSRT\Widgets\WidgetForumBoardList');
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WeChat Official Account (weixingzh) QR / code login helpers.
|
||||||
|
*
|
||||||
|
* Complements OAuth snsapi_userinfo web auth with:
|
||||||
|
* - static QR + keyword verification code (subscribe mode)
|
||||||
|
* - temporary scene QR + poll state (service mode, best-effort)
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Auth;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class WeixinGzh {
|
||||||
|
public const OPT_ACCESS = 'aksrt_weixingzh_access_token';
|
||||||
|
public const OPT_CODES = 'aksrt_weixingzh_login_codes';
|
||||||
|
public const OPT_SCANS = 'aksrt_weixingzh_scan_states';
|
||||||
|
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
public static function get_instance(): self {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init(): void {
|
||||||
|
add_action('rest_api_init', [$this, 'register_routes']);
|
||||||
|
add_action('template_redirect', [$this, 'maybe_server_verify'], 1);
|
||||||
|
add_action('template_redirect', [$this, 'maybe_server_message'], 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register_routes(): void {
|
||||||
|
register_rest_route('aksrt/v1', '/auth/qrcode-login', [
|
||||||
|
'methods' => ['GET', 'POST'],
|
||||||
|
'callback' => [$this, 'rest_qrcode_login'],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aksrt/v1', '/auth/check-qrcode-login', [
|
||||||
|
'methods' => ['GET', 'POST'],
|
||||||
|
'callback' => [$this, 'rest_check_qrcode_login'],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aksrt/v1', '/auth/weixingzh-code', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => [$this, 'rest_code_login'],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function settings(): array {
|
||||||
|
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_weixingzh_settings')) {
|
||||||
|
return (array) \AKSRT\Settings\Settings::get_weixingzh_settings();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function is_enabled(): bool {
|
||||||
|
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||||||
|
if (!empty($s['oauth_weixingzh_s'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !empty($s['weixingzh_appid']) && !empty($s['weixingzh_appsecret']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rest_qrcode_login(\WP_REST_Request $request) {
|
||||||
|
if (!self::is_enabled()) {
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => __('微信公众号登录未开启', 'aksrt')], 400);
|
||||||
|
}
|
||||||
|
$cfg = self::settings();
|
||||||
|
$appid = (string) ($cfg['appid'] ?? '');
|
||||||
|
$secret = (string) ($cfg['appsecret'] ?? '');
|
||||||
|
$gzh_type = (string) ($cfg['gzh_type'] ?? 'service');
|
||||||
|
|
||||||
|
// Subscribe / uncertified: static QR + code form.
|
||||||
|
if ($gzh_type === 'subscribe' || $gzh_type === 'not' || empty($secret)) {
|
||||||
|
$qr = (string) ($cfg['code_qrcode'] ?? '');
|
||||||
|
$keyword = (string) ($cfg['code_keyword'] ?? __('登录', 'aksrt'));
|
||||||
|
if ($qr === '') {
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => __('未配置公众号二维码', 'aksrt')], 400);
|
||||||
|
}
|
||||||
|
$html = '<img class="signin-qrcode-img" src="' . esc_url($qr) . '" alt="" style="width:160px;height:160px;object-fit:contain;" />';
|
||||||
|
$html .= '<form class="aksrt-weixingzh-code-form" data-aksrt-weixingzh-code>';
|
||||||
|
$html .= '<p class="muted-2-color em09">' . esc_html(sprintf(__('关注公众号后发送「%s」获取验证码', 'aksrt'), $keyword)) . '</p>';
|
||||||
|
$html .= '<input type="text" name="code" class="form-control" maxlength="8" placeholder="' . esc_attr__('请输入验证码', 'aksrt') . '" required />';
|
||||||
|
$html .= '<button type="submit" class="but c-blue btn-block mt10">' . esc_html__('登录', 'aksrt') . '</button>';
|
||||||
|
$html .= '</form>';
|
||||||
|
return new \WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'mode' => 'code',
|
||||||
|
'qr_html' => $html,
|
||||||
|
'check_url' => '',
|
||||||
|
'token' => '',
|
||||||
|
],
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service account: temporary QR with scene string (state).
|
||||||
|
try {
|
||||||
|
$token = $this->get_access_token($appid, $secret);
|
||||||
|
$state = 'aksrt_' . wp_generate_password(16, false, false);
|
||||||
|
$scene = substr(md5($state . wp_salt('auth')), 0, 24);
|
||||||
|
$create = $this->api_post(
|
||||||
|
'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=' . rawurlencode($token),
|
||||||
|
[
|
||||||
|
'expire_seconds' => 300,
|
||||||
|
'action_name' => 'QR_STR_SCENE',
|
||||||
|
'action_info' => ['scene' => ['scene_str' => $scene]],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (empty($create['ticket'])) {
|
||||||
|
$msg = (string) ($create['errmsg'] ?? __('创建二维码失败', 'aksrt'));
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => $msg], 400);
|
||||||
|
}
|
||||||
|
$img = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=' . rawurlencode((string) $create['ticket']);
|
||||||
|
$this->put_scan_state($state, [
|
||||||
|
'status' => 'pending',
|
||||||
|
'scene' => $scene,
|
||||||
|
'created' => time(),
|
||||||
|
'openid' => '',
|
||||||
|
'user_id' => 0,
|
||||||
|
]);
|
||||||
|
$html = '<img class="signin-qrcode-img" src="' . esc_url($img) . '" alt="" style="width:160px;height:160px;" />';
|
||||||
|
$html .= '<p class="text-center mt10 em09 muted-2-color"><i class="fab fa-weixin c-green"></i> ' . esc_html__('微信扫码登录', 'aksrt') . '</p>';
|
||||||
|
return new \WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'mode' => 'scan',
|
||||||
|
'qr_html' => $html,
|
||||||
|
'check_url' => '/auth/check-qrcode-login',
|
||||||
|
'token' => $state,
|
||||||
|
],
|
||||||
|
], 200);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Fallback to static QR form if API fails.
|
||||||
|
$qr = (string) ($cfg['code_qrcode'] ?? '');
|
||||||
|
if ($qr === '') {
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => $e->getMessage()], 400);
|
||||||
|
}
|
||||||
|
$html = '<img class="signin-qrcode-img" src="' . esc_url($qr) . '" alt="" style="width:160px;height:160px;" />';
|
||||||
|
$html .= '<p class="muted-2-color em09">' . esc_html__('请使用公众号二维码 + 验证码登录', 'aksrt') . '</p>';
|
||||||
|
return new \WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'data' => ['mode' => 'code', 'qr_html' => $html, 'check_url' => '', 'token' => ''],
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rest_check_qrcode_login(\WP_REST_Request $request) {
|
||||||
|
$params = $request->get_json_params();
|
||||||
|
if (!is_array($params)) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
}
|
||||||
|
$token = sanitize_text_field((string) ($params['token'] ?? $request->get_param('token') ?? ''));
|
||||||
|
if ($token === '') {
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => __('缺少 token', 'aksrt')], 400);
|
||||||
|
}
|
||||||
|
$row = $this->get_scan_state($token);
|
||||||
|
if (!$row) {
|
||||||
|
return new \WP_REST_Response(['success' => true, 'data' => ['logged_in' => false, 'status' => 'expired']], 200);
|
||||||
|
}
|
||||||
|
if (($row['status'] ?? '') === 'ok' && !empty($row['user_id'])) {
|
||||||
|
$user = get_user_by('id', (int) $row['user_id']);
|
||||||
|
if ($user instanceof \WP_User) {
|
||||||
|
wp_set_current_user($user->ID);
|
||||||
|
wp_set_auth_cookie($user->ID, true);
|
||||||
|
do_action('wp_login', $user->user_login, $user);
|
||||||
|
$this->delete_scan_state($token);
|
||||||
|
return new \WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'logged_in' => true,
|
||||||
|
'redirect' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'),
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->ID,
|
||||||
|
'display_name' => $user->display_name,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new \WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'data' => ['logged_in' => false, 'status' => (string) ($row['status'] ?? 'pending')],
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rest_code_login(\WP_REST_Request $request) {
|
||||||
|
$params = $request->get_json_params();
|
||||||
|
if (!is_array($params)) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
}
|
||||||
|
$code = strtoupper(sanitize_text_field((string) ($params['code'] ?? '')));
|
||||||
|
if ($code === '') {
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => __('请输入验证码', 'aksrt')], 400);
|
||||||
|
}
|
||||||
|
$openid = $this->consume_code($code);
|
||||||
|
if ($openid === null) {
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => __('验证码无效或已过期', 'aksrt')], 400);
|
||||||
|
}
|
||||||
|
$user = $this->login_or_create_by_openid($openid, []);
|
||||||
|
if (is_wp_error($user)) {
|
||||||
|
return new \WP_REST_Response(['success' => false, 'message' => $user->get_error_message()], 400);
|
||||||
|
}
|
||||||
|
return new \WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'logged_in' => true,
|
||||||
|
'redirect' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'),
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->ID,
|
||||||
|
'display_name' => $user->display_name,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WeChat server URL verification: /?aksrt_weixingzh=1&signature&echostr
|
||||||
|
*/
|
||||||
|
public function maybe_server_verify(): void {
|
||||||
|
if (empty($_GET['aksrt_weixingzh']) || empty($_GET['echostr']) || empty($_GET['signature'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$cfg = self::settings();
|
||||||
|
if (!empty($cfg['token_nocheck'])) {
|
||||||
|
echo (string) wp_unslash($_GET['echostr']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$token = (string) ($cfg['token'] ?? '');
|
||||||
|
$tmp = [(string) $token, (string) ($_GET['timestamp'] ?? ''), (string) ($_GET['nonce'] ?? '')];
|
||||||
|
sort($tmp, SORT_STRING);
|
||||||
|
if (sha1(implode($tmp)) === (string) $_GET['signature']) {
|
||||||
|
echo (string) wp_unslash($_GET['echostr']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
status_header(403);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive WeChat server push (XML) on same endpoint.
|
||||||
|
*/
|
||||||
|
public function maybe_server_message(): void {
|
||||||
|
if (empty($_GET['aksrt_weixingzh']) || empty($_GET['signature']) || !empty($_GET['echostr'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
if (!is_string($raw) || $raw === '') {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$xml = @simplexml_load_string($raw, 'SimpleXMLElement', LIBXML_NOCDATA);
|
||||||
|
if (!$xml) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$from = (string) ($xml->FromUserName ?? '');
|
||||||
|
$to = (string) ($xml->ToUserName ?? '');
|
||||||
|
$msg_type = (string) ($xml->MsgType ?? '');
|
||||||
|
$event = strtolower((string) ($xml->Event ?? ''));
|
||||||
|
$content = trim((string) ($xml->Content ?? ''));
|
||||||
|
$cfg = self::settings();
|
||||||
|
$keyword = (string) ($cfg['code_keyword'] ?? '登录');
|
||||||
|
|
||||||
|
// Scene scan login (service QR).
|
||||||
|
if ($msg_type === 'event' && in_array($event, ['subscribe', 'scan'], true)) {
|
||||||
|
$scene = (string) ($xml->EventKey ?? '');
|
||||||
|
$scene = ltrim($scene, 'qrscene_');
|
||||||
|
if ($scene !== '') {
|
||||||
|
$this->mark_scan_by_scene($scene, $from);
|
||||||
|
}
|
||||||
|
$reply = (string) ($cfg['subscribe_msg'] ?? $cfg['scan_msg'] ?? '');
|
||||||
|
if ($reply !== '') {
|
||||||
|
$this->echo_text_reply($from, $to, $reply);
|
||||||
|
}
|
||||||
|
echo 'success';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyword → verification code.
|
||||||
|
if ($msg_type === 'text' && $content !== '' && mb_strtolower($content) === mb_strtolower($keyword)) {
|
||||||
|
$code = $this->issue_code($from);
|
||||||
|
$tpl = (string) ($cfg['code_reply'] ?? __('您的验证码是:%s,有效期 %d 分钟。', 'aksrt'));
|
||||||
|
// Support %s/%d and %code%/%time%.
|
||||||
|
$msg = $tpl;
|
||||||
|
if (str_contains($msg, '%code%')) {
|
||||||
|
$msg = str_replace('%code%', $code, $msg);
|
||||||
|
}
|
||||||
|
if (str_contains($msg, '%time%')) {
|
||||||
|
$msg = str_replace('%time%', '5', $msg);
|
||||||
|
}
|
||||||
|
if (substr_count($msg, '%') >= 2 && str_contains($msg, '%s')) {
|
||||||
|
$msg = sprintf($msg, $code, 5);
|
||||||
|
} elseif (str_contains($msg, '%s')) {
|
||||||
|
$msg = sprintf($msg, $code);
|
||||||
|
}
|
||||||
|
$this->echo_text_reply($from, $to, $msg);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional auto replies.
|
||||||
|
if ($msg_type === 'text' && !empty($cfg['auto_reply_text'])) {
|
||||||
|
$this->echo_text_reply($from, $to, (string) $cfg['auto_reply_text']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
echo 'success';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function echo_text_reply(string $to_user, string $from_user, string $content): void {
|
||||||
|
$time = time();
|
||||||
|
$content = htmlspecialchars($content, ENT_XML1 | ENT_QUOTES, 'UTF-8');
|
||||||
|
echo "<xml><ToUserName><![CDATA[{$to_user}]]></ToUserName><FromUserName><![CDATA[{$from_user}]]></FromUserName><CreateTime>{$time}</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[{$content}]]></Content></xml>";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function issue_code(string $openid): string {
|
||||||
|
$charset = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
$code = '';
|
||||||
|
for ($i = 0; $i < 4; $i++) {
|
||||||
|
$code .= $charset[random_int(0, strlen($charset) - 1)];
|
||||||
|
}
|
||||||
|
$data = get_option(self::OPT_CODES, []);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
$data = [];
|
||||||
|
}
|
||||||
|
$now = time();
|
||||||
|
// prune
|
||||||
|
foreach ($data as $k => $row) {
|
||||||
|
if (!is_array($row) || (int) ($row['time'] ?? 0) + 300 < $now) {
|
||||||
|
unset($data[$k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data[$code] = ['time' => $now, 'openid' => $openid];
|
||||||
|
update_option(self::OPT_CODES, $data, false);
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function consume_code(string $code): ?string {
|
||||||
|
$data = get_option(self::OPT_CODES, []);
|
||||||
|
if (!is_array($data) || empty($data[$code])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$row = $data[$code];
|
||||||
|
unset($data[$code]);
|
||||||
|
update_option(self::OPT_CODES, $data, false);
|
||||||
|
if ((int) ($row['time'] ?? 0) + 300 < time()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$openid = (string) ($row['openid'] ?? '');
|
||||||
|
return $openid !== '' ? $openid : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $profile
|
||||||
|
* @return \WP_User|\WP_Error
|
||||||
|
*/
|
||||||
|
private function login_or_create_by_openid(string $openid, array $profile) {
|
||||||
|
$oauth = class_exists(OAuth::class) ? OAuth::get_instance() : null;
|
||||||
|
$user_data = [
|
||||||
|
'provider' => 'weixingzh',
|
||||||
|
'provider_id' => $openid,
|
||||||
|
'username' => (string) ($profile['nickname'] ?? ''),
|
||||||
|
'name' => (string) ($profile['nickname'] ?? ''),
|
||||||
|
'email' => '',
|
||||||
|
'avatar_url' => (string) ($profile['headimgurl'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Find by meta.
|
||||||
|
$users = get_users([
|
||||||
|
'meta_key' => 'aksrt_weixingzh_id',
|
||||||
|
'meta_value' => $openid,
|
||||||
|
'number' => 1,
|
||||||
|
'fields' => 'all',
|
||||||
|
]);
|
||||||
|
if (!empty($users[0]) && $users[0] instanceof \WP_User) {
|
||||||
|
$user = $users[0];
|
||||||
|
wp_set_current_user($user->ID);
|
||||||
|
wp_set_auth_cookie($user->ID, true);
|
||||||
|
do_action('wp_login', $user->user_login, $user);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
$login = 'wx' . substr(md5($openid), 0, 10);
|
||||||
|
$uid = wp_insert_user([
|
||||||
|
'user_login' => $login,
|
||||||
|
'user_pass' => wp_generate_password(24, true, true),
|
||||||
|
'display_name' => $user_data['name'] !== '' ? $user_data['name'] : $login,
|
||||||
|
'nickname' => $user_data['name'] !== '' ? $user_data['name'] : $login,
|
||||||
|
'role' => get_option('default_role', 'subscriber'),
|
||||||
|
]);
|
||||||
|
if (is_wp_error($uid)) {
|
||||||
|
return $uid;
|
||||||
|
}
|
||||||
|
update_user_meta((int) $uid, 'aksrt_weixingzh_id', $openid);
|
||||||
|
$bindings = get_user_meta((int) $uid, 'aksrt_oauth_bindings', true);
|
||||||
|
$bindings = is_array($bindings) ? $bindings : [];
|
||||||
|
if (!in_array('weixingzh', $bindings, true)) {
|
||||||
|
$bindings[] = 'weixingzh';
|
||||||
|
update_user_meta((int) $uid, 'aksrt_oauth_bindings', $bindings);
|
||||||
|
}
|
||||||
|
if ($user_data['avatar_url'] !== '') {
|
||||||
|
update_user_meta((int) $uid, 'aksrt_oauth_avatar', esc_url_raw($user_data['avatar_url']));
|
||||||
|
}
|
||||||
|
$user = get_user_by('id', (int) $uid);
|
||||||
|
if (!$user instanceof \WP_User) {
|
||||||
|
return new \WP_Error('aksrt_wx_user', __('创建用户失败', 'aksrt'));
|
||||||
|
}
|
||||||
|
wp_set_current_user($user->ID);
|
||||||
|
wp_set_auth_cookie($user->ID, true);
|
||||||
|
do_action('wp_login', $user->user_login, $user);
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $row
|
||||||
|
*/
|
||||||
|
private function put_scan_state(string $state, array $row): void {
|
||||||
|
$all = get_option(self::OPT_SCANS, []);
|
||||||
|
if (!is_array($all)) {
|
||||||
|
$all = [];
|
||||||
|
}
|
||||||
|
$now = time();
|
||||||
|
foreach ($all as $k => $v) {
|
||||||
|
if (!is_array($v) || (int) ($v['created'] ?? 0) + 600 < $now) {
|
||||||
|
unset($all[$k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$all[$state] = $row;
|
||||||
|
update_option(self::OPT_SCANS, $all, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
private function get_scan_state(string $state): ?array {
|
||||||
|
$all = get_option(self::OPT_SCANS, []);
|
||||||
|
if (!is_array($all) || empty($all[$state]) || !is_array($all[$state])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $all[$state];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function delete_scan_state(string $state): void {
|
||||||
|
$all = get_option(self::OPT_SCANS, []);
|
||||||
|
if (!is_array($all)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unset($all[$state]);
|
||||||
|
update_option(self::OPT_SCANS, $all, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mark_scan_by_scene(string $scene, string $openid): void {
|
||||||
|
$all = get_option(self::OPT_SCANS, []);
|
||||||
|
if (!is_array($all)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($all as $state => $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((string) ($row['scene'] ?? '') === $scene) {
|
||||||
|
$user = $this->login_or_create_by_openid($openid, []);
|
||||||
|
if ($user instanceof \WP_User) {
|
||||||
|
// Don't keep session for server push path; store id for poller.
|
||||||
|
wp_logout();
|
||||||
|
$all[$state]['status'] = 'ok';
|
||||||
|
$all[$state]['openid'] = $openid;
|
||||||
|
$all[$state]['user_id'] = $user->ID;
|
||||||
|
update_option(self::OPT_SCANS, $all, false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function get_access_token(string $appid, string $secret): string {
|
||||||
|
$cached = get_option(self::OPT_ACCESS, []);
|
||||||
|
if (is_array($cached) && !empty($cached['access_token']) && (int) ($cached['expiration_time'] ?? 0) > time() + 60) {
|
||||||
|
return (string) $cached['access_token'];
|
||||||
|
}
|
||||||
|
$url = add_query_arg([
|
||||||
|
'grant_type' => 'client_credential',
|
||||||
|
'appid' => $appid,
|
||||||
|
'secret' => $secret,
|
||||||
|
], 'https://api.weixin.qq.com/cgi-bin/token');
|
||||||
|
// Prefer stable_token when available.
|
||||||
|
$res = wp_remote_post('https://api.weixin.qq.com/cgi-bin/stable_token', [
|
||||||
|
'timeout' => 10,
|
||||||
|
'headers' => ['Content-Type' => 'application/json'],
|
||||||
|
'body' => wp_json_encode([
|
||||||
|
'grant_type' => 'client_credential',
|
||||||
|
'appid' => $appid,
|
||||||
|
'secret' => $secret,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
if (is_wp_error($res)) {
|
||||||
|
$res = wp_remote_get($url, ['timeout' => 10]);
|
||||||
|
}
|
||||||
|
if (is_wp_error($res)) {
|
||||||
|
throw new \Exception($res->get_error_message());
|
||||||
|
}
|
||||||
|
$data = json_decode((string) wp_remote_retrieve_body($res), true);
|
||||||
|
if (empty($data['access_token'])) {
|
||||||
|
throw new \Exception((string) ($data['errmsg'] ?? 'access_token failed'));
|
||||||
|
}
|
||||||
|
$data['expiration_time'] = time() + (int) ($data['expires_in'] ?? 7200);
|
||||||
|
update_option(self::OPT_ACCESS, $data, false);
|
||||||
|
return (string) $data['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $body
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function api_post(string $url, array $body): array {
|
||||||
|
$res = wp_remote_post($url, [
|
||||||
|
'timeout' => 10,
|
||||||
|
'headers' => ['Content-Type' => 'application/json'],
|
||||||
|
'body' => wp_json_encode($body, JSON_UNESCAPED_UNICODE),
|
||||||
|
]);
|
||||||
|
if (is_wp_error($res)) {
|
||||||
|
return ['errcode' => 1, 'errmsg' => $res->get_error_message()];
|
||||||
|
}
|
||||||
|
$data = json_decode((string) wp_remote_retrieve_body($res), true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,6 +147,14 @@ trait FrontSubmitPay {
|
|||||||
$meta['vip2_points'] = 0;
|
$meta['vip2_points'] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pay_limit = (int) ($raw['pay_limit'] ?? $data['pay_limit'] ?? 0);
|
||||||
|
$meta['pay_limit'] = in_array($pay_limit, [0, 1, 2], true) ? $pay_limit : 0;
|
||||||
|
$meta['require_login'] = !empty($raw['require_login']) || !empty($data['require_login']);
|
||||||
|
$meta['coupon_s'] = !empty($raw['coupon_s']) || !empty($data['coupon_s']);
|
||||||
|
if (isset($raw['coupon_desc']) || isset($data['coupon_desc'])) {
|
||||||
|
$meta['coupon_desc'] = sanitize_textarea_field((string) ($raw['coupon_desc'] ?? $data['coupon_desc'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
$this->write_pay_meta($post_id, $meta);
|
$this->write_pay_meta($post_id, $meta);
|
||||||
return ['success' => true];
|
return ['success' => true];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Simple image slider widget (zibll widget_ui_slider lite).
|
||||||
|
*
|
||||||
|
* @package AKSRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AKSRT\Widgets;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
class WidgetSlider extends \WP_Widget {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(
|
||||||
|
'aksrt_slider',
|
||||||
|
esc_html__('AKSRT 幻灯片', 'aksrt'),
|
||||||
|
[
|
||||||
|
'description' => esc_html__('多图轮播,支持链接与标题。', 'aksrt'),
|
||||||
|
'classname' => 'widget_aksrt_slider',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function widget($args, $instance): void {
|
||||||
|
$slides = $this->normalize_slides($instance['slides'] ?? []);
|
||||||
|
if (!$slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$interval = max(2, min(20, (int) ($instance['interval'] ?? 4)));
|
||||||
|
$loop = !empty($instance['loop']);
|
||||||
|
$dots = !empty($instance['pagination']);
|
||||||
|
$nav = !empty($instance['button']);
|
||||||
|
$blank = !empty($instance['blank']);
|
||||||
|
$height = max(80, min(800, (int) ($instance['height'] ?? 220)));
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
$uid = 'aksrt-slider-' . esc_attr($this->id);
|
||||||
|
printf(
|
||||||
|
'<div class="aksrt-widget-slider" id="%s" data-interval="%d" data-loop="%s" style="--aksrt-slider-h:%dpx">',
|
||||||
|
$uid,
|
||||||
|
$interval * 1000,
|
||||||
|
$loop ? '1' : '0',
|
||||||
|
$height
|
||||||
|
);
|
||||||
|
echo '<div class="aksrt-widget-slider__track">';
|
||||||
|
foreach ($slides as $i => $slide) {
|
||||||
|
$active = $i === 0 ? ' is-active' : '';
|
||||||
|
echo '<div class="aksrt-widget-slider__slide' . esc_attr($active) . '">';
|
||||||
|
$img = '<img src="' . esc_url($slide['image']) . '" alt="' . esc_attr($slide['title']) . '" loading="lazy" />';
|
||||||
|
if ($slide['href'] !== '') {
|
||||||
|
printf(
|
||||||
|
'<a href="%s"%s class="aksrt-widget-slider__link">%s</a>',
|
||||||
|
esc_url($slide['href']),
|
||||||
|
$blank ? ' target="_blank" rel="noopener noreferrer"' : '',
|
||||||
|
$img // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
}
|
||||||
|
if ($slide['title'] !== '' || $slide['desc'] !== '') {
|
||||||
|
echo '<div class="aksrt-widget-slider__caption">';
|
||||||
|
if ($slide['title'] !== '') {
|
||||||
|
echo '<strong>' . esc_html($slide['title']) . '</strong>';
|
||||||
|
}
|
||||||
|
if ($slide['desc'] !== '') {
|
||||||
|
echo '<span>' . esc_html($slide['desc']) . '</span>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
if ($nav) {
|
||||||
|
echo '<button type="button" class="aksrt-widget-slider__prev" aria-label="' . esc_attr__('上一张', 'aksrt') . '">‹</button>';
|
||||||
|
echo '<button type="button" class="aksrt-widget-slider__next" aria-label="' . esc_attr__('下一张', 'aksrt') . '">›</button>';
|
||||||
|
}
|
||||||
|
if ($dots) {
|
||||||
|
echo '<div class="aksrt-widget-slider__dots" role="tablist">';
|
||||||
|
foreach ($slides as $i => $slide) {
|
||||||
|
printf(
|
||||||
|
'<button type="button" class="aksrt-widget-slider__dot%s" data-index="%d" aria-label="%s"></button>',
|
||||||
|
$i === 0 ? ' is-active' : '',
|
||||||
|
(int) $i,
|
||||||
|
esc_attr(sprintf(__('第 %d 张', 'aksrt'), $i + 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
|
// Inline init once per widget (no extra enqueue dependency).
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var root=document.getElementById(<?php echo wp_json_encode($uid); ?>);
|
||||||
|
if(!root||root._aksrtSlider)return; root._aksrtSlider=1;
|
||||||
|
var slides=[].slice.call(root.querySelectorAll('.aksrt-widget-slider__slide'));
|
||||||
|
var dots=[].slice.call(root.querySelectorAll('.aksrt-widget-slider__dot'));
|
||||||
|
var i=0, timer=null, ms=parseInt(root.getAttribute('data-interval')||'4000',10)||4000;
|
||||||
|
var loop=root.getAttribute('data-loop')==='1';
|
||||||
|
function go(n){
|
||||||
|
if(!slides.length)return;
|
||||||
|
i=(n+slides.length)%slides.length;
|
||||||
|
slides.forEach(function(s,idx){s.classList.toggle('is-active',idx===i);});
|
||||||
|
dots.forEach(function(d,idx){d.classList.toggle('is-active',idx===i);});
|
||||||
|
}
|
||||||
|
function next(){ if(!loop && i>=slides.length-1){go(0);return;} go(i+1); }
|
||||||
|
function start(){ stop(); if(slides.length>1) timer=setInterval(next, ms); }
|
||||||
|
function stop(){ if(timer){clearInterval(timer);timer=null;} }
|
||||||
|
var prev=root.querySelector('.aksrt-widget-slider__prev');
|
||||||
|
var nxt=root.querySelector('.aksrt-widget-slider__next');
|
||||||
|
if(prev) prev.addEventListener('click', function(){ go(i-1); start(); });
|
||||||
|
if(nxt) nxt.addEventListener('click', function(){ go(i+1); start(); });
|
||||||
|
dots.forEach(function(d){ d.addEventListener('click', function(){ go(parseInt(d.getAttribute('data-index')||'0',10)); start(); }); });
|
||||||
|
root.addEventListener('mouseenter', stop);
|
||||||
|
root.addEventListener('mouseleave', start);
|
||||||
|
start();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form($instance): void {
|
||||||
|
$title = (string) ($instance['title'] ?? '');
|
||||||
|
$interval = (int) ($instance['interval'] ?? 4);
|
||||||
|
$height = (int) ($instance['height'] ?? 220);
|
||||||
|
$slides = $this->normalize_slides($instance['slides'] ?? []);
|
||||||
|
if (!$slides) {
|
||||||
|
$slides = [
|
||||||
|
['image' => '', 'href' => '', 'title' => '', 'desc' => ''],
|
||||||
|
['image' => '', 'href' => '', 'title' => '', 'desc' => ''],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<p>
|
||||||
|
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr($title); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><?php esc_html_e('切换间隔(秒)', 'aksrt'); ?>
|
||||||
|
<input class="tiny-text" type="number" min="2" max="20" name="<?php echo esc_attr($this->get_field_name('interval')); ?>" value="<?php echo esc_attr((string) $interval); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><?php esc_html_e('高度(px)', 'aksrt'); ?>
|
||||||
|
<input class="tiny-text" type="number" min="80" max="800" name="<?php echo esc_attr($this->get_field_name('height')); ?>" value="<?php echo esc_attr((string) $height); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('loop')); ?>" value="1" <?php checked(!empty($instance['loop']) || !isset($instance['loop'])); ?>> <?php esc_html_e('循环播放', 'aksrt'); ?></label><br>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('button')); ?>" value="1" <?php checked(!empty($instance['button']) || !isset($instance['button'])); ?>> <?php esc_html_e('翻页按钮', 'aksrt'); ?></label><br>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('pagination')); ?>" value="1" <?php checked(!empty($instance['pagination']) || !isset($instance['pagination'])); ?>> <?php esc_html_e('指示点', 'aksrt'); ?></label><br>
|
||||||
|
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('blank')); ?>" value="1" <?php checked(!empty($instance['blank'])); ?>> <?php esc_html_e('新窗口打开', 'aksrt'); ?></label>
|
||||||
|
</p>
|
||||||
|
<?php foreach ($slides as $i => $slide) : ?>
|
||||||
|
<p style="border-top:1px solid #ddd;padding-top:8px;">
|
||||||
|
<strong><?php echo esc_html(sprintf(__('幻灯片 %d', 'aksrt'), $i + 1)); ?></strong><br>
|
||||||
|
<label><?php esc_html_e('图片 URL', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][image]"); ?>" value="<?php echo esc_attr($slide['image']); ?>">
|
||||||
|
</label>
|
||||||
|
<label><?php esc_html_e('链接', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][href]"); ?>" value="<?php echo esc_attr($slide['href']); ?>">
|
||||||
|
</label>
|
||||||
|
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][title]"); ?>" value="<?php echo esc_attr($slide['title']); ?>">
|
||||||
|
</label>
|
||||||
|
<label><?php esc_html_e('简介', 'aksrt'); ?>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][desc]"); ?>" value="<?php echo esc_attr($slide['desc']); ?>">
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<p class="description"><?php esc_html_e('保存后可继续编辑;需要更多张可先填满再在下次编辑时追加(当前表单固定 2–6 槽)。', 'aksrt'); ?></p>
|
||||||
|
<?php
|
||||||
|
// Ensure at least 4 slots for editing convenience.
|
||||||
|
$extra = max(0, 4 - count($slides));
|
||||||
|
for ($j = 0; $j < $extra; $j++) {
|
||||||
|
$i = count($slides) + $j;
|
||||||
|
?>
|
||||||
|
<p style="border-top:1px dashed #eee;padding-top:8px;">
|
||||||
|
<strong><?php echo esc_html(sprintf(__('空槽 %d', 'aksrt'), $i + 1)); ?></strong>
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][image]"); ?>" placeholder="<?php esc_attr_e('图片 URL', 'aksrt'); ?>">
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][href]"); ?>" placeholder="<?php esc_attr_e('链接', 'aksrt'); ?>">
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][title]"); ?>" placeholder="<?php esc_attr_e('标题', 'aksrt'); ?>">
|
||||||
|
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('slides') . "[{$i}][desc]"); ?>" placeholder="<?php esc_attr_e('简介', 'aksrt'); ?>">
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($new_instance, $old_instance): array {
|
||||||
|
$slides = $this->normalize_slides($new_instance['slides'] ?? []);
|
||||||
|
return [
|
||||||
|
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
|
||||||
|
'interval' => max(2, min(20, (int) ($new_instance['interval'] ?? 4))),
|
||||||
|
'height' => max(80, min(800, (int) ($new_instance['height'] ?? 220))),
|
||||||
|
'loop' => !empty($new_instance['loop']) ? 1 : 0,
|
||||||
|
'button' => !empty($new_instance['button']) ? 1 : 0,
|
||||||
|
'pagination' => !empty($new_instance['pagination']) ? 1 : 0,
|
||||||
|
'blank' => !empty($new_instance['blank']) ? 1 : 0,
|
||||||
|
'slides' => $slides,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $raw
|
||||||
|
* @return array<int, array{image:string,href:string,title:string,desc:string}>
|
||||||
|
*/
|
||||||
|
private function normalize_slides($raw): array {
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($raw as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$image = esc_url_raw(trim((string) ($row['image'] ?? $row['link'] ?? '')));
|
||||||
|
if ($image === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = [
|
||||||
|
'image' => $image,
|
||||||
|
'href' => esc_url_raw(trim((string) ($row['href'] ?? ''))),
|
||||||
|
'title' => sanitize_text_field((string) ($row['title'] ?? '')),
|
||||||
|
'desc' => sanitize_text_field((string) ($row['desc'] ?? $row['dec'] ?? '')),
|
||||||
|
];
|
||||||
|
if (count($out) >= 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,34 @@ $downloads = is_array($pay['downloads'] ?? null) ? $pay['downloads'] : [];
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="aksrt-submit-pay__row">
|
||||||
|
<span><?php esc_html_e('购买限制', 'aksrt'); ?></span>
|
||||||
|
<select name="aksrt_pay[pay_limit]" class="form-control">
|
||||||
|
<?php $pl = (int) ($pay['pay_limit'] ?? 0); ?>
|
||||||
|
<option value="0" <?php selected($pl, 0); ?>><?php esc_html_e('所有人可购', 'aksrt'); ?></option>
|
||||||
|
<option value="1" <?php selected($pl, 1); ?>><?php esc_html_e('仅 VIP1 及以上', 'aksrt'); ?></option>
|
||||||
|
<option value="2" <?php selected($pl, 2); ?>><?php esc_html_e('仅 VIP2', 'aksrt'); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="aksrt-submit-pay__row">
|
||||||
|
<span><?php esc_html_e('登录后可购', 'aksrt'); ?></span>
|
||||||
|
<label class="aksrt-switch">
|
||||||
|
<input type="checkbox" name="aksrt_pay[require_login]" value="1" <?php checked(!empty($pay['require_login'])); ?>>
|
||||||
|
<span class="aksrt-switch__ui"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="aksrt-submit-pay__row">
|
||||||
|
<span><?php esc_html_e('允许优惠码', 'aksrt'); ?></span>
|
||||||
|
<label class="aksrt-switch">
|
||||||
|
<input type="checkbox" name="aksrt_pay[coupon_s]" value="1" <?php checked(!empty($pay['coupon_s'])); ?>>
|
||||||
|
<span class="aksrt-switch__ui"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="aksrt-submit-field">
|
||||||
|
<span><?php esc_html_e('优惠说明', 'aksrt'); ?></span>
|
||||||
|
<input type="text" class="aksrt-form-input" name="aksrt_pay[coupon_desc]" value="<?php echo esc_attr((string) ($pay['coupon_desc'] ?? '')); ?>" placeholder="<?php esc_attr_e('收银台展示的优惠提示', 'aksrt'); ?>">
|
||||||
|
</label>
|
||||||
|
|
||||||
<?php if (!empty($ctx['can_pay_download'])) : ?>
|
<?php if (!empty($ctx['can_pay_download'])) : ?>
|
||||||
<div class="aksrt-submit-pay-downloads" data-pay-downloads>
|
<div class="aksrt-submit-pay-downloads" data-pay-downloads>
|
||||||
<div class="aksrt-submit-pay__row">
|
<div class="aksrt-submit-pay__row">
|
||||||
|
|||||||
Reference in new issue
Block a user