feat(store): 添加门店管理功能和订单配送功能
- 在app.config.ts中添加门店相关路由配置 - 在config/app.ts中添加租户名称常量 - 在Header.tsx中实现门店选择功能,包括定位、距离计算和门店切换 - 更新ShopOrder模型,添加门店ID、门店名称、配送员ID和仓库ID字段 - 新增ShopStore相关API和服务,支持门店的增删改查 - 新增ShopStoreRider相关API和服务,支持配送员管理 - 新增ShopStoreUser相关API和服务,支持店员管理 - 新增ShopWarehouse相关API和服务,支持仓库管理 - 添加配送订单页面,支持订单状态管理和送达确认功能 - 优化经销商页面的样式布局
This commit is contained in:
3
src/store/index.config.ts
Normal file
3
src/store/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '门店中心'
|
||||
})
|
||||
0
src/store/index.scss
Normal file
0
src/store/index.scss
Normal file
281
src/store/index.tsx
Normal file
281
src/store/index.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
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'
|
||||
|
||||
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) {
|
||||
Taro.showToast({title: '请先登录', icon: 'none', duration: 1500})
|
||||
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={() => Taro.navigateTo({url: '/passport/login'})}>
|
||||
去登录
|
||||
</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-800">当前门店</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
|
||||
4
src/store/orders/index.config.ts
Normal file
4
src/store/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '门店订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
83
src/store/orders/index.tsx
Normal file
83
src/store/orders/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import OrderList from '@/user/order/components/OrderList'
|
||||
import {getSelectedStoreFromStorage} from '@/utils/storeSelection'
|
||||
import {listShopStoreUser} from '@/api/shop/shopStoreUser'
|
||||
|
||||
export default function StoreOrders() {
|
||||
const [boundStoreId, setBoundStoreId] = useState<number | undefined>(undefined)
|
||||
|
||||
const isLoggedIn = useMemo(() => {
|
||||
return !!Taro.getStorageSync('access_token') && !!Taro.getStorageSync('UserId')
|
||||
}, [])
|
||||
|
||||
const selectedStore = useMemo(() => getSelectedStoreFromStorage(), [])
|
||||
const storeId = boundStoreId || selectedStore?.id
|
||||
|
||||
useEffect(() => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// 优先按“店员绑定关系”确定门店归属:门店看到的是自己的订单
|
||||
const userId = Number(Taro.getStorageSync('UserId'))
|
||||
if (!Number.isFinite(userId) || userId <= 0) return
|
||||
listShopStoreUser({userId}).then(list => {
|
||||
const first = (list || []).find(i => i?.isDelete !== 1 && i?.storeId)
|
||||
if (first?.storeId) setBoundStoreId(first.storeId)
|
||||
}).catch(() => {
|
||||
// fallback to SelectedStore
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen p-4">
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-700">请先登录</Text>
|
||||
<View className="mt-3">
|
||||
<Button type="primary" size="small" onClick={() => Taro.navigateTo({url: '/passport/login'})}>
|
||||
去登录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
|
||||
<View className="px-3">
|
||||
<View className="bg-white rounded-lg p-3 mb-3">
|
||||
<Text className="text-sm text-gray-600">当前门店:</Text>
|
||||
<Text className="text-base font-medium">
|
||||
{boundStoreId
|
||||
? (selectedStore?.id === boundStoreId ? (selectedStore?.name || `门店ID: ${boundStoreId}`) : `门店ID: ${boundStoreId}`)
|
||||
: (selectedStore?.name || '未选择门店')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!storeId ? (
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<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>
|
||||
) : (
|
||||
<OrderList mode="store" baseParams={{storeId}} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user