Files
template-10579/src/pages/find/find.tsx
赵忠林 f4a1fab4cb feat(credit): 重构订单管理页面并优化公司模块功能
- 新增 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
- 优化信用订单页面结构,替换为新的订单管理界面
- 实现订单搜索、筛选、日期范围选择等功能
- 添加订单统计信息展示(总数、本金、利息)
- 实现模拟数据加载和分页功能
2026-03-05 12:50:57 +08:00

259 lines
9.0 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 { 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