- 新增 company/add 和 company/edit 页面路由配置 - 移除 Banner.tsx 中的调试日志 - 从 find.tsx 中移除未使用的 total 状态变量 - 更新 credit/order/index.config.ts 页面标题为订单管理并添加自定义导航栏样式 - 从 credit/company/index.tsx 中移除内联添加客户的对话框相关代码 - 将添加客户按钮跳转到独立的 /credit/company/add 页面 - 为公司列表项添加编辑功能点击事件,可跳转至 /credit/company/edit?id=xx - 优化信用订单页面结构,替换为新的订单管理界面 - 实现订单搜索、筛选、日期范围选择等功能 - 添加订单统计信息展示(总数、本金、利息) - 实现模拟数据加载和分页功能
259 lines
9.0 KiB
TypeScript
259 lines
9.0 KiB
TypeScript
import { useMemo, useRef, useState } from 'react'
|
||
import Taro, { useDidShow } from '@tarojs/taro'
|
||
import {Input, Text, View} from '@tarojs/components'
|
||
import { Empty, InfiniteLoading, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
|
||
import {Search} from '@nutui/icons-react-taro'
|
||
import { pageShopStore } from '@/api/shop/shopStore'
|
||
import type { ShopStore } from '@/api/shop/shopStore/model'
|
||
import { getCurrentLngLat } from '@/utils/location'
|
||
import './find.scss'
|
||
|
||
const PAGE_SIZE = 10
|
||
|
||
type LngLat = { lng: number; lat: number }
|
||
type ShopStoreView = ShopStore & { __distanceMeter?: number }
|
||
|
||
const parseLngLat = (raw: string | undefined): LngLat | null => {
|
||
const text = (raw || '').trim()
|
||
if (!text) return null
|
||
const parts = text.split(/[,\s]+/).filter(Boolean)
|
||
if (parts.length < 2) return null
|
||
const a = Number(parts[0])
|
||
const b = Number(parts[1])
|
||
if (!Number.isFinite(a) || !Number.isFinite(b)) return null
|
||
|
||
// Accept both "lng,lat" and "lat,lng".
|
||
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 distanceMeters = (a: LngLat, b: LngLat) => {
|
||
const toRad = (x: number) => (x * Math.PI) / 180
|
||
const R = 6371000
|
||
const dLat = toRad(b.lat - a.lat)
|
||
const dLng = toRad(b.lng - a.lng)
|
||
const lat1 = toRad(a.lat)
|
||
const lat2 = toRad(b.lat)
|
||
const sin1 = Math.sin(dLat / 2)
|
||
const sin2 = Math.sin(dLng / 2)
|
||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
|
||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
|
||
}
|
||
|
||
const formatDistance = (meter: number | undefined) => {
|
||
if (!Number.isFinite(meter as number)) return ''
|
||
const m = Math.max(0, Math.round(meter as number))
|
||
if (m < 1000) return `${m}米`
|
||
const km = m / 1000
|
||
return `${km.toFixed(km >= 10 ? 0 : 1)}km`
|
||
}
|
||
|
||
const Find = () => {
|
||
const [keyword, setKeyword] = useState<string>('')
|
||
const [storeList, setStoreList] = useState<ShopStoreView[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [hasMore, setHasMore] = useState(true)
|
||
const [userLngLat, setUserLngLat] = useState<LngLat | null>(null)
|
||
|
||
const pageRef = useRef(1)
|
||
const latestListRef = useRef<ShopStoreView[]>([])
|
||
const loadingRef = useRef(false)
|
||
const coordsRef = useRef<LngLat | null>(null)
|
||
|
||
const viewList = useMemo<ShopStoreView[]>(() => {
|
||
const me = userLngLat
|
||
if (!me) return storeList
|
||
|
||
// Keep backend order; only attach distance for display.
|
||
return storeList.map((s) => {
|
||
const coords = parseLngLat(s.lngAndLat || s.location)
|
||
if (!coords) return s
|
||
return { ...s, __distanceMeter: distanceMeters(me, coords) }
|
||
})
|
||
}, [storeList, userLngLat])
|
||
|
||
const loadStores = async (isRefresh = true, keywordsOverride?: string) => {
|
||
if (loadingRef.current) return
|
||
loadingRef.current = true
|
||
setLoading(true)
|
||
|
||
if (isRefresh) {
|
||
pageRef.current = 1
|
||
latestListRef.current = []
|
||
setStoreList([])
|
||
setHasMore(true)
|
||
}
|
||
|
||
try {
|
||
if (!coordsRef.current) {
|
||
const me = await getCurrentLngLat('为您展示附近网点,需要获取定位信息。')
|
||
const lng = me ? Number(me.lng) : NaN
|
||
const lat = me ? Number(me.lat) : NaN
|
||
coordsRef.current = Number.isFinite(lng) && Number.isFinite(lat) ? { lng, lat } : null
|
||
setUserLngLat(coordsRef.current)
|
||
}
|
||
|
||
const currentPage = pageRef.current
|
||
const kw = (keywordsOverride ?? keyword).trim()
|
||
const res = await pageShopStore({
|
||
page: currentPage,
|
||
limit: PAGE_SIZE,
|
||
keywords: kw || undefined
|
||
})
|
||
|
||
const resList = res?.list || []
|
||
const nextList = isRefresh ? resList : [...latestListRef.current, ...resList]
|
||
latestListRef.current = nextList
|
||
setStoreList(nextList)
|
||
|
||
const count = typeof res?.count === 'number' ? res.count : nextList.length
|
||
setHasMore(nextList.length < count)
|
||
|
||
if (resList.length > 0) {
|
||
pageRef.current = currentPage + 1
|
||
} else {
|
||
setHasMore(false)
|
||
}
|
||
} catch (e) {
|
||
console.error('获取网点列表失败:', e)
|
||
Taro.showToast({ title: '获取网点失败', icon: 'none' })
|
||
setHasMore(false)
|
||
} finally {
|
||
loadingRef.current = false
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useDidShow(() => {
|
||
loadStores(true).then()
|
||
})
|
||
|
||
const onNavigate = (item: ShopStore) => {
|
||
const coords = parseLngLat(item.lngAndLat || item.location)
|
||
if (!coords) {
|
||
Taro.showToast({ title: '网点暂无坐标,无法导航', icon: 'none' })
|
||
return
|
||
}
|
||
Taro.openLocation({
|
||
latitude: coords.lat,
|
||
longitude: coords.lng,
|
||
name: item.name || item.city || '网点',
|
||
address: item.address || ''
|
||
})
|
||
}
|
||
|
||
const onCall = (phone: string | undefined) => {
|
||
const p = (phone || '').trim()
|
||
if (!p) {
|
||
Taro.showToast({ title: '暂无联系电话', icon: 'none' })
|
||
return
|
||
}
|
||
Taro.makePhoneCall({ phoneNumber: p })
|
||
}
|
||
|
||
const onSearch = () => {
|
||
loadStores(true).then()
|
||
}
|
||
|
||
return (
|
||
<View className='sitePage'>
|
||
<View className='searchArea'>
|
||
<View className='searchBox'>
|
||
<Input
|
||
className='searchInput'
|
||
value={keyword}
|
||
placeholder='请输入城市名称查询'
|
||
placeholderClass='searchPlaceholder'
|
||
confirmType='search'
|
||
onInput={(e) => setKeyword(e.detail.value)}
|
||
onConfirm={onSearch}
|
||
/>
|
||
<View className='searchIconWrap' onClick={onSearch}>
|
||
<Search size={18} color='#b51616' />
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<PullToRefresh onRefresh={() => loadStores(true)} headHeight={60}>
|
||
<View style={{ height: 'calc(100vh) - 160px', overflowY: 'auto' }} id='store-scroll'>
|
||
{viewList.length === 0 && !loading ? (
|
||
<View className='emptyWrap'>
|
||
<Empty description='暂无网点' style={{ backgroundColor: 'transparent' }} />
|
||
</View>
|
||
) : (
|
||
<View className='siteList'>
|
||
<InfiniteLoading
|
||
target='store-scroll'
|
||
hasMore={hasMore}
|
||
onLoadMore={() => loadStores(false)}
|
||
loadingText={
|
||
<View className='emptyWrap'>
|
||
<Loading />
|
||
<Text className='emptyText' style={{ marginLeft: '8px' }}>
|
||
加载中...
|
||
</Text>
|
||
</View>
|
||
}
|
||
loadMoreText={
|
||
<View className='emptyWrap'>
|
||
<Text className='emptyText'>
|
||
{viewList.length === 0 ? '暂无网点' : '没有更多了'}
|
||
</Text>
|
||
</View>
|
||
}
|
||
>
|
||
{viewList.map((item, idx) => {
|
||
const name = item?.name || item?.city || item?.province || '网点'
|
||
const contact = item?.managerName || '--'
|
||
const distanceText = formatDistance(item?.__distanceMeter)
|
||
return (
|
||
<View key={String(item?.id ?? `${name}-${idx}`)} className='siteCard'>
|
||
<View className='siteCardInner'>
|
||
<View className='siteInfo'>
|
||
<View className='siteRow siteRowTop'>
|
||
<Text className='siteLabel'>网点名称:</Text>
|
||
<Text className='siteValue siteValueStrong'>{name}</Text>
|
||
</View>
|
||
<View className='siteDivider' />
|
||
<View className='siteRow'>
|
||
<Text className='siteLabel'>网点地址:</Text>
|
||
<Text className='siteValue'>{item?.address || '--'}</Text>
|
||
</View>
|
||
<View className='siteRow'>
|
||
<Text className='siteLabel'>联系电话:</Text>
|
||
<Text className='siteValue' onClick={() => onCall(item?.phone)}>
|
||
{item?.phone || '--'}
|
||
</Text>
|
||
</View>
|
||
<View className='siteRow'>
|
||
<Text className='siteLabel'>联系人:</Text>
|
||
<Text className='siteValue'>{contact}</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<View className='siteSide' onClick={() => onNavigate(item)}>
|
||
<View className='navArrow' />
|
||
<Text className='distanceText'>
|
||
{distanceText ? `${distanceText}` : '查看导航'}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)
|
||
})}
|
||
</InfiniteLoading>
|
||
|
||
</View>
|
||
)}
|
||
</View>
|
||
</PullToRefresh>
|
||
|
||
<View className='bottomSafe' />
|
||
</View>
|
||
)
|
||
}
|
||
export default Find
|