From 5964399012df6569f6b325696fcf58861ee526a4 Mon Sep 17 00:00:00 2001 From: DU Date: Wed, 17 Jun 2026 18:37:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=99=BB=E5=BD=95=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=B8=BA=20=E9=9D=99=E9=BB=98=E7=99=BB=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=20=E4=BF=AE=E6=94=B9=E4=BB=98=E6=AC=BE=E5=90=8E?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=B1=95=E7=A4=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 32 ++++- .../closeTable/components/FooterButtons.tsx | 30 ++-- src/pageScan/closeTable/index.tsx | 6 +- src/pageScan/couponStart/index.tsx | 4 +- src/pageScan/selfStart/index.tsx | 4 +- src/pageScan/timedStart/index.tsx | 4 +- src/pages/index/index.scss | 128 +++++++++++++++++ src/pages/index/index.tsx | 129 ++++++++++++++++- src/pages/qrHandler/index.tsx | 20 ++- src/pages/user/index.tsx | 130 +++++------------- src/store/user.ts | 14 ++ 11 files changed, 374 insertions(+), 127 deletions(-) diff --git a/src/app.ts b/src/app.ts index 4f440e2..4e8e148 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,13 +3,14 @@ import { useLaunch } from '@tarojs/taro' import Taro from '@tarojs/taro' import { userStore } from './store' import { getCustomerByOpenId } from './api/customer' +import { wechatLogin } from './api/login' import './app.scss' function App({ children }: PropsWithChildren) { useLaunch(() => { console.log('台球馆小程序启动') - // 先从本地缓存恢复用户信息(快速) + // 先从本地缓存恢复用户信息(快速恢复登录态) try { const savedUserInfo = Taro.getStorageSync('userInfo') if (savedUserInfo) { @@ -19,9 +20,10 @@ function App({ children }: PropsWithChildren) { console.warn('初始化用户信息失败:', error) } - // 后台静默刷新(获取最新余额等信息),不阻塞启动 const openId = userStore.userInfo?.wxOpenId || Taro.getStorageSync('openId') + if (openId) { + // 已有 openId:后台静默刷新最新用户信息(余额、会员等),不阻塞启动 getCustomerByOpenId(openId) .then(res => { if (res.data) { @@ -29,6 +31,32 @@ function App({ children }: PropsWithChildren) { } }) .catch(e => console.warn('启动刷新用户信息失败:', e)) + } else { + // 无本地 openId:执行无感登录,wx.login() 不需要用户任何操作 + Taro.login({ + success: async ({ code }) => { + try { + const res = await wechatLogin(code) + if (res.success && res.data) { + userStore.setUserInfoFromWechat(res.data.customer) + // 新用户:触发头像/昵称补全弹窗 + if (res.data.isNewCustomer) { + userStore.setIsNewUser(true) + } + } + } catch (e) { + console.warn('无感登录失败:', e) + } finally { + // 无论成功失败,都标记登录流程结束,让 qrHandler 继续处理 + userStore.setLoginReady() + } + }, + fail: (err) => { + console.warn('wx.login 调用失败:', err) + // 登录失败也要标记结束,避免 qrHandler 永久等待 + userStore.setLoginReady() + } + }) } }) diff --git a/src/pageScan/closeTable/components/FooterButtons.tsx b/src/pageScan/closeTable/components/FooterButtons.tsx index 9883cb4..e94fabe 100644 --- a/src/pageScan/closeTable/components/FooterButtons.tsx +++ b/src/pageScan/closeTable/components/FooterButtons.tsx @@ -13,9 +13,11 @@ interface FooterButtonsProps { type: 'owner' | 'guest' grpSn: string operationId: number + /** 是否从支付完成后跳转而来,true 时只显示「立即关台」 */ + fromPayment?: boolean } -const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { +const FooterButtons = ({ type, grpSn, operationId, fromPayment = false }: FooterButtonsProps) => { const [loading, setLoading] = useState(false) // 立即关台 @@ -153,16 +155,6 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { } } - // 邀请球友 - const handleInviteFriend = () => { - console.log('邀请球友') - Taro.showToast({ - title: '功能开发中', - icon: 'none', - duration: 2000 - }) - } - // 关灯重开 const handleTurnOffRestart = () => { console.log('关灯重开') @@ -173,9 +165,14 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { return ( - {type === 'owner' ? ( + {fromPayment ? ( + // 刚完成支付跳转而来:只显示「立即关台」 + + {loading ? '处理中...' : '立即关台'} + + ) : type === 'owner' ? ( <> - {/* 开台人员扫码 - 图1 */} + {/* 开台人员扫码 */} {loading ? '处理中...' : '立即关台'} @@ -184,9 +181,6 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { - 分享球台 - - 邀请球友 @@ -196,7 +190,7 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { ) : ( <> - {/* 非开台人扫码 - 图2 */} + {/* 非开台人扫码 */} 帮他付款关台 @@ -204,7 +198,7 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { AA制付款关台 - + 邀请球友 diff --git a/src/pageScan/closeTable/index.tsx b/src/pageScan/closeTable/index.tsx index b24cc1e..eb01337 100644 --- a/src/pageScan/closeTable/index.tsx +++ b/src/pageScan/closeTable/index.tsx @@ -16,7 +16,9 @@ const locationIcon = require('../../assets/icon/locationIcon.png') const CloseTable = observer(() => { const router = useRouter() - const { type = 'owner' } = router.params as { type?: 'owner' | 'guest' } // owner: 开台人员, guest: 非开台人 + const { type = 'owner', fromPayment } = router.params as { type?: 'owner' | 'guest'; fromPayment?: string } + // fromPayment=1 表示刚完成支付跳转而来,只显示「立即关台」 + const isFromPayment = fromPayment === '1' const [expanded, setExpanded] = useState(false) // 订单详情是否展开 const [couponExpanded, setCouponExpanded] = useState(false) // 优惠券详情是否展开 const [priceExpanded, setPriceExpanded] = useState(false) // 收费标准详情是否展开 @@ -242,7 +244,7 @@ const CloseTable = observer(() => { {/* 底部按钮区域 */} - + ) }) diff --git a/src/pageScan/couponStart/index.tsx b/src/pageScan/couponStart/index.tsx index 37d05df..f72046b 100644 --- a/src/pageScan/couponStart/index.tsx +++ b/src/pageScan/couponStart/index.tsx @@ -192,7 +192,7 @@ const CouponStart = observer(() => { Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1` }) }, 2000) } else { @@ -217,7 +217,7 @@ const CouponStart = observer(() => { Taro.showToast({ title: '兑换成功,开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1` }) }, 2000) } diff --git a/src/pageScan/selfStart/index.tsx b/src/pageScan/selfStart/index.tsx index 2bf0da0..dad6524 100644 --- a/src/pageScan/selfStart/index.tsx +++ b/src/pageScan/selfStart/index.tsx @@ -169,7 +169,7 @@ const SelfStart = observer(() => { Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${confirmedOperationId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${confirmedOperationId}&fromPayment=1` }) }, 2000) } else { @@ -202,7 +202,7 @@ const SelfStart = observer(() => { Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1` }) }, 2000) } diff --git a/src/pageScan/timedStart/index.tsx b/src/pageScan/timedStart/index.tsx index 5b365a2..0851907 100644 --- a/src/pageScan/timedStart/index.tsx +++ b/src/pageScan/timedStart/index.tsx @@ -137,7 +137,7 @@ const TimedStart = observer(() => { Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1` }) }, 2000) } else { @@ -162,7 +162,7 @@ const TimedStart = observer(() => { Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}&fromPayment=1` }) }, 2000) } diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss index 2241b19..1b0d1ca 100644 --- a/src/pages/index/index.scss +++ b/src/pages/index/index.scss @@ -162,3 +162,131 @@ position: relative; overflow: hidden; } + +/* 新用户信息补全弹窗 */ +.new-user-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + + .new-user-popup { + width: 620px; + background: #fff; + border-radius: 24px; + padding: 60px 48px 48px; + display: flex; + flex-direction: column; + align-items: center; + + .new-user-title { + font-size: 36px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 16px; + } + + .new-user-desc { + font-size: 26px; + color: #888; + margin-bottom: 48px; + text-align: center; + } + + .new-user-avatar-wrap { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 40px; + + .new-user-avatar { + width: 140px; + height: 140px; + border-radius: 50%; + margin-bottom: 20px; + border: 2PX solid #eee; + } + + .new-user-avatar-btn { + font-size: 26px; + color: #fff; + background: #07c160; + border: none; + border-radius: 32px; + padding: 0 36px; + height: 64px; + line-height: 64px; + + &::after { + border: none; + } + } + } + + .new-user-input-wrap { + width: 100%; + background: #f7f7f7; + border-radius: 16px; + padding: 0 28px; + margin-bottom: 48px; + height: 88px; + display: flex; + align-items: center; + + .new-user-input { + width: 100%; + font-size: 30px; + color: #1a1a1a; + height: 88px; + line-height: 88px; + } + + .new-user-placeholder { + color: #bbb; + font-size: 28px; + } + } + + .new-user-actions { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + + .new-user-save { + width: 100%; + height: 88px; + background: #1a1a2e; + color: #fff; + border: none; + border-radius: 44px; + font-size: 32px; + font-weight: 500; + line-height: 88px; + margin: 0; + + &.disabled { + opacity: 0.6; + } + + &::after { + border: none; + } + } + + .new-user-skip { + font-size: 26px; + color: #aaa; + padding: 28px 0 8px; + text-align: center; + } + } + } +} diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index d1c79b7..27ec383 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -1,25 +1,94 @@ /** * 首页 */ -import { View, Image } from '@tarojs/components' +import { View, Image, Text, Button as WxButton, Input } from '@tarojs/components' import { useLoad } from '@tarojs/taro' import Taro from '@tarojs/taro' import { observer } from 'mobx-react' -import { Swiper } from '@taroify/core' +import { Swiper, Image as TaroifyImage } from '@taroify/core' import { LocationOutlined } from '@taroify/icons' import { useEffect, useState } from 'react' import { userStore } from '../../store' -import { getBigImage, BIG_IMAGES } from '@/utils/imageHelper' +import { updateCustomer, readImageAsBase64 } from '@/api/customer' +import { getBigImage } from '@/utils/imageHelper' +import myAvatar from '@/assets/icon/my_avatar.png' import './index.scss' const Index = observer(() => { const [currentDistance, setCurrentDistance] = useState('--') - const [storeName, setStoreName] = useState('黑八大师联盟球厅店') + const [storeName] = useState('黑八大师联盟球厅店') + + // 新用户头像/昵称弹窗状态 + const [showNewUserPopup, setShowNewUserPopup] = useState(false) + const [newNickname, setNewNickname] = useState('') + const [newAvatarPath, setNewAvatarPath] = useState('') + const [newAvatarUrl, setNewAvatarUrl] = useState('') + const [saving, setSaving] = useState(false) useLoad(() => { console.log('首页加载') }) + // 监听新用户标记,延迟弹出避免与登录动画冲突 + useEffect(() => { + if (userStore.isNewUser) { + setTimeout(() => setShowNewUserPopup(true), 800) + } + }, [userStore.isNewUser]) + + // 选择微信头像回调 + const handleChooseAvatar = (e: any) => { + const url = e.detail.avatarUrl + setNewAvatarUrl(url) + setNewAvatarPath(url) + } + + // 保存头像和昵称到后台 + const handleSaveProfile = async () => { + const userId = userStore.userInfo?.id + if (!userId || saving) return + + setSaving(true) + try { + let avatarImage: string | undefined + if (newAvatarPath) { + try { + avatarImage = await readImageAsBase64(newAvatarPath) + } catch (e) { + console.warn('头像转 Base64 失败,跳过头像更新:', e) + } + } + + await updateCustomer(userId, { + id: userId, + name: newNickname.trim() || undefined, + avatarImage, + }) + + // 同步更新本地 store + userStore.setUserInfo({ + ...userStore.userInfo!, + name: newNickname.trim() || userStore.userInfo?.name, + nickname: newNickname.trim() || userStore.userInfo?.nickname, + avatar: newAvatarPath || userStore.userInfo?.avatar, + }) + + userStore.setIsNewUser(false) + setShowNewUserPopup(false) + Taro.showToast({ title: '欢迎加入!', icon: 'success' }) + } catch (e) { + console.warn('保存用户信息失败:', e) + } finally { + setSaving(false) + } + } + + // 跳过,不填写 + const handleSkip = () => { + userStore.setIsNewUser(false) + setShowNewUserPopup(false) + } + // 请求定位权限并获取位置 const requestLocationPermission = async () => { try { @@ -91,6 +160,58 @@ const Index = observer(() => { return ( + {/* 新用户头像/昵称补全弹窗 */} + {showNewUserPopup && ( + + + 完善个人信息 + 设置头像和昵称,让台球馆认识你 + + {/* 微信头像选择按钮 */} + + + + {newAvatarUrl ? '更换头像' : '选择头像'} + + + + {/* 微信昵称输入(type="nickname" 会自动弹出微信昵称建议) */} + + setNewNickname(e.detail.value)} + placeholder="点击填写昵称(可使用微信昵称)" + placeholderClass="new-user-placeholder" + maxlength={20} + /> + + + + + {saving ? '保存中...' : '完成'} + + + 跳过,暂不设置 + + + + + )} {/* 轮播图区域 */} { useLoad(async () => { console.log('二维码处理页面加载,参数:', router.params) - + + // 等待登录就绪(最多等待 5 秒),确保 openId 可用 + // 老用户:initUserInfo 同步恢复,isLoginReady 已为 true,无需等待 + // 新用户外部扫码:app.ts 异步登录中,需要等待 + if (!userStore.isLoginReady) { + await new Promise((resolve) => { + const maxWait = 5000 + const interval = 100 + let elapsed = 0 + const timer = setInterval(() => { + elapsed += interval + if (userStore.isLoginReady || elapsed >= maxWait) { + clearInterval(timer) + resolve() + } + }, interval) + }) + } + try { // 从路由参数获取二维码内容 const { q, scene, qrContent } = router.params diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx index 785b88a..fd40254 100644 --- a/src/pages/user/index.tsx +++ b/src/pages/user/index.tsx @@ -5,9 +5,8 @@ import { View, Text, Image } from '@tarojs/components' import { useLoad } from '@tarojs/taro' import Taro from '@tarojs/taro' import { observer } from 'mobx-react' -import { Image as TaroifyImage, Button } from '@taroify/core' +import { Image as TaroifyImage } from '@taroify/core' import { userStore } from '../../store' -import { wechatLogin } from '../../api/login' import { getCustomerByOpenId } from '../../api/customer' import { getBigImage } from '@/utils/imageHelper' import './index.scss' @@ -84,44 +83,6 @@ const UserIndex = observer(() => { }) } - // 微信登录 - const handleWechatLogin = async () => { - try { - userStore.setLoading(true) - - // 调用微信登录获取code - const loginResult = await Taro.login() - if (!loginResult.code) { - throw new Error('获取微信登录凭证失败') - } - - console.log('loginResult', loginResult.code) - // return - // 调用后端接口 - const response = await wechatLogin(loginResult.code) - - if (response.success && response.data) { - // 保存用户信息到store - userStore.setUserInfoFromWechat(response.data.customer) - - Taro.showToast({ - title: response.data.isNewCustomer ? '注册成功' : '登录成功', - icon: 'success' - }) - } else { - throw new Error(response.data?.message || response.message || '登录失败') - } - } catch (error: any) { - console.error('微信登录失败:', error) - Taro.showToast({ - title: error.message || '登录失败,请重试', - icon: 'error' - }) - } finally { - userStore.setLoading(false) - } - } - return ( {/* 上部分:背景图和用户信息 */} @@ -143,61 +104,42 @@ const UserIndex = observer(() => { {/* 右侧信息 */} - {userStore.isLoggedIn ? ( - <> - {/* 已登录状态 */} - {/* 用户名称和编辑按钮 */} - - - {userStore.userInfo?.nickname || '微信用户'} - - - - {userStore.userInfo?.typeName || 'VIP'} - - - - {/* 成长值 */} - - 成长值: - {growthIcons.map((icon, index) => ( - - ))} - - - ) : ( - <> - {/* 未登录状态 */} - - - 未登录 - - + {/* 用户名称和编辑按钮 */} + + + + {userStore.isLoggedIn ? (userStore.userInfo?.nickname || '微信用户') : '加载中...'} + + {userStore.isLoggedIn && ( + + )} + + {userStore.isLoggedIn && ( + + {userStore.userInfo?.typeName || 'VIP'} - - {/* 成长值显示为暂无 */} - - 成长值: - 暂无 - - - )} + )} + + + {/* 成长值 */} + + 成长值: + {userStore.isLoggedIn ? ( + growthIcons.map((icon, index) => ( + + )) + ) : ( + -- + )} + diff --git a/src/store/user.ts b/src/store/user.ts index b48ec8c..355c3a6 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -31,6 +31,10 @@ class UserStore { isLoggedIn: boolean = false balance: number = 0 isLoading: boolean = false + /** 本次会话是否为新注册用户,用于触发头像/昵称补全弹窗 */ + isNewUser: boolean = false + /** 登录流程是否已完成(缓存恢复 或 接口登录),供 qrHandler 等页面等待 */ + isLoginReady: boolean = false constructor() { makeAutoObservable(this) @@ -48,6 +52,8 @@ class UserStore { if (userInfo.amount !== undefined) { this.balance = userInfo.amount } + // 本地缓存恢复成功,登录态立即就绪 + this.isLoginReady = true } } catch (error) { console.error('读取用户信息失败:', error) @@ -100,6 +106,14 @@ class UserStore { this.balance = balance } + setIsNewUser = (val: boolean) => { + this.isNewUser = val + } + + setLoginReady = () => { + this.isLoginReady = true + } + setLoading = (loading: boolean) => { this.isLoading = loading }