275 lines
8.3 KiB
Vue
275 lines
8.3 KiB
Vue
<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="oc-field-help">修改后会同步影响个人主页、画廊和地图中的展示。</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="oc-icon-button"
|
||
aria-label="关闭编辑窗口"
|
||
title="关闭编辑窗口"
|
||
@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="oc-field-label">类别</label>
|
||
<select
|
||
:value="form.cloudCategoryId ?? ''"
|
||
class="oc-field-control"
|
||
@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="oc-field-label">自定义云型</label>
|
||
<input
|
||
v-model="form.customCloudType"
|
||
type="text"
|
||
class="oc-field-control"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="oc-field-label">拍摄时间</label>
|
||
<input
|
||
v-model="form.capturedAt"
|
||
type="datetime-local"
|
||
class="oc-field-control"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="oc-field-label">位置名称</label>
|
||
<input
|
||
v-model="form.locationName"
|
||
type="text"
|
||
class="oc-field-control"
|
||
placeholder="如:北京、成都"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="mb-1 flex items-center justify-between gap-3">
|
||
<label class="oc-field-label mb-0">经纬度</label>
|
||
<button
|
||
v-if="hasCoordinates"
|
||
type="button"
|
||
class="oc-text-button oc-text-button--muted text-xs"
|
||
@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="oc-field-control"
|
||
@input="onLatInput"
|
||
/>
|
||
<input
|
||
:value="form.longitude !== null ? form.longitude : ''"
|
||
type="number"
|
||
step="any"
|
||
placeholder="经度"
|
||
class="oc-field-control"
|
||
@input="onLngInput"
|
||
/>
|
||
<button
|
||
type="button"
|
||
class="oc-icon-button oc-icon-button--sky"
|
||
title="地图选点"
|
||
aria-label="地图选点"
|
||
@click="mapPickerOpen = true"
|
||
>
|
||
<span class="text-lg leading-none">🗺️</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="oc-field-label">图片描述</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 type="default" class="oc-panel-button oc-panel-button--neutral" @click="close">取消</NButton>
|
||
<NButton type="default" secondary strong class="oc-panel-button oc-panel-button--sky" :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>
|