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

283 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {useCallback, useState} from 'react'
import {View, Text} from '@tarojs/components'
import {Avatar, Button, ConfigProvider, Grid} from '@nutui/nutui-react-taro'
import {Location, Scan, Shop, Shopping, User} from '@nutui/icons-react-taro'
import Taro, {useDidShow} from '@tarojs/taro'
import {useThemeStyles} from '@/hooks/useTheme'
import {useUser} from '@/hooks/useUser'
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()
const {isLoggedIn, loading: userLoading, getAvatarUrl, getDisplayName, getRoleName, hasRole} = useUser()
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
const [selectedStore, setSelectedStore] = useState<ShopStoreModel | null>(getSelectedStoreFromStorage())
const [store, setStore] = useState<ShopStoreModel | null>(selectedStore)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const storeId = boundStoreId || selectedStore?.id
const parseStoreCoords = (s: ShopStoreModel): {lng: number; lat: number} | null => {
const raw = (s.lngAndLat || s.location || '').trim()
if (!raw) return null
const parts = raw.split(/[,\s]+/).filter(Boolean)
if (parts.length < 2) return null
const a = parseFloat(parts[0])
const b = parseFloat(parts[1])
if (Number.isNaN(a) || Number.isNaN(b)) return null
// 常见格式是 "lng,lat";这里做一个简单兜底
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
if (looksLikeLngLat) return {lng: a, lat: b}
if (looksLikeLatLng) return {lng: b, lat: a}
return null
}
const navigateToPage = (url: string) => {
if (!isLoggedIn) {
goToRegister({ redirect: '/store/index' })
return
}
Taro.navigateTo({url})
}
const refresh = useCallback(async () => {
setError(null)
setLoading(true)
try {
const latestSelectedStore = getSelectedStoreFromStorage()
setSelectedStore(latestSelectedStore)
const userIdRaw = Number(Taro.getStorageSync('UserId'))
const userId = Number.isFinite(userIdRaw) && userIdRaw > 0 ? userIdRaw : undefined
let foundStoreId: number | undefined = undefined
if (userId) {
// 优先按“店员绑定关系”确定门店归属
try {
const list = await listShopStoreUser({userId})
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
foundStoreId = first?.storeId
setBoundStoreId(foundStoreId)
} catch {
// fallback to SelectedStore
foundStoreId = undefined
setBoundStoreId(undefined)
}
} else {
foundStoreId = undefined
setBoundStoreId(undefined)
}
const nextStoreId = (foundStoreId || latestSelectedStore?.id)
if (!nextStoreId) {
setStore(latestSelectedStore)
return
}
// 获取门店详情(用于展示门店名称/地址/仓库等)
const full = await getShopStore(nextStoreId)
setStore(full || (latestSelectedStore?.id === nextStoreId ? latestSelectedStore : ({id: nextStoreId} as ShopStoreModel)))
} catch (e: any) {
const msg = e?.message || '获取门店信息失败'
setError(msg)
} finally {
setLoading(false)
}
}, [])
// 返回/切换到该页面时,同步最新的已选门店与绑定门店
useDidShow(() => {
refresh().catch(() => {})
})
const openStoreLocation = () => {
if (!store?.id) {
return Taro.showToast({title: '请先选择门店', icon: 'none'})
}
const coords = parseStoreCoords(store)
if (!coords) {
return Taro.showToast({title: '门店未配置定位', icon: 'none'})
}
Taro.openLocation({
latitude: coords.lat,
longitude: coords.lng,
name: store.name || '门店',
address: store.address || ''
})
}
if (!isLoggedIn && !userLoading) {
return (
<View className="bg-gray-100 min-h-screen p-4">
<View className="bg-white rounded-xl p-4">
<Text className="text-gray-700"></Text>
<View className="mt-3">
<Button type="primary" onClick={() => goToRegister({ redirect: '/store/index' })}>
/
</Button>
</View>
</View>
</View>
)
}
return (
<View className="bg-gray-100 min-h-screen">
{/* 头部信息 */}
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
<View
className="absolute w-32 h-32 rounded-full"
style={{backgroundColor: 'rgba(255, 255, 255, 0.1)', top: '-16px', right: '-16px'}}
></View>
<View
className="absolute w-24 h-24 rounded-full"
style={{backgroundColor: 'rgba(255, 255, 255, 0.08)', bottom: '-12px', left: '-12px'}}
></View>
<View
className="absolute w-16 h-16 rounded-full"
style={{backgroundColor: 'rgba(255, 255, 255, 0.05)', top: '60px', left: '120px'}}
></View>
<View className="flex items-center justify-between relative z-10">
<Avatar
size="50"
src={getAvatarUrl()}
icon={<User />}
className="mr-4"
style={{border: '2px solid rgba(255, 255, 255, 0.3)'}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1">
{getDisplayName()}
</View>
<View className="text-sm" style={{color: 'rgba(255, 255, 255, 0.8)'}}>
{hasRole('store') ? '门店' : hasRole('rider') ? '配送员' : getRoleName()}
</View>
</View>
<Button
size="small"
style={{
background: 'rgba(255, 255, 255, 0.18)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.25)'
}}
loading={loading}
onClick={refresh}
>
</Button>
</View>
</View>
{/* 门店信息 */}
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10 bg-white">
<View className="flex items-center justify-between mb-2">
<Text className="font-semibold text-gray-400"></Text>
<View
className="text-gray-400 text-sm"
onClick={() => Taro.switchTab({url: '/pages/index/index'})}
>
</View>
</View>
{!storeId ? (
<View>
<Text className="text-sm text-gray-600">
</Text>
<View className="mt-3">
<Button type="primary" size="small" onClick={() => Taro.switchTab({url: '/pages/index/index'})}>
</Button>
</View>
</View>
) : (
<View>
<View className="text-base font-medium text-gray-900">
{store?.name || `门店ID: ${storeId}`}
</View>
{!!store?.address && (
<View className="text-sm text-gray-600 mt-1">
{store.address}
</View>
)}
{!!store?.warehouseName && (
<View className="text-sm text-gray-500 mt-1">
{store.warehouseName}
</View>
)}
{!!error && (
<View className="mt-2">
<Text className="text-sm text-red-600">{error}</Text>
</View>
)}
</View>
)}
</View>
{/* 功能入口 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="门店订单" onClick={() => navigateToPage('/store/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20" />
</View>
</View>
</Grid.Item>
<Grid.Item text="礼品卡核销" onClick={() => navigateToPage('/user/store/verification')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Scan color="#10b981" size="20" />
</View>
</View>
</Grid.Item>
<Grid.Item text="门店导航" onClick={openStoreLocation}>
<View className="text-center">
<View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Location color="#f59e0b" size="20" />
</View>
</View>
</Grid.Item>
<Grid.Item text="首页选店" onClick={() => Taro.switchTab({url: '/pages/index/index'})}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shop color="#8b5cf6" size="20" />
</View>
</View>
</Grid.Item>
</Grid>
</ConfigProvider>
</View>
<View className="h-20"></View>
</View>
)
}
export default StoreIndex