diff --git a/assets/css/routes/user-auth.css b/assets/css/routes/user-auth.css new file mode 100644 index 00000000..723a08e6 --- /dev/null +++ b/assets/css/routes/user-auth.css @@ -0,0 +1,89 @@ +/** + * User identity auth page styles (zibll user-auth). + * + * @package AKSRT + */ + +.aksrt-user-auth-panel { + overflow: hidden; + margin-top: 20px; +} + +.aksrt-user-auth-panel .colorful-bg.c-blue { + background: linear-gradient(135deg, #4e9bff 0%, #2f6df6 100%); + color: #fff; +} + +.aksrt-user-auth-panel .muted-2-color { + color: var(--muted-2-color, #888); +} + +.aksrt-user-auth-panel .form-control { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--main-border-color, #ddd); + border-radius: 6px; + box-sizing: border-box; +} + +.aksrt-user-auth-panel .mb20 { + margin-bottom: 20px; +} + +.aksrt-user-auth-panel .mb6 { + margin-bottom: 6px; +} + +.aksrt-user-auth-panel .but.c-blue, +.aksrt-user-auth-panel button.but { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + background: var(--focus-color, #2f6df6); + color: #fff; + border: 0; + border-radius: 6px; + padding: 10px 18px; + cursor: pointer; +} + +.aksrt-user-auth-panel .btn-block { + width: 100%; +} + +.aksrt-user-auth-panel__imgs { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.aksrt-user-auth-panel__imgs .preview-item { + width: 96px; + height: 96px; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--main-border-color, #eee); +} + +.aksrt-user-auth-panel__imgs img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.aksrt-user-auth-panel .c-red { + color: #e74c3c; +} + +.page-template-user-auth .content-wrap { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.page-template-user-auth .content-layout { + flex: 1 1 0; + min-width: 0; +} diff --git a/assets/js/user-auth-page.js b/assets/js/user-auth-page.js new file mode 100644 index 00000000..8de307e5 --- /dev/null +++ b/assets/js/user-auth-page.js @@ -0,0 +1,64 @@ +/** + * User auth apply form AJAX. + * + * @package AKSRT + */ +(function () { + 'use strict'; + + function ajaxUrl() { + if (window.aksrtData && window.aksrtData.ajaxUrl) return window.aksrtData.ajaxUrl; + return '/wp-admin/admin-ajax.php'; + } + + function toast(msg, type) { + if (window.aksrt && typeof window.aksrt.toast === 'function') { + window.aksrt.toast(msg, type || 'info'); + return; + } + // eslint-disable-next-line no-alert + window.alert(msg); + } + + function init() { + var form = document.querySelector('.aksrt-user-auth-form'); + if (!form) return; + + form.addEventListener('submit', function (e) { + e.preventDefault(); + var btn = form.querySelector('button[type="submit"]'); + if (btn) btn.disabled = true; + var fd = new FormData(form); + if (!fd.get('action')) fd.set('action', 'aksrt_user_auth_apply'); + + fetch(ajaxUrl(), { + method: 'POST', + body: fd, + credentials: 'same-origin', + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (!data || data.error) { + toast((data && data.msg) || '提交失败', 'danger'); + return; + } + toast(data.msg || '提交成功', 'success'); + if (data.reload) { + window.setTimeout(function () { window.location.reload(); }, 500); + } + }) + .catch(function () { + toast('网络错误', 'danger'); + }) + .finally(function () { + if (btn) btn.disabled = false; + }); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/functions.php b/functions.php index 4a31e905..5a9f176e 100644 --- a/functions.php +++ b/functions.php @@ -258,6 +258,9 @@ if (class_exists('\AKSRT\Core\LinksPageMeta')) { if (class_exists('\AKSRT\Core\PostsNavs')) { \AKSRT\Core\PostsNavs::get_instance()->init(); } +if (class_exists('\AKSRT\User\AuthApply')) { + \AKSRT\User\AuthApply::get_instance()->init(); +} if (class_exists('\AKSRT\Messages\SiteNotice')) { \AKSRT\Messages\SiteNotice::get_instance()->init(); } diff --git a/src/AKSRT/Admin/Native/UsersMenus.php b/src/AKSRT/Admin/Native/UsersMenus.php index 7a579e85..f3ea77ac 100644 --- a/src/AKSRT/Admin/Native/UsersMenus.php +++ b/src/AKSRT/Admin/Native/UsersMenus.php @@ -550,6 +550,18 @@ class UsersMenus { } elseif ($status === 'verified') { delete_user_meta($user_id, 'aksrt_user_verify_reason'); } + // Promote apply payload to public auth info when approved. + if ($status === 'verified') { + $apply = get_user_meta($user_id, 'aksrt_user_verify_apply', true); + if (is_array($apply)) { + update_user_meta($user_id, 'aksrt_user_verify_info', [ + 'name' => (string) ($apply['name'] ?? ''), + 'desc' => (string) ($apply['desc'] ?? ''), + 'type' => (string) ($apply['type'] ?? ''), + 'time' => current_time('Y-m-d H:i'), + ]); + } + } $log = get_option('aksrt_verify_review_log', []); $log = is_array($log) ? $log : []; $log[] = [ diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index eb17842a..c370c7e1 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -437,6 +437,29 @@ trait ScriptsPart1b { } } + // 用户身份认证页(zibll user-auth.php) + if (is_page_template('templates/pages/page-user-auth.php')) { + $ua_css = get_template_directory() . '/assets/css/routes/user-auth.css'; + if (file_exists($ua_css)) { + wp_enqueue_style( + 'aksrt-route-user-auth', + $base_uri . '/assets/css/routes/user-auth.css', + ['aksrt-tokens'], + (string) filemtime($ua_css) + ); + } + $ua_js = get_template_directory() . '/assets/js/user-auth-page.js'; + if (file_exists($ua_js)) { + wp_enqueue_script( + 'aksrt-user-auth-page', + $base_uri . '/assets/js/user-auth-page.js', + [], + (string) filemtime($ua_js), + true + ); + } + } + // 交易中心共享壳层(购物车 / 订单 / 收益) $is_commerce_center = is_page_template('templates/pages/page-orders.php') || is_page_template('templates/pages/page-cart.php') diff --git a/src/AKSRT/Core/Templates.php b/src/AKSRT/Core/Templates.php index 24c858b7..e653710f 100644 --- a/src/AKSRT/Core/Templates.php +++ b/src/AKSRT/Core/Templates.php @@ -47,6 +47,7 @@ class Templates { 'templates/pages/page-posts-navs.php' => __('AKSRT - 文章导航', 'aksrt'), 'templates/pages/page-friends.php' => __('AKSRT - 友情链接', 'aksrt'), 'templates/pages/page-links.php' => __('AKSRT - 网址导航', 'aksrt'), + 'templates/pages/page-user-auth.php' => __('AKSRT - 用户身份认证', 'aksrt'), 'templates/pages/page-orders.php' => __('AKSRT - 订单中心', 'aksrt'), 'templates/pages/page-revenue.php' => __('AKSRT - 收益中心', 'aksrt'), 'templates/pages/page-cart.php' => __('AKSRT - 购物车', 'aksrt'), diff --git a/src/AKSRT/User/AuthApply.php b/src/AKSRT/User/AuthApply.php new file mode 100644 index 00000000..f6107796 --- /dev/null +++ b/src/AKSRT/User/AuthApply.php @@ -0,0 +1,207 @@ + + * } + */ + public static function settings(): array { + $all = class_exists('\AKSRT\Settings\Settings') + ? (array) \AKSRT\Settings\Settings::get_all() + : []; + $enabled = !isset($all['user_verify_enabled']) || !empty($all['user_verify_enabled']); + $types_raw = (string) ($all['user_verify_apply_option'] ?? "real_name|实名认证\norganization|企业认证"); + $types = []; + foreach (preg_split('/\r\n|\r|\n/', $types_raw) as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + $parts = array_map('trim', explode('|', $line, 2)); + $slug = sanitize_key($parts[0] ?? ''); + $label = $parts[1] ?? $parts[0]; + if ($slug === '') { + continue; + } + $types[] = ['slug' => $slug, 'label' => (string) $label]; + } + return [ + 'enabled' => $enabled, + 'allow_apply' => !isset($all['user_verify_allow_apply']) || !empty($all['user_verify_allow_apply']), + 'public_display' => !isset($all['user_verify_public_display']) || !empty($all['user_verify_public_display']), + 'badge_text' => (string) ($all['user_verify_badge_text'] ?? __('已认证', 'aksrt')), + 'limit_level' => max(0, (int) ($all['user_verify_limit_level'] ?? 0)), + 'desc' => (string) ($all['user_verify_desc'] ?? ''), + 'disable_desc' => (string) ($all['user_verify_disable_desc'] ?? ''), + 'types' => $types, + ]; + } + + public static function get_status(int $user_id): string { + if ($user_id <= 0) { + return ''; + } + return sanitize_key((string) get_user_meta($user_id, self::STATUS_META, true)); + } + + public static function is_verified(int $user_id): bool { + return self::get_status($user_id) === 'verified'; + } + + /** + * @return array + */ + public static function get_auth_info(int $user_id): array { + $info = get_user_meta($user_id, self::INFO_META, true); + return is_array($info) ? $info : []; + } + + /** + * @return array + */ + public static function get_apply_payload(int $user_id): array { + $apply = get_user_meta($user_id, self::APPLY_META, true); + return is_array($apply) ? $apply : []; + } + + public function ajax_apply(): void { + if (!is_user_logged_in()) { + wp_send_json(['error' => 1, 'msg' => __('请先登录', 'aksrt')]); + } + if (!check_ajax_referer('aksrt_user_auth_apply', '_wpnonce', false)) { + wp_send_json(['error' => 1, 'msg' => __('安全校验失败', 'aksrt')]); + } + + $user_id = get_current_user_id(); + $s = self::settings(); + if (!$s['enabled'] || !$s['allow_apply']) { + wp_send_json(['error' => 1, 'msg' => __('暂不能申请身份认证', 'aksrt')]); + } + if (self::is_verified($user_id)) { + wp_send_json(['error' => 1, 'msg' => __('您已是认证用户', 'aksrt')]); + } + if (self::get_status($user_id) === 'pending') { + wp_send_json(['error' => 1, 'msg' => __('您的申请正在审核中', 'aksrt')]); + } + if ($s['limit_level'] > 0 && class_exists('\AKSRT\User\Level')) { + $lv = (int) Level::get_level($user_id); + if ($lv < $s['limit_level']) { + wp_send_json([ + 'error' => 1, + 'msg' => sprintf( + /* translators: %d: required level */ + __('等级不足,需达到 LV.%d 后申请', 'aksrt'), + $s['limit_level'] + ), + ]); + } + } + + $name = isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : ''; + $desc = isset($_POST['desc']) ? sanitize_textarea_field(wp_unslash((string) $_POST['desc'])) : ''; + $msg = isset($_POST['msg']) ? sanitize_textarea_field(wp_unslash((string) $_POST['msg'])) : ''; + $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : ''; + if ($name === '') { + wp_send_json(['error' => 1, 'msg' => __('请填写认证身份名称', 'aksrt')]); + } + + $images = []; + if (!empty($_FILES['image']['name'])) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + + $files = $_FILES['image']; + // Normalize multi-file upload. + $names = $files['name']; + if (!is_array($names)) { + $names = [$names]; + $tmp = [ + 'name' => $names, + 'type' => [$files['type']], + 'tmp_name' => [$files['tmp_name']], + 'error' => [$files['error']], + 'size' => [$files['size']], + ]; + $files = $tmp; + } + $count = min(3, count($files['name'])); + for ($i = 0; $i < $count; $i++) { + if (empty($files['name'][$i]) || (int) $files['error'][$i] !== UPLOAD_ERR_OK) { + continue; + } + $file = [ + 'name' => $files['name'][$i], + 'type' => $files['type'][$i], + 'tmp_name' => $files['tmp_name'][$i], + 'error' => $files['error'][$i], + 'size' => $files['size'][$i], + ]; + $_FILES['aksrt_auth_img'] = $file; + $aid = media_handle_upload('aksrt_auth_img', 0); + if (!is_wp_error($aid)) { + $url = wp_get_attachment_url((int) $aid); + if ($url) { + $images[] = $url; + } + } + } + unset($_FILES['aksrt_auth_img']); + } + + $payload = [ + 'name' => $name, + 'desc' => $desc, + 'msg' => $msg, + 'type' => $type, + 'img' => $images, + 'time' => current_time('Y-m-d H:i'), + ]; + update_user_meta($user_id, self::APPLY_META, $payload); + update_user_meta($user_id, self::STATUS_META, 'pending'); + delete_user_meta($user_id, self::REASON_META); + + /** + * @param int $user_id + * @param array $payload + */ + do_action('aksrt_user_auth_apply_submitted', $user_id, $payload); + + wp_send_json([ + 'error' => 0, + 'msg' => __('提交成功,请等待管理员审核', 'aksrt'), + 'reload' => true, + ]); + } +} diff --git a/templates/pages/page-user-auth.php b/templates/pages/page-user-auth.php new file mode 100644 index 00000000..62a6ef8e --- /dev/null +++ b/templates/pages/page-user-auth.php @@ -0,0 +1,203 @@ + true, 'allow_apply' => true]; +$user_id = get_current_user_id(); +$status = $user_id && class_exists(AuthApply::class) ? AuthApply::get_status($user_id) : ''; +$info = $user_id && class_exists(AuthApply::class) ? AuthApply::get_auth_info($user_id) : []; +$apply = $user_id && class_exists(AuthApply::class) ? AuthApply::get_apply_payload($user_id) : []; +$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url(get_permalink()) : wp_login_url(get_permalink()); + +get_header(); +?> +
+
+
+ +
+
+ '

', + 'after' => '

', + ]); + ?> +
+
+ + +
+ +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ +
+ + +
+ +
+
+ +
+
+
+
+

+ +

+
+
+ +
+
+
+ +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ + 0 && class_exists('\AKSRT\User\Level')) { + $lv = (int) \AKSRT\User\Level::get_level($user_id); + if ($lv < $limit) { + $blocked_level = true; + } + } + ?> + +
+
+
+ +
+
+ +
+
+
+
+
+ +
+ + ' . esc_html(sprintf(__('原因:%s', 'aksrt'), $reason)) . '
'; + } + ?> +
+ + +
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ + + +
+
+ + +
+ + +
+ + +
+