feat(hjm): 实现车辆位置坐标转换与违章查询功能

- 新增 GPS 坐标转换工具类,支持 WGS84、GCJ02 和 BD09 坐标系互转
- 在车辆定位页面集成坐标转换逻辑,确保地图显示准确性
-优化车辆列表分页加载功能,提升大数据量下的用户体验- 修改车辆热销组件属性类型定义,增强代码可维护性
- 调整违章记录列表页面,新增通过车辆编号查询功能
- 更新最佳销售车辆组件键值生成逻辑,提高组件稳定性
- 完善地图组件 markers 和 polygons 数据校验,防止无效坐标渲染
- 在车辆详情页增加违章查询跳转按钮,方便用户操作
- 移除违章记录删除功能,避免误删重要数据
-优化空状态提示文案,使用户理解更清晰
This commit is contained in:
2025-10-27 17:19:03 +08:00
parent 78f94c0afa
commit 0ff976b4e9
6 changed files with 497 additions and 69 deletions

View File

@@ -1,10 +1,15 @@
import {useEffect} from "react";
import {Image, Space} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {HjmCar} from "@/api/hjm/hjmCar/model";
const BestSellers = (props: any) => {
interface BestSellersProps {
data: HjmCar[];
}
const BestSellers = (props: BestSellersProps) => {
const reload = () => {
// 可以在这里添加重新加载逻辑
}
useEffect(() => {
@@ -16,7 +21,7 @@ const BestSellers = (props: any) => {
<div className={'flex flex-col justify-between items-center rounded-lg px-3'}>
{props.data?.map((item, index) => {
return (
<div key={index} className={'flex bg-white rounded-lg w-full p-3 mb-3'}
<div key={item.id || index} className={'flex bg-white rounded-lg w-full p-3 mb-3'}
onClick={() => Taro.navigateTo({url: '/hjm/query?id=' + item.code})}>
{ item.image && (
<Image src={JSON.parse(item.image)[0].url} mode={'scaleToFill'}
@@ -40,8 +45,8 @@ const BestSellers = (props: any) => {
</div>
)
})}
{props.data.length === 0 && (
<div className={'flex justify-center items-center'}>
{(!props.data || props.data.length === 0) && (
<div className={'flex justify-center items-center py-10'}>
<div className={'text-gray-500 text-sm'}></div>
</div>
)}
@@ -50,4 +55,4 @@ const BestSellers = (props: any) => {
</div>
)
}
export default BestSellers
export default BestSellers

View File

@@ -1,4 +1,4 @@
import {useEffect, useState} from "react";
import {useEffect, useState, CSSProperties} from "react";
import {Search} from '@nutui/icons-react-taro'
import {Button, Input, InfiniteLoading} from '@nutui/nutui-react-taro'
import {pageHjmCar} from "@/api/hjm/hjmCar";
@@ -8,6 +8,14 @@ import './location.scss'
import BestSellers from "./BestSellers";
import {getUserInfo} from "@/api/layout";
const InfiniteUlStyle: CSSProperties = {
height: '80vh',
width: '100%',
padding: '0',
overflowY: 'auto',
overflowX: 'hidden',
}
/**
* 文章终极列表
* @constructor
@@ -15,14 +23,20 @@ import {getUserInfo} from "@/api/layout";
const List = () => {
const [keywords, setKeywords] = useState<string>('')
const [list, setList] = useState<HjmCar[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const onKeywords = (keywords: string) => {
setKeywords(keywords)
}
const reload = async () => {
const loadList = async (pageNum: number, isRefresh = false) => {
if (loading) return;
setLoading(true)
// 搜索条件
const where = {status: 1, deleted: 0, keywords}
const where = {status: 1, deleted: 0, keywords, page: pageNum, limit: 10}
// 读取用户信息
const user = await getUserInfo();
@@ -46,20 +60,53 @@ const List = () => {
where.installerId = user.userId;
}
if(roleCode == 'user'){
setLoading(false)
return false;
}
// 获取车辆列表
pageHjmCar(where).then(res => {
setList(res?.list || [])
})
try {
const res = await pageHjmCar(where);
if (res?.list && res?.list.length > 0) {
if (isRefresh) {
setList(res.list);
} else {
setList(prevList => [...prevList, ...res.list]);
}
setHasMore(res.list.length >= 10); // 如果返回的数据少于10条说明没有更多了
} else {
if (isRefresh) {
setList([]);
}
setHasMore(false);
}
} catch (error) {
console.error('获取车辆列表失败:', error);
if (isRefresh) {
setList([]);
}
setHasMore(false);
} finally {
setLoading(false);
}
}
const reload = async () => {
setPage(1);
await loadList(1, true);
}
const loadMore = async () => {
if (!hasMore || loading) return;
const nextPage = page + 1;
setPage(nextPage);
await loadList(nextPage);
}
useEffect(() => {
reload()
}, [])
return (
<>
<div className={'fixed z-20 top-5 left-0 w-full'}>
@@ -90,12 +137,19 @@ const List = () => {
</div>
</div>
</div>
<InfiniteLoading
className={'w-full fixed left-0 top-20'}
>
<BestSellers data={list}/>
</InfiniteLoading>
<div style={InfiniteUlStyle} id="scroll">
<InfiniteLoading
target="scroll"
className={'w-full fixed left-0 top-20'}
hasMore={hasMore}
onLoadMore={loadMore}
loadingText="加载中..."
loadMoreText="没有更多了"
>
<BestSellers data={list}/>
</InfiniteLoading>
</div>
</>
)
}
export default List
export default List

View File

@@ -5,6 +5,8 @@ import {Button, Input} from '@nutui/nutui-react-taro'
import {useRouter} from '@tarojs/taro'
import {getHjmCarByCode} from "@/api/hjm/hjmCar";
import {HjmCar} from "@/api/hjm/hjmCar/model";
// 导入整个GPS对象因为gpsUtil.js导出的是一个对象而不是命名导出
import GPS from '@/utils/gpsUtil.js';
import './location.scss'
/**
@@ -36,9 +38,17 @@ const Location = () => {
// 转为多边形点数组
const points = coordPairs.map(coord => {
const [lat, lng] = coord.split(',');
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
// 确保解析后的坐标是有效数字
if (isNaN(latitude) || isNaN(longitude)) {
throw new Error(`无效坐标: ${lat}, ${lng}`);
}
return {
latitude: parseFloat(lat),
longitude: parseFloat(lng)
latitude: latitude,
longitude: longitude
}
});
@@ -57,10 +67,41 @@ const Location = () => {
getHjmCarByCode(keywords).then(data => {
console.log('执行搜索', data)
setItem(data)
// setLatitude(data.latitude)
// setLongitude(data.longitude)
// wgs48坐标转gcj02坐标代
// 转换WGS84坐标到GCJ02坐标
if (data.latitude && data.longitude &&
!isNaN(data.latitude) &&
!isNaN(data.longitude) &&
isFinite(data.latitude) &&
isFinite(data.longitude)) {
console.log('原始坐标:', data.latitude, data.longitude)
try {
// 确保坐标是数字类型
const lat = Number(data.latitude);
const lng = Number(data.longitude);
const transformed = GPS.gcj_encrypt(lat, lng);
console.log(transformed, 'transformedtransformedtransformedtransformedtransformed')
// 确保转换结果有效
if (transformed && !isNaN(transformed.lat) && !isNaN(transformed.lng)) {
setLatitude(transformed.lat);
setLongitude(transformed.lng);
console.log('转换结果:', transformed)
} else {
// 如果转换结果无效,使用原始坐标
setLatitude(data.latitude);
setLongitude(data.longitude);
console.log('转换失败:', data.longitude)
}
} catch (error) {
console.error('坐标转换错误:', error);
// 如果转换出错,使用原始坐标
setLatitude(data.latitude);
setLongitude(data.longitude);
}
} else {
// 如果坐标无效,使用原始坐标
setLatitude(data.latitude);
setLongitude(data.longitude);
}
setKeywords(data.code)
if (data.fence) {
@@ -69,8 +110,44 @@ const Location = () => {
// 使用通用函数解析坐标字符串
const {points} = parseCoordinateString(coordStr);
// 转换围栏坐标点
if (points.length > 0) {
const transformedPoints = points.map(point => {
// 确保坐标有效
if (point.latitude && point.longitude &&
!isNaN(point.latitude) &&
!isNaN(point.longitude) &&
isFinite(point.latitude) &&
isFinite(point.longitude)) {
try {
// 确保坐标是数字类型
const lat = Number(point.latitude);
const lng = Number(point.longitude);
const transformed = GPS.gcj_encrypt(lat, lng);
// 确保转换结果有效
if (transformed && !isNaN(transformed.lat) && !isNaN(transformed.lng)) {
return {
latitude: transformed.lat,
longitude: transformed.lng
};
} else {
// 如果转换结果无效,返回原始坐标
return point;
}
} catch (error) {
console.error('围栏坐标转换错误:', error);
// 如果转换出错,返回原始坐标
return point;
}
}
// 如果坐标无效,返回原始坐标
return point;
});
setPoints(transformedPoints);
}
console.log('解析结果 - 多边形点:', points);
setPoints(points);
setShowCircles(true)
}
})
@@ -80,8 +157,38 @@ const Location = () => {
if (code) {
getHjmCarByCode(code).then(data => {
setItem(data)
setLatitude(data.latitude)
setLongitude(data.longitude)
// 转换WGS84坐标到GCJ02坐标
if (data.latitude && data.longitude &&
!isNaN(data.latitude) &&
!isNaN(data.longitude) &&
isFinite(data.latitude) &&
isFinite(data.longitude)) {
try {
// 确保坐标是数字类型
const lat = Number(data.latitude);
const lng = Number(data.longitude);
const transformed = GPS.gcj_encrypt(lat, lng);
// 确保转换结果有效
if (transformed && !isNaN(transformed.lat) && !isNaN(transformed.lng)) {
setLatitude(transformed.lat);
setLongitude(transformed.lng);
} else {
// 如果转换结果无效,使用原始坐标
setLatitude(data.latitude);
setLongitude(data.longitude);
}
} catch (error) {
console.error('坐标转换错误:', error);
// 如果转换出错,使用原始坐标
setLatitude(data.latitude);
setLongitude(data.longitude);
}
} else {
// 如果坐标无效,使用原始坐标
setLatitude(data.latitude);
setLongitude(data.longitude);
}
setKeywords(data.code)
if (data.fence) {
// 方法2使用实际的 fence 数据(如果是字符串格式)
@@ -89,8 +196,44 @@ const Location = () => {
// 使用通用函数解析坐标字符串
const {points} = parseCoordinateString(coordStr);
// 转换围栏坐标点
if (points.length > 0) {
const transformedPoints = points.map(point => {
// 确保坐标有效
if (point.latitude && point.longitude &&
!isNaN(point.latitude) &&
!isNaN(point.longitude) &&
isFinite(point.latitude) &&
isFinite(point.longitude)) {
try {
// 确保坐标是数字类型
const lat = Number(point.latitude);
const lng = Number(point.longitude);
const transformed = GPS.gcj_encrypt(lat, lng);
// 确保转换结果有效
if (transformed && !isNaN(transformed.lat) && !isNaN(transformed.lng)) {
return {
latitude: transformed.lat,
longitude: transformed.lng
};
} else {
// 如果转换结果无效,返回原始坐标
return point;
}
} catch (error) {
console.error('围栏坐标转换错误:', error);
// 如果转换出错,返回原始坐标
return point;
}
}
// 如果坐标无效,返回原始坐标
return point;
});
setPoints(transformedPoints);
}
console.log('解析结果 - 多边形点:', points);
setPoints(points);
setShowCircles(true)
}
@@ -194,31 +337,40 @@ const Location = () => {
longitude={longitude}
latitude={latitude}
scale={scale}
markers={[{
id: 1,
latitude: latitude,
longitude: longitude,
label: {
content: `${item?.code}`,
color: '#000000',
fontSize: 12,
borderRadius: 5,
bgColor: '#FFFFFF',
// @ts-ignore
padding: '5px 5px',
borderWidth: 1
},
}]}
polygons={points.length > 0 ? [
{
points: points,
color: '#ff0000',
strokeWidth: 3
}
] : []}
markers={
latitude && longitude &&
!isNaN(latitude) && !isNaN(longitude) &&
isFinite(latitude) && isFinite(longitude)
? [{
id: 1,
latitude: latitude,
longitude: longitude,
iconPath: ''
}]
: []
}
polygons={
points.length > 0 &&
points.every(point =>
point.latitude && point.longitude &&
!isNaN(point.latitude) && !isNaN(point.longitude) &&
isFinite(point.latitude) && isFinite(point.longitude)
)
? [
{
points: points,
color: '#ff0000',
strokeWidth: 3
}
]
: []
}
onTap={() => {
console.log('map tap')
}}
onError={(e) => {
console.error('地图组件错误:', e)
}}
style={{width: '100%', height: '100vh'}}
/>

View File

@@ -839,6 +839,15 @@ const Query = () => {
}>
</Button>
<Button nativeType="submit" type="default" onClick={
() => {
Taro.navigateTo({
url: `/hjm/violation/list?id=${FormData?.code}`
})
}
}>
</Button>
</div>
)
}

View File

@@ -8,9 +8,10 @@ import {
Space,
Pagination
} from '@nutui/nutui-react-taro'
import {useRouter} from '@tarojs/taro'
import {Search, Calendar, Truck, File, AddCircle} from '@nutui/icons-react-taro'
import Taro, {useDidShow} from '@tarojs/taro'
import {pageHjmViolation, removeHjmViolation} from "@/api/hjm/hjmViolation";
import {pageHjmViolation} from "@/api/hjm/hjmViolation";
import {HjmViolation} from "@/api/hjm/hjmViolation/model";
@@ -18,6 +19,7 @@ import {HjmViolation} from "@/api/hjm/hjmViolation/model";
* 报险记录列表页面
*/
const List: React.FC = () => {
const {params} = useRouter();
const [list, setList] = useState<HjmViolation[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [keywords, setKeywords] = useState<string>('')
@@ -64,6 +66,9 @@ const List: React.FC = () => {
where.organizationId = Taro.getStorageSync('OrganizationId');
}
}
if(params.id){
where.code = params.id;
}
const res = await pageHjmViolation(where)
setList(res?.list || [])
@@ -183,7 +188,7 @@ const List: React.FC = () => {
<Loading type="spinner">...</Loading>
</div>
) : list.length === 0 ? (
<Empty description="暂无报险记录">
<Empty description="暂无违章记录">
</Empty>
) : (
<div style={{padding: '0 16px'}}>
@@ -292,22 +297,22 @@ const List: React.FC = () => {
url: `/hjm/violation/add?id=${item.id}`
})
}}></Button>
<Button type="primary" onClick={(event: any) => {
event.stopPropagation()
removeHjmViolation(item.id).then(() => {
Taro.showToast({
title: '删除成功',
icon: 'success'
})
// 删除成功后重新加载列表
reload(false)
}).catch((error) => {
Taro.showToast({
title: error.message || '删除失败',
icon: 'error'
})
})
}}></Button>
{/*<Button type="primary" onClick={(event: any) => {*/}
{/* event.stopPropagation()*/}
{/* removeHjmViolation(item.id).then(() => {*/}
{/* Taro.showToast({*/}
{/* title: '删除成功',*/}
{/* icon: 'success'*/}
{/* })*/}
{/* // 删除成功后重新加载列表*/}
{/* reload(false)*/}
{/* }).catch((error) => {*/}
{/* Taro.showToast({*/}
{/* title: error.message || '删除失败',*/}
{/* icon: 'error'*/}
{/* })*/}
{/* })*/}
{/*}}>删除</Button>*/}
</Space>
</div>
)}

203
src/utils/gpsUtil.js Normal file
View File

@@ -0,0 +1,203 @@
var GPS = {
PI: 3.14159265358979324,
x_pi: 3.14159265358979324 * 3000.0 / 180.0,
delta: function(lat, lng) {
// Krasovsky 1940
//
// a = 6378245.0, 1/f = 298.3
// b = a * (1 - f)
// ee = (a^2 - b^2) / a^2;
var a = 6378245.0; // a: 卫星椭球坐标投影到平面地图坐标系的投影因子。
var ee = 0.00669342162296594323; // ee: 椭球的偏心率。
var dLat = this.transformLat(lng - 105.0, lat - 35.0);
var dLon = this.transformLon(lng - 105.0, lat - 35.0);
var radLat = lat / 180.0 * this.PI;
var magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
var sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * this.PI);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * this.PI);
return {
'lat': dLat,
'lng': dLon
};
},
//WGS-84 to GCJ-02 ,传入的数据要是Number
gcj_encrypt: function(wgsLat, wgsLon) {
if (this.outOfChina(wgsLat, wgsLon))
return {
'lat': wgsLat,
'lng': wgsLon
};
var d = this.delta(wgsLat, wgsLon);
return {
'lat': Number( (wgsLat + d.lat).toFixed(6)),
'lng': Number( (wgsLon + d.lng).toFixed(6))
};
},
//WGS-84 to GCJ-02
gcj_encrypt222: function(wgsLat, wgsLon) {
if (this.outOfChina(wgsLat, wgsLon))
return wgsLat + "," + wgsLon;
var d = this.delta(wgsLat, wgsLon);
var lat = (wgsLat + d.lat).toFixed(6);
var lng = (wgsLon + d.lng).toFixed(6);
return lat+ "," + lng
},
//GCJ-02 to WGS-84
gcj_decrypt: function(gcjLat, gcjLon) {
if (this.outOfChina(gcjLat, gcjLon))
return {
'lat': Number((gcjLat).toFixed(6)),
'lng': Number((gcjLon).toFixed(6))
};
var d = this.delta(gcjLat, gcjLon);
return {
'lat': Number( (gcjLat - d.lat).toFixed(6)),
'lng': Number( (gcjLon - d.lng).toFixed(6))
};
},
//GCJ-02 to WGS-84 exactly
gcj_decrypt_exact: function(gcjLat, gcjLon) {
var initDelta = 0.01;
var threshold = 0.000000001;
var dLat = initDelta,
dLon = initDelta;
var mLat = gcjLat - dLat,
mLon = gcjLon - dLon;
var pLat = gcjLat + dLat,
pLon = gcjLon + dLon;
var wgsLat, wgsLon, i = 0;
while (1) {
wgsLat = (mLat + pLat) / 2;
wgsLon = (mLon + pLon) / 2;
var tmp = this.gcj_encrypt(wgsLat, wgsLon)
dLat = tmp.lat - gcjLat;
dLon = tmp.lng - gcjLon;
if ((Math.abs(dLat) < threshold) && (Math.abs(dLon) < threshold))
break;
if (dLat > 0) pLat = wgsLat;
else mLat = wgsLat;
if (dLon > 0) pLon = wgsLon;
else mLon = wgsLon;
if (++i > 10000) break;
}
//console.log(i);
return {
'lat': (wgsLat).toFixed(6),
'lng': (wgsLon).toFixed(6)
};
},
//GCJ-02 to BD-09
bd_encrypt: function(gcjLat, gcjLon) {
var x = gcjLon,
y = gcjLat;
var z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * this.x_pi);
var theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * this.x_pi);
bdLon = z * Math.cos(theta) + 0.0065;
bdLat = z * Math.sin(theta) + 0.006;
return {
'lat': (bdLat).toFixed(6),
'lng': (bdLon).toFixed(6)
};
},
//BD-09 to GCJ-02
bd_decrypt: function(bdLat, bdLon) {
var x = bdLon - 0.0065,
y = bdLat - 0.006;
var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * this.x_pi);
var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * this.x_pi);
var gcjLon = z * Math.cos(theta);
var gcjLat = z * Math.sin(theta);
return {
'lat': (gcjLat).toFixed(6),
'lng': (gcjLon).toFixed(6)
};
},
//WGS-84 to Web mercator
//mercatorLat -> y mercatorLon -> x
mercator_encrypt: function(wgsLat, wgsLon) {
var x = wgsLon * 20037508.34 / 180.;
var y = Math.log(Math.tan((90. + wgsLat) * this.PI / 360.)) / (this.PI / 180.);
y = y * 20037508.34 / 180.;
return {
'lat': (y).toFixed(6),
'lng': (x).toFixed(6)
};
/*
if ((Math.abs(wgsLon) > 180 || Math.abs(wgsLat) > 90))
return null;
var x = 6378137.0 * wgsLon * 0.017453292519943295;
var a = wgsLat * 0.017453292519943295;
var y = 3189068.5 * Math.log((1.0 + Math.sin(a)) / (1.0 - Math.sin(a)));
return {'lat' : y, 'lng' : x};
//*/
},
// Web mercator to WGS-84
// mercatorLat -> y mercatorLon -> x
mercator_decrypt: function(mercatorLat, mercatorLon) {
var x = mercatorLon / 20037508.34 * 180.;
var y = mercatorLat / 20037508.34 * 180.;
y = 180 / this.PI * (2 * Math.atan(Math.exp(y * this.PI / 180.)) - this.PI / 2);
return {
'lat': (y).toFixed(6),
'lng': (x).toFixed(6)
};
/*
if (Math.abs(mercatorLon) < 180 && Math.abs(mercatorLat) < 90)
return null;
if ((Math.abs(mercatorLon) > 20037508.3427892) || (Math.abs(mercatorLat) > 20037508.3427892))
return null;
var a = mercatorLon / 6378137.0 * 57.295779513082323;
var x = a - (Math.floor(((a + 180.0) / 360.0)) * 360.0);
var y = (1.5707963267948966 - (2.0 * Math.atan(Math.exp((-1.0 * mercatorLat) / 6378137.0)))) * 57.295779513082323;
return {'lat' : y, 'lng' : x};
//*/
},
// two point's distance
distance: function(latA, lonA, latB, lonB) {
var earthR = 6371000.;
var x = Math.cos(latA * this.PI / 180.) * Math.cos(latB * this.PI / 180.) * Math.cos((lonA - lonB) * this.PI / 180);
var y = Math.sin(latA * this.PI / 180.) * Math.sin(latB * this.PI / 180.);
var s = x + y;
if (s > 1) s = 1;
if (s < -1) s = -1;
var alpha = Math.acos(s);
var distance = alpha * earthR;
return distance;
},
outOfChina: function(lat, lng) {
if (lng < 72.004 || lng > 137.8347)
return true;
if (lat < 0.8293 || lat > 55.8271)
return true;
return false;
},
transformLat: function(x, y) {
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * this.PI) + 20.0 * Math.sin(2.0 * x * this.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * this.PI) + 40.0 * Math.sin(y / 3.0 * this.PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * this.PI) + 320 * Math.sin(y * this.PI / 30.0)) * 2.0 / 3.0;
return ret;
},
transformLon: function(x, y) {
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * this.PI) + 20.0 * Math.sin(2.0 * x * this.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * this.PI) + 40.0 * Math.sin(x / 3.0 * this.PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * this.PI) + 300.0 * Math.sin(x / 30.0 * this.PI)) * 2.0 / 3.0;
return ret;
}
};
module.exports = GPS;