forked from gxwebsoft/mp-10550
- 新增 auth 工具模块,包含 isLoggedIn、goToRegister、ensureLoggedIn 方法 - 将硬编码的服务器URL更新为 glt-server 域名 - 重构多个页面的登录检查逻辑,使用统一的认证工具 - 在用户注册/登录流程中集成邀请关系处理 - 更新注册页面配置和实现,支持跳转参数传递 - 优化分销商二维码页面的加载状态和错误处理 - 在水票使用页面添加无票时的购买引导 - 统一文件上传和API请求的服务器地址 - 添加加密库类型定义文件
283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
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
|