feat: enhance cloud image management
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { NAlert, NButton } from 'naive-ui'
|
||||
import MapPickerModal from '@/components/cloud/MapPickerModal.vue'
|
||||
import type { CloudType } from '@/types/database'
|
||||
|
||||
export interface CloudEditFormValue {
|
||||
cloudCategoryId: number | 'other' | null
|
||||
customCloudType: string
|
||||
capturedAt: string
|
||||
latitude: number | null
|
||||
longitude: number | null
|
||||
locationName: string
|
||||
description: string
|
||||
isPublic: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
open: boolean
|
||||
saving?: boolean
|
||||
error?: string
|
||||
cloudTypes: CloudType[]
|
||||
initialValue: CloudEditFormValue | null
|
||||
}>(), {
|
||||
saving: false,
|
||||
error: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
save: [value: CloudEditFormValue]
|
||||
}>()
|
||||
|
||||
const form = ref<CloudEditFormValue>(createEmptyForm())
|
||||
const mapPickerOpen = ref(false)
|
||||
|
||||
const hasCoordinates = computed(() => form.value.latitude !== null || form.value.longitude !== null)
|
||||
|
||||
function createEmptyForm(): CloudEditFormValue {
|
||||
return {
|
||||
cloudCategoryId: null,
|
||||
customCloudType: '',
|
||||
capturedAt: '',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
locationName: '',
|
||||
description: '',
|
||||
isPublic: true,
|
||||
}
|
||||
}
|
||||
|
||||
function cloneForm(value: CloudEditFormValue | null) {
|
||||
return value ? { ...value } : createEmptyForm()
|
||||
}
|
||||
|
||||
function onCategoryChange(event: Event) {
|
||||
const value = (event.target as HTMLSelectElement).value
|
||||
if (value === 'other') {
|
||||
form.value.cloudCategoryId = 'other'
|
||||
} else if (!value) {
|
||||
form.value.cloudCategoryId = null
|
||||
} else {
|
||||
form.value.cloudCategoryId = Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
function onLatInput(event: Event) {
|
||||
const value = Number.parseFloat((event.target as HTMLInputElement).value)
|
||||
form.value.latitude = Number.isNaN(value) ? null : value
|
||||
}
|
||||
|
||||
function onLngInput(event: Event) {
|
||||
const value = Number.parseFloat((event.target as HTMLInputElement).value)
|
||||
form.value.longitude = Number.isNaN(value) ? null : value
|
||||
}
|
||||
|
||||
function clearCoordinates() {
|
||||
form.value.latitude = null
|
||||
form.value.longitude = null
|
||||
}
|
||||
|
||||
function confirmMapPicker(payload: { latitude: number; longitude: number }) {
|
||||
form.value.latitude = payload.latitude
|
||||
form.value.longitude = payload.longitude
|
||||
mapPickerOpen.value = false
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('save', { ...form.value })
|
||||
}
|
||||
|
||||
watch(() => props.open, open => {
|
||||
if (open) {
|
||||
form.value = cloneForm(props.initialValue)
|
||||
} else {
|
||||
mapPickerOpen.value = false
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.initialValue, value => {
|
||||
if (props.open) {
|
||||
form.value = cloneForm(value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="open"
|
||||
class="fixed inset-0 z-[130] flex items-center justify-center bg-slate-950/60 px-4 py-6"
|
||||
@click="close"
|
||||
>
|
||||
<div class="w-full max-w-2xl bg-white shadow-2xl" @click.stop>
|
||||
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-slate-900">编辑图片信息</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">修改后会同步影响个人主页、画廊和地图中的展示。</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||
@click="close"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="max-h-[70vh] overflow-y-auto px-6 py-5">
|
||||
<NAlert
|
||||
v-if="error"
|
||||
class="mb-5"
|
||||
type="error"
|
||||
:show-icon="false"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ error }}
|
||||
</NAlert>
|
||||
|
||||
<div class="grid gap-5">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">类别</label>
|
||||
<select
|
||||
:value="form.cloudCategoryId ?? ''"
|
||||
class="w-full border border-slate-300 bg-white px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<option value="" disabled>请选择</option>
|
||||
<option v-for="ct in cloudTypes" :key="ct.id" :value="ct.id">
|
||||
{{ ct.name }}({{ ct.name_en }})
|
||||
</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="form.cloudCategoryId === 'other'">
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">自定义云型</label>
|
||||
<input
|
||||
v-model="form.customCloudType"
|
||||
type="text"
|
||||
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">拍摄时间</label>
|
||||
<input
|
||||
v-model="form.capturedAt"
|
||||
type="datetime-local"
|
||||
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">位置名称</label>
|
||||
<input
|
||||
v-model="form.locationName"
|
||||
type="text"
|
||||
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
placeholder="如:北京、成都"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between gap-3">
|
||||
<label class="block text-sm font-medium text-slate-700">经纬度</label>
|
||||
<button
|
||||
v-if="hasCoordinates"
|
||||
type="button"
|
||||
class="text-xs font-medium text-slate-400 transition-colors hover:text-slate-700"
|
||||
@click="clearCoordinates"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] gap-2">
|
||||
<input
|
||||
:value="form.latitude !== null ? form.latitude : ''"
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="纬度"
|
||||
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@input="onLatInput"
|
||||
/>
|
||||
<input
|
||||
:value="form.longitude !== null ? form.longitude : ''"
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="经度"
|
||||
class="w-full border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@input="onLngInput"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center border border-sky-200 bg-sky-50 text-sky-700 transition-colors hover:bg-sky-100"
|
||||
title="地图选点"
|
||||
aria-label="地图选点"
|
||||
@click="mapPickerOpen = true"
|
||||
>
|
||||
<span class="text-lg leading-none">🗺️</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-slate-700">图片描述</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="4"
|
||||
class="w-full resize-none border border-slate-300 px-3 py-2.5 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
placeholder="描述一下这张图片..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<label class="flex items-center justify-between gap-4 border border-slate-200 bg-slate-50 px-4 py-3">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">公开展示</span>
|
||||
<span class="mt-1 block text-xs text-slate-500">关闭后不会出现在画廊、地图和公开主页中。</span>
|
||||
</span>
|
||||
<input
|
||||
v-model="form.isPublic"
|
||||
type="checkbox"
|
||||
class="h-5 w-5 border-slate-300 text-sky-500 focus:ring-sky-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 border-t border-slate-200 px-6 py-4">
|
||||
<NButton secondary strong @click="close">取消</NButton>
|
||||
<NButton type="primary" secondary strong :loading="saving" @click="save">
|
||||
保存修改
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<MapPickerModal
|
||||
:open="mapPickerOpen"
|
||||
:latitude="form.latitude"
|
||||
:longitude="form.longitude"
|
||||
description="点击地图即可回填这张图片的经纬度。"
|
||||
footer-text="保存时会继续做模糊化处理。"
|
||||
@close="mapPickerOpen = false"
|
||||
@confirm="confirmMapPicker"
|
||||
/>
|
||||
</template>
|
||||
Reference in New Issue
Block a user