feat(location): add positioning function

This commit is contained in:
Dawn
2025-09-24 08:48:14 +08:00
parent 7ddfbabb44
commit 1f10a825d2
12 changed files with 287 additions and 14 deletions
+6 -1
View File
@@ -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"
+2 -1
View File
@@ -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"]
}
}
Generated Vendored
+29
View File
@@ -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
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -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
View File
@@ -210,7 +210,9 @@ export enum MsgEnum {
/** 回复 16*/
REPLY,
/** AI 17*/
AI
AI,
/** 位置 18*/
LOCATION
}
/**
+1 -1
View File
@@ -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>
+2 -1
View File
@@ -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
}
+220
View File
@@ -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 }
}
}
+5
View File
@@ -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 ''
}