diff --git a/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue b/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue index 3b1939a2..236df1b4 100644 --- a/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue +++ b/assets/admin/src/pages/users/components/VerifyReviewDrawer.vue @@ -14,6 +14,21 @@ {{ detail.request.reviewed_at || '-' }} +
+
申请资料
+ + {{ detail.request.application?.type || '-' }} + {{ detail.request.application?.name || '-' }} + {{ detail.request.application?.desc || '-' }} + {{ detail.request.application?.msg || '-' }} + +
+ + 认证证明材料 + +
+
+
当前备注
@@ -149,6 +164,21 @@ async function handleSubmit() { color: #1f2937; } +.aksrt-verify-drawer__proofs { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 12px; +} + +.aksrt-verify-drawer__proofs img { + display: block; + width: 92px; + height: 92px; + border-radius: 10px; + object-fit: cover; +} + .aksrt-verify-drawer__timeline { display: grid; gap: 12px; diff --git a/assets/admin/src/schema/sections/users/features.js b/assets/admin/src/schema/sections/users/features.js index ac02b98b..35f39740 100644 --- a/assets/admin/src/schema/sections/users/features.js +++ b/assets/admin/src/schema/sections/users/features.js @@ -4,7 +4,7 @@ export const featuresSchema = { icon: 'fas fa-user-cog', description: '配置用户中心和个人主页的功能模块。', defaults: { - user_center_tabs: 'profile,posts,comments,favorites,vip,credits', + user_center_tabs: 'profile,posts,comments,favorites,vip,credits,auth', user_avatar_upload: true, user_cover_upload: true, user_social_links: true, @@ -26,7 +26,7 @@ export const featuresSchema = { title: '用户中心', icon: 'fas fa-user-shield', fields: [ - { key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,favorites,vip,credits', help: '逗号分隔。vip=我的会员(会员功能开启时也会自动注入)' }, + { key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,posts,comments,favorites,vip,credits,auth', help: '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入' }, { key: 'user_avatar_upload', label: '头像上传', type: 'switch', default: true }, { key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true }, { key: 'user_social_links', label: '社交链接', type: 'switch', default: true }, diff --git a/assets/css/routes/user-auth.css b/assets/css/routes/user-auth.css deleted file mode 100644 index bb2db9f7..00000000 --- a/assets/css/routes/user-auth.css +++ /dev/null @@ -1,80 +0,0 @@ -/** - * 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; -} - -/* Dual-column geometry owned by layout.css site-layout float rules (Zibll). */ diff --git a/assets/css/routes/user-center/settings.css b/assets/css/routes/user-center/settings.css index 7dcb58f1..a28f3818 100644 --- a/assets/css/routes/user-center/settings.css +++ b/assets/css/routes/user-center/settings.css @@ -359,3 +359,79 @@ margin-right: 8px; margin-top: 4px; } + + +/* ==================== 官方认证面板 ==================== */ +.uc-auth-panel { overflow: hidden; } + +.uc-auth-card { + display: grid; + grid-template-columns: 128px minmax(0, 1fr); + min-height: 210px; + overflow: hidden; + border-radius: 14px; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.13), rgba(124, 92, 220, 0.08)); +} + +.uc-auth-card__art { + display: grid; + place-items: center; + background: linear-gradient(150deg, #3988f4, #7567de); + color: #fff; + font-size: 46px; +} + +.uc-auth-card__content { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 28px; +} + +.uc-auth-card__content small { color: var(--color-primary); font-weight: 700; } +.uc-auth-card__content h2 { margin: 5px 0 9px; font-size: 24px; } +.uc-auth-card__content p, +.uc-auth-card__content time { margin: 0; color: var(--color-ink-4); font-size: 13px; line-height: 1.7; } +.uc-auth-card__notice { margin-top: 14px; color: var(--color-error, #dc2626); font-size: 13px; } +.uc-auth-apply-open { margin-top: 18px; min-width: 112px; } +.uc-auth-card--verified { background: linear-gradient(135deg, rgba(16, 185, 129, 0.12), rgba(59, 130, 246, 0.08)); } +.uc-auth-card--verified .uc-auth-card__art { background: linear-gradient(150deg, #20a77d, #3287df); } + +.uc-auth-modal { z-index: 620; } +.uc-auth-modal .aksrt-modal__dialog { width: min(620px, calc(100vw - 28px)); max-height: min(760px, calc(100vh - 40px)); overflow: auto; } +.uc-auth-modal__header > div { display: flex; align-items: center; gap: 10px; } +.uc-auth-modal__header > div > span { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 11px; background: var(--color-primary-soft); color: var(--color-primary); } +.uc-auth-modal__body { padding: 22px; } + +.uc-auth-form { display: grid; gap: 17px; } +.uc-auth-form > label { display: grid; gap: 7px; color: var(--color-ink-2); font-size: 13px; font-weight: 600; } +.uc-auth-form .form-control { width: 100%; min-height: 42px; padding: 9px 11px; border: 1px solid var(--color-line-soft, rgba(15, 23, 42, 0.1)); border-radius: 9px; background: var(--surface-1); color: var(--color-ink); font: inherit; font-weight: 400; box-sizing: border-box; } +.uc-auth-form textarea.form-control { resize: vertical; } +.uc-auth-upload small { color: var(--color-ink-4); font-weight: 400; } +.uc-auth-submit { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 44px; } +.uc-auth-form__message { min-height: 18px; color: var(--color-error, #dc2626); font-size: 12px; text-align: center; } + +.uc-auth-upload__previews { display: flex; flex-wrap: wrap; gap: 8px; } +.uc-auth-upload__previews img, +.uc-auth-proof-list .preview-item { width: 82px; height: 82px; border-radius: 8px; overflow: hidden; object-fit: cover; } +.uc-auth-proof-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } +.uc-auth-proof-list img { width: 100%; height: 100%; object-fit: cover; } + +.uc-auth-pending { text-align: center; padding: 4px 0 18px; } +.uc-auth-pending > span { display: grid; place-items: center; width: 58px; height: 58px; margin: 0 auto 12px; border-radius: 18px; background: rgba(245, 158, 11, 0.14); color: #a66e0b; font-size: 24px; } +.uc-auth-pending h3 { margin: 0 0 7px; } +.uc-auth-pending p { margin: 0; color: var(--color-ink-4); font-size: 13px; } +.uc-auth-apply-detail { display: grid; gap: 0; border-top: 1px solid var(--color-line-soft); } +.uc-auth-apply-detail dl { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px; margin: 0; padding: 12px 0; border-bottom: 1px solid var(--color-line-soft); font-size: 13px; } +.uc-auth-apply-detail dt { color: var(--color-ink-4); } +.uc-auth-apply-detail dd { margin: 0; color: var(--color-ink-2); } +.uc-auth-modal__confirm { width: 100%; margin-top: 20px; } + +@media (max-width: 640px) { + .uc-auth-card { grid-template-columns: 92px minmax(0, 1fr); min-height: 180px; } + .uc-auth-card__art { font-size: 34px; } + .uc-auth-card__content { padding: 20px 17px; } + .uc-auth-card__content h2 { font-size: 20px; } + .uc-auth-modal .aksrt-modal__dialog { position: fixed; inset: auto 0 0; width: 100%; max-height: 88vh; margin: 0; border-radius: 18px 18px 0 0; } +} diff --git a/assets/css/routes/user-center/shell.css b/assets/css/routes/user-center/shell.css index 6d7d2f2f..208af3ff 100644 --- a/assets/css/routes/user-center/shell.css +++ b/assets/css/routes/user-center/shell.css @@ -631,10 +631,15 @@ body.aksrt-user-center .uc-content, .uc-service-grid__item--profile .uc-service-grid__icon { color: #6550c7; background: rgba(124, 92, 220, 0.12); } .uc-service-grid__item--security .uc-service-grid__icon { color: #16816c; background: rgba(16, 185, 129, 0.12); } -.uc-service-grid__item:hover { +.uc-service-grid__item:hover, +.uc-service-grid__item.is-active { color: var(--color-primary); } +.uc-service-grid__item.is-active .uc-service-grid__icon { + box-shadow: 0 0 0 2px var(--color-primary-soft), 0 5px 14px rgba(15, 23, 42, 0.08); +} + @media (prefers-reduced-motion: reduce) { .uc-order-grid__icon { transition: none; } } diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js index 2fdffcaa..82478f7b 100644 --- a/assets/js/routes/user-center.js +++ b/assets/js/routes/user-center.js @@ -27,7 +27,7 @@ if (!tabContainer) return; var tabs = tabContainer.querySelectorAll('[data-tab]'); - var sidebarBtns = document.querySelectorAll('.uc-sidebar__btn[data-tab]'); + var sidebarBtns = document.querySelectorAll('.uc-sidebar__btn[data-tab], .uc-service-grid__item[data-tab]'); var panels = {}; var defaultTab = cfg.defaultTab || 'profile'; var drawerTrigger = document.getElementById('uc-drawer-trigger'); @@ -193,6 +193,92 @@ }); } + function initAuthForm() { + var modal = document.getElementById('uc-auth-modal'); + if (!modal || modal.dataset.authModalBound === '1') return; + modal.dataset.authModalBound = '1'; + + function openModal() { + modal.classList.add('is-open'); + modal.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + var first = modal.querySelector('input:not([type="hidden"]), select, textarea, button'); + if (first) window.setTimeout(function() { first.focus(); }, 80); + } + + function closeModal() { + modal.classList.remove('is-open'); + modal.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + } + + document.querySelectorAll('[data-auth-modal-open]').forEach(function(button) { + button.addEventListener('click', openModal); + }); + modal.querySelectorAll('[data-auth-modal-close]').forEach(function(button) { + button.addEventListener('click', closeModal); + }); + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape' && modal.classList.contains('is-open')) closeModal(); + }); + + var fileInput = modal.querySelector('[data-auth-files]'); + var previews = modal.querySelector('[data-auth-previews]'); + if (fileInput && previews) { + fileInput.addEventListener('change', function() { + previews.innerHTML = ''; + var files = Array.prototype.slice.call(fileInput.files || []); + if (files.length > 3) { + showToast('证明材料最多上传 3 张'); + fileInput.value = ''; + return; + } + for (var index = 0; index < files.length; index++) { + if (files[index].size > 2 * 1024 * 1024) { + showToast('单张图片不能超过 2MB'); + fileInput.value = ''; + previews.innerHTML = ''; + return; + } + var image = document.createElement('img'); + image.src = URL.createObjectURL(files[index]); + image.alt = ''; + image.onload = function() { URL.revokeObjectURL(this.src); }; + previews.appendChild(image); + } + }); + } + + var form = modal.querySelector('.aksrt-user-auth-form'); + if (!form) return; + form.addEventListener('submit', function(e) { + e.preventDefault(); + var button = form.querySelector('[type="submit"]'); + var message = form.querySelector('[data-auth-message]'); + var original = button ? button.textContent : ''; + if (button) { + button.disabled = true; + button.textContent = i18n.submitting || '提交中…'; + } + if (message) message.textContent = ''; + fetch(ajaxUrl, { method: 'POST', body: new FormData(form), credentials: 'same-origin' }) + .then(function(response) { return response.json(); }) + .then(function(response) { + if (!response || response.error) throw new Error((response && response.msg) || '提交失败'); + showToast(response.msg || '提交成功'); + window.setTimeout(function() { window.location.assign(tabUrls.auth || '?tab=auth'); }, 500); + }) + .catch(function(error) { + if (message) message.textContent = error.message || '提交失败'; + showToast(error.message || '提交失败'); + if (button) { + button.disabled = false; + button.textContent = original; + } + }); + }); + } + /* ==================== 列表内取消收藏 ==================== */ function initUnfavorite() { var list = document.querySelector('.profile-list[data-list="favorites"]'); @@ -678,6 +764,7 @@ document.addEventListener('DOMContentLoaded', function() { initDashboardNavigation(); initProfileForm(); + initAuthForm(); initUnfavorite(); initNotifications(); initSettings(); @@ -685,6 +772,7 @@ } else { initDashboardNavigation(); initProfileForm(); + initAuthForm(); initUnfavorite(); initNotifications(); initSettings(); diff --git a/assets/js/user-auth-page.js b/assets/js/user-auth-page.js deleted file mode 100644 index 8de307e5..00000000 --- a/assets/js/user-auth-page.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * 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/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php index d2897f58..41d2962d 100644 --- a/inc/helpers/aksrt-user-center.php +++ b/inc/helpers/aksrt-user-center.php @@ -148,6 +148,7 @@ if (!function_exists('aksrt_capture_user_center_view')) { 'credits' => 'credits', 'vip' => 'vip', 'orders' => 'orders', + 'auth' => 'auth', 'settings' => 'settings', 'header' => 'header', 'sidebar' => 'sidebar', @@ -172,7 +173,7 @@ if (!function_exists('aksrt_capture_user_center_panel')) { * Capture a built-in or registered user-center panel body. */ function aksrt_capture_user_center_panel(string $tab_key, array $tab, array $context): string { - $built_in_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'orders', 'settings']; + $built_in_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'settings']; if (in_array($tab_key, $built_in_tabs, true)) { return aksrt_capture_user_center_view($tab_key, $context); } diff --git a/inc/helpers/user-center-renderers/profile-auth.php b/inc/helpers/user-center-renderers/profile-auth.php new file mode 100644 index 00000000..6b7733b4 --- /dev/null +++ b/inc/helpers/user-center-renderers/profile-auth.php @@ -0,0 +1,108 @@ + false, 'allow_apply' => false]; +$status = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_status($user_id) : ''; +$info = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_auth_info($user_id) : []; +$apply = $user_id > 0 && class_exists(AuthApply::class) ? AuthApply::get_apply_payload($user_id) : []; +$type_labels = []; +foreach ((array) ($settings['types'] ?? []) as $type) { + $type_labels[(string) ($type['slug'] ?? '')] = (string) ($type['label'] ?? ''); +} +$apply_type = sanitize_key((string) ($apply['type'] ?? '')); +$limit = (int) ($settings['limit_level'] ?? 0); +$current_level = class_exists('\AKSRT\User\Level') ? (int) \AKSRT\User\Level::get_level($user_id) : 0; +$blocked_level = $limit > 0 && $current_level < $limit; +$can_apply = !empty($settings['enabled']) && !empty($settings['allow_apply']) && !$blocked_level && $status !== 'verified'; +?> +
+ +
+ +
+ +

+

+ +
+
+ +
+ +
+ +

+

+ +
+ +
+ + + +
+
+ + + + + +
diff --git a/inc/helpers/user-center-renderers/sidebar-nav.php b/inc/helpers/user-center-renderers/sidebar-nav.php index fb24a19e..717802e7 100644 --- a/inc/helpers/user-center-renderers/sidebar-nav.php +++ b/inc/helpers/user-center-renderers/sidebar-nav.php @@ -11,10 +11,7 @@ $cart_url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url $level_url = isset($dashboard_tabs['credits']) ? add_query_arg('asset_tab', 'points', aksrt_get_user_center_tab_url('credits')) : ''; -$auth_settings = class_exists('\AKSRT\User\AuthApply') ? \AKSRT\User\AuthApply::settings() : []; -$auth_url = !empty($auth_settings['enabled']) && function_exists('aksrt_get_user_auth_page_url') - ? aksrt_get_user_auth_page_url() - : ''; +$auth_url = isset($dashboard_tabs['auth']) ? aksrt_get_user_center_tab_url('auth') : ''; $services = array_values(array_filter([ [ @@ -34,6 +31,7 @@ $services = array_values(array_filter([ 'icon' => 'id-badge', 'tone' => 'auth', 'url' => $auth_url, + 'tab' => 'auth', ], ], static fn(array $item): bool => (string) $item['url'] !== '')); @@ -63,7 +61,7 @@ $render_grid = static function (array $items): void { ?>
- + data-tab=""> diff --git a/inc/modules/user-parts/routing.php b/inc/modules/user-parts/routing.php index 618c9e96..27bbdd3c 100644 --- a/inc/modules/user-parts/routing.php +++ b/inc/modules/user-parts/routing.php @@ -18,7 +18,6 @@ add_filter('query_vars', function ($vars) { $vars[] = 'aksrt_orders'; $vars[] = 'aksrt_cart'; $vars[] = 'aksrt_revenue'; - $vars[] = 'aksrt_user_auth'; return $vars; }); @@ -43,14 +42,12 @@ add_action('init', function () { add_rewrite_tag('%aksrt_orders%', '([0-1])'); add_rewrite_tag('%aksrt_cart%', '([0-1])'); add_rewrite_tag('%aksrt_revenue%', '([0-1])'); - add_rewrite_tag('%aksrt_user_auth%', '([0-1])'); add_rewrite_rule('^profile/?$', 'index.php?aksrt_user_center=1', 'top'); add_rewrite_rule('^user/([^/]+)/?$', 'index.php?user_nicename=$matches[1]', 'top'); // Commerce center aliases (work even before WP page is assigned). add_rewrite_rule('^orders/?$', 'index.php?aksrt_orders=1', 'top'); add_rewrite_rule('^cart/?$', 'index.php?aksrt_cart=1', 'top'); add_rewrite_rule('^revenue/?$', 'index.php?aksrt_revenue=1', 'top'); - add_rewrite_rule('^user-auth/?$', 'index.php?aksrt_user_auth=1', 'top'); }); // --- 3b. 统一模板拦截器(核心)--- @@ -169,7 +166,7 @@ add_action('init', function () { } // 统一 flush 检查 — bump when user-home rewrite / author override changes - $rewrite_version = AKSRT_VERSION . ':user-home-profile-routing-v4'; + $rewrite_version = AKSRT_VERSION . ':user-home-profile-routing-v5'; $flushed = get_option('aksrt_user_rules_flushed'); $rules = get_option('rewrite_rules'); $need_flush = ($flushed !== $rewrite_version) @@ -218,9 +215,6 @@ add_filter('request', function ($query_vars) { if (empty($query_vars['aksrt_user_center']) && isset($_GET['aksrt_user_center'])) { $query_vars['aksrt_user_center'] = '1'; } - if (empty($query_vars['aksrt_user_auth']) && isset($_GET['aksrt_user_auth'])) { - $query_vars['aksrt_user_auth'] = '1'; - } return $query_vars; }); diff --git a/inc/modules/user-parts/template-registry.php b/inc/modules/user-parts/template-registry.php index a7e7f35e..263f6178 100644 --- a/inc/modules/user-parts/template-registry.php +++ b/inc/modules/user-parts/template-registry.php @@ -131,12 +131,6 @@ $_aksrt_special_templates = [ 'title' => '消息中心', 'fallback' => '/messages/', ], - 'user-auth' => [ - 'file' => 'templates/pages/page-user-auth.php', - 'slug' => 'user-auth', - 'title' => '官方认证', - 'fallback' => '/user-auth/', - ], 'submit' => [ 'file' => 'templates/pages/page-submit.php', 'slug' => 'submit', @@ -282,7 +276,6 @@ function aksrt_resolve_page_template_file(string $template_file = '', string $po 'categories' => 'templates/pages/page-categories.php', 'friends' => 'templates/pages/page-friends.php', 'profile' => 'templates/pages/page-profile.php', - 'user-auth' => 'templates/pages/page-user-auth.php', ]; if (!empty($slug_map[$post_slug]) && aksrt_locate_template_file($slug_map[$post_slug])) { @@ -444,11 +437,6 @@ function aksrt_get_current_special_template_path(): string { return $revenue_template ?: ''; } - if (get_query_var('aksrt_user_auth')) { - $auth_template = aksrt_locate_template_file('templates/pages/page-user-auth.php'); - return $auth_template ?: ''; - } - if (get_query_var('aksrt_sign')) { $auth_template = aksrt_get_auth_template_file((string) get_query_var('aksrt_auth_tab'), true); $auth_template_path = aksrt_locate_template_file($auth_template); diff --git a/inc/modules/user-parts/urls.php b/inc/modules/user-parts/urls.php index 034d5e1c..bbb546e3 100644 --- a/inc/modules/user-parts/urls.php +++ b/inc/modules/user-parts/urls.php @@ -106,21 +106,6 @@ function aksrt_get_revenue_page_url(): string { } endif; -if (!function_exists('aksrt_get_user_auth_page_url')) : -/** - * 官方认证页面 URL。 - */ -function aksrt_get_user_auth_page_url(): string { - if (function_exists('aksrt_get_template_url')) { - return aksrt_get_template_url('user-auth'); - } - if (get_option('permalink_structure')) { - return home_url('/user-auth/'); - } - return add_query_arg('aksrt_user_auth', '1', home_url('/')); -} -endif; - if (!function_exists('aksrt_get_messages_page_url')) : /** * 消息中心页面 URL(模板 templates/pages/page-messages.php)。 diff --git a/src/AKSRT/Admin/Native/UsersMenus.php b/src/AKSRT/Admin/Native/UsersMenus.php index be36339a..f3e5128a 100644 --- a/src/AKSRT/Admin/Native/UsersMenus.php +++ b/src/AKSRT/Admin/Native/UsersMenus.php @@ -547,7 +547,8 @@ class UsersMenus { echo '
' . esc_html((string) $apply['msg']) . '
'; } foreach ((array) ($apply['img'] ?? []) as $image_url) { - echo '
'; + $evidence_src = class_exists('\AKSRT\User\AuthApply') ? \AKSRT\User\AuthApply::evidence_src((string) $image_url) : ''; + echo ''; } } if (!empty($row['verify_reason'])) { @@ -588,7 +589,17 @@ class UsersMenus { check_admin_referer('aksrt_users_review_verify_' . $user_id); $ok = false; - if ($user_id > 0 && in_array($status, ['verified', 'rejected'], true) && get_userdata($user_id)) { + $current_status = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true)); + $application = get_user_meta($user_id, 'aksrt_user_verify_apply', true); + if ( + $user_id > 0 + && in_array($status, ['verified', 'rejected'], true) + && get_userdata($user_id) + && $current_status === 'pending' + && is_array($application) + && !empty($application['name']) + && !empty($application['type']) + ) { update_user_meta($user_id, 'aksrt_user_verify_status', $status); update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql')); update_user_meta($user_id, 'aksrt_user_verify_reviewer', get_current_user_id()); @@ -619,6 +630,15 @@ class UsersMenus { 'time' => current_time('mysql'), ]; update_option('aksrt_verify_review_log', array_slice($log, -500), false); + if (class_exists('\AKSRT\Messages\Message')) { + $message = $status === 'verified' + ? __('您的官方认证申请已通过审核', 'aksrt') + : sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note !== '' ? $note : __('请修改资料后重新申请', 'aksrt')); + \AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [ + 'title' => __('官方认证审核结果', 'aksrt'), + 'target_url' => aksrt_get_user_center_tab_url('auth'), + ]); + } $ok = true; } $this->redirect('aksrt_user_auth', $ok ? 'verify_updated' : 'verify_failed', ['status' => 'pending']); diff --git a/src/AKSRT/Admin/Schema/SchemaUsers.php b/src/AKSRT/Admin/Schema/SchemaUsers.php index 950916bf..4ca861a4 100644 --- a/src/AKSRT/Admin/Schema/SchemaUsers.php +++ b/src/AKSRT/Admin/Schema/SchemaUsers.php @@ -515,7 +515,7 @@ class SchemaUsers { 'option_key' => 'aksrt_settings', 'groups' => [ ['key' => 'center', 'title' => '用户中心', 'fields' => [ - ['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,favorites,vip,credits', 'help' => '逗号分隔。vip=我的会员(会员功能开启时也会自动注入)'], + ['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,posts,comments,favorites,vip,credits,auth', 'help' => '逗号分隔。auth=官方认证;vip、订单和认证会按功能开关自动注入'], ['key' => 'user_avatar_upload', 'label' => '头像上传', 'type' => 'switch', 'default' => true], ['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true], ['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true], diff --git a/src/AKSRT/Admin/UserControllerVerifyIntegrations.php b/src/AKSRT/Admin/UserControllerVerifyIntegrations.php index 63cfd898..43a0bfc6 100644 --- a/src/AKSRT/Admin/UserControllerVerifyIntegrations.php +++ b/src/AKSRT/Admin/UserControllerVerifyIntegrations.php @@ -179,6 +179,11 @@ trait UserControllerVerifyIntegrations { if (!$user) { return $this->error(__('用户不存在', 'aksrt'), 404); } + $current_status = sanitize_key((string) get_user_meta($user_id, 'aksrt_user_verify_status', true)); + $application = get_user_meta($user_id, 'aksrt_user_verify_apply', true); + if ($current_status !== 'pending' || !is_array($application) || empty($application['name']) || empty($application['type'])) { + return $this->error(__('该用户没有可审核的认证申请', 'aksrt'), 409); + } update_user_meta($user_id, 'aksrt_user_verify_status', $status); update_user_meta($user_id, 'aksrt_user_verify_reviewed_at', current_time('mysql')); @@ -216,6 +221,15 @@ trait UserControllerVerifyIntegrations { $log = array_slice($log, -500); } update_option('aksrt_verify_review_log', $log, false); + if (class_exists('\AKSRT\Messages\Message')) { + $message = $status === 'verified' + ? __('您的官方认证申请已通过审核', 'aksrt') + : sprintf(__('您的官方认证申请未通过:%s', 'aksrt'), $note !== '' ? $note : __('请修改资料后重新申请', 'aksrt')); + \AKSRT\Messages\Message::get_instance()->send($user_id, 'auth_apply_reply', $message, [ + 'title' => __('官方认证审核结果', 'aksrt'), + 'target_url' => aksrt_get_user_center_tab_url('auth'), + ]); + } return $this->success([ 'message' => $status === 'verified' @@ -239,7 +253,7 @@ trait UserControllerVerifyIntegrations { 'email' => (string) ($row['user_email'] ?? ''), 'status' => $status, 'reason' => (string) ($row['verify_reason'] ?? ''), - 'requested_at' => (string) ($row['user_registered'] ?? ''), + 'requested_at' => (string) ($application['time'] ?? $row['user_registered'] ?? ''), 'reviewed_at' => (string) ($row['reviewed_at'] ?? ''), 'email_verified' => $email_verified, 'application' => $application, diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index fec29acb..8f09d24f 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -448,7 +448,9 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; 'aksrt-route-user-center', $base_uri . '/assets/js/routes/user-center.js', ['aksrt-script'], - $version, + file_exists(get_template_directory() . '/assets/js/routes/user-center.js') + ? (string) filemtime(get_template_directory() . '/assets/js/routes/user-center.js') + : $version, true ); // 绑定手机短信发送按钮依赖 sign-register 的 data-aksrt-sms-trigger 契约 @@ -623,38 +625,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; } } - // 用户身份认证页(zibll user-auth.php) - $is_user_auth_page = (bool) get_query_var('aksrt_user_auth') - || is_page_template('templates/pages/page-user-auth.php') - || ( - is_page() - && ( - (string) get_post_field('post_name', get_queried_object_id()) === 'user-auth' - || (string) get_post_meta(get_queried_object_id(), '_wp_page_template', true) === 'templates/pages/page-user-auth.php' - ) - ); - if ($is_user_auth_page) { - $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 - ); - } - } - // 交易中心共享壳层(购物车 / 订单 / 收益) // Virtual rewrite routes (aksrt_orders / aksrt_cart / aksrt_revenue) also count. $is_commerce_center = is_page_template('templates/pages/page-orders.php') diff --git a/src/AKSRT/Core/Templates.php b/src/AKSRT/Core/Templates.php index d2a864de..afe37e70 100644 --- a/src/AKSRT/Core/Templates.php +++ b/src/AKSRT/Core/Templates.php @@ -48,7 +48,6 @@ 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-ban-appeal.php' => __('AKSRT - 禁封申诉', 'aksrt'), 'templates/pages/page-orders.php' => __('AKSRT - 订单中心', 'aksrt'), 'templates/pages/page-revenue.php' => __('AKSRT - 收益中心', 'aksrt'), diff --git a/src/AKSRT/Settings/UserRuntime.php b/src/AKSRT/Settings/UserRuntime.php index 52796fe0..46b9b14a 100644 --- a/src/AKSRT/Settings/UserRuntime.php +++ b/src/AKSRT/Settings/UserRuntime.php @@ -83,14 +83,14 @@ class UserRuntime { */ public static function get_user_center_settings(): array { $settings = Settings::get_all(); - $raw_tabs = $settings['user_center_tabs'] ?? 'profile,posts,comments,credits,favorites'; + $raw_tabs = $settings['user_center_tabs'] ?? 'profile,posts,comments,credits,favorites,auth'; $tabs = is_array($raw_tabs) ? $raw_tabs : preg_split('/[\s,]+/', (string) $raw_tabs, -1, PREG_SPLIT_NO_EMPTY); // Built-in switchable panels. VIP is always auto-injected when enabled // by AKSRT\User\Center — keep it allowed for explicit config too. - $allowed_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'settings']; + $allowed_tabs = ['profile', 'posts', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'settings']; $normalized_tabs = ['profile']; foreach ((array) $tabs as $tab) { diff --git a/src/AKSRT/User/AuthApply.php b/src/AKSRT/User/AuthApply.php index c33b79a6..57ed853b 100644 --- a/src/AKSRT/User/AuthApply.php +++ b/src/AKSRT/User/AuthApply.php @@ -56,6 +56,12 @@ class AuthApply { } $types[] = ['slug' => $slug, 'label' => (string) $label]; } + if ($types === []) { + $types = [ + ['slug' => 'real_name', 'label' => __('实名认证', 'aksrt')], + ['slug' => 'organization', 'label' => __('企业认证', 'aksrt')], + ]; + } return [ 'enabled' => $enabled, 'allow_apply' => !isset($all['user_verify_allow_apply']) || !empty($all['user_verify_allow_apply']), @@ -95,6 +101,13 @@ class AuthApply { return is_array($apply) ? $apply : []; } + public static function evidence_src(string $value): string { + if (preg_match('#^data:image/(?:jpeg|png|gif|webp);base64,[A-Za-z0-9+/=]+$#', $value)) { + return $value; + } + return esc_url_raw($value); + } + public function ajax_apply(): void { if (!is_user_logged_in()) { wp_send_json(['error' => 1, 'msg' => __('请先登录', 'aksrt')]); @@ -133,8 +146,9 @@ class AuthApply { $msg = isset($_POST['msg']) ? sanitize_textarea_field(wp_unslash((string) $_POST['msg'])) : ''; $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : ''; $allowed_types = array_column((array) $s['types'], 'slug'); - if ($name === '') { - wp_send_json(['error' => 1, 'msg' => __('请填写认证身份名称', 'aksrt')]); + $name_length = function_exists('mb_strlen') ? mb_strlen($name) : strlen($name); + if ($name_length < 4 || $name_length > 16) { + wp_send_json(['error' => 1, 'msg' => __('认证身份长度需在 4-16 个字符之间', 'aksrt')]); } if ($type === '' || !in_array($type, $allowed_types, true)) { wp_send_json(['error' => 1, 'msg' => __('请选择有效的认证类型', 'aksrt')]); @@ -142,46 +156,43 @@ class AuthApply { $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 = [ + $names = is_array($files['name']) ? $files['name'] : [$files['name']]; + if (!is_array($files['name'])) { + $files = [ '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) { + if (count($files['name']) > 3) { + wp_send_json(['error' => 1, 'msg' => __('证明材料最多上传 3 张', 'aksrt')]); + } + foreach ($files['name'] as $index => $filename) { + if ($filename === '') { 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; - } + if ((int) ($files['error'][$index] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + wp_send_json(['error' => 1, 'msg' => __('证明材料上传失败,请重新选择', 'aksrt')]); } + $size = (int) ($files['size'][$index] ?? 0); + $tmp_name = (string) ($files['tmp_name'][$index] ?? ''); + if ($size <= 0 || $size > 2 * MB_IN_BYTES || !is_uploaded_file($tmp_name)) { + wp_send_json(['error' => 1, 'msg' => __('单张证明图片不能超过 2MB', 'aksrt')]); + } + $file_check = wp_check_filetype_and_ext($tmp_name, (string) $filename); + $mime = (string) ($file_check['type'] ?? ''); + if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], true)) { + wp_send_json(['error' => 1, 'msg' => __('证明材料仅支持 JPG、PNG、GIF 或 WebP 图片', 'aksrt')]); + } + $bytes = file_get_contents($tmp_name); + if (!is_string($bytes) || $bytes === '') { + wp_send_json(['error' => 1, 'msg' => __('证明材料读取失败', 'aksrt')]); + } + $images[] = 'data:' . $mime . ';base64,' . base64_encode($bytes); } - unset($_FILES['aksrt_auth_img']); } $payload = [ diff --git a/src/AKSRT/User/Center.php b/src/AKSRT/User/Center.php index c20216f8..21cfb58e 100644 --- a/src/AKSRT/User/Center.php +++ b/src/AKSRT/User/Center.php @@ -91,6 +91,12 @@ class Center { 'priority' => 60, 'type' => 'link', ], + 'auth' => [ + 'label' => __('官方认证', 'aksrt'), + 'icon' => 'id-badge', + 'priority' => 65, + 'type' => 'panel', + ], 'settings' => [ 'label' => __('账号设置', 'aksrt'), 'icon' => 'cog', @@ -144,6 +150,11 @@ class Center { if (($credits_on || $balance_on) && !in_array('credits', $order, true)) { $order[] = 'credits'; } + $auth_on = class_exists('\AKSRT\User\AuthApply') + && !empty(\AKSRT\User\AuthApply::settings()['enabled']); + if ($auth_on && !in_array('auth', $order, true)) { + $order[] = 'auth'; + } if (!in_array('settings', $order, true)) { $order[] = 'settings'; } @@ -161,6 +172,9 @@ class Center { if ($key === 'orders' && !$orders_on) { continue; } + if ($key === 'auth' && !$auth_on) { + continue; + } $panel = $definitions[$key]; if ($key === 'credits' && $balance_on) { $panel['label'] = __('我的资产', 'aksrt'); diff --git a/templates/pages/page-user-auth.php b/templates/pages/page-user-auth.php deleted file mode 100644 index 326a3167..00000000 --- a/templates/pages/page-user-auth.php +++ /dev/null @@ -1,203 +0,0 @@ - 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(aksrt_get_user_auth_page_url()) : wp_login_url(aksrt_get_user_auth_page_url()); - -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)) . '
'; - } - ?> -
- - -
- -
- -
-
- -
- -
-
- -
-
-
- -
-
-
- -
-
-
- -
-
- - - -
-
- - -
- - -
-
- - -