feat(location): add connect tencent map api

This commit is contained in:
Dawn
2025-09-24 08:48:14 +08:00
parent 1f10a825d2
commit e4739b0820
11 changed files with 722 additions and 15 deletions
-5
View File
@@ -1,7 +1,2 @@
# gitee token
VITE_GITEE_TOKEN="a9029798336825cea39ac9e4413b8579"
# 腾讯地图API密钥
# 请到腾讯位置服务官网申请:https://lbs.qq.com/
# 注意:如果未开启WebserviceAPI功能,系统会自动使用本地算法进行坐标转换
VITE_TENCENT_MAP_KEY="PFOBZ-34XCC-J6N2D-AX7BX-6QUQ3-5KF4G"
+1
View File
@@ -214,6 +214,7 @@ HuLa is an instant messaging system built with Tauri, Vite 7, Vue 3, and TypeScr
| 📤 | Message Forwarding | ![Completed](https://img.shields.io/badge/✅-Completed-008080?style=flat&labelColor=e6f7f7&color=008080) |
| 📋 | Group Announcements | ![Completed](https://img.shields.io/badge/✅-Completed-008080?style=flat&labelColor=e6f7f7&color=008080) |
| 🏷️ | Nickname & Remark Management | ![Completed](https://img.shields.io/badge/✅-Completed-008080?style=flat&labelColor=e6f7f7&color=008080) |
| 📍 | Get and Send Location | ![In Progress](https://img.shields.io/badge/🐣-进行中-ee9f20?style=flat&labelColor=fef7e6&color=ee9f20) |
### 🎨 User Experience
| Feature | Description | Status |
+1
View File
@@ -215,6 +215,7 @@ HuLa 是一款基于 Tauri、Vite 7、Vue 3 和 TypeScript 构建的即时通讯
| 📤 | 消息转发 | ![完成](https://img.shields.io/badge/✅-完成-008080?style=flat&labelColor=e6f7f7&color=008080) |
| 📋 | 群公告功能 | ![完成](https://img.shields.io/badge/✅-完成-008080?style=flat&labelColor=e6f7f7&color=008080) |
| 🏷️ | 备注昵称管理 | ![完成](https://img.shields.io/badge/✅-完成-008080?style=flat&labelColor=e6f7f7&color=008080) |
| 📍 | 获取和发送位置 | ![进行中](https://img.shields.io/badge/🐣-进行中-ee9f20?style=flat&labelColor=fef7e6&color=ee9f20) |
### 🎨 界面体验
| 功能 | 描述 | 状态 |
@@ -133,6 +133,12 @@
</template>
<span>语音信息</span>
</n-popover>
<n-popover trigger="hover" :show-arrow="false" placement="bottom">
<template #trigger>
<svg @click="showLocationModal = true" class="mr-18px"><use href="#local"></use></svg>
</template>
<span>位置</span>
</n-popover>
</n-flex>
<n-popover trigger="hover" :show-arrow="false" placement="bottom">
@@ -150,6 +156,12 @@
<MsgInput ref="MsgInputRef" :height="inputAreaHeight" />
</div>
</div>
<!-- 位置选择弹窗 -->
<LocationModal
v-model:visible="showLocationModal"
@location-selected="handleLocationSelected"
@cancel="showLocationModal = false" />
</main>
</template>
@@ -158,6 +170,7 @@ import { join } from '@tauri-apps/api/path'
import { open } from '@tauri-apps/plugin-dialog'
import { copyFile, readFile } from '@tauri-apps/plugin-fs'
import { FOOTER_HEIGHT, MAX_FOOTER_HEIGHT, MIN_FOOTER_HEIGHT, TOOLBAR_HEIGHT } from '@/common/constants'
import LocationModal from '@/components/rightBox/location/LocationModal.vue'
import { MittEnum, MsgEnum, RoomTypeEnum } from '@/enums'
import { useChatLayoutGlobal } from '@/hooks/useChatLayout'
import { type SelectionRange, useCommon } from '@/hooks/useCommon.ts'
@@ -187,6 +200,7 @@ const MsgInputRef = ref()
const msgInputDom = ref<HTMLInputElement | null>(null)
const emojiShow = ref(false)
const recentlyTip = ref(false)
const showLocationModal = ref(false)
const isConceal = computed({
get: () => settingStore.screenshot.isConceal,
set: (value: boolean) => settingStore.setScreenshotConceal(value)
@@ -565,6 +579,28 @@ const handleVoiceRecord = () => {
useMitt.emit(MittEnum.VOICE_RECORD_TOGGLE)
}
// 处理位置选择
const handleLocationSelected = async (locationData: any) => {
try {
// 发送位置消息
const messageContent = {
type: MsgEnum.LOCATION,
body: {
latitude: locationData.latitude,
longitude: locationData.longitude,
address: locationData.address,
precision: locationData.precision,
timestamp: locationData.timestamp
}
}
console.log('发送位置消息:', messageContent)
showLocationModal.value = false
} catch (error) {
console.error('发送位置消息失败:', error)
}
}
// 打开聊天记录窗口
const openChatHistory = async () => {
const currentRoomId = globalStore.currentSession?.roomId
@@ -0,0 +1,132 @@
<template>
<div class="relative size-full">
<!-- 地图容器 -->
<TlbsMap
ref="mapRef"
:api-key="apiKey"
:center="mapCenter"
:zoom="zoom"
:map-type-id="'vector'"
:control="mapControl"
:style="{ height: `${height}px` }"
@map_inited="() => emit('map-ready')">
<!-- 位置标记 -->
<TlbsMultiMarker
id="location-marker"
:styles="markerStyles"
:geometries="markerGeometries"
@click="handleMarkerClick"
@dragend="handleMarkerDragEnd" />
</TlbsMap>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
type LocationData = {
latitude: number
longitude: number
address?: string
timestamp: number
}
type LocationMapProps = {
location: LocationData
zoom?: number
height?: number
draggable?: boolean
}
type LocationMapEmits = {
'location-change': [location: { lat: number; lng: number }]
'map-ready': []
'map-error': [error: string]
}
const props = withDefaults(defineProps<LocationMapProps>(), {
zoom: 15,
height: 300,
draggable: true
})
const emit = defineEmits<LocationMapEmits>()
// 地图实例和状态
const mapRef = ref()
// 计算属性
const apiKey = computed(() => import.meta.env.VITE_TENCENT_MAP_KEY)
const mapCenter = computed(() => ({
lat: props.location.latitude,
lng: props.location.longitude
}))
// 地图配置
const mapControl = {
scale: true,
zoom: false,
mapType: false
}
const markerStyles = {
'current-location': {
width: 25,
height: 35,
anchor: { x: 12, y: 35 },
color: '#FF4444'
}
}
const markerGeometries = computed(() => [
{
id: 'current',
styleId: 'current-location',
position: mapCenter.value,
properties: { title: '当前位置' },
draggable: props.draggable
}
])
// 事件处理
const handleMarkerClick = (event: any) => {
if (props.draggable) {
emit('location-change', {
lat: event.latLng.lat,
lng: event.latLng.lng
})
}
}
const handleMarkerDragEnd = (event: any) => {
emit('location-change', {
lat: event.geometry.position.lat,
lng: event.geometry.position.lng
})
}
// 响应式监听
watch(
() => props.location,
(newLocation) => {
if (mapRef.value?.map && newLocation) {
mapRef.value.map.setCenter({
lat: newLocation.latitude,
lng: newLocation.longitude
})
}
},
{ deep: true }
)
watch(
() => props.zoom,
(newZoom) => {
if (mapRef.value?.map) {
mapRef.value.map.setZoom(newZoom)
}
}
)
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,247 @@
<template>
<n-modal v-model:show="modalVisible" :mask-closable="false" class="rounded-8px" transform-origin="center">
<div class="h-full w-480px bg-[--bg-edit] box-border flex flex-col items-center justify-between">
<!-- 标题栏 -->
<n-flex :size="6" vertical class="w-full">
<div
v-if="isMac()"
@click="modalVisible = false"
class="mac-close size-13px shadow-inner bg-#ed6a5eff rounded-50% mt-6px select-none absolute left-6px">
<svg class="hidden size-7px color-#000 select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
<n-flex class="text-(14px [--text-color]) select-none pt-6px" justify="center">{{ modalTitle }}</n-flex>
<svg
v-if="isWindows()"
class="size-14px cursor-pointer pt-6px select-none absolute right-6px"
@click="modalVisible = false">
<use href="#close"></use>
</svg>
<span class="h-1px w-full bg-[--line-color]"></span>
</n-flex>
<!-- 地图加载错误 -->
<div v-if="mapError" class="h-340px flex-center">
<n-result status="error" title="地图加载失败" :description="mapError">
<template #footer>
<n-flex justify="center" :size="12">
<n-button secondary @click="modalVisible = false">取消</n-button>
<n-button type="primary" secondary @click="retryMapLoad">重试</n-button>
</n-flex>
</template>
</n-result>
</div>
<!-- 位置获取失败 -->
<div v-else-if="locationState.error && !selectedLocation" class="h-340px flex-center">
<n-result status="warning" title="位置获取失败" :description="locationState.error">
<template #footer>
<n-flex justify="center" :size="12">
<n-button secondary @click="modalVisible = false">取消</n-button>
<n-button type="primary" secondary @click="relocate">重试</n-button>
</n-flex>
</template>
</n-result>
</div>
<!-- 地图容器 -->
<div v-else class="flex flex-col gap-16px p-8px">
<!-- 地图区域 -->
<div class="relative rounded-8px overflow-hidden flex-center h-340px">
<!-- 地图加载中 -->
<div v-if="locationState.loading || mapLoading" class="flex-col-center gap-42px">
<n-spin :size="42" />
<p class="text-(14px [--text-cplor])">{{ locationState.loading ? '正在获取位置...' : '地图加载中...' }}</p>
</div>
<!-- 地图组件 -->
<LocationMap
v-else-if="selectedLocation"
:location="selectedLocation"
:zoom="18"
:height="340"
@location-change="handleLocationChange"
@map-ready="() => (mapLoading = false)"
@map-error="handleMapError" />
</div>
<!-- 位置信息显示 -->
<div v-if="selectedLocation" class="rounded-6px bg-#fefefe dark:bg-#303030 p-12px">
<n-flex vertical :size="8">
<span class="text-14px font-medium">当前位置</span>
<div class="text-12px text-gray-500">
{{ selectedLocation.address || '获取地址中...' }}
</div>
<div class="text-11px text-gray-400">
坐标: {{ selectedLocation.latitude.toFixed(6) }}, {{ selectedLocation.longitude.toFixed(6) }}
</div>
</n-flex>
</div>
</div>
<!-- 操作按钮 -->
<n-flex v-if="showActionButtons" align="center" :size="24" class="py-8px">
<n-button type="primary" secondary :loading="sendingLocation" @click="handleConfirm">发送位置</n-button>
</n-flex>
</div>
</n-modal>
</template>
<script setup lang="ts">
import { useGeolocation } from '@/hooks/useGeolocation'
import { reverseGeocode } from '@/services/mapApi'
import { isMac, isWindows } from '@/utils/PlatformConstants'
import LocationMap from './LocationMap.vue'
type LocationData = {
latitude: number
longitude: number
address?: string
timestamp: number
}
type LocationModalProps = {
visible: boolean
}
type LocationModalEmits = {
'update:visible': [visible: boolean]
'location-selected': [location: LocationData]
cancel: []
}
const props = withDefaults(defineProps<LocationModalProps>(), {
visible: false
})
const emit = defineEmits<LocationModalEmits>()
// 地理位置Hook
const { state: locationState, getLocationWithTransform } = useGeolocation()
// 响应式状态
const modalVisible = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
})
const selectedLocation = ref<LocationData | null>(null)
const mapLoading = ref(false)
const mapError = ref<string | null>(null)
const sendingLocation = ref(false)
// 计算属性
const modalTitle = computed(() => {
if (mapError.value) return '地图错误'
if (locationState.error) return '位置获取失败'
return '选择位置'
})
const showActionButtons = computed(() => {
return !mapLoading.value && !locationState.loading && selectedLocation.value !== null && !mapError.value
})
// 获取位置
const getLocation = async () => {
try {
mapError.value = null
const result = await getLocationWithTransform({
enableHighAccuracy: true
})
// 获取地址信息
const geocodeResult = await reverseGeocode(result.transformed.lat, result.transformed.lng).catch((error) => {
console.warn('获取地址失败:', error)
return null
})
const address = geocodeResult?.formatted_addresses?.recommend || geocodeResult?.address || '未知地址'
selectedLocation.value = {
latitude: result.transformed.lat,
longitude: result.transformed.lng,
address,
timestamp: result.timestamp
}
} catch (error) {
console.error('获取位置失败:', error)
}
}
// 监听弹窗显示
watch(modalVisible, (visible) => {
if (visible) {
// 重置状态
selectedLocation.value = null
mapError.value = null
mapLoading.value = false
// 获取位置
getLocation()
}
})
// 重新定位
const relocate = async () => {
selectedLocation.value = null
mapError.value = null
await getLocation()
}
// 重试地图加载
const retryMapLoad = () => {
mapError.value = null
mapLoading.value = true
// 触发地图重新加载
if (selectedLocation.value) {
// 重新创建地图组件
const currentLocation = selectedLocation.value
selectedLocation.value = null
nextTick(() => {
selectedLocation.value = currentLocation
mapLoading.value = false
})
}
}
// 地图事件处理
const handleLocationChange = async (newLocation: { lat: number; lng: number }) => {
if (!selectedLocation.value) return
// 获取新位置的地址
const geocodeResult = await reverseGeocode(newLocation.lat, newLocation.lng).catch((error) => {
console.warn('获取地址失败:', error)
return null
})
const address =
geocodeResult?.formatted_addresses?.recommend || geocodeResult?.address || selectedLocation.value.address
selectedLocation.value = {
...selectedLocation.value,
latitude: newLocation.lat,
longitude: newLocation.lng,
address,
timestamp: Date.now()
}
}
const handleMapError = (error: string) => {
mapError.value = error
mapLoading.value = false
}
// 确认发送位置
const handleConfirm = async () => {
if (!selectedLocation.value) return
sendingLocation.value = true
emit('location-selected', selectedLocation.value)
modalVisible.value = false
sendingLocation.value = false
}
</script>
<style scoped lang="scss"></style>
-1
View File
@@ -1,6 +1,5 @@
import { BaseDirectory, exists, mkdir, writeFile } from '@tauri-apps/plugin-fs'
import { createEventHook } from '@vueuse/core'
import { ref } from 'vue'
export const useDownload = () => {
const process = ref(0)
+132
View File
@@ -0,0 +1,132 @@
import { transformCoordinates } from '@/services/mapApi'
type GeolocationState = {
loading: boolean
error: string | null
position: GeolocationPosition | null
permission: PermissionState | null
precision: 'high' | 'low'
}
type GeolocationOptions = {
enableHighAccuracy?: boolean
timeout?: number
maximumAge?: number
}
export const useGeolocation = () => {
const state = ref<GeolocationState>({
loading: false,
error: null,
position: null,
permission: null,
precision: 'high'
})
const isSupported = computed(() => 'geolocation' in navigator)
const hasPermission = computed(() => state.value.permission === 'granted')
const isLoading = computed(() => state.value.loading)
const error = computed(() => state.value.error)
const currentPosition = computed(() => state.value.position)
// 检查权限状态
const checkPermission = async (): Promise<PermissionState> => {
if ('permissions' in navigator) {
try {
const permission = await navigator.permissions.query({ name: 'geolocation' })
state.value.permission = permission.state
return permission.state
} catch (error) {
console.warn('检查地理位置权限失败:', error)
}
}
return 'prompt'
}
// 获取当前位置
const getCurrentPosition = async (options?: GeolocationOptions): Promise<GeolocationPosition> => {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('浏览器不支持地理位置功能'))
return
}
const defaultOptions: PositionOptions = {
enableHighAccuracy: state.value.precision === 'high',
timeout: 10000,
maximumAge: 300000, // 5分钟缓存
...options
}
state.value.loading = true
state.value.error = null
navigator.geolocation.getCurrentPosition(
(position) => {
state.value.loading = false
state.value.position = position
resolve(position)
},
(error) => {
state.value.loading = false
let errorMessage = '获取位置失败'
switch (error.code) {
case error.PERMISSION_DENIED:
errorMessage = '位置权限被拒绝'
break
case error.POSITION_UNAVAILABLE:
errorMessage = '位置信息不可用'
break
case error.TIMEOUT:
errorMessage = '获取位置超时'
break
}
state.value.error = errorMessage
reject(new Error(errorMessage))
},
defaultOptions
)
})
}
// 获取位置并转换坐标
const getLocationWithTransform = async (options?: GeolocationOptions) => {
const position = await getCurrentPosition(options)
const { latitude, longitude } = position.coords
// 转换坐标
const transformed = await transformCoordinates(latitude, longitude)
return {
original: { lat: latitude, lng: longitude },
transformed,
position,
address: '', // 后续可以通过逆地理编码获取
precision: state.value.precision,
timestamp: Date.now()
}
}
// 清除错误状态
const clearError = () => {
state.value.error = null
}
return {
// 状态
state: state.value,
isSupported,
hasPermission,
isLoading,
error,
currentPosition,
// 方法
checkPermission,
getCurrentPosition,
getLocationWithTransform,
clearError
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import 'uno.css'
import '@unocss/reset/eric-meyer.css' // unocss提供的浏览器默认样式重置
import TlbsMap from 'tlbs-map-vue'
import { initMobileClient } from '#/mobile-client/MobileClient'
import App from '@/App.vue'
import { AppException } from '@/common/exception.ts'
@@ -14,7 +15,7 @@ initializePlatform()
import('@/services/webSocketAdapter')
const app = createApp(App)
app.use(router).use(pinia).directive('resize', vResize).directive('slide', vSlide).mount('#app')
app.use(router).use(pinia).use(TlbsMap).directive('resize', vResize).directive('slide', vSlide).mount('#app')
app.config.errorHandler = (err) => {
if (err instanceof AppException) {
window.$message.error(err.message)
+165
View File
@@ -0,0 +1,165 @@
import { wgs84ToGcj02 } from '@/utils/CoordinateTransform'
type TransformedCoordinate = {
lat: number
lng: number
}
type AddressComponent = {
province: string
city: string
district: string
street: string
street_number: string
}
type ReverseGeocodeResult = {
address: string
formatted_addresses: {
recommend: string
rough: string
}
address_component: AddressComponent
ad_info: {
nation_code: string
adcode: string
city_code: string
}
}
// JSONP回调函数存储
const jsonpCallbacks: { [key: string]: (data: any) => void } = {}
// 创建JSONP请求
const createJsonpRequest = (url: string, callbackName: string): Promise<any> => {
return new Promise((resolve, reject) => {
// 创建script标签
const script = document.createElement('script')
const timeoutId = setTimeout(() => {
cleanup()
reject(new Error('请求超时'))
}, 10000)
const cleanup = () => {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
delete (window as any)[callbackName]
delete jsonpCallbacks[callbackName]
clearTimeout(timeoutId)
}
// 设置全局回调函数
jsonpCallbacks[callbackName] = (data: any) => {
cleanup()
resolve(data)
}
;(window as any)[callbackName] = jsonpCallbacks[callbackName]
script.onerror = () => {
cleanup()
reject(new Error('脚本加载失败'))
}
script.src = `${url}&callback=${callbackName}`
document.head.appendChild(script)
})
}
// 坐标系转换(WGS84 -> GCJ-02
export const transformCoordinates = async (lat: number, lng: number): Promise<TransformedCoordinate> => {
// 验证坐标范围
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
throw new Error('坐标范围无效')
}
const callbackName = `coordTransform_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
const params = {
locations: `${lat},${lng}`,
type: '1', // GPS坐标(WGS84)
key: import.meta.env.VITE_TENCENT_MAP_KEY || '',
output: 'jsonp',
from: '1', // 明确指定源坐标系为GPS
to: '5' // 明确指定目标坐标系为GCJ02
}
try {
const queryString = new URLSearchParams(params).toString()
const url = `https://apis.map.qq.com/ws/coord/v1/translate?${queryString}`
console.log('腾讯地图坐标转换API请求:', { url, params, callbackName })
// 验证API密钥
if (!params.key) {
throw new Error('腾讯地图API密钥未配置')
}
const data = await createJsonpRequest(url, callbackName)
console.log('腾讯地图API响应:', data)
if (data.status !== 0) {
const errorMsg = data.message || `状态码: ${data.status}`
throw new Error(`API错误: ${data.status} - ${errorMsg}`)
}
const location = data.locations?.[0]
if (!location) {
throw new Error('转换结果为空')
}
// 验证返回的坐标
if (typeof location.lat !== 'number' || typeof location.lng !== 'number') {
throw new Error('API返回的坐标格式无效')
}
const transformed = {
lat: location.lat,
lng: location.lng
}
console.debug('坐标转换成功:', { original: { lat, lng }, transformed })
return transformed
} catch (error) {
console.warn('腾讯地图API坐标转换失败,使用本地算法转换:', error)
// 降级方案:使用本地坐标转换算法
const localTransformed = wgs84ToGcj02(lat, lng)
return localTransformed
}
}
// 逆地理编码(获取地址信息)
export const reverseGeocode = async (lat: number, lng: number): Promise<ReverseGeocodeResult | null> => {
// 验证坐标范围
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
throw new Error('坐标范围无效')
}
const callbackName = `geocode_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
const params = {
location: `${lat},${lng}`,
key: import.meta.env.VITE_TENCENT_MAP_KEY || '',
get_poi: '1',
output: 'jsonp'
}
try {
const queryString = new URLSearchParams(params as any).toString()
const url = `https://apis.map.qq.com/ws/geocoder/v1/?${queryString}`
const data = await createJsonpRequest(url, callbackName)
if (data.status !== 0) {
throw new Error(`API错误: ${data.status} - ${data.message || '未知错误'}`)
}
return data.result
} catch (error) {
console.warn('腾讯地图API逆地理编码失败:', error)
return null
}
}
+6 -8
View File
@@ -37,6 +37,12 @@ declare module 'vue' {
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
LoadingSpinner: typeof import('./../components/common/LoadingSpinner.vue')['default']
LocationDetailModal: typeof import('./../components/rightBox/location/LocationDetailModal.vue')['default']
LocationMap: typeof import('./../components/rightBox/location/LocationMap.vue')['default']
LocationMessage: typeof import('./../components/rightBox/location/LocationMessage.vue')['default']
LocationModal: typeof import('./../components/rightBox/location/LocationModal.vue')['default']
LocationPermission: typeof import('./../components/common/LocationPermission.vue')['default']
LocationPrecision: typeof import('./../components/common/LocationPrecision.vue')['default']
MeasuredItem: typeof import('./../mobile/components/virtual-scroll/MeasuredItem.vue')['default']
MergeMessage: typeof import('./../components/rightBox/renderMessage/MergeMessage.vue')['default']
MessageContainer: typeof import('./../mobile/components/chat-room/MessageContainer.vue')['default']
@@ -44,7 +50,6 @@ declare module 'vue' {
MsgInput: typeof import('./../components/rightBox/MsgInput.vue')['default']
MyMessageItem: typeof import('./../mobile/components/my/MyMessageItem.vue')['default']
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
NAvatar: typeof import('naive-ui')['NAvatar']
NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
@@ -56,7 +61,6 @@ declare module 'vue' {
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDatePicker: typeof import('naive-ui')['NDatePicker']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
@@ -73,28 +77,23 @@ declare module 'vue' {
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
NImage: typeof import('naive-ui')['NImage']
NImageGroup: typeof import('naive-ui')['NImageGroup']
NInfiniteScroll: typeof import('naive-ui')['NInfiniteScroll']
NInput: typeof import('naive-ui')['NInput']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NPopselect: typeof import('naive-ui')['NPopselect']
NProgress: typeof import('naive-ui')['NProgress']
NQrCode: typeof import('naive-ui')['NQrCode']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NResult: typeof import('naive-ui')['NResult']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NStep: typeof import('naive-ui')['NStep']
NSteps: typeof import('naive-ui')['NSteps']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane']
@@ -121,7 +120,6 @@ declare module 'vue' {
SystemMessage: typeof import('./../components/rightBox/renderMessage/special/SystemMessage.vue')['default']
Text: typeof import('./../components/rightBox/renderMessage/Text.vue')['default']
Validation: typeof import('./../components/common/Validation.vue')['default']
VanIcon: typeof import('vant/es')['Icon']
VanPullRefresh: typeof import('vant/es')['PullRefresh']
VanSwipeCell: typeof import('vant/es')['SwipeCell']
VanUploader: typeof import('vant/es')['Uploader']