Browse Source

修改登录逻辑为 静默登录, 修改付款后按钮展示逻辑

master
DU 4 weeks ago
parent
commit
5964399012
  1. 32
      src/app.ts
  2. 30
      src/pageScan/closeTable/components/FooterButtons.tsx
  3. 6
      src/pageScan/closeTable/index.tsx
  4. 4
      src/pageScan/couponStart/index.tsx
  5. 4
      src/pageScan/selfStart/index.tsx
  6. 4
      src/pageScan/timedStart/index.tsx
  7. 128
      src/pages/index/index.scss
  8. 129
      src/pages/index/index.tsx
  9. 18
      src/pages/qrHandler/index.tsx
  10. 84
      src/pages/user/index.tsx
  11. 14
      src/store/user.ts

32
src/app.ts

@ -3,13 +3,14 @@ import { useLaunch } from '@tarojs/taro' @@ -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<any>) {
useLaunch(() => {
console.log('台球馆小程序启动')
// 先从本地缓存恢复用户信息(快速)
// 先从本地缓存恢复用户信息(快速恢复登录态
try {
const savedUserInfo = Taro.getStorageSync('userInfo')
if (savedUserInfo) {
@ -19,9 +20,10 @@ function App({ children }: PropsWithChildren<any>) { @@ -19,9 +20,10 @@ function App({ children }: PropsWithChildren<any>) {
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<any>) { @@ -29,6 +31,32 @@ function App({ children }: PropsWithChildren<any>) {
}
})
.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()
}
})
}
})

30
src/pageScan/closeTable/components/FooterButtons.tsx

@ -13,9 +13,11 @@ interface FooterButtonsProps { @@ -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) => { @@ -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) => { @@ -173,9 +165,14 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => {
return (
<View className="footer-buttons">
{type === 'owner' ? (
{fromPayment ? (
// 刚完成支付跳转而来:只显示「立即关台」
<View className={`primary-button green ${loading ? 'disabled' : ''}`} onClick={handleCloseNow}>
<Text className="button-text">{loading ? '处理中...' : '立即关台'}</Text>
</View>
) : type === 'owner' ? (
<>
{/* 开台人员扫码 - 图1 */}
{/* 开台人员扫码 */}
<View className={`primary-button green ${loading ? 'disabled' : ''}`} onClick={handleCloseNow}>
<Text className="button-text">{loading ? '处理中...' : '立即关台'}</Text>
</View>
@ -184,9 +181,6 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { @@ -184,9 +181,6 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => {
</View>
<View className="secondary-buttons">
<View className="secondary-button" onClick={handleShareTable}>
<Text className="secondary-text"></Text>
</View>
<View className="secondary-button" onClick={handleInviteFriend}>
<Text className="secondary-text"></Text>
</View>
<View className="secondary-button right" onClick={handleTurnOffRestart}>
@ -196,7 +190,7 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { @@ -196,7 +190,7 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => {
</>
) : (
<>
{/* 非开台人扫码 - 图2 */}
{/* 非开台人扫码 */}
<View className="primary-button green" onClick={handlePayForOther}>
<Text className="button-text"></Text>
</View>
@ -204,7 +198,7 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => { @@ -204,7 +198,7 @@ const FooterButtons = ({ type, grpSn, operationId }: FooterButtonsProps) => {
<Text className="button-text">AA制付款关台</Text>
</View>
<View className="secondary-buttons">
<View className="secondary-button" onClick={handleInviteFriend}>
<View className="secondary-button" onClick={handleShareTable}>
<Text className="secondary-text"></Text>
</View>
<View className="secondary-button right" onClick={handleTurnOffRestart}>

6
src/pageScan/closeTable/index.tsx

@ -16,7 +16,9 @@ const locationIcon = require('../../assets/icon/locationIcon.png') @@ -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(() => { @@ -242,7 +244,7 @@ const CloseTable = observer(() => {
</View>
{/* 底部按钮区域 */}
<FooterButtons type={type} grpSn={grpSn} operationId={operationId} />
<FooterButtons type={type} grpSn={grpSn} operationId={operationId} fromPayment={isFromPayment} />
</View>
)
})

4
src/pageScan/couponStart/index.tsx

@ -192,7 +192,7 @@ const CouponStart = observer(() => { @@ -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(() => { @@ -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)
}

4
src/pageScan/selfStart/index.tsx

@ -169,7 +169,7 @@ const SelfStart = observer(() => { @@ -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(() => { @@ -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)
}

4
src/pageScan/timedStart/index.tsx

@ -137,7 +137,7 @@ const TimedStart = observer(() => { @@ -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(() => { @@ -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)
}

128
src/pages/index/index.scss

@ -162,3 +162,131 @@ @@ -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;
}
}
}
}

129
src/pages/index/index.tsx

@ -1,25 +1,94 @@ @@ -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(() => { @@ -91,6 +160,58 @@ const Index = observer(() => {
return (
<View className="index-page">
{/* 新用户头像/昵称补全弹窗 */}
{showNewUserPopup && (
<View className="new-user-mask">
<View className="new-user-popup">
<Text className="new-user-title"></Text>
<Text className="new-user-desc"></Text>
{/* 微信头像选择按钮 */}
<View className="new-user-avatar-wrap">
<TaroifyImage
className="new-user-avatar"
src={newAvatarUrl || myAvatar}
round
mode="aspectFill"
/>
<WxButton
className="new-user-avatar-btn"
openType="chooseAvatar"
onChooseAvatar={handleChooseAvatar}
>
{newAvatarUrl ? '更换头像' : '选择头像'}
</WxButton>
</View>
{/* 微信昵称输入(type="nickname" 会自动弹出微信昵称建议) */}
<View className="new-user-input-wrap">
<Input
className="new-user-input"
type="nickname"
value={newNickname}
onInput={(e) => setNewNickname(e.detail.value)}
placeholder="点击填写昵称(可使用微信昵称)"
placeholderClass="new-user-placeholder"
maxlength={20}
/>
</View>
<View className="new-user-actions">
<WxButton
className={`new-user-save ${saving ? 'disabled' : ''}`}
loading={saving}
onClick={handleSaveProfile}
>
{saving ? '保存中...' : '完成'}
</WxButton>
<View className="new-user-skip" onClick={handleSkip}>
</View>
</View>
</View>
</View>
)}
{/* 轮播图区域 */}
<View className="swiper-container">
<Swiper

18
src/pages/qrHandler/index.tsx

@ -30,6 +30,24 @@ const QRHandlerPage = () => { @@ -30,6 +30,24 @@ const QRHandlerPage = () => {
useLoad(async () => {
console.log('二维码处理页面加载,参数:', router.params)
// 等待登录就绪(最多等待 5 秒),确保 openId 可用
// 老用户:initUserInfo 同步恢复,isLoginReady 已为 true,无需等待
// 新用户外部扫码:app.ts 异步登录中,需要等待
if (!userStore.isLoginReady) {
await new Promise<void>((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

84
src/pages/user/index.tsx

@ -5,9 +5,8 @@ import { View, Text, Image } from '@tarojs/components' @@ -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(() => { @@ -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 (
<View className="user-page">
{/* 上部分:背景图和用户信息 */}
@ -143,63 +104,44 @@ const UserIndex = observer(() => { @@ -143,63 +104,44 @@ const UserIndex = observer(() => {
{/* 右侧信息 */}
<View className="profile-right">
{userStore.isLoggedIn ? (
<>
{/* 已登录状态 */}
{/* 用户名称和编辑按钮 */}
<View className="name-row">
<View className="name-edit-wrapper">
<Text className="username">{userStore.userInfo?.nickname || '微信用户'}</Text>
<Text className="username">
{userStore.isLoggedIn ? (userStore.userInfo?.nickname || '微信用户') : '加载中...'}
</Text>
{userStore.isLoggedIn && (
<Image
className="edit-icon"
src={userEdite}
onClick={handleEdit}
/>
)}
</View>
{userStore.isLoggedIn && (
<View className="vip-badge" onClick={handleVipClick}>
{userStore.userInfo?.typeName || 'VIP'}
</View>
)}
</View>
{/* 成长值 */}
<View className="growth-row">
<Text className="growth-label"></Text>
{growthIcons.map((icon, index) => (
{userStore.isLoggedIn ? (
growthIcons.map((icon, index) => (
<Image
key={index}
className="growth-icon"
src={icon}
/>
))}
</View>
</>
))
) : (
<>
{/* 未登录状态 */}
<View className="name-row">
<View className="name-edit-wrapper">
<Text className="username"></Text>
</View>
<Button
className="login-btn"
size="small"
color="primary"
loading={userStore.isLoading}
onClick={handleWechatLogin}
>
{userStore.isLoading ? '登录中...' : '微信登录'}
</Button>
</View>
{/* 成长值显示为暂无 */}
<View className="growth-row">
<Text className="growth-label"></Text>
<Text className="no-data"></Text>
</View>
</>
<Text className="no-data">--</Text>
)}
</View>
</View>
</View>
{/* 下半部分:账户余额、积分、优惠券 */}
<View className="user-stats">

14
src/store/user.ts

@ -31,6 +31,10 @@ class UserStore { @@ -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 { @@ -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 { @@ -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
}

Loading…
Cancel
Save