From 19ca568d60d87a9b00c755e54d611bd19b792ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 15 Jul 2026 01:25:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(user):=20=E6=B7=BB=E5=8A=A0=E5=85=B3?= =?UTF-8?q?=E4=BA=8E=E9=A1=B5=E9=9A=90=E8=97=8F=E5=BC=80=E5=8F=91=E8=80=85?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在底部版权信息连续点击11次时弹出开发者登录弹窗 - 登录方式采用手机号加短信验证码,复用现有登录API - 点击计数器使用useRef实现,3秒无操作自动重置 - 第4次及以后点击显示剩余步数提示,前3次触发轻震动反馈 - 登录成功后同步React登录态,关闭弹窗并重置表单 - 弹窗中使用NutUI组件,实现验证码发送和倒计时功能 --- .workbuddy/memory/2025-07-15.md | 9 ++ src/pages/user/about/index.tsx | 199 +++++++++++++++++++++++++++++++- src/utils/privacy.ts | 79 +++++++++++++ 3 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 .workbuddy/memory/2025-07-15.md create mode 100644 src/utils/privacy.ts diff --git a/.workbuddy/memory/2025-07-15.md b/.workbuddy/memory/2025-07-15.md new file mode 100644 index 0000000..567c6ee --- /dev/null +++ b/.workbuddy/memory/2025-07-15.md @@ -0,0 +1,9 @@ +# 2025-07-15 工作日志 + +## 关于页面隐藏开发者登录 +- 在 `src/pages/user/about/index.tsx` 底部版权信息上添加了点击 11 次弹出隐藏登录弹窗的功能 +- 登录方式:手机号 + 短信验证码,复用 `loginBySms` / `sendSmsCaptcha` API(from `@/api/passport/login`) +- 点击计数使用 `useRef` 保证快速点击时的同步性,超过 3 秒未点击自动重置 +- 第 4 次点击起显示步数提示(类似 Android 开发者模式),前 3 次仅轻震动反馈 +- 登录成功后调用 `useUserContext().syncFromStorage()` 同步 React 登录态(`loginBySms` 内部已写入 storage 但不返回数据) +- 弹窗 UI 使用 NutUI `Input` + `Button`,带 60 秒倒计时 diff --git a/src/pages/user/about/index.tsx b/src/pages/user/about/index.tsx index 8fd8524..d7b3058 100644 --- a/src/pages/user/about/index.tsx +++ b/src/pages/user/about/index.tsx @@ -1,11 +1,135 @@ -import React from 'react' +import React, { useState, useRef, useEffect } from 'react' +import Taro from '@tarojs/taro' import { View, Text, ScrollView } from '@tarojs/components' +import { Input, Button } from '@nutui/nutui-react-taro' +import { loginBySms, sendSmsCaptcha } from '@/api/passport/login' +import type { LoginParam } from '@/api/passport/login/model' +import { useUserContext } from '@/contexts/UserContext' definePageConfig({ navigationBarTitleText: '关于我们', }) +/** 触发隐藏登录的点击次数 */ +const DEV_LOGIN_CLICK_COUNT = 11 + const AboutPage: React.FC = () => { + const { syncFromStorage } = useUserContext() + + // ---- 隐藏开发者登录相关状态 ---- + const clickCountRef = useRef(0) + const lastClickTime = useRef(0) + const [showLogin, setShowLogin] = useState(false) + + const [loading, setLoading] = useState(false) + const [sendingCode, setSendingCode] = useState(false) + const [countdown, setCountdown] = useState(0) + const [formData, setFormData] = useState({ phone: '', code: '' }) + + // 倒计时 + useEffect(() => { + if (countdown <= 0) return + const timer = setTimeout(() => setCountdown(countdown - 1), 1000) + return () => clearTimeout(timer) + }, [countdown]) + + /** 点击版权信息 —— 累计 11 次弹出隐藏登录 */ + const handleCopyrightClick = () => { + const now = Date.now() + // 超过 3 秒未点击则重新计数 + if (now - lastClickTime.current > 3000) { + clickCountRef.current = 1 + } else { + clickCountRef.current += 1 + } + lastClickTime.current = now + + const current = clickCountRef.current + const remaining = DEV_LOGIN_CLICK_COUNT - current + + if (current >= DEV_LOGIN_CLICK_COUNT) { + clickCountRef.current = 0 + setShowLogin(true) + return + } + + // 从第 4 次开始给步数提示 + if (current >= 4) { + Taro.showToast({ + title: `再点击 ${remaining} 次开启开发者登录`, + icon: 'none', + duration: 1200, + }) + } else { + // 前 3 次给极轻的震动反馈,避免用户误触无感 + Taro.vibrateShort({ type: 'light' }) + } + } + + /** 校验手机号 */ + const validatePhone = (phone: string): boolean => /^1[3-9]\d{9}$/.test(phone) + + /** 发送短信验证码 */ + const handleSendCode = async () => { + if (!formData.phone) { + Taro.showToast({ title: '请输入手机号码', icon: 'none' }) + return + } + if (!validatePhone(formData.phone)) { + Taro.showToast({ title: '请输入正确的手机号码', icon: 'none' }) + return + } + if (sendingCode || countdown > 0) return + + try { + setSendingCode(true) + await sendSmsCaptcha({ phone: formData.phone }) + Taro.showToast({ title: '验证码已发送', icon: 'success' }) + setCountdown(60) + } catch (error: any) { + Taro.showToast({ title: error.message || '发送失败', icon: 'none' }) + } finally { + setSendingCode(false) + } + } + + /** 登录提交 */ + const handleLogin = async () => { + if (loading) return + if (!formData.phone || !validatePhone(formData.phone)) { + Taro.showToast({ title: '请输入正确的手机号码', icon: 'none' }) + return + } + if (!formData.code || formData.code.length !== 6) { + Taro.showToast({ title: '请输入6位验证码', icon: 'none' }) + return + } + + try { + setLoading(true) + await loginBySms({ phone: formData.phone, code: formData.code }) + // loginBySms 内部已写入 storage,这里同步 React 登录态 + syncFromStorage() + Taro.showToast({ title: '登录成功', icon: 'success' }) + // 重置表单并关闭弹窗 + setFormData({ phone: '', code: '' }) + setCountdown(0) + setTimeout(() => setShowLogin(false), 800) + } catch (error: any) { + Taro.showToast({ title: error.message || '登录失败', icon: 'none' }) + } finally { + setLoading(false) + } + } + + /** 关闭弹窗时重置状态 */ + const handleCloseLogin = () => { + if (loading) return + setShowLogin(false) + setFormData({ phone: '', code: '' }) + setCountdown(0) + } + return ( {/* 头部品牌区 */} @@ -74,10 +198,81 @@ const AboutPage: React.FC = () => { {/* 底部版权 */} - + Copyright © 2024 鑫龙商贸电器 All Rights Reserved + + {/* 隐藏的开发者登录弹窗 */} + {showLogin && ( + + e.stopPropagation()} + > + + 开发者登录 + + ✕ + + + + 仅限开发测试使用,通过手机号+验证码登录指定账号 + + + setFormData({ ...formData, phone: val })} + style={{ backgroundColor: '#f5f5f5', borderRadius: '8px' }} + /> + + + + setFormData({ ...formData, code: val })} + style={{ backgroundColor: '#f5f5f5', borderRadius: '8px' }} + /> + + + + + + + + )} ) } diff --git a/src/utils/privacy.ts b/src/utils/privacy.ts new file mode 100644 index 0000000..f53df96 --- /dev/null +++ b/src/utils/privacy.ts @@ -0,0 +1,79 @@ +import Taro from '@tarojs/taro' + +/** + * 在调用隐私相关 API / 组件前,确保用户已完成微信隐私协议授权。 + * 基础库 3.16.1+ 强制要求,未授权会抛 errno:112 / buttonId is wrong 等错误。 + * 旧基础库或不支持隐私协议的版本直接放行。 + */ +export const ensurePrivacyAuthorized = (): Promise => { + return new Promise((resolve) => { + const wxAny: any = Taro + if (typeof wxAny.requirePrivacyAuthorize !== 'function') { + resolve() + return + } + if (typeof wxAny.getPrivacySetting === 'function') { + wxAny.getPrivacySetting({ + success: (res: any) => { + if (res && res.needAuthorization) { + wxAny.requirePrivacyAuthorize({ + success: () => resolve(), + fail: () => resolve(), + }) + } else { + resolve() + } + }, + fail: () => resolve(), + }) + } else { + wxAny.requirePrivacyAuthorize({ + success: () => resolve(), + fail: () => resolve(), + }) + } + }) +} + +/** + * 打开官方隐私协议页面(半屏协议)。 + * 返回是否成功打开。 + */ +export const openPrivacyContract = (): Promise => { + return new Promise((resolve) => { + const wxAny: any = Taro + if (typeof wxAny.openPrivacyContract !== 'function') { + resolve(false) + return + } + wxAny.openPrivacyContract({ + success: () => resolve(true), + fail: () => resolve(false), + }) + }) +} + +/** + * 检查当前用户是否需要同意隐私协议。 + */ +export const checkPrivacyAuthorization = (): Promise<{ + needAuthorization: boolean + isAuthorization: boolean +}> => { + return new Promise((resolve) => { + const wxAny: any = Taro + if (typeof wxAny.getPrivacySetting !== 'function') { + resolve({ needAuthorization: false, isAuthorization: true }) + return + } + wxAny.getPrivacySetting({ + success: (res: any) => { + resolve({ + needAuthorization: !!res?.needAuthorization, + isAuthorization: !!res?.isAuthorization, + }) + }, + fail: () => resolve({ needAuthorization: false, isAuthorization: true }), + }) + }) +}