feat: enhance cloud image management
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { NAlert, NButton, NEmpty, NIcon, NSkeleton, NTag } from 'naive-ui'
|
||||
import { NAlert, NButton, NDropdown, NEmpty, NIcon, NSkeleton, NTag, useMessage } from 'naive-ui'
|
||||
import { Clock, Location, Settings, User } from '@vicons/tabler'
|
||||
import CloudEditModal, { type CloudEditFormValue } from '@/components/cloud/CloudEditModal.vue'
|
||||
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCloudsStore } from '@/stores/clouds'
|
||||
import { Clock, User ,Location} from '@vicons/tabler'
|
||||
import { useProfileStore } from '@/stores/profile'
|
||||
|
||||
import type { CloudType } from '@/types/database'
|
||||
|
||||
interface GalleryCloud {
|
||||
id: string
|
||||
user_id: string
|
||||
cloud_type_id: number | null
|
||||
custom_cloud_type: string | null
|
||||
image_url: string
|
||||
thumbnail_url: string | null
|
||||
location_name: string | null
|
||||
@@ -18,6 +24,8 @@ interface GalleryCloud {
|
||||
longitude: number | null
|
||||
captured_at: string | null
|
||||
created_at: string
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
is_hidden: boolean
|
||||
cloudTypeName: string
|
||||
cloudTypeRarity: CloudType['rarity']
|
||||
username: string
|
||||
@@ -25,7 +33,10 @@ interface GalleryCloud {
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const cloudsStore = useCloudsStore()
|
||||
const profileStore = useProfileStore()
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(true)
|
||||
const loadingMore = ref(false)
|
||||
@@ -36,6 +47,10 @@ const selectedCloud = ref<GalleryCloud | null>(null)
|
||||
const sentinel = ref<HTMLDivElement | null>(null)
|
||||
const totalLoaded = ref(0)
|
||||
const hasMore = ref(true)
|
||||
const manageError = ref('')
|
||||
const editModalOpen = ref(false)
|
||||
const editSaving = ref(false)
|
||||
const editInitialValue = ref<CloudEditFormValue | null>(null)
|
||||
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
@@ -76,6 +91,9 @@ function toGalleryCloud(row: Record<string, unknown>) {
|
||||
|
||||
return {
|
||||
id: row.id as string,
|
||||
user_id: row.user_id as string,
|
||||
cloud_type_id: (row.cloud_type_id as number | null) ?? null,
|
||||
custom_cloud_type: (row.custom_cloud_type as string | null) ?? null,
|
||||
image_url: row.image_url as string,
|
||||
thumbnail_url: (row.thumbnail_url as string | null) ?? null,
|
||||
location_name: (row.location_name as string | null) ?? null,
|
||||
@@ -84,6 +102,8 @@ function toGalleryCloud(row: Record<string, unknown>) {
|
||||
longitude: (row.longitude as number | null) ?? null,
|
||||
captured_at: (row.captured_at as string | null) ?? null,
|
||||
created_at: row.created_at as string,
|
||||
status: row.status as GalleryCloud['status'],
|
||||
is_hidden: (row.is_hidden as boolean) ?? false,
|
||||
cloudTypeName: (cloudType?.name as string) || (row.custom_cloud_type as string) || '未知',
|
||||
cloudTypeRarity: (cloudType?.rarity as CloudType['rarity']) || 'common',
|
||||
username: (profile?.username as string) || '匿名',
|
||||
@@ -93,7 +113,7 @@ function toGalleryCloud(row: Record<string, unknown>) {
|
||||
async function fetchPage(offset: number) {
|
||||
let query = supabase
|
||||
.from('clouds')
|
||||
.select('id,image_url,thumbnail_url,location_name,description,latitude,longitude,captured_at,created_at,custom_cloud_type,cloud_types(name,rarity),profiles(username)')
|
||||
.select('id,user_id,cloud_type_id,image_url,thumbnail_url,location_name,description,latitude,longitude,captured_at,created_at,status,is_hidden,custom_cloud_type,cloud_types(name,rarity),profiles(username)')
|
||||
.eq('status', 'approved')
|
||||
.eq('is_hidden', false)
|
||||
.order('created_at', { ascending: false })
|
||||
@@ -167,6 +187,164 @@ function openDetail(cloud: GalleryCloud) {
|
||||
|
||||
function closeDetail() {
|
||||
selectedCloud.value = null
|
||||
editModalOpen.value = false
|
||||
manageError.value = ''
|
||||
}
|
||||
|
||||
const selectedCloudIsMine = computed(() => {
|
||||
return !!selectedCloud.value && selectedCloud.value.user_id === authStore.user?.id
|
||||
})
|
||||
|
||||
const cloudActionOptions = computed(() => [
|
||||
{ label: '编辑图片信息', key: 'edit' },
|
||||
{ label: '删除', key: 'delete' },
|
||||
{ label: '更改为私密', key: 'toggle-privacy' },
|
||||
])
|
||||
|
||||
function formatDatetimeLocal(iso: string | null) {
|
||||
if (!iso) return ''
|
||||
const date = new Date(iso)
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
function blurCoordinate(value: number): number {
|
||||
return Math.round(value * 100) / 100
|
||||
}
|
||||
|
||||
function openEditModal() {
|
||||
if (!selectedCloud.value) return
|
||||
|
||||
manageError.value = ''
|
||||
editInitialValue.value = {
|
||||
cloudCategoryId: selectedCloud.value.cloud_type_id ?? (selectedCloud.value.custom_cloud_type ? 'other' : null),
|
||||
customCloudType: selectedCloud.value.custom_cloud_type || '',
|
||||
capturedAt: formatDatetimeLocal(selectedCloud.value.captured_at),
|
||||
latitude: selectedCloud.value.latitude,
|
||||
longitude: selectedCloud.value.longitude,
|
||||
locationName: selectedCloud.value.location_name || '',
|
||||
description: selectedCloud.value.description || '',
|
||||
isPublic: !selectedCloud.value.is_hidden,
|
||||
}
|
||||
editModalOpen.value = true
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
editModalOpen.value = false
|
||||
editSaving.value = false
|
||||
manageError.value = ''
|
||||
}
|
||||
|
||||
function patchGalleryCloud(updated: GalleryCloud) {
|
||||
galleryItems.value = galleryItems.value.map(item => (item.id === updated.id ? updated : item))
|
||||
if (selectedCloud.value?.id === updated.id) {
|
||||
selectedCloud.value = updated
|
||||
}
|
||||
}
|
||||
|
||||
async function submitEditModal(value: CloudEditFormValue) {
|
||||
if (!selectedCloud.value || !authStore.user?.id) return
|
||||
|
||||
if (!value.cloudCategoryId) {
|
||||
manageError.value = '请选择云型类别。'
|
||||
return
|
||||
}
|
||||
if (value.cloudCategoryId === 'other' && !value.customCloudType.trim()) {
|
||||
manageError.value = '请输入自定义云型名称。'
|
||||
return
|
||||
}
|
||||
if (!value.capturedAt) {
|
||||
manageError.value = '请选择拍摄时间。'
|
||||
return
|
||||
}
|
||||
const hasLat = typeof value.latitude === 'number' && !Number.isNaN(value.latitude)
|
||||
const hasLng = typeof value.longitude === 'number' && !Number.isNaN(value.longitude)
|
||||
if (hasLat !== hasLng) {
|
||||
manageError.value = '经纬度必须同时填写。'
|
||||
return
|
||||
}
|
||||
|
||||
const current = selectedCloud.value
|
||||
editSaving.value = true
|
||||
manageError.value = ''
|
||||
|
||||
try {
|
||||
const updated = await profileStore.updateCloud(authStore.user.id, current.id, {
|
||||
cloud_type_id: value.cloudCategoryId === 'other' ? null : value.cloudCategoryId,
|
||||
custom_cloud_type: value.cloudCategoryId === 'other' ? value.customCloudType.trim() : null,
|
||||
latitude: hasLat ? blurCoordinate(value.latitude as number) : null,
|
||||
longitude: hasLng ? blurCoordinate(value.longitude as number) : null,
|
||||
location_name: value.locationName.trim() || null,
|
||||
description: value.description.trim() || null,
|
||||
captured_at: new Date(value.capturedAt).toISOString(),
|
||||
is_hidden: !value.isPublic,
|
||||
})
|
||||
|
||||
if (updated.is_hidden || updated.status !== 'approved') {
|
||||
removeGalleryCloud(updated.id)
|
||||
} else {
|
||||
patchGalleryCloud({
|
||||
...current,
|
||||
...updated,
|
||||
user_id: current.user_id,
|
||||
username: current.username,
|
||||
})
|
||||
}
|
||||
closeEditModal()
|
||||
message.success('图片信息已更新')
|
||||
} catch (error) {
|
||||
manageError.value = error instanceof Error ? error.message : '图片信息更新失败'
|
||||
message.error(manageError.value)
|
||||
} finally {
|
||||
editSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeGalleryCloud(cloudId: string) {
|
||||
galleryItems.value = galleryItems.value.filter(item => item.id !== cloudId)
|
||||
if (selectedCloud.value?.id === cloudId) {
|
||||
selectedCloud.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSelectedCloud() {
|
||||
if (!selectedCloud.value || !authStore.user?.id) return
|
||||
const cloudId = selectedCloud.value.id
|
||||
const confirmed = window.confirm('确定删除这张图片吗?删除后无法在页面中恢复。')
|
||||
if (!confirmed) return
|
||||
|
||||
manageError.value = ''
|
||||
|
||||
try {
|
||||
const deletedCount = await profileStore.deleteClouds(authStore.user.id, [cloudId])
|
||||
removeGalleryCloud(cloudId)
|
||||
message.success(`已成功删除 ${deletedCount} 张照片`)
|
||||
} catch (error) {
|
||||
manageError.value = error instanceof Error ? error.message : '图片删除失败'
|
||||
message.error(manageError.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function hideSelectedCloud() {
|
||||
if (!selectedCloud.value || !authStore.user?.id) return
|
||||
const cloudId = selectedCloud.value.id
|
||||
|
||||
manageError.value = ''
|
||||
|
||||
try {
|
||||
await profileStore.updateCloudVisibility(authStore.user.id, cloudId, true)
|
||||
removeGalleryCloud(cloudId)
|
||||
message.success('已更改为私密图片')
|
||||
} catch (error) {
|
||||
manageError.value = error instanceof Error ? error.message : '私密状态更新失败'
|
||||
message.error(manageError.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloudAction(key: string | number) {
|
||||
if (key === 'edit') openEditModal()
|
||||
if (key === 'delete') deleteSelectedCloud()
|
||||
if (key === 'toggle-privacy') hideSelectedCloud()
|
||||
}
|
||||
|
||||
const filterTabs = computed(() => [
|
||||
@@ -226,6 +404,10 @@ onUnmounted(() => {
|
||||
{{ loadError }}
|
||||
</NAlert>
|
||||
|
||||
<NAlert v-if="manageError" class="mt-6" type="error" :show-icon="false" :bordered="false" title="操作失败">
|
||||
{{ manageError }}
|
||||
</NAlert>
|
||||
|
||||
<section v-if="loading" class="mt-6 columns-2 gap-3 md:columns-3 xl:columns-4 [column-gap:0.75rem]">
|
||||
<div
|
||||
v-for="n in 8"
|
||||
@@ -307,6 +489,7 @@ onUnmounted(() => {
|
||||
v-if="selectedCloud"
|
||||
:open="!!selectedCloud"
|
||||
:image-url="selectedCloud.image_url"
|
||||
:thumbnail-url="selectedCloud.thumbnail_url"
|
||||
:image-alt="selectedCloud.cloudTypeName"
|
||||
:title="selectedCloud.cloudTypeName"
|
||||
:subtitle="`上传者:${selectedCloud.username}`"
|
||||
@@ -341,7 +524,40 @@ onUnmounted(() => {
|
||||
{{ selectedCloud.description || '上传者没有留下额外说明。' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="selectedCloudIsMine" #actions>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<NDropdown
|
||||
trigger="click"
|
||||
placement="top-start"
|
||||
:options="cloudActionOptions"
|
||||
@select="handleCloudAction"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center border border-slate-200 bg-slate-50 text-slate-600 transition-colors hover:border-slate-900 hover:text-slate-900"
|
||||
title="图片管理"
|
||||
aria-label="图片管理"
|
||||
>
|
||||
<NIcon size="20">
|
||||
<Settings />
|
||||
</NIcon>
|
||||
</button>
|
||||
</NDropdown>
|
||||
<!-- <span class="text-xs text-slate-500">这是你上传的公开图片</span> -->
|
||||
</div>
|
||||
</template>
|
||||
</ImageDetailModal>
|
||||
|
||||
<CloudEditModal
|
||||
:open="editModalOpen && !!selectedCloud"
|
||||
:saving="editSaving"
|
||||
:error="manageError"
|
||||
:cloud-types="cloudsStore.cloudTypes"
|
||||
:initial-value="editInitialValue"
|
||||
@close="closeEditModal"
|
||||
@save="submitEditModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user