feat: enhance cloud image management
This commit is contained in:
+34
-174
@@ -5,8 +5,8 @@ import { useRouter } from 'vue-router'
|
||||
import { useCloudsStore } from '@/stores/clouds'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { downloadCloudBadgeCard } from '@/lib/cloudBadges'
|
||||
import { loadAMap } from '@/lib/amap'
|
||||
import { useUpload } from '@/composables/useUpload'
|
||||
import MapPickerModal from '@/components/cloud/MapPickerModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
@@ -20,15 +20,6 @@ const unlockedBadges = ref<Awaited<ReturnType<typeof uploadAll>>['unlockedBadges
|
||||
const errorMsg = ref('')
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const mapPickerOpen = ref(false)
|
||||
const mapPickerLoading = ref(false)
|
||||
const mapPickerError = ref('')
|
||||
const mapPickerLat = ref<number | null>(null)
|
||||
const mapPickerLng = ref<number | null>(null)
|
||||
const miniMapEl = ref<HTMLDivElement | null>(null)
|
||||
|
||||
let mapPickerAMap: typeof AMap | null = null
|
||||
let mapPickerInstance: AMap.Map | null = null
|
||||
let mapPickerMarker: AMap.Marker | null = null
|
||||
|
||||
const activeItem = computed(() => {
|
||||
if (!activeId.value) return null
|
||||
@@ -70,10 +61,6 @@ function handleRemove(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatCoordinate(value: number | null) {
|
||||
return value === null ? '未选择' : value.toFixed(6)
|
||||
}
|
||||
|
||||
function updateActiveCoordinates(latitude: number | null, longitude: number | null) {
|
||||
if (!activeItem.value) return
|
||||
|
||||
@@ -84,101 +71,18 @@ function updateActiveCoordinates(latitude: number | null, longitude: number | nu
|
||||
if (activeItem.value.errors.longitude) delete activeItem.value.errors.longitude
|
||||
}
|
||||
|
||||
function destroyMapPicker() {
|
||||
mapPickerMarker?.setMap(null)
|
||||
mapPickerMarker = null
|
||||
mapPickerInstance?.destroy()
|
||||
mapPickerInstance = null
|
||||
}
|
||||
|
||||
function placeMapPickerMarker(longitude: number, latitude: number) {
|
||||
if (!mapPickerAMap || !mapPickerInstance) return
|
||||
|
||||
if (!mapPickerMarker) {
|
||||
mapPickerMarker = new mapPickerAMap.Marker({
|
||||
position: [longitude, latitude],
|
||||
title: '拍摄位置',
|
||||
} as AMap.MarkerOptions)
|
||||
mapPickerMarker.setMap(mapPickerInstance)
|
||||
} else {
|
||||
mapPickerMarker.setPosition([longitude, latitude])
|
||||
}
|
||||
}
|
||||
|
||||
function extractLngLat(event: unknown) {
|
||||
const payload = event as { lnglat?: { lng?: number; lat?: number } } | undefined
|
||||
const lng = payload?.lnglat?.lng
|
||||
const lat = payload?.lnglat?.lat
|
||||
|
||||
if (typeof lng !== 'number' || typeof lat !== 'number') return null
|
||||
return { lng, lat }
|
||||
}
|
||||
|
||||
function handleMapPickerClick(event: unknown) {
|
||||
const picked = extractLngLat(event)
|
||||
if (!picked) return
|
||||
|
||||
mapPickerLng.value = picked.lng
|
||||
mapPickerLat.value = picked.lat
|
||||
placeMapPickerMarker(picked.lng, picked.lat)
|
||||
}
|
||||
|
||||
async function openMapPicker() {
|
||||
if (!activeItem.value) return
|
||||
|
||||
mapPickerOpen.value = true
|
||||
mapPickerLoading.value = true
|
||||
mapPickerError.value = ''
|
||||
mapPickerLat.value = activeItem.value.latitude
|
||||
mapPickerLng.value = activeItem.value.longitude
|
||||
|
||||
await nextTick()
|
||||
destroyMapPicker()
|
||||
|
||||
try {
|
||||
mapPickerAMap = await loadAMap()
|
||||
|
||||
const hasCoordinates =
|
||||
typeof activeItem.value.latitude === 'number' &&
|
||||
typeof activeItem.value.longitude === 'number'
|
||||
|
||||
const center: [number, number] = hasCoordinates
|
||||
? [activeItem.value.longitude as number, activeItem.value.latitude as number]
|
||||
: [104.07, 30.67]
|
||||
|
||||
mapPickerInstance = new mapPickerAMap.Map(miniMapEl.value!, {
|
||||
viewMode: '2D',
|
||||
pitch: 0,
|
||||
rotation: 0,
|
||||
zoom: hasCoordinates ? 11 : 4,
|
||||
center,
|
||||
mapStyle: 'amap://styles/normal',
|
||||
resizeEnable: true,
|
||||
} as AMap.MapOptions)
|
||||
|
||||
mapPickerInstance.on('click', handleMapPickerClick)
|
||||
|
||||
if (hasCoordinates) {
|
||||
placeMapPickerMarker(activeItem.value.longitude as number, activeItem.value.latitude as number)
|
||||
}
|
||||
} catch (error) {
|
||||
mapPickerError.value = error instanceof Error ? error.message : '地图加载失败'
|
||||
} finally {
|
||||
mapPickerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeMapPicker() {
|
||||
mapPickerOpen.value = false
|
||||
mapPickerLoading.value = false
|
||||
mapPickerError.value = ''
|
||||
destroyMapPicker()
|
||||
}
|
||||
|
||||
function confirmMapPicker() {
|
||||
if (mapPickerLat.value === null || mapPickerLng.value === null) return
|
||||
|
||||
updateActiveCoordinates(mapPickerLat.value, mapPickerLng.value)
|
||||
function confirmMapPicker(payload: { latitude: number; longitude: number }) {
|
||||
updateActiveCoordinates(payload.latitude, payload.longitude)
|
||||
closeMapPicker()
|
||||
}
|
||||
|
||||
@@ -186,6 +90,11 @@ function clearCoordinates() {
|
||||
updateActiveCoordinates(null, null)
|
||||
}
|
||||
|
||||
function updateActivePublicState(isPublic: boolean) {
|
||||
if (!activeItem.value) return
|
||||
activeItem.value.isHidden = !isPublic
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
errorMsg.value = ''
|
||||
const allValid = validateAll()
|
||||
@@ -289,7 +198,6 @@ function onCategoryChange(e: Event) {
|
||||
onMounted(() => { cloudsStore.fetchCloudTypes() })
|
||||
onUnmounted(() => {
|
||||
for (const item of items.value) URL.revokeObjectURL(item.preview)
|
||||
destroyMapPicker()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -316,9 +224,9 @@ onUnmounted(() => {
|
||||
<template v-if="unlockedBadges.length">
|
||||
新点亮了 {{ unlockedBadges.length }} 枚图鉴徽章。
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- <template v-else>
|
||||
这批云图已经进入你的收藏记录。
|
||||
</template>
|
||||
</template> -->
|
||||
</p>
|
||||
</template>
|
||||
</NResult>
|
||||
@@ -542,10 +450,21 @@ onUnmounted(() => {
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 隐身模式 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="activeItem.isHidden" type="checkbox" :id="'isHidden-' + activeItem.id" class="w-4 h-4 text-sky-500 border-gray-300 rounded focus:ring-sky-500" />
|
||||
<label :for="'isHidden-' + activeItem.id" class="text-sm text-gray-600">隐身模式(不在地图上显示)</label>
|
||||
<!-- 公开状态 -->
|
||||
<div class="border border-slate-200 bg-slate-50 px-4 py-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-800">公开展示</p>
|
||||
<p class="mt-1 text-xs text-gray-500">关闭后不会出现在画廊、地图和公开主页中。</p>
|
||||
</div>
|
||||
<input
|
||||
:checked="!activeItem.isHidden"
|
||||
type="checkbox"
|
||||
:id="'isPublic-' + activeItem.id"
|
||||
class="h-5 w-5 border-gray-300 text-sky-500 focus:ring-sky-500"
|
||||
@change="updateActivePublicState(($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
@@ -570,72 +489,13 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="mapPickerOpen"
|
||||
class="fixed inset-0 z-[120] flex items-center justify-center bg-slate-950/55 px-4 py-6"
|
||||
@click="closeMapPicker"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-3xl overflow-hidden bg-white shadow-2xl"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-start justify-between border-b border-gray-200 px-6 py-5">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900">地图选点</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">点击地图即可回填当前图片的经纬度,也可以继续手动修改。</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="closeMapPicker"
|
||||
class="rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-5">
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3 border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
||||
<span>当前选择:</span>
|
||||
<span class="font-medium text-slate-900">纬度 {{ formatCoordinate(mapPickerLat) }}</span>
|
||||
<span class="font-medium text-slate-900">经度 {{ formatCoordinate(mapPickerLng) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="relative overflow-hidden border border-gray-200 bg-slate-100">
|
||||
<div ref="miniMapEl" class="h-[420px] w-full"></div>
|
||||
|
||||
<div
|
||||
v-if="mapPickerLoading"
|
||||
class="absolute inset-0 flex items-center justify-center bg-white/80 text-sm font-medium text-slate-600"
|
||||
>
|
||||
正在加载地图...
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="mapPickerError"
|
||||
class="absolute inset-0 flex items-center justify-center bg-white/90 px-6 text-center text-sm text-red-600"
|
||||
>
|
||||
{{ mapPickerError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-gray-200 px-6 py-4">
|
||||
<p class="text-sm text-gray-500">建议点选大致拍摄位置,上传时会继续做模糊化处理。</p>
|
||||
<div class="flex gap-3">
|
||||
<NButton secondary strong @click="closeMapPicker">取消</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
secondary
|
||||
strong
|
||||
:disabled="mapPickerLat === null || mapPickerLng === null"
|
||||
@click="confirmMapPicker"
|
||||
>
|
||||
使用这个位置
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
<MapPickerModal
|
||||
:open="mapPickerOpen"
|
||||
:latitude="activeItem?.latitude ?? null"
|
||||
:longitude="activeItem?.longitude ?? null"
|
||||
description="点击地图即可回填当前图片的经纬度,也可以继续手动修改。"
|
||||
footer-text="建议点选大致拍摄位置,上传时会继续做模糊化处理。"
|
||||
@close="closeMapPicker"
|
||||
@confirm="confirmMapPicker"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user