Browse Source

feat(media): grace expiry column; widen orphans page; align toolbar

Made-with: Cursor
main
npmrun 10 hours ago
parent
commit
707252eb2f
  1. 46
      app/pages/me/media/orphans.vue
  2. 17
      server/service/media/index.ts

46
app/pages/me/media/orphans.vue

@ -12,6 +12,8 @@ type OrphanItem = {
createdAt: string createdAt: string
firstReferencedAt: string | null firstReferencedAt: string | null
dereferencedAt: string | null dereferencedAt: string | null
/** 宽限结束、允许删除的绝对时间(ISO);无法推算时为 null */
graceExpiresAt: string | null
state: 'deletable' | 'cooling' state: 'deletable' | 'cooling'
} }
@ -186,7 +188,7 @@ async function executeDelete() {
</script> </script>
<template> <template>
<UContainer class="py-8 space-y-6 max-w-5xl"> <UContainer class="py-8 space-y-6 w-full max-w-[min(100%,88rem)] px-4 sm:px-6">
<div class="flex flex-wrap justify-between items-start gap-3"> <div class="flex flex-wrap justify-between items-start gap-3">
<div> <div>
<h1 class="text-2xl font-semibold"> <h1 class="text-2xl font-semibold">
@ -201,21 +203,24 @@ async function executeDelete() {
</UButton> </UButton>
</div> </div>
<div class="flex flex-wrap items-center gap-3"> <div class="flex flex-wrap items-end gap-x-5 gap-y-3">
<UFormField label="筛选" class="min-w-40"> <UFormField label="筛选" class="w-44 shrink-0">
<USelect v-model="filter" :items="filterItems" value-key="value" /> <USelect v-model="filter" :items="filterItems" value-key="value" class="w-full" />
</UFormField> </UFormField>
<UFormField label="每页" class="min-w-36"> <UFormField label="每页" class="w-40 shrink-0">
<USelect v-model="pageSize" :items="pageSizeItems" value-key="value" /> <USelect v-model="pageSize" :items="pageSizeItems" value-key="value" class="w-full" />
</UFormField> </UFormField>
<UButton <div class="shrink-0">
:color="batchCount > 0 ? 'error' : 'neutral'" <UButton
variant="outline" :color="batchCount > 0 ? 'error' : 'neutral'"
:disabled="batchCount === 0" variant="outline"
@click="openConfirm([...selectedIds])" :disabled="batchCount === 0"
> class="min-h-8"
批量删除{{ batchCount }} @click="openConfirm([...selectedIds])"
</UButton> >
批量删除{{ batchCount }}
</UButton>
</div>
</div> </div>
<div v-if="loading" class="text-muted"> <div v-if="loading" class="text-muted">
@ -223,7 +228,7 @@ async function executeDelete() {
</div> </div>
<UEmpty v-else-if="!items.length" title="暂无记录" description="当前筛选下没有孤儿图片" /> <UEmpty v-else-if="!items.length" title="暂无记录" description="当前筛选下没有孤儿图片" />
<div v-else class="overflow-x-auto rounded-lg border border-default"> <div v-else class="overflow-x-auto rounded-lg border border-default">
<table class="w-full min-w-[720px] table-fixed text-sm"> <table class="w-full min-w-[960px] table-fixed text-sm">
<thead> <thead>
<tr class="text-left text-muted border-b border-default bg-elevated/40"> <tr class="text-left text-muted border-b border-default bg-elevated/40">
<th class="p-3 w-10 align-middle"> <th class="p-3 w-10 align-middle">
@ -251,6 +256,9 @@ async function executeDelete() {
<th class="p-3 whitespace-nowrap hidden lg:table-cell w-36 align-middle"> <th class="p-3 whitespace-nowrap hidden lg:table-cell w-36 align-middle">
解除引用 解除引用
</th> </th>
<th class="p-3 whitespace-nowrap w-40 align-middle">
到期可删
</th>
<th class="p-3 w-24 align-middle whitespace-nowrap"> <th class="p-3 w-24 align-middle whitespace-nowrap">
状态 状态
</th> </th>
@ -297,6 +305,14 @@ async function executeDelete() {
<td class="p-3 align-middle text-xs whitespace-nowrap hidden lg:table-cell"> <td class="p-3 align-middle text-xs whitespace-nowrap hidden lg:table-cell">
{{ formatDt(row.dereferencedAt) }} {{ formatDt(row.dereferencedAt) }}
</td> </td>
<td class="p-3 align-middle text-xs whitespace-nowrap tabular-nums">
<span
:class="row.state === 'deletable' ? 'text-muted' : 'text-default'"
:title="row.graceExpiresAt ? `到达该时间后即可删除(或已可删)` : ''"
>
{{ formatDt(row.graceExpiresAt) }}
</span>
</td>
<td class="p-3 align-middle"> <td class="p-3 align-middle">
<div class="flex items-center justify-start"> <div class="flex items-center justify-start">
<UBadge <UBadge

17
server/service/media/index.ts

@ -87,6 +87,21 @@ export function isAssetDeletable(row: {
return row.dereferencedAt.getTime() <= now - AFTER_DEREF_MS; return row.dereferencedAt.getTime() <= now - AFTER_DEREF_MS;
} }
/** 孤儿资源「宽限结束、允许删除」的绝对时间;无法推算时返回 null */
export function computeOrphanGraceExpiresAt(row: {
firstReferencedAt: Date | null;
dereferencedAt: Date | null;
createdAt: Date;
}): Date | null {
if (row.firstReferencedAt == null) {
return new Date(row.createdAt.getTime() + NEVER_REF_MS);
}
if (row.dereferencedAt == null) {
return null;
}
return new Date(row.dereferencedAt.getTime() + AFTER_DEREF_MS);
}
function orphanCondition() { function orphanCondition() {
return notExists( return notExists(
dbGlobal.select({ x: sql`1` }).from(postMediaRefs).where(eq(postMediaRefs.assetId, mediaAssets.id)), dbGlobal.select({ x: sql`1` }).from(postMediaRefs).where(eq(postMediaRefs.assetId, mediaAssets.id)),
@ -216,6 +231,7 @@ export async function listOrphanCandidatesForUser(
createdAt: Date; createdAt: Date;
firstReferencedAt: Date | null; firstReferencedAt: Date | null;
dereferencedAt: Date | null; dereferencedAt: Date | null;
graceExpiresAt: Date | null;
state: "deletable" | "cooling"; state: "deletable" | "cooling";
}>; }>;
total: number; total: number;
@ -258,6 +274,7 @@ export async function listOrphanCandidatesForUser(
createdAt: row.createdAt, createdAt: row.createdAt,
firstReferencedAt: row.firstReferencedAt, firstReferencedAt: row.firstReferencedAt,
dereferencedAt: row.dereferencedAt, dereferencedAt: row.dereferencedAt,
graceExpiresAt: computeOrphanGraceExpiresAt(row),
state, state,
}; };
}); });

Loading…
Cancel
Save