Browse Source

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

Made-with: Cursor
main
npmrun 7 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
firstReferencedAt: string | null
dereferencedAt: string | null
/** 宽限结束、允许删除的绝对时间(ISO);无法推算时为 null */
graceExpiresAt: string | null
state: 'deletable' | 'cooling'
}
@ -186,7 +188,7 @@ async function executeDelete() {
</script>
<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>
<h1 class="text-2xl font-semibold">
@ -201,21 +203,24 @@ async function executeDelete() {
</UButton>
</div>
<div class="flex flex-wrap items-center gap-3">
<UFormField label="筛选" class="min-w-40">
<USelect v-model="filter" :items="filterItems" value-key="value" />
<div class="flex flex-wrap items-end gap-x-5 gap-y-3">
<UFormField label="筛选" class="w-44 shrink-0">
<USelect v-model="filter" :items="filterItems" value-key="value" class="w-full" />
</UFormField>
<UFormField label="每页" class="min-w-36">
<USelect v-model="pageSize" :items="pageSizeItems" value-key="value" />
<UFormField label="每页" class="w-40 shrink-0">
<USelect v-model="pageSize" :items="pageSizeItems" value-key="value" class="w-full" />
</UFormField>
<UButton
:color="batchCount > 0 ? 'error' : 'neutral'"
variant="outline"
:disabled="batchCount === 0"
@click="openConfirm([...selectedIds])"
>
批量删除{{ batchCount }}
</UButton>
<div class="shrink-0">
<UButton
:color="batchCount > 0 ? 'error' : 'neutral'"
variant="outline"
:disabled="batchCount === 0"
class="min-h-8"
@click="openConfirm([...selectedIds])"
>
批量删除{{ batchCount }}
</UButton>
</div>
</div>
<div v-if="loading" class="text-muted">
@ -223,7 +228,7 @@ async function executeDelete() {
</div>
<UEmpty v-else-if="!items.length" title="暂无记录" description="当前筛选下没有孤儿图片" />
<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>
<tr class="text-left text-muted border-b border-default bg-elevated/40">
<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>
<th class="p-3 whitespace-nowrap w-40 align-middle">
到期可删
</th>
<th class="p-3 w-24 align-middle whitespace-nowrap">
状态
</th>
@ -297,6 +305,14 @@ async function executeDelete() {
<td class="p-3 align-middle text-xs whitespace-nowrap hidden lg:table-cell">
{{ formatDt(row.dereferencedAt) }}
</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">
<div class="flex items-center justify-start">
<UBadge

17
server/service/media/index.ts

@ -87,6 +87,21 @@ export function isAssetDeletable(row: {
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() {
return notExists(
dbGlobal.select({ x: sql`1` }).from(postMediaRefs).where(eq(postMediaRefs.assetId, mediaAssets.id)),
@ -216,6 +231,7 @@ export async function listOrphanCandidatesForUser(
createdAt: Date;
firstReferencedAt: Date | null;
dereferencedAt: Date | null;
graceExpiresAt: Date | null;
state: "deletable" | "cooling";
}>;
total: number;
@ -258,6 +274,7 @@ export async function listOrphanCandidatesForUser(
createdAt: row.createdAt,
firstReferencedAt: row.firstReferencedAt,
dereferencedAt: row.dereferencedAt,
graceExpiresAt: computeOrphanGraceExpiresAt(row),
state,
};
});

Loading…
Cancel
Save