feat: improve map and upload experience

This commit is contained in:
2026-05-21 16:54:38 +08:00
parent cb2581afb2
commit 2b7f393d7c
6 changed files with 386 additions and 85 deletions
+53 -73
View File
@@ -3,19 +3,16 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useCloudsStore } from '@/stores/clouds'
import { useUpload } from '@/composables/useUpload'
import { loadAMap } from '@/lib/amap'
const router = useRouter()
const cloudsStore = useCloudsStore()
const { items, uploading, overallProgress, currentItemIndex, totalItems, addFiles, removeItem, validateAll, uploadAll } = useUpload()
const { items, uploading, overallProgress, currentItemIndex, totalItems, addFiles, removeItem, clearAll, validateAll, uploadAll } = useUpload()
const activeId = ref<string | null>(null)
const dragOver = ref(false)
const successMsg = ref(false)
const errorMsg = ref('')
const fileInput = ref<HTMLInputElement | null>(null)
const locating = ref(false)
const locationError = ref('')
const activeItem = computed(() => {
if (!activeId.value) return null
@@ -80,46 +77,21 @@ async function handleSubmit() {
}
}
async function getLocation() {
if (!activeItem.value) return
locating.value = true
locationError.value = ''
function onLatInput(e: Event) {
const val = parseFloat((e.target as HTMLInputElement).value)
if (activeItem.value) {
activeItem.value.latitude = isNaN(val) ? null : val
if (activeItem.value.errors.latitude) delete activeItem.value.errors.latitude
if (activeItem.value.errors.longitude) delete activeItem.value.errors.longitude
}
}
try {
const AMap = await loadAMap()
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true,
timeout: 10000,
})
await new Promise<void>((resolve, reject) => {
geolocation.getCurrentPosition(
(data: { position: { lng: number; lat: number } }) => {
if (activeItem.value) {
activeItem.value.latitude = data.position.lat
activeItem.value.longitude = data.position.lng
}
resolve()
},
() => reject(new Error('AMap error')),
)
})
} catch {
try {
const pos = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
})
})
if (activeItem.value) {
activeItem.value.latitude = pos.coords.latitude
activeItem.value.longitude = pos.coords.longitude
}
} catch {
locationError.value = '无法获取位置,请确认定位权限已开启,或手动输入'
}
} finally {
locating.value = false
function onLngInput(e: Event) {
const val = parseFloat((e.target as HTMLInputElement).value)
if (activeItem.value) {
activeItem.value.longitude = isNaN(val) ? null : val
if (activeItem.value.errors.latitude) delete activeItem.value.errors.latitude
if (activeItem.value.errors.longitude) delete activeItem.value.errors.longitude
}
}
@@ -158,10 +130,8 @@ onUnmounted(() => { for (const item of items.value) URL.revokeObjectURL(item.pre
<template>
<div class="max-w-6xl mx-auto px-4 py-8">
<!-- 全局隐藏文件输入 -->
<input ref="fileInput" type="file" accept="image/*" multiple class="hidden" @change="handleFileSelect" />
<!-- 成功提示 -->
<div v-if="successMsg" class="flex items-center justify-center min-h-[60vh]">
<div class="text-center">
<span class="text-5xl block mb-4"></span>
@@ -173,7 +143,6 @@ onUnmounted(() => { for (const item of items.value) URL.revokeObjectURL(item.pre
<template v-else>
<h1 class="text-2xl font-bold text-gray-900 mb-6">📷 上传云图</h1>
<!-- 上传进度 -->
<div v-if="uploading" class="mb-6">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700">正在上传 {{ currentItemIndex }} / {{ totalItems }}...</span>
@@ -184,7 +153,6 @@ onUnmounted(() => { for (const item of items.value) URL.revokeObjectURL(item.pre
</div>
</div>
<!-- 空状态 -->
<div
v-if="items.length === 0"
@dragover.prevent="dragOver = true"
@@ -199,10 +167,8 @@ onUnmounted(() => { for (const item of items.value) URL.revokeObjectURL(item.pre
<p class="text-sm text-gray-400">支持 JPGPNG 格式可一次选择多张</p>
</div>
<!-- 编辑界面 -->
<template v-else>
<div class="flex gap-6">
<!-- 左侧 -->
<div class="flex-shrink-0 w-[480px]">
<div v-if="activeItem" class="bg-gray-900 rounded-xl overflow-hidden mb-3">
<img :src="activeItem.preview" alt="预览" class="w-full h-[360px] object-contain" />
@@ -232,7 +198,6 @@ onUnmounted(() => { for (const item of items.value) URL.revokeObjectURL(item.pre
</div>
</div>
<!-- 右侧表单 -->
<div class="flex-1 min-w-0" v-if="activeItem">
<div class="bg-white border border-gray-200 rounded-xl p-6 space-y-5">
<div class="flex items-center justify-between">
@@ -282,29 +247,45 @@ onUnmounted(() => { for (const item of items.value) URL.revokeObjectURL(item.pre
<p v-if="activeItem.errors.capturedAt" class="text-sm text-red-500 mt-1">{{ activeItem.errors.capturedAt }}</p>
</div>
<!-- 位置 -->
<!-- 经纬度 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">位置</label>
<div class="flex gap-2">
<input
v-model="activeItem.locationName"
type="text"
placeholder="城市或区域名"
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
/>
<button
@click="getLocation"
type="button"
:disabled="locating"
class="px-3 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 transition-colors text-sm whitespace-nowrap disabled:opacity-50"
>
{{ locating ? '定位中...' : '📍 获取位置' }}
</button>
<label class="block text-sm font-medium text-gray-700 mb-1">经纬度</label>
<div class="grid grid-cols-2 gap-2">
<div>
<input
:value="activeItem.latitude !== null ? activeItem.latitude : ''"
@input="onLatInput"
type="number"
step="any"
placeholder="纬度(如 39.90"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
/>
<p v-if="activeItem.errors.latitude" class="text-sm text-red-500 mt-1">{{ activeItem.errors.latitude }}</p>
</div>
<div>
<input
:value="activeItem.longitude !== null ? activeItem.longitude : ''"
@input="onLngInput"
type="number"
step="any"
placeholder="经度(如 116.40"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
/>
<p v-if="activeItem.errors.longitude" class="text-sm text-red-500 mt-1">{{ activeItem.errors.longitude }}</p>
</div>
</div>
<p v-if="locationError" class="text-xs text-red-400 mt-1">{{ locationError }}</p>
<p v-else-if="activeItem.latitude && activeItem.longitude" class="text-xs text-gray-400 mt-1">
已获取坐标 ({{ activeItem.latitude.toFixed(2) }}, {{ activeItem.longitude.toFixed(2) }})
</p>
<p class="text-xs text-gray-400 mt-1">选填纬度范围 -90 ~ 90经度范围 -180 ~ 180</p>
</div>
<!-- 位置名称 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">位置名称</label>
<input
v-model="activeItem.locationName"
type="text"
placeholder="如:北京、成都"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-colors"
/>
</div>
<!-- 图片描述 -->
@@ -327,12 +308,11 @@ onUnmounted(() => { for (const item of items.value) URL.revokeObjectURL(item.pre
</div>
</div>
<!-- 底部操作栏 -->
<div class="mt-6 flex items-center justify-between">
<p class="text-sm text-gray-500"> {{ items.length }} 张图片类别和拍摄时间为必填项</p>
<div class="flex gap-3">
<button
@click="() => { for (const i of items) URL.revokeObjectURL(i.preview); items = [] }"
@click="clearAll()"
:disabled="uploading"
class="px-5 py-2.5 border border-gray-300 text-gray-600 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
>