| @@ -160,6 +160,16 @@ | |||||
| <!-- 底部操作 --> | <!-- 底部操作 --> | ||||
| <div v-if="!loading && !errorMsg" class="flex shrink-0 flex-wrap justify-end gap-3 border-t border-[var(--c-eef2f7)] px-5 py-4"> | <div v-if="!loading && !errorMsg" class="flex shrink-0 flex-wrap justify-end gap-3 border-t border-[var(--c-eef2f7)] px-5 py-4"> | ||||
| <button | |||||
| v-if="canDelete" | |||||
| type="button" | |||||
| :disabled="deleting" | |||||
| class="mr-auto inline-flex items-center gap-1.5 rounded-full border border-[var(--c-f04438)]/30 px-5 py-2.5 text-sm font-bold text-[var(--c-f04438)] transition-colors hover:bg-[var(--c-fff1f0)] disabled:cursor-not-allowed disabled:opacity-60" | |||||
| @click="deleteRecord" | |||||
| > | |||||
| <UIcon name="i-lucide-trash-2" class="size-4" /> | |||||
| {{ deleting ? '删除中...' : '删除记录' }} | |||||
| </button> | |||||
| <button | <button | ||||
| v-if="canCompare" | v-if="canCompare" | ||||
| type="button" | type="button" | ||||
| @@ -259,12 +269,13 @@ const props = defineProps({ | |||||
| /** 作品项:需含 work_type 与 chat_open_id(或 id) */ | /** 作品项:需含 work_type 与 chat_open_id(或 id) */ | ||||
| work: { type: Object, default: () => ({}) }, | work: { type: Object, default: () => ({}) }, | ||||
| }) | }) | ||||
| const emit = defineEmits(['update:modelValue']) | |||||
| const emit = defineEmits(['update:modelValue', 'deleted']) | |||||
| const { post } = useApi() | const { post } = useApi() | ||||
| const toast = useToast() | const toast = useToast() | ||||
| const loading = ref(false) | const loading = ref(false) | ||||
| const deleting = ref(false) | |||||
| const errorMsg = ref('') | const errorMsg = ref('') | ||||
| const detail = ref({}) | const detail = ref({}) | ||||
| const compareOpen = ref(false) | const compareOpen = ref(false) | ||||
| @@ -379,9 +390,14 @@ const canCompare = computed(() => | |||||
| && Boolean(originalUrl.value && imageUrl.value) | && Boolean(originalUrl.value && imageUrl.value) | ||||
| && !isPending.value | && !isPending.value | ||||
| && !isFailed.value) | && !isFailed.value) | ||||
| const canDelete = computed(() => Boolean(resolveWorkId())) | |||||
| function resolveWorkId() { | |||||
| return props.work?.photo_open_id || props.work?.chat_open_id || props.work?.id || '' | |||||
| } | |||||
| async function load() { | async function load() { | ||||
| const id = props.work?.photo_open_id || props.work?.chat_open_id || props.work?.id | |||||
| const id = resolveWorkId() | |||||
| if (!id) { | if (!id) { | ||||
| errorMsg.value = '缺少作品标识' | errorMsg.value = '缺少作品标识' | ||||
| return | return | ||||
| @@ -414,6 +430,34 @@ async function load() { | |||||
| } | } | ||||
| } | } | ||||
| function getDeleteRequest() { | |||||
| const id = resolveWorkId() | |||||
| if (!id) return null | |||||
| if (isPhoto.value) return { path: '/idphoto/del', body: { photo_open_id: id } } | |||||
| if (isOldPhoto.value) return { path: '/oldphoto/del', body: { photo_open_id: id } } | |||||
| if (isPortrait.value) return { path: '/portrait/del', body: { photo_open_id: id } } | |||||
| if (isName.value) return { path: '/ainame/del', body: { chat_open_id: id } } | |||||
| if (props.work?.work_type === 'anime') return { path: '/anime/del', body: { chat_open_id: id } } | |||||
| return { path: '/chat/del', body: { chat_open_id: id } } | |||||
| } | |||||
| async function deleteRecord() { | |||||
| const req = getDeleteRequest() | |||||
| if (!req || deleting.value) return | |||||
| if (!window.confirm('确认删除该作品记录?删除后将不再展示。')) return | |||||
| deleting.value = true | |||||
| try { | |||||
| await post(req.path, req.body) | |||||
| toast.add({ title: '删除成功', color: 'success' }) | |||||
| emit('deleted', props.work) | |||||
| close() | |||||
| } catch (e) { | |||||
| toast.add({ title: e.message || '删除失败', color: 'error' }) | |||||
| } finally { | |||||
| deleting.value = false | |||||
| } | |||||
| } | |||||
| let pendingTimer = null | let pendingTimer = null | ||||
| function schedulePendingReload() { | function schedulePendingReload() { | ||||
| clearPendingReload() | clearPendingReload() | ||||
| @@ -44,9 +44,17 @@ | |||||
| <div | <div | ||||
| v-for="item in list" | v-for="item in list" | ||||
| :key="item.id" | :key="item.id" | ||||
| class="group cursor-pointer overflow-hidden rounded-2xl bg-[var(--c-ffffff)] shadow-[0_10px_28px_var(--sh-34-68-112-60)] transition-shadow hover:shadow-[0_14px_34px_var(--sh-34-68-112-120)]" | |||||
| class="group relative cursor-pointer overflow-hidden rounded-2xl bg-[var(--c-ffffff)] shadow-[0_10px_28px_var(--sh-34-68-112-60)] transition-shadow hover:shadow-[0_14px_34px_var(--sh-34-68-112-120)]" | |||||
| @click="openWork(item)" | @click="openWork(item)" | ||||
| > | > | ||||
| <button | |||||
| type="button" | |||||
| class="absolute right-3 top-3 z-10 flex size-8 items-center justify-center rounded-full bg-white/90 text-[var(--c-f04438)] opacity-0 shadow-[0_8px_20px_var(--sh-15-23-42-160)] backdrop-blur transition-opacity hover:bg-[var(--c-fff1f0)] group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--c-f04438)]" | |||||
| aria-label="删除作品" | |||||
| @click.stop="deleteWork(item)" | |||||
| > | |||||
| <UIcon name="i-lucide-trash-2" class="size-4" /> | |||||
| </button> | |||||
| <!-- AI 取名卡片 --> | <!-- AI 取名卡片 --> | ||||
| <div | <div | ||||
| v-if="item.work_type === 'ainame'" | v-if="item.work_type === 'ainame'" | ||||
| @@ -148,7 +156,7 @@ | |||||
| </div> | </div> | ||||
| <!-- 作品详情弹窗 --> | <!-- 作品详情弹窗 --> | ||||
| <WorkDetailModal v-model="detailOpen" :work="currentWork" /> | |||||
| <WorkDetailModal v-model="detailOpen" :work="currentWork" @deleted="handleDeleted" /> | |||||
| </div> | </div> | ||||
| </template> | </template> | ||||
| @@ -263,6 +271,55 @@ function openWork(item) { | |||||
| detailOpen.value = true | detailOpen.value = true | ||||
| } | } | ||||
| function resolveWorkId(item = {}) { | |||||
| return item.photo_open_id || item.chat_open_id || item.id || '' | |||||
| } | |||||
| function getDeleteRequest(item = {}) { | |||||
| const id = resolveWorkId(item) | |||||
| if (!id) return null | |||||
| if (item.work_type === 'photo') return { path: '/idphoto/del', body: { photo_open_id: id } } | |||||
| if (item.work_type === 'oldphoto') return { path: '/oldphoto/del', body: { photo_open_id: id } } | |||||
| if (item.work_type === 'portrait') return { path: '/portrait/del', body: { photo_open_id: id } } | |||||
| if (item.work_type === 'ainame') return { path: '/ainame/del', body: { chat_open_id: id } } | |||||
| if (item.work_type === 'anime') return { path: '/anime/del', body: { chat_open_id: id } } | |||||
| return { path: '/chat/del', body: { chat_open_id: id } } | |||||
| } | |||||
| function removeDeletedFromList(item = {}) { | |||||
| const id = String(resolveWorkId(item)) | |||||
| if (!id) return | |||||
| const deleted = list.value.find(row => String(resolveWorkId(row)) === id) | |||||
| list.value = list.value.filter(row => String(resolveWorkId(row)) !== id) | |||||
| if (deleted) { | |||||
| total.value = Math.max(0, total.value - 1) | |||||
| const type = deleted.work_type || 'all' | |||||
| counts.value = { | |||||
| ...counts.value, | |||||
| all: Math.max(0, Number(counts.value.all || 0) - 1), | |||||
| [type]: Math.max(0, Number(counts.value[type] || 0) - 1), | |||||
| } | |||||
| } | |||||
| } | |||||
| async function deleteWork(item) { | |||||
| const req = getDeleteRequest(item) | |||||
| if (!req) return | |||||
| if (!window.confirm('确认删除该作品记录?删除后将不再展示。')) return | |||||
| try { | |||||
| await post(req.path, req.body) | |||||
| removeDeletedFromList(item) | |||||
| toast.add({ title: '删除成功', color: 'success' }) | |||||
| } catch (e) { | |||||
| toast.add({ title: e.message || '删除失败', color: 'error' }) | |||||
| } | |||||
| } | |||||
| function handleDeleted(item) { | |||||
| removeDeletedFromList(item) | |||||
| fetchList(true, true) | |||||
| } | |||||
| function statusClass(item) { | function statusClass(item) { | ||||
| const status = Number(item.status) | const status = Number(item.status) | ||||
| if (status === 0) return 'bg-[var(--c-fff5e8)] text-[var(--c-ff8a00)]' | if (status === 0) return 'bg-[var(--c-fff5e8)] text-[var(--c-ff8a00)]' | ||||