feat(auth): 添加统一认证工具和优化登录流程

- 新增 auth 工具模块,包含 isLoggedIn、goToRegister、ensureLoggedIn 方法
- 将硬编码的服务器URL更新为 glt-server 域名
- 重构多个页面的登录检查逻辑,使用统一的认证工具
- 在用户注册/登录流程中集成邀请关系处理
- 更新注册页面配置和实现,支持跳转参数传递
- 优化分销商二维码页面的加载状态和错误处理
- 在水票使用页面添加无票时的购买引导
- 统一文件上传和API请求的服务器地址
- 添加加密库类型定义文件
This commit is contained in:
2026-02-13 21:30:58 +08:00
parent 52ef8d4199
commit e22cfe4646
23 changed files with 712 additions and 154 deletions

View File

@@ -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>
);
};