forked from gxwebsoft/mp-10550
feat(auth): 添加统一认证工具和优化登录流程
- 新增 auth 工具模块,包含 isLoggedIn、goToRegister、ensureLoggedIn 方法 - 将硬编码的服务器URL更新为 glt-server 域名 - 重构多个页面的登录检查逻辑,使用统一的认证工具 - 在用户注册/登录流程中集成邀请关系处理 - 更新注册页面配置和实现,支持跳转参数传递 - 优化分销商二维码页面的加载状态和错误处理 - 在水票使用页面添加无票时的购买引导 - 统一文件上传和API请求的服务器地址 - 添加加密库类型定义文件
This commit is contained in:
@@ -90,7 +90,7 @@ const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?:
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
|
||||
@@ -144,7 +144,7 @@ function UserCard() {
|
||||
success: function () {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code,
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function uploadOssByPath(filePath: string) {
|
||||
let stsExpired = Taro.getStorageSync('stsExpiredAt');
|
||||
if (!sts || (stsExpired && dayjs().isBefore(dayjs(stsExpired)))) {
|
||||
// @ts-ignore
|
||||
const {data: {data: {credentials}}} = await request.get(`https://server.websoft.top/api/oss/getSTSToken`)
|
||||
const {data: {data: {credentials}}} = await request.get(`https://gle-server.websoft.top/api/oss/getSTSToken`)
|
||||
Taro.setStorageSync('sts', credentials)
|
||||
Taro.setStorageSync('stsExpiredAt', credentials.expiration)
|
||||
sts = credentials
|
||||
@@ -49,7 +49,7 @@ export async function uploadOssByPath(filePath: string) {
|
||||
})
|
||||
}
|
||||
|
||||
const computeSignature = (accessKeySecret, canonicalString) => {
|
||||
const computeSignature = (accessKeySecret: string, canonicalString: string): string => {
|
||||
return crypto.enc.Base64.stringify(crypto.HmacSHA1(canonicalString, accessKeySecret));
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function uploadFile() {
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
// 上传图片到OSS
|
||||
Taro.uploadFile({
|
||||
url: 'https://server.websoft.top/api/oss/upload',
|
||||
url: 'https://glt-server.websoft.top/api/oss/upload',
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
"root": "passport",
|
||||
"pages": [
|
||||
"login",
|
||||
"register",
|
||||
"forget",
|
||||
"setting",
|
||||
"agreement",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {getUserInfo} from "@/api/layout";
|
||||
import {useEffect, useState} from "react";
|
||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import { goToRegister } from '@/utils/auth'
|
||||
|
||||
function AddCartBar() {
|
||||
const { router } = getCurrentInstance();
|
||||
@@ -13,13 +14,8 @@ function AddCartBar() {
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const onPay = () => {
|
||||
if (!IsLogin) {
|
||||
Taro.showToast({title: `请先登录`, icon: 'error'})
|
||||
setTimeout(() => {
|
||||
Taro.switchTab(
|
||||
{
|
||||
url: '/pages/user/user',
|
||||
},
|
||||
)
|
||||
goToRegister({ redirect: '/pages/user/user' })
|
||||
}, 1000)
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ const AddUserAddress = () => {
|
||||
setFormData(tempFormData)
|
||||
|
||||
Taro.uploadFile({
|
||||
url: 'https://server.websoft.top/api/oss/upload',
|
||||
url: 'https://glt-server.websoft.top/api/oss/upload',
|
||||
filePath: detail.avatarUrl,
|
||||
name: 'file',
|
||||
header: {
|
||||
@@ -144,8 +144,8 @@ const AddUserAddress = () => {
|
||||
const nickname = values.realName || FormData?.nickname || '';
|
||||
if (!nickname || nickname.trim() === '') {
|
||||
Taro.showToast({
|
||||
title: '请填写昵称',
|
||||
icon: 'error'
|
||||
title: '请上传头像和填写昵称',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import {businessGradients} from '@/styles/gradients'
|
||||
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [codeLoading, setCodeLoading] = useState<boolean>(false)
|
||||
const [saving, setSaving] = useState<boolean>(false)
|
||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const {dealerUser} = useDealerUser()
|
||||
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
|
||||
|
||||
// 生成小程序码
|
||||
const generateMiniProgramCode = async () => {
|
||||
@@ -23,7 +23,7 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setCodeLoading(true)
|
||||
|
||||
// 生成邀请小程序码
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||
@@ -41,7 +41,7 @@ const DealerQrcode: React.FC = () => {
|
||||
// 清空之前的二维码
|
||||
setMiniProgramCodeUrl('')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setCodeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ const DealerQrcode: React.FC = () => {
|
||||
// })
|
||||
// }
|
||||
|
||||
if (!dealerUser) {
|
||||
if (dealerLoading) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
@@ -257,6 +257,33 @@ const DealerQrcode: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<Text className="text-gray-800 font-semibold">加载失败</Text>
|
||||
<Text className="text-gray-500 text-sm mt-2">{error}</Text>
|
||||
<Button className="mt-6" type="primary" onClick={refresh}>重试</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// 未成为分销商时给出明确引导,避免一直停留在“加载中”
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<Text className="text-gray-800 font-semibold">你还不是分销商</Text>
|
||||
<Text className="text-gray-500 text-sm mt-2 text-center">申请成为分销商后即可生成分享码</Text>
|
||||
<Button
|
||||
className="mt-6"
|
||||
type="primary"
|
||||
onClick={() => Taro.navigateTo({url: '/dealer/apply/add'})}
|
||||
>
|
||||
去申请
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 头部卡片 */}
|
||||
@@ -282,7 +309,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* 小程序码展示区 */}
|
||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||
<View className="text-center">
|
||||
{loading ? (
|
||||
{codeLoading ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||
@@ -344,7 +371,7 @@ const DealerQrcode: React.FC = () => {
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading || saving}
|
||||
disabled={!miniProgramCodeUrl || codeLoading || saving}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
@@ -355,7 +382,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* block*/}
|
||||
{/* icon={<Copy/>}*/}
|
||||
{/* onClick={copyInviteInfo}*/}
|
||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
||||
{/* disabled={!dealerUser?.userId || codeLoading}*/}
|
||||
{/* >*/}
|
||||
{/* 复制邀请信息*/}
|
||||
{/* </Button>*/}
|
||||
@@ -367,7 +394,7 @@ const DealerQrcode: React.FC = () => {
|
||||
{/* fill="outline"*/}
|
||||
{/* icon={<Share/>}*/}
|
||||
{/* onClick={shareMiniProgramCode}*/}
|
||||
{/* disabled={!dealerUser?.userId || loading}*/}
|
||||
{/* disabled={!dealerUser?.userId || codeLoading}*/}
|
||||
{/* >*/}
|
||||
{/* 分享给好友*/}
|
||||
{/* </Button>*/}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {ArrowLeft, Del} from '@nutui/icons-react-taro';
|
||||
import {View} from '@tarojs/components';
|
||||
import {CartItem, useCart} from "@/hooks/useCart";
|
||||
import './cart.scss';
|
||||
import { ensureLoggedIn } from '@/utils/auth'
|
||||
|
||||
function Cart() {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
||||
@@ -150,6 +151,9 @@ function Cart() {
|
||||
// 将选中的商品信息存储到本地,供结算页面使用
|
||||
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
|
||||
|
||||
// 未登录则引导去注册/登录;登录后回到购物车结算页
|
||||
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
|
||||
|
||||
// 跳转到购物车结算页面
|
||||
Taro.navigateTo({
|
||||
url: '/shop/orderConfirmCart/index'
|
||||
|
||||
@@ -9,6 +9,7 @@ import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
|
||||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||
import { ensureLoggedIn } from '@/utils/auth'
|
||||
import './index.scss'
|
||||
|
||||
function Home() {
|
||||
@@ -201,19 +202,28 @@ function Home() {
|
||||
key: 'ticket',
|
||||
title: '我的水票',
|
||||
icon: <Ticket size={30} />,
|
||||
onClick: () => Taro.navigateTo({ url: '/user/ticket/index' }),
|
||||
onClick: () => {
|
||||
if (!ensureLoggedIn('/user/ticket/index')) return
|
||||
Taro.navigateTo({ url: '/user/ticket/index' })
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'order',
|
||||
title: '立即送水',
|
||||
icon: <Cart size={30} />,
|
||||
onClick: () => Taro.navigateTo({ url: '/user/ticket/use?goodsId=10074' }),
|
||||
onClick: () => {
|
||||
if (!ensureLoggedIn('/user/ticket/use?goodsId=10074')) return
|
||||
Taro.navigateTo({ url: '/user/ticket/use?goodsId=10074' })
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'invite',
|
||||
title: '邀请有礼',
|
||||
icon: <Gift size={30} />,
|
||||
onClick: () => Taro.navigateTo({ url: '/dealer/qrcode/index' }),
|
||||
onClick: () => {
|
||||
if (!ensureLoggedIn('/dealer/qrcode/index')) return
|
||||
Taro.navigateTo({ url: '/dealer/qrcode/index' })
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: 'coupon',
|
||||
@@ -313,7 +323,10 @@ function Home() {
|
||||
<View className="goods-card__actions">
|
||||
<View
|
||||
className="goods-card__btn goods-card__btn--ghost"
|
||||
onClick={() => Taro.navigateTo({ url: '/shop/orderConfirm/index?goodsId=10074' })}
|
||||
onClick={() => {
|
||||
if (!ensureLoggedIn('/shop/orderConfirm/index?goodsId=10074')) return
|
||||
Taro.navigateTo({ url: '/shop/orderConfirm/index?goodsId=10074' })
|
||||
}}
|
||||
>
|
||||
<Text className="goods-card__btnText">买水票更优惠</Text>
|
||||
</View>
|
||||
|
||||
@@ -8,7 +8,7 @@ import navTo from "@/utils/common";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {useUserData} from "@/hooks/useUserData";
|
||||
import {getStoredInviteParams} from "@/utils/invite";
|
||||
import {checkAndHandleInviteRelation, getStoredInviteParams, hasPendingInvite} from "@/utils/invite";
|
||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||
import {useThemeStyles} from "@/hooks/useTheme";
|
||||
import {getRootDomain} from "@/utils/domain";
|
||||
@@ -206,6 +206,13 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
// 登录态已就绪后刷新卡片统计(余额/积分/券/水票)
|
||||
refresh().then()
|
||||
reloadTicketTotal()
|
||||
|
||||
// 登录成功后(可能是新注册用户),检查是否存在待处理的邀请关系并尝试绑定
|
||||
if (hasPendingInvite()) {
|
||||
checkAndHandleInviteRelation().catch((e) => {
|
||||
console.error('个人中心登录后处理邀请关系失败:', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, RichText} from '@tarojs/components'
|
||||
import { Loading } from '@nutui/nutui-react-taro'
|
||||
import { RichText, View } from '@tarojs/components'
|
||||
import { getByCode } from '@/api/cms/cmsArticle'
|
||||
import { wxParse } from '@/utils/common'
|
||||
|
||||
const Agreement = () => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [content, setContent] = useState<string>('')
|
||||
|
||||
const [content, setContent] = useState<any>('')
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
setContent('<p>' +
|
||||
'<span style="font-size: 14px;">欢迎使用</span>' +
|
||||
'<span style="font-size: 14px;"> </span>' +
|
||||
'<span style="font-size: 14px;"><strong><span style="color: rgb(255, 0, 0);">【WebSoft】</span></strong></span>' +
|
||||
'<span style="font-size: 14px;">服务协议 </span>' +
|
||||
'</p>')
|
||||
const reload = async () => {
|
||||
try {
|
||||
Taro.hideTabBar()
|
||||
} catch (_) {
|
||||
// ignore (e.g. H5 / unsupported env)
|
||||
}
|
||||
|
||||
try {
|
||||
const article = await getByCode('xieyi')
|
||||
setContent(article?.content ? wxParse(article.content) : '<p>暂无协议内容</p>')
|
||||
} catch (e) {
|
||||
// Keep UI usable even if CMS/API fails.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('load agreement failed', e)
|
||||
setContent('<p>协议内容加载失败</p>')
|
||||
Taro.showToast({ title: '协议加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'content text-gray-700 text-sm p-4'}>
|
||||
<RichText nodes={content}/>
|
||||
<RichText nodes={content} />
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
||||
5
src/passport/register.config.ts
Normal file
5
src/passport/register.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '注册/登录',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
295
src/passport/register.tsx
Normal file
295
src/passport/register.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button, Radio } from '@nutui/nutui-react-taro'
|
||||
import { TenantId } from '@/config/app'
|
||||
import { getUserInfo, getWxOpenId } from '@/api/layout'
|
||||
import { saveStorageByLoginUser } from '@/utils/server'
|
||||
import {
|
||||
getStoredInviteParams,
|
||||
parseInviteParams,
|
||||
saveInviteParams,
|
||||
trackInviteSource,
|
||||
checkAndHandleInviteRelation,
|
||||
} from '@/utils/invite'
|
||||
|
||||
interface GetPhoneNumberDetail {
|
||||
code?: string
|
||||
encryptedData?: string
|
||||
iv?: string
|
||||
errMsg?: string
|
||||
}
|
||||
|
||||
interface GetPhoneNumberEvent {
|
||||
detail: GetPhoneNumberDetail
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
data: {
|
||||
code?: number
|
||||
message?: string
|
||||
data?: {
|
||||
access_token: string
|
||||
user: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getWeappLoginCode(): Promise<string | undefined> {
|
||||
try {
|
||||
const res = await new Promise<{ code?: string }>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (r) => resolve(r as any),
|
||||
fail: (e) => reject(e),
|
||||
})
|
||||
})
|
||||
return res?.code
|
||||
} catch (_e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWxOpenIdSaved(opts: { user?: any; wxLoginCode?: string }) {
|
||||
// JSAPI 微信支付必须有 openid;注册/登录后立刻补齐,避免后续创建支付单失败。
|
||||
try {
|
||||
if (Taro.getEnv() !== Taro.ENV_TYPE.WEAPP) return
|
||||
} catch (_e) {
|
||||
if (process.env.TARO_ENV !== 'weapp') return
|
||||
}
|
||||
|
||||
if (opts.user?.openid) return
|
||||
|
||||
const code = opts.wxLoginCode || (await getWeappLoginCode())
|
||||
if (!code) return
|
||||
|
||||
// 该接口一般会在服务端把 openid 绑定到当前登录用户;返回值并不一定包含 openid。
|
||||
await getWxOpenId({ code })
|
||||
|
||||
// 同步本地 User(让后续页面/逻辑能直接读到 openid)
|
||||
try {
|
||||
const fresh = await getUserInfo()
|
||||
if (fresh) Taro.setStorageSync('User', fresh)
|
||||
} catch (_e) {
|
||||
// ignore: openid 已在服务端绑定,本地不同步也不影响后端创建支付订单
|
||||
}
|
||||
}
|
||||
|
||||
function safeDecodeMaybeEncoded(input?: string): string {
|
||||
if (!input) return ''
|
||||
try {
|
||||
// Taro 路由参数通常是 URL 编码过的字符串
|
||||
return decodeURIComponent(input)
|
||||
} catch (_e) {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
function isTabBarUrl(url: string) {
|
||||
const pure = url.split('?')[0]
|
||||
return (
|
||||
pure === '/pages/index/index' ||
|
||||
pure === '/pages/cart/cart' ||
|
||||
pure === '/pages/user/user' ||
|
||||
pure === '/pages/category/index'
|
||||
)
|
||||
}
|
||||
|
||||
const Register = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 短信验证码登录仅在非微信小程序端展示
|
||||
const isWeapp = useMemo(() => {
|
||||
try {
|
||||
return Taro.getEnv() === Taro.ENV_TYPE.WEAPP
|
||||
} catch (_e) {
|
||||
return process.env.TARO_ENV === 'weapp'
|
||||
}
|
||||
}, [])
|
||||
|
||||
const router = Taro.getCurrentInstance().router
|
||||
|
||||
useEffect(() => {
|
||||
// 注册/登录页不需要展示 tabBar
|
||||
Taro.hideTabBar()
|
||||
}, [])
|
||||
|
||||
const redirectUrl = useMemo(() => {
|
||||
const raw = (router?.params as any)?.redirect as string | undefined
|
||||
const decoded = safeDecodeMaybeEncoded(raw)
|
||||
if (!decoded) return ''
|
||||
return decoded.startsWith('/') ? decoded : `/${decoded}`
|
||||
}, [router?.params])
|
||||
|
||||
// 如果从分享/二维码直接进入注册页(携带 inviter/source/t),先暂存邀请信息
|
||||
useEffect(() => {
|
||||
try {
|
||||
const inviteParams = parseInviteParams({ query: router?.params })
|
||||
if (inviteParams?.inviter) {
|
||||
saveInviteParams(inviteParams)
|
||||
trackInviteSource(inviteParams.source || 'qrcode', parseInt(inviteParams.inviter, 10))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('注册页处理邀请参数失败:', e)
|
||||
}
|
||||
}, [router?.params])
|
||||
|
||||
const navigateAfterLogin = async () => {
|
||||
if (!redirectUrl) {
|
||||
await Taro.reLaunch({ url: '/pages/index/index' })
|
||||
return
|
||||
}
|
||||
|
||||
if (isTabBarUrl(redirectUrl)) {
|
||||
// switchTab 不支持携带 query,这里按纯路径跳转
|
||||
await Taro.switchTab({ url: redirectUrl.split('?')[0] })
|
||||
return
|
||||
}
|
||||
|
||||
// 替换当前注册页,避免返回栈里再回到注册页
|
||||
await Taro.redirectTo({ url: redirectUrl })
|
||||
}
|
||||
|
||||
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
|
||||
if (!isAgree) {
|
||||
Taro.showToast({ title: '请先勾选同意协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (loading) return
|
||||
|
||||
const { code: phoneCode, encryptedData, iv, errMsg } = detail || {}
|
||||
if (!phoneCode || (errMsg && errMsg.includes('fail'))) {
|
||||
Taro.showToast({ title: '未授权手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 获取存储的邀请参数(推荐人ID)
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter, 10) : 0
|
||||
|
||||
// 获取小程序登录 code(用于后续绑定 openid)
|
||||
const wxLoginCode = await getWeappLoginCode()
|
||||
|
||||
const res = (await Taro.request({
|
||||
url: 'https://glt-server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: phoneCode,
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: refereeId,
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId,
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId,
|
||||
},
|
||||
})) as unknown as LoginResponse
|
||||
|
||||
if ((res as any)?.data?.code === 1) {
|
||||
Taro.showToast({ title: res.data.message || '登录失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const token = res?.data?.data?.access_token
|
||||
const user = res?.data?.data?.user
|
||||
if (!token || !user?.userId) {
|
||||
Taro.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
saveStorageByLoginUser(token, user)
|
||||
|
||||
// 注册/登录成功后,立即补齐 openid(JSAPI 支付必需)
|
||||
try {
|
||||
await ensureWxOpenIdSaved({ user, wxLoginCode })
|
||||
} catch (e) {
|
||||
console.error('注册页绑定 openid 失败:', e)
|
||||
}
|
||||
|
||||
// 登录成功后尝试绑定推荐关系(如果有待处理 inviter,会自动处理并清理参数)
|
||||
try {
|
||||
await checkAndHandleInviteRelation()
|
||||
} catch (e) {
|
||||
console.error('注册页登录后处理邀请关系失败:', e)
|
||||
}
|
||||
|
||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
navigateAfterLogin().catch((e) => console.error('登录后跳转失败:', e))
|
||||
}, 800)
|
||||
} catch (e: any) {
|
||||
console.error('注册/登录失败:', e)
|
||||
Taro.showToast({ title: e?.message || '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const goSmsLogin = () => {
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const inviter = inviteParams?.inviter
|
||||
const source = inviteParams?.source
|
||||
const t = inviteParams?.t
|
||||
|
||||
const params: Record<string, any> = {}
|
||||
if (redirectUrl) params.redirect = redirectUrl
|
||||
// 兜底:把 inviter 带过去,避免“先点注册再进入”时丢失
|
||||
if (inviter) params.inviter = inviter
|
||||
if (source) params.source = source
|
||||
if (t) params.t = t
|
||||
|
||||
const qs = Object.entries(params)
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||
.join('&')
|
||||
Taro.navigateTo({ url: `/passport/sms-login${qs ? `?${qs}` : ''}` })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5 pt-5'}>
|
||||
<div className={'text-3xl text-center py-5 font-normal my-6'}>注册/登录</div>
|
||||
|
||||
<div className={'flex flex-col gap-3 bg-green-600 py-1'} style={{ borderRadius: '100px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
fill="solid"
|
||||
color="#07c160"
|
||||
block
|
||||
loading={loading}
|
||||
disabled={!isAgree || loading}
|
||||
open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
>
|
||||
手机号一键注册/登录
|
||||
</Button>
|
||||
|
||||
{!isWeapp && (
|
||||
<Button type="default" block disabled={!isAgree || loading} onClick={goSmsLogin}>
|
||||
短信验证码注册/登录
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={'mt-6 flex text-sm items-center px-1'}>
|
||||
<Radio style={{ color: '#333333' }} checked={isAgree} onClick={() => setIsAgree(!isAgree)} />
|
||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>
|
||||
勾选表示您已阅读并同意
|
||||
</span>
|
||||
<a
|
||||
onClick={() => Taro.navigateTo({ url: '/passport/agreement' })}
|
||||
className={'text-blue-600'}
|
||||
>
|
||||
《服务协议及隐私政策》
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Register
|
||||
@@ -3,7 +3,7 @@ import Taro from '@tarojs/taro'
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {loginBySms, sendSmsCaptcha} from "@/api/passport/login";
|
||||
import {LoginParam} from "@/api/passport/login/model";
|
||||
import {checkAndHandleInviteRelation, hasPendingInvite} from "@/utils/invite";
|
||||
import {checkAndHandleInviteRelation, hasPendingInvite, parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
|
||||
|
||||
const SmsLogin = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -14,6 +14,46 @@ const SmsLogin = () => {
|
||||
code: ''
|
||||
})
|
||||
|
||||
const router = Taro.getCurrentInstance().router
|
||||
const redirectParam = (router?.params as any)?.redirect as string | undefined
|
||||
|
||||
const safeDecodeMaybeEncoded = (input?: string) => {
|
||||
if (!input) return ''
|
||||
try {
|
||||
return decodeURIComponent(input)
|
||||
} catch (_e) {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
const redirectUrl = (() => {
|
||||
const decoded = safeDecodeMaybeEncoded(redirectParam)
|
||||
if (!decoded) return ''
|
||||
return decoded.startsWith('/') ? decoded : `/${decoded}`
|
||||
})()
|
||||
|
||||
const isTabBarUrl = (url: string) => {
|
||||
const pure = url.split('?')[0]
|
||||
return (
|
||||
pure === '/pages/index/index' ||
|
||||
pure === '/pages/cart/cart' ||
|
||||
pure === '/pages/user/user' ||
|
||||
pure === '/pages/category/index'
|
||||
)
|
||||
}
|
||||
|
||||
const navigateAfterLogin = async () => {
|
||||
if (!redirectUrl) {
|
||||
await Taro.reLaunch({ url: '/pages/index/index' })
|
||||
return
|
||||
}
|
||||
if (isTabBarUrl(redirectUrl)) {
|
||||
await Taro.switchTab({ url: redirectUrl.split('?')[0] })
|
||||
return
|
||||
}
|
||||
await Taro.redirectTo({ url: redirectUrl })
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
@@ -22,6 +62,19 @@ const SmsLogin = () => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
// 如果从分享/二维码链接进入短信登录页,先暂存邀请信息
|
||||
useEffect(() => {
|
||||
try {
|
||||
const inviteParams = parseInviteParams({ query: router?.params })
|
||||
if (inviteParams?.inviter) {
|
||||
saveInviteParams(inviteParams)
|
||||
trackInviteSource(inviteParams.source || 'share', parseInt(inviteParams.inviter, 10))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('短信登录页处理邀请参数失败:', e)
|
||||
}
|
||||
}, [router?.params])
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout
|
||||
@@ -148,8 +201,9 @@ const SmsLogin = () => {
|
||||
|
||||
// 延迟跳转到首页
|
||||
setTimeout(() => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
navigateAfterLogin().catch((e) => {
|
||||
console.error('短信登录后跳转失败:', e)
|
||||
Taro.reLaunch({ url: '/pages/index/index' })
|
||||
})
|
||||
}, 1500)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import "./index.scss";
|
||||
import {useCart} from "@/hooks/useCart";
|
||||
import {useConfig} from "@/hooks/useConfig";
|
||||
import {parseInviteParams, saveInviteParams, trackInviteSource} from "@/utils/invite";
|
||||
import { ensureLoggedIn } from '@/utils/auth'
|
||||
|
||||
const GoodsDetail = () => {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(44);
|
||||
@@ -62,13 +63,7 @@ const GoodsDetail = () => {
|
||||
const handleAddToCart = () => {
|
||||
if (!goods) return;
|
||||
|
||||
if (!Taro.getStorageSync('UserId')) {
|
||||
return Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
if (!ensureLoggedIn(`/shop/goodsDetail/index?id=${goods.goodsId}`)) return
|
||||
|
||||
// 如果有规格,显示规格选择器
|
||||
if (specs.length > 0) {
|
||||
@@ -90,13 +85,7 @@ const GoodsDetail = () => {
|
||||
const handleBuyNow = () => {
|
||||
if (!goods) return;
|
||||
|
||||
if (!Taro.getStorageSync('UserId')) {
|
||||
return Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
if (!ensureLoggedIn(`/shop/orderConfirm/index?goodsId=${goods.goodsId}`)) return
|
||||
|
||||
// 如果有规格,显示规格选择器
|
||||
if (specs.length > 0) {
|
||||
|
||||
@@ -43,6 +43,7 @@ import dayjs from 'dayjs'
|
||||
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||
import { ensureLoggedIn, isLoggedIn } from '@/utils/auth'
|
||||
|
||||
|
||||
const OrderConfirm = () => {
|
||||
@@ -88,6 +89,16 @@ const OrderConfirm = () => {
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const goodsId = router?.params?.goodsId;
|
||||
|
||||
// 页面级兜底:未登录直接进入下单页时,引导去注册/登录并回跳
|
||||
useEffect(() => {
|
||||
if (!goodsId) {
|
||||
// 也可能是 orderData 模式;这里只做最小兜底
|
||||
if (!ensureLoggedIn('/shop/orderConfirm/index')) return
|
||||
return
|
||||
}
|
||||
if (!ensureLoggedIn(`/shop/orderConfirm/index?goodsId=${goodsId}`)) return
|
||||
}, [goodsId])
|
||||
|
||||
const isTicketTemplateActive =
|
||||
!!ticketTemplate &&
|
||||
ticketTemplate.enabled !== false &&
|
||||
@@ -607,6 +618,9 @@ const OrderConfirm = () => {
|
||||
|
||||
// 统一的数据加载函数
|
||||
const loadAllData = async () => {
|
||||
// 未登录时不发起接口请求;页面会被登录兜底逻辑引导走注册/登录页
|
||||
if (!isLoggedIn()) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
@@ -694,12 +708,14 @@ const OrderConfirm = () => {
|
||||
|
||||
useDidShow(() => {
|
||||
// 返回/切换到该页面时,刷新一下当前已选门店
|
||||
if (!isLoggedIn()) return
|
||||
setSelectedStore(getSelectedStoreFromStorage())
|
||||
loadAllData()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// 切换商品时重置配送时间,避免沿用上一次选择
|
||||
if (!isLoggedIn()) return
|
||||
setSendTime(dayjs().startOf('day').toDate())
|
||||
setSendTimePickerVisible(false)
|
||||
loadAllData()
|
||||
|
||||
@@ -2,8 +2,6 @@ import {useEffect, useState} from "react";
|
||||
import {Image, Button, Cell, CellGroup, Input, Space} from '@nutui/nutui-react-taro'
|
||||
import {Location, ArrowRight} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {getShopGoods} from "@/api/shop/shopGoods";
|
||||
import {View} from '@tarojs/components';
|
||||
import {listShopUserAddress} from "@/api/shop/shopUserAddress";
|
||||
import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
|
||||
@@ -12,14 +10,12 @@ import {useCart, CartItem} from "@/hooks/useCart";
|
||||
import Gap from "@/components/Gap";
|
||||
import {Payment} from "@/api/system/payment/model";
|
||||
import {PaymentHandler, PaymentType, buildCartOrder} from "@/utils/payment";
|
||||
import { ensureLoggedIn } from '@/utils/auth'
|
||||
|
||||
const OrderConfirm = () => {
|
||||
const [goods, setGoods] = useState<ShopGoods | null>(null);
|
||||
const [address, setAddress] = useState<ShopUserAddress>()
|
||||
const [payment, setPayment] = useState<Payment>()
|
||||
const [payment] = useState<Payment>()
|
||||
const [checkoutItems, setCheckoutItems] = useState<CartItem[]>([]);
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const goodsId = router?.params?.goodsId;
|
||||
|
||||
const {
|
||||
cartItems,
|
||||
@@ -27,13 +23,18 @@ const OrderConfirm = () => {
|
||||
} = useCart();
|
||||
|
||||
const reload = async () => {
|
||||
const address = await listShopUserAddress({isDefault: true});
|
||||
if (address.length > 0) {
|
||||
console.log(address, '111')
|
||||
setAddress(address[0])
|
||||
const addressList = await listShopUserAddress({isDefault: true});
|
||||
if (addressList.length > 0) {
|
||||
setAddress(addressList[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 页面级兜底:防止未登录时进入结算页导致接口报错/仅提示“请先登录”
|
||||
useEffect(() => {
|
||||
// redirect 到当前结算页,登录成功后返回继续支付
|
||||
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
|
||||
}, [])
|
||||
|
||||
// 加载结算商品数据
|
||||
const loadCheckoutItems = () => {
|
||||
try {
|
||||
@@ -57,6 +58,8 @@ const OrderConfirm = () => {
|
||||
* 统一支付入口
|
||||
*/
|
||||
const onPay = async () => {
|
||||
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
|
||||
|
||||
// 基础校验
|
||||
if (!address) {
|
||||
Taro.showToast({
|
||||
@@ -77,7 +80,7 @@ const OrderConfirm = () => {
|
||||
// 构建订单数据
|
||||
const orderData = buildCartOrder(
|
||||
checkoutItems.map(item => ({
|
||||
goodsId: item.goodsId!,
|
||||
goodsId: item.goodsId,
|
||||
quantity: item.quantity || 1
|
||||
})),
|
||||
address.id,
|
||||
@@ -102,16 +105,11 @@ const OrderConfirm = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (goodsId) {
|
||||
getShopGoods(Number(goodsId)).then(res => {
|
||||
setGoods(res);
|
||||
}).catch(error => {
|
||||
console.error("Failed to fetch goods detail:", error);
|
||||
});
|
||||
}
|
||||
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
|
||||
|
||||
reload().then();
|
||||
loadCheckoutItems();
|
||||
}, [goodsId, cartItems]);
|
||||
}, [cartItems]);
|
||||
|
||||
// 计算总价
|
||||
const getTotalPrice = () => {
|
||||
@@ -157,19 +155,19 @@ const OrderConfirm = () => {
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
{checkoutItems.map((goods, _) => (
|
||||
<Cell key={goods.goodsId}>
|
||||
{checkoutItems.map((item) => (
|
||||
<Cell key={item.goodsId}>
|
||||
<Space>
|
||||
<Image src={goods.image} mode={'aspectFill'} style={{
|
||||
<Image src={item.image} mode={'aspectFill'} style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
}} lazyLoad={false}/>
|
||||
<View className={'flex flex-col'}>
|
||||
<View className={'font-medium w-full'}>{goods.name}</View>
|
||||
<View className={'font-medium w-full'}>{item.name}</View>
|
||||
<View className={'number text-gray-400 text-sm py-2'}>80g/袋</View>
|
||||
<Space className={'flex justify-start items-center'}>
|
||||
<View className={'text-red-500'}>¥{goods.price}</View>
|
||||
<View className={'text-gray-500 text-sm'}>x {goods.quantity}</View>
|
||||
<View className={'text-red-500'}>¥{item.price}</View>
|
||||
<View className={'text-gray-500 text-sm'}>x {item.quantity}</View>
|
||||
</Space>
|
||||
</View>
|
||||
</Space>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
import {getShopStore} from '@/api/shop/shopStore'
|
||||
import type {ShopStore as ShopStoreModel} from '@/api/shop/shopStore/model'
|
||||
import { goToRegister } from '@/utils/auth'
|
||||
|
||||
const StoreIndex: React.FC = () => {
|
||||
const themeStyles = useThemeStyles()
|
||||
@@ -43,7 +44,7 @@ const StoreIndex: React.FC = () => {
|
||||
|
||||
const navigateToPage = (url: string) => {
|
||||
if (!isLoggedIn) {
|
||||
Taro.showToast({title: '请先登录', icon: 'none', duration: 1500})
|
||||
goToRegister({ redirect: '/store/index' })
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({url})
|
||||
@@ -121,8 +122,8 @@ const StoreIndex: React.FC = () => {
|
||||
<View className="bg-white rounded-xl p-4">
|
||||
<Text className="text-gray-700">请先登录后再进入门店中心</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" onClick={() => Taro.navigateTo({url: '/passport/login'})}>
|
||||
去登录
|
||||
<Button type="primary" onClick={() => goToRegister({ redirect: '/store/index' })}>
|
||||
去注册/登录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -73,7 +73,7 @@ function Profile() {
|
||||
avatar: `${detail.avatarUrl}`,
|
||||
})
|
||||
Taro.uploadFile({
|
||||
url: 'https://server.websoft.top/api/oss/upload',
|
||||
url: 'https://glt-server.websoft.top/api/oss/upload',
|
||||
filePath: detail.avatarUrl,
|
||||
name: 'file',
|
||||
header: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Cell,
|
||||
CellGroup,
|
||||
ConfigProvider,
|
||||
Empty,
|
||||
Input,
|
||||
InputNumber,
|
||||
Popup,
|
||||
@@ -70,6 +71,7 @@ const OrderConfirm = () => {
|
||||
const [selectedTicketId, setSelectedTicketId] = useState<number | undefined>(undefined)
|
||||
const [ticketPopupVisible, setTicketPopupVisible] = useState(false)
|
||||
const [ticketLoading, setTicketLoading] = useState(false)
|
||||
const noTicketPromptedRef = useRef(false)
|
||||
|
||||
// Delivery range (geofence): block ordering if address/current location is outside.
|
||||
const [fences, setFences] = useState<ShopStoreFence[]>([])
|
||||
@@ -119,6 +121,11 @@ const OrderConfirm = () => {
|
||||
return Number(selectedTicket?.availableQty || 0)
|
||||
}, [selectedTicket?.availableQty])
|
||||
|
||||
const noUsableTickets = useMemo(() => {
|
||||
// Only show "go buy tickets" guidance after we have finished loading.
|
||||
return !!userId && !ticketLoading && usableTickets.length === 0
|
||||
}, [ticketLoading, usableTickets.length, userId])
|
||||
|
||||
const maxQuantity = useMemo(() => {
|
||||
const stockMax = goods?.stock ?? 999
|
||||
return Math.max(0, Math.min(stockMax, availableTicketTotal))
|
||||
@@ -428,6 +435,21 @@ const OrderConfirm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const goBuyTickets = async () => {
|
||||
try {
|
||||
setTicketPopupVisible(false)
|
||||
// If this page is opened with a goodsId, guide user back to that goods detail to purchase.
|
||||
if (numericGoodsId) {
|
||||
await Taro.navigateTo({ url: `/shop/goodsDetail/index?id=${numericGoodsId}` })
|
||||
return
|
||||
}
|
||||
await Taro.switchTab({ url: '/pages/index/index' })
|
||||
} catch (e) {
|
||||
console.error('跳转购买水票失败:', e)
|
||||
Taro.showToast({ title: '跳转失败,请稍后重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (submitLoading) return
|
||||
if (deliveryRangeCheckingRef.current) return
|
||||
@@ -623,6 +645,26 @@ const OrderConfirm = () => {
|
||||
}
|
||||
}, [usableTickets, selectedTicketId])
|
||||
|
||||
// If user has no usable tickets, proactively guide them to purchase (only once per page lifecycle).
|
||||
useEffect(() => {
|
||||
if (!noUsableTickets) return
|
||||
if (noTicketPromptedRef.current) return
|
||||
noTicketPromptedRef.current = true
|
||||
|
||||
;(async () => {
|
||||
const r = await Taro.showModal({
|
||||
title: '暂无可用水票',
|
||||
content: '您当前没有可用水票,购买后再来下单更方便。',
|
||||
confirmText: '去购买',
|
||||
cancelText: '暂不'
|
||||
})
|
||||
if (r.confirm) {
|
||||
await goBuyTickets()
|
||||
}
|
||||
})()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [noUsableTickets])
|
||||
|
||||
// 重新加载数据
|
||||
const handleRetry = () => {
|
||||
loadAllData()
|
||||
@@ -738,26 +780,50 @@ const OrderConfirm = () => {
|
||||
<Text>选择水票</Text>
|
||||
</View>
|
||||
)}
|
||||
extra={(
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<View className={'text-gray-900'}>
|
||||
{ticketLoading
|
||||
? '加载中...'
|
||||
: (selectedTicket
|
||||
? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})`
|
||||
: '请选择'
|
||||
)
|
||||
}
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
extra={(
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<View className={'text-gray-900'}>
|
||||
{ticketLoading
|
||||
? '加载中...'
|
||||
: (selectedTicket
|
||||
? `${selectedTicket.templateName || '水票'}(可用${selectedTicket.availableQty ?? 0})`
|
||||
: (noUsableTickets ? '暂无可用水票' : '请选择')
|
||||
)
|
||||
}
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)}
|
||||
onClick={async () => {
|
||||
if (ticketLoading) return
|
||||
if (noUsableTickets) {
|
||||
const r = await Taro.showModal({
|
||||
title: '暂无可用水票',
|
||||
content: '您还没有可用水票,是否前往购买?',
|
||||
confirmText: '去购买',
|
||||
cancelText: '暂不'
|
||||
})
|
||||
if (r.confirm) await goBuyTickets()
|
||||
return
|
||||
}
|
||||
setTicketPopupVisible(true)
|
||||
}}
|
||||
/>
|
||||
{noUsableTickets && (
|
||||
<Cell
|
||||
title={<Text className="text-gray-500">还没有购买水票</Text>}
|
||||
description="购买水票后即可在这里直接下单送水"
|
||||
extra={(
|
||||
<Button type="primary" size="small" onClick={goBuyTickets}>
|
||||
去购买
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
onClick={() => !ticketLoading && setTicketPopupVisible(true)}
|
||||
/>
|
||||
<Cell
|
||||
title={'本次使用'}
|
||||
extra={<View className={'font-medium'}>{displayQty} 张</View>}
|
||||
/>
|
||||
<Cell
|
||||
title={'本次使用'}
|
||||
extra={<View className={'font-medium'}>{displayQty} 张</View>}
|
||||
/>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
@@ -795,26 +861,37 @@ const OrderConfirm = () => {
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<CellGroup>
|
||||
{usableTickets.map((t) => {
|
||||
const active = selectedTicket?.id && Number(selectedTicket.id) === Number(t.id)
|
||||
return (
|
||||
<Cell
|
||||
key={t.id}
|
||||
title={<Text className={active ? 'text-green-600' : ''}>票号 {t.id}</Text>}
|
||||
description={t.orderNo ? `来源订单:${t.orderNo}` : ''}
|
||||
extra={<Text className="text-gray-700">可用 {t.availableQty ?? 0}</Text>}
|
||||
onClick={() => {
|
||||
setSelectedTicketId(Number(t.id))
|
||||
setTicketPopupVisible(false)
|
||||
Taro.showToast({ title: '水票已选择', icon: 'success' })
|
||||
}}
|
||||
/>
|
||||
)})}
|
||||
{!usableTickets.length && (
|
||||
<Cell title={<Text className="text-gray-500">暂无可用水票</Text>} />
|
||||
<>
|
||||
{!!usableTickets.length ? (
|
||||
<CellGroup>
|
||||
{usableTickets.map((t) => {
|
||||
const active = selectedTicket?.id && Number(selectedTicket.id) === Number(t.id)
|
||||
return (
|
||||
<Cell
|
||||
key={t.id}
|
||||
title={<Text className={active ? 'text-green-600' : ''}>票号 {t.id}</Text>}
|
||||
description={t.orderNo ? `来源订单:${t.orderNo}` : ''}
|
||||
extra={<Text className="text-gray-700">可用 {t.availableQty ?? 0}</Text>}
|
||||
onClick={() => {
|
||||
setSelectedTicketId(Number(t.id))
|
||||
setTicketPopupVisible(false)
|
||||
Taro.showToast({ title: '水票已选择', icon: 'success' })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</CellGroup>
|
||||
) : (
|
||||
<View className="py-10 text-center">
|
||||
<Empty description="暂无可用水票" />
|
||||
<View className="mt-4 flex justify-center">
|
||||
<Button type="primary" onClick={goBuyTickets}>
|
||||
去购买水票
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</CellGroup>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Popup>
|
||||
@@ -889,29 +966,35 @@ const OrderConfirm = () => {
|
||||
</span>
|
||||
<span className={'text-sm text-gray-500'}>张</span>
|
||||
</View>
|
||||
</div>
|
||||
<div className={'buy-btn mx-4'}>
|
||||
<Button
|
||||
type="success"
|
||||
size="large"
|
||||
loading={submitLoading || deliveryRangeChecking}
|
||||
disabled={
|
||||
deliveryRangeChecking ||
|
||||
inDeliveryRange === false ||
|
||||
!selectedTicket?.id ||
|
||||
availableTicketTotal <= 0 ||
|
||||
!canStartOrder
|
||||
}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{deliveryRangeChecking
|
||||
? '校验配送范围...'
|
||||
: (inDeliveryRange === false ? '不在配送范围' : (submitLoading ? '提交中...' : '立即提交'))
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'buy-btn mx-4'}>
|
||||
{noUsableTickets ? (
|
||||
<Button type="primary" size="large" onClick={goBuyTickets}>
|
||||
去购买水票
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="success"
|
||||
size="large"
|
||||
loading={submitLoading || deliveryRangeChecking}
|
||||
disabled={
|
||||
deliveryRangeChecking ||
|
||||
inDeliveryRange === false ||
|
||||
!selectedTicket?.id ||
|
||||
availableTicketTotal <= 0 ||
|
||||
!canStartOrder
|
||||
}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{deliveryRangeChecking
|
||||
? '校验配送范围...'
|
||||
: (inDeliveryRange === false ? '不在配送范围' : (submitLoading ? '提交中...' : '立即提交'))
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</View>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
30
src/utils/auth.ts
Normal file
30
src/utils/auth.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { goTo } from '@/utils/navigation'
|
||||
import { getStoredInviteParams } from '@/utils/invite'
|
||||
|
||||
export function isLoggedIn(): boolean {
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
const userId = Taro.getStorageSync('UserId')
|
||||
return !!token && !!userId
|
||||
}
|
||||
|
||||
export function goToRegister(options?: { redirect?: string }) {
|
||||
const inviteParams = getStoredInviteParams()
|
||||
goTo('/passport/register', {
|
||||
redirect: options?.redirect,
|
||||
inviter: inviteParams?.inviter,
|
||||
source: inviteParams?.source,
|
||||
t: inviteParams?.t,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user is logged in; if not, navigate to register/login page.
|
||||
* @returns true when already logged in; false when redirected to register
|
||||
*/
|
||||
export function ensureLoggedIn(redirect?: string): boolean {
|
||||
if (isLoggedIn()) return true
|
||||
goToRegister({ redirect })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { goTo } from './navigation'
|
||||
import { goToRegister, isLoggedIn } from '@/utils/auth'
|
||||
|
||||
export default function navTo(url: string, isLogin = false) {
|
||||
if (isLogin) {
|
||||
if (!Taro.getStorageSync('access_token') || !Taro.getStorageSync('UserId')) {
|
||||
Taro.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none',
|
||||
duration: 500
|
||||
});
|
||||
return false;
|
||||
if (!isLoggedIn()) {
|
||||
const redirect = url.startsWith('/') ? url : `/${url}`
|
||||
goToRegister({ redirect })
|
||||
return false
|
||||
}
|
||||
}
|
||||
// 使用新的导航工具,自动处理路径格式化
|
||||
|
||||
22
types/crypto-js.d.ts
vendored
Normal file
22
types/crypto-js.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
declare module 'crypto-js' {
|
||||
// Minimal typings for the subset used in `src/api/system/file/index.ts`.
|
||||
export interface WordArray {
|
||||
toString(encoder?: unknown): string;
|
||||
}
|
||||
|
||||
export const enc: {
|
||||
Base64: {
|
||||
stringify(wordArray: WordArray): string;
|
||||
};
|
||||
};
|
||||
|
||||
export function HmacSHA1(message: string, key: string): WordArray;
|
||||
|
||||
const CryptoJS: {
|
||||
enc: typeof enc;
|
||||
HmacSHA1: typeof HmacSHA1;
|
||||
};
|
||||
|
||||
export default CryptoJS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user