Ver código fonte

feat: add work deletion actions

main
leiyun 1 mês atrás
pai
commit
be6db84da0
2 arquivos alterados com 105 adições e 4 exclusões
  1. +46
    -2
      app/components/WorkDetailModal.vue
  2. +59
    -2
      app/pages/works.vue

+ 46
- 2
app/components/WorkDetailModal.vue Ver arquivo

@@ -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">
<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
v-if="canCompare"
type="button"
@@ -259,12 +269,13 @@ const props = defineProps({
/** 作品项:需含 work_type 与 chat_open_id(或 id) */
work: { type: Object, default: () => ({}) },
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'deleted'])

const { post } = useApi()
const toast = useToast()

const loading = ref(false)
const deleting = ref(false)
const errorMsg = ref('')
const detail = ref({})
const compareOpen = ref(false)
@@ -379,9 +390,14 @@ const canCompare = computed(() =>
&& Boolean(originalUrl.value && imageUrl.value)
&& !isPending.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() {
const id = props.work?.photo_open_id || props.work?.chat_open_id || props.work?.id
const id = resolveWorkId()
if (!id) {
errorMsg.value = '缺少作品标识'
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
function schedulePendingReload() {
clearPendingReload()


+ 59
- 2
app/pages/works.vue Ver arquivo

@@ -44,9 +44,17 @@
<div
v-for="item in list"
: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)"
>
<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 取名卡片 -->
<div
v-if="item.work_type === 'ainame'"
@@ -148,7 +156,7 @@
</div>

<!-- 作品详情弹窗 -->
<WorkDetailModal v-model="detailOpen" :work="currentWork" />
<WorkDetailModal v-model="detailOpen" :work="currentWork" @deleted="handleDeleted" />
</div>
</template>

@@ -263,6 +271,55 @@ function openWork(item) {
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) {
const status = Number(item.status)
if (status === 0) return 'bg-[var(--c-fff5e8)] text-[var(--c-ff8a00)]'


Carregando…
Cancelar
Salvar