mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-09-19 09:50:38 +08:00
feat(location): ✨ add positioning function
This commit is contained in:
@@ -1,2 +1,7 @@
|
||||
# gitee token
|
||||
VITE_GITEE_TOKEN="a9029798336825cea39ac9e4413b8579"
|
||||
VITE_GITEE_TOKEN="a9029798336825cea39ac9e4413b8579"
|
||||
|
||||
# 腾讯地图API密钥
|
||||
# 请到腾讯位置服务官网申请:https://lbs.qq.com/
|
||||
# 注意:如果未开启WebserviceAPI功能,系统会自动使用本地算法进行坐标转换
|
||||
VITE_TENCENT_MAP_KEY="PFOBZ-34XCC-J6N2D-AX7BX-6QUQ3-5KF4G"
|
||||
|
||||
Vendored
+2
-1
@@ -121,6 +121,7 @@
|
||||
"seemly": "^0.3.10",
|
||||
"tauri-plugin-mic-recorder-api": "^2.0.0",
|
||||
"tauri-plugin-safe-area-insets": "^0.1.0",
|
||||
"tlbs-map-vue": "^1.3.1",
|
||||
"vant": "^4.9.21",
|
||||
"vue": "^3.5.18",
|
||||
"vue-cropper": "1.1.4",
|
||||
@@ -176,6 +177,6 @@
|
||||
}
|
||||
},
|
||||
"pnpm": {
|
||||
"ignoredBuiltDependencies": ["esbuild", "vue-demi", "@parcel/watcher", "sharp"]
|
||||
"ignoredBuiltDependencies": ["esbuild", "vue-demi", "@parcel/watcher", "sharp", "tlbs-map-vue"]
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -134,6 +134,9 @@ importers:
|
||||
tauri-plugin-safe-area-insets:
|
||||
specifier: ^0.1.0
|
||||
version: 0.1.0
|
||||
tlbs-map-vue:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(vue@3.5.18(typescript@5.9.2))
|
||||
vant:
|
||||
specifier: ^4.9.21
|
||||
version: 4.9.21(vue@3.5.18(typescript@5.9.2))
|
||||
@@ -2860,6 +2863,10 @@ packages:
|
||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fs-extra@10.1.0:
|
||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
fs-extra@9.1.0:
|
||||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4285,6 +4292,16 @@ packages:
|
||||
resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tlbs-map-vue@1.3.1:
|
||||
resolution: {integrity: sha512-oh239gdtis/b6VInlmFuxP3fnwssEG4Hy5sTzLyiJ4bTeqR8pB7MPzl81AqaL+fC4W7aFsSkofbBGmgh4rD/4w==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.4.9
|
||||
vue: ^2.6.0 || >=3.0.0
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
|
||||
tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
@@ -7268,6 +7285,12 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fs-extra@10.1.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs-extra@9.1.0:
|
||||
dependencies:
|
||||
at-least-node: 1.0.0
|
||||
@@ -8699,6 +8722,12 @@ snapshots:
|
||||
|
||||
tinyspy@4.0.3: {}
|
||||
|
||||
tlbs-map-vue@1.3.1(vue@3.5.18(typescript@5.9.2)):
|
||||
dependencies:
|
||||
fs-extra: 10.1.0
|
||||
vue: 3.5.18(typescript@5.9.2)
|
||||
vue-demi: 0.14.6(vue@3.5.18(typescript@5.9.2))
|
||||
|
||||
tmp@0.0.33:
|
||||
dependencies:
|
||||
os-tmpdir: 1.0.2
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -13,5 +13,6 @@ export const MSG_REPLY_TEXT_MAP: Record<number, string> = {
|
||||
[MsgEnum.NOTICE]: '[公告]',
|
||||
[MsgEnum.VIDEO_CALL]: '[视频通话]',
|
||||
[MsgEnum.AUDIO_CALL]: '[语音通话]',
|
||||
[MsgEnum.BOT]: '[小管家]'
|
||||
[MsgEnum.BOT]: '[小管家]',
|
||||
[MsgEnum.LOCATION]: '[位置]'
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<n-scrollbar class="flex-1">
|
||||
<template v-for="session in filteredSessionList" :key="session.roomId">
|
||||
<n-flex align="center" :size="8" class="text-(12px #303030) py-8px px-4px">
|
||||
<n-flex align="center" :size="8" class="text-12px text-#303030 dark:text-#fefefe py-8px px-4px">
|
||||
<n-checkbox v-model:checked="session.isCheck" @click.stop />
|
||||
<n-avatar class="rounded-8px" :size="30" :src="AvatarUtils.getAvatarUrl(session.avatar)" />
|
||||
<p>{{ session.remark ? session.remark : session.name }}</p>
|
||||
@@ -206,7 +206,6 @@ const handleRemoveSession = (roomId: string) => {
|
||||
}
|
||||
|
||||
const sendMsg = async () => {
|
||||
console.log('发送聊天记录:', mergeMessageType)
|
||||
const selectedRoomIds = selectedSessions.value.map((item) => item.roomId)
|
||||
const selectedMsgIds = selectedMsgs.value.map((item) => item.message.id)
|
||||
|
||||
@@ -227,6 +226,10 @@ const sendMsg = async () => {
|
||||
showModal.value = false
|
||||
chatStore.clearMsgCheck()
|
||||
chatStore.setMsgMultiChoose(false)
|
||||
// 重置选中的会话
|
||||
chatStore.sessionList.forEach((session) => {
|
||||
session.isCheck = false
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<main
|
||||
style="cursor: default; user-select: none"
|
||||
class="w-230px flex flex-col h-fit bg-[--group-notice-bg] hover:bg-#fefefe99 dark:hover:bg-#60606040 rounded-8px p-8px box-border custom-shadow transition-colors duration-200"
|
||||
@click.stop="openMultiMsgWindow">
|
||||
<main class="multi-msg" @click.stop="openMultiMsgWindow">
|
||||
<p class="text-(14px [--text-color]) pb-12px truncate">{{ chatRecordTitle }}</p>
|
||||
|
||||
<div class="max-h-90px overflow-hidden mx-6px">
|
||||
@@ -107,4 +104,13 @@ const openMultiMsgWindow = async () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.multi-msg {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
@apply: w-230px flex flex-col h-fit bg-[--group-notice-bg]
|
||||
border-(1px solid #e3e3e3) dark:border-(1px solid #404040)
|
||||
hover:bg-#fefefe99 dark:hover:bg-#60606040 rounded-8px p-8px box-border
|
||||
custom-shadow transition-colors duration-200;
|
||||
}
|
||||
</style>
|
||||
|
||||
+3
-1
@@ -210,7 +210,9 @@ export enum MsgEnum {
|
||||
/** 回复 16*/
|
||||
REPLY,
|
||||
/** AI 17*/
|
||||
AI
|
||||
AI,
|
||||
/** 位置 18*/
|
||||
LOCATION
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="avatar-wrapper relative" @click="openAvatarCropper">
|
||||
<n-avatar :size="80" :src="AvatarUtils.getAvatarUrl(editInfo.content.avatar!)" round />
|
||||
<div class="avatar-hover absolute size-full rounded-50% flex-center">
|
||||
<span class="text-12px color-white">更换头像</span>
|
||||
<span class="text-12px color-#fff dark:color-#606060">更换头像</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1335,5 +1335,6 @@ export const messageStrategyMap: Record<MsgEnum, MessageStrategy> = {
|
||||
[MsgEnum.AI]: unsupportedMessageStrategy,
|
||||
[MsgEnum.BOT]: unsupportedMessageStrategy,
|
||||
[MsgEnum.VIDEO_CALL]: videoCallMessageStrategy,
|
||||
[MsgEnum.AUDIO_CALL]: audioCallMessageStrategy
|
||||
[MsgEnum.AUDIO_CALL]: audioCallMessageStrategy,
|
||||
[MsgEnum.LOCATION]: unsupportedMessageStrategy
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 坐标转换工具类
|
||||
* 用于在不同坐标系之间进行转换
|
||||
*/
|
||||
|
||||
// 坐标转换常量
|
||||
const PI = Math.PI
|
||||
const A = 6378245.0 // 长半轴
|
||||
const EE = 0.006693421622965943 // 偏心率平方(保持JavaScript安全精度)
|
||||
|
||||
type Coordinate = {
|
||||
lat: number
|
||||
lng: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断坐标是否在中国境内
|
||||
* @param lat 纬度
|
||||
* @param lng 经度
|
||||
* @returns 是否在中国境内
|
||||
*/
|
||||
const isInChina = (lat: number, lng: number): boolean => {
|
||||
return lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算偏移量
|
||||
* @param lat 纬度
|
||||
* @param lng 经度
|
||||
* @returns 偏移坐标
|
||||
*/
|
||||
const transformLat = (lat: number, lng: number): number => {
|
||||
let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng))
|
||||
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
|
||||
ret += ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) / 3.0
|
||||
ret += ((160.0 * Math.sin((lat / 12.0) * PI) + 320 * Math.sin((lat * PI) / 30.0)) * 2.0) / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
const transformLng = (lat: number, lng: number): number => {
|
||||
let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
|
||||
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
|
||||
ret += ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) / 3.0
|
||||
ret += ((150.0 * Math.sin((lng / 12.0) * PI) + 300.0 * Math.sin((lng / 30.0) * PI)) * 2.0) / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* WGS84转换为GCJ02(火星坐标系)
|
||||
* @param wgsLat WGS84纬度
|
||||
* @param wgsLng WGS84经度
|
||||
* @returns GCJ02坐标
|
||||
*/
|
||||
export const wgs84ToGcj02 = (wgsLat: number, wgsLng: number): Coordinate => {
|
||||
// 如果不在中国境内,直接返回原坐标
|
||||
if (!isInChina(wgsLat, wgsLng)) {
|
||||
return { lat: wgsLat, lng: wgsLng }
|
||||
}
|
||||
|
||||
let dLat = transformLat(wgsLat - 35.0, wgsLng - 105.0)
|
||||
let dLng = transformLng(wgsLat - 35.0, wgsLng - 105.0)
|
||||
|
||||
const radLat = (wgsLat / 180.0) * PI
|
||||
let magic = Math.sin(radLat)
|
||||
magic = 1 - EE * magic * magic
|
||||
const sqrtMagic = Math.sqrt(magic)
|
||||
|
||||
dLat = (dLat * 180.0) / (((A * (1 - EE)) / (magic * sqrtMagic)) * PI)
|
||||
dLng = (dLng * 180.0) / ((A / sqrtMagic) * Math.cos(radLat) * PI)
|
||||
|
||||
return {
|
||||
lat: wgsLat + dLat,
|
||||
lng: wgsLng + dLng
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GCJ02转换为BD09(百度坐标系)
|
||||
* @param gcjLat GCJ02纬度
|
||||
* @param gcjLng GCJ02经度
|
||||
* @returns BD09坐标
|
||||
*/
|
||||
export const gcj02ToBd09 = (gcjLat: number, gcjLng: number): Coordinate => {
|
||||
const z = Math.sqrt(gcjLng * gcjLng + gcjLat * gcjLat) + 0.00002 * Math.sin((gcjLat * PI * 3000.0) / 180.0)
|
||||
const theta = Math.atan2(gcjLat, gcjLng) + 0.000003 * Math.cos((gcjLng * PI * 3000.0) / 180.0)
|
||||
|
||||
return {
|
||||
lat: z * Math.sin(theta) + 0.006,
|
||||
lng: z * Math.cos(theta) + 0.0065
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BD09转换为GCJ02
|
||||
* @param bdLat BD09纬度
|
||||
* @param bdLng BD09经度
|
||||
* @returns GCJ02坐标
|
||||
*/
|
||||
export const bd09ToGcj02 = (bdLat: number, bdLng: number): Coordinate => {
|
||||
const x = bdLng - 0.0065
|
||||
const y = bdLat - 0.006
|
||||
const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin((y * PI * 3000.0) / 180.0)
|
||||
const theta = Math.atan2(y, x) - 0.000003 * Math.cos((x * PI * 3000.0) / 180.0)
|
||||
|
||||
return {
|
||||
lat: z * Math.sin(theta),
|
||||
lng: z * Math.cos(theta)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GCJ02转换为WGS84
|
||||
* @param gcjLat GCJ02纬度
|
||||
* @param gcjLng GCJ02经度
|
||||
* @returns WGS84坐标
|
||||
*/
|
||||
export const gcj02ToWgs84 = (gcjLat: number, gcjLng: number): Coordinate => {
|
||||
// 如果不在中国境内,直接返回原坐标
|
||||
if (!isInChina(gcjLat, gcjLng)) {
|
||||
return { lat: gcjLat, lng: gcjLng }
|
||||
}
|
||||
|
||||
let dLat = transformLat(gcjLat - 35.0, gcjLng - 105.0)
|
||||
let dLng = transformLng(gcjLat - 35.0, gcjLng - 105.0)
|
||||
|
||||
const radLat = (gcjLat / 180.0) * PI
|
||||
let magic = Math.sin(radLat)
|
||||
magic = 1 - EE * magic * magic
|
||||
const sqrtMagic = Math.sqrt(magic)
|
||||
|
||||
dLat = (dLat * 180.0) / (((A * (1 - EE)) / (magic * sqrtMagic)) * PI)
|
||||
dLng = (dLng * 180.0) / ((A / sqrtMagic) * Math.cos(radLat) * PI)
|
||||
|
||||
return {
|
||||
lat: gcjLat - dLat,
|
||||
lng: gcjLng - dLng
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WGS84直接转换为BD09
|
||||
* @param wgsLat WGS84纬度
|
||||
* @param wgsLng WGS84经度
|
||||
* @returns BD09坐标
|
||||
*/
|
||||
export const wgs84ToBd09 = (wgsLat: number, wgsLng: number): Coordinate => {
|
||||
const gcj02 = wgs84ToGcj02(wgsLat, wgsLng)
|
||||
return gcj02ToBd09(gcj02.lat, gcj02.lng)
|
||||
}
|
||||
|
||||
/**
|
||||
* BD09直接转换为WGS84
|
||||
* @param bdLat BD09纬度
|
||||
* @param bdLng BD09经度
|
||||
* @returns WGS84坐标
|
||||
*/
|
||||
export const bd09ToWgs84 = (bdLat: number, bdLng: number): Coordinate => {
|
||||
const gcj02 = bd09ToGcj02(bdLat, bdLng)
|
||||
return gcj02ToWgs84(gcj02.lat, gcj02.lng)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个坐标点之间的距离(单位:米)
|
||||
* 使用Haversine公式
|
||||
* @param lat1 起点纬度
|
||||
* @param lng1 起点经度
|
||||
* @param lat2 终点纬度
|
||||
* @param lng2 终点经度
|
||||
* @returns 距离(米)
|
||||
*/
|
||||
export const calculateDistance = (lat1: number, lng1: number, lat2: number, lng2: number): number => {
|
||||
const R = 6371000 // 地球半径(米)
|
||||
const dLat = ((lat2 - lat1) * PI) / 180
|
||||
const dLng = ((lng2 - lng1) * PI) / 180
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos((lat1 * PI) / 180) * Math.cos((lat2 * PI) / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2)
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
return R * c
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐标转换入口函数
|
||||
* 自动选择最佳转换方案
|
||||
* @param lat 原始纬度
|
||||
* @param lng 原始经度
|
||||
* @param fromType 原始坐标系类型
|
||||
* @param toType 目标坐标系类型
|
||||
* @returns 转换后的坐标
|
||||
*/
|
||||
export const transformCoordinate = (
|
||||
lat: number,
|
||||
lng: number,
|
||||
fromType: 'WGS84' | 'GCJ02' | 'BD09' = 'WGS84',
|
||||
toType: 'WGS84' | 'GCJ02' | 'BD09' = 'GCJ02'
|
||||
): Coordinate => {
|
||||
if (fromType === toType) {
|
||||
return { lat, lng }
|
||||
}
|
||||
|
||||
switch (`${fromType}_TO_${toType}`) {
|
||||
case 'WGS84_TO_GCJ02':
|
||||
return wgs84ToGcj02(lat, lng)
|
||||
case 'WGS84_TO_BD09':
|
||||
return wgs84ToBd09(lat, lng)
|
||||
case 'GCJ02_TO_WGS84':
|
||||
return gcj02ToWgs84(lat, lng)
|
||||
case 'GCJ02_TO_BD09':
|
||||
return gcj02ToBd09(lat, lng)
|
||||
case 'BD09_TO_WGS84':
|
||||
return bd09ToWgs84(lat, lng)
|
||||
case 'BD09_TO_GCJ02':
|
||||
return bd09ToGcj02(lat, lng)
|
||||
default:
|
||||
console.warn(`不支持的坐标转换: ${fromType} -> ${toType}`)
|
||||
return { lat, lng }
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,11 @@ export const renderReplyContent = (name?: string, type?: MsgEnum, content?: stri
|
||||
case MsgEnum.BOT: {
|
||||
return roomType === RoomTypeEnum.GROUP ? `${name}:${content}` : MSG_REPLY_TEXT_MAP[MsgEnum.BOT]
|
||||
}
|
||||
case MsgEnum.LOCATION: {
|
||||
return roomType === RoomTypeEnum.GROUP
|
||||
? `${name}:${MSG_REPLY_TEXT_MAP[MsgEnum.LOCATION]}`
|
||||
: MSG_REPLY_TEXT_MAP[MsgEnum.LOCATION]
|
||||
}
|
||||
default: {
|
||||
return ''
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user