feat: improve map and upload experience
This commit is contained in:
@@ -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">支持 JPG、PNG 格式,可一次选择多张</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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user