Files
websopy-taro/src/store/index.tsx
赵忠林 07f5c92f4b feat(add): 新增多页面新增和编辑表单功能
- 添加编辑和新增收货地址页面,支持表单数据加载和提交
- 新增应用密钥凭证、新增应用操作动态、新增应用成员、新增应用版本页面配置
- 实现文章新增及编辑页面,包含图片上传及多种文章属性配置
- 增加注册会员页面,支持头像上传、手机号获取和邀请人关系处理
- 引入统一表单提交成功和失败处理,支持编辑模式数据回显
- 配置统一eslint和editorconfig规则,增强代码规范和编辑体验
- 新增.gitignore规则,屏蔽无关文件和目录,优化版本管理
2026-04-11 12:22:29 +08:00

282 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'
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-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-500 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