diff --git a/src/modules/contents/pages/DeletedSinglePageList.vue b/src/modules/contents/pages/DeletedSinglePageList.vue
new file mode 100644
index 00000000..a802f30e
--- /dev/null
+++ b/src/modules/contents/pages/DeletedSinglePageList.vue
@@ -0,0 +1,413 @@
+<script lang="ts" setup>
+import {
+  IconAddCircle,
+  IconRefreshLine,
+  IconDeleteBin,
+  VButton,
+  VCard,
+  VPagination,
+  VSpace,
+  Dialog,
+  VEmpty,
+  VAvatar,
+  VEntity,
+  VEntityField,
+  VPageHeader,
+  VStatusDot,
+} from "@halo-dev/components";
+import { onMounted, ref, watch } from "vue";
+import type { ListedSinglePageList, SinglePage } from "@halo-dev/api-client";
+import { apiClient } from "@/utils/api-client";
+import { formatDatetime } from "@/utils/date";
+import { onBeforeRouteLeave, RouterLink } from "vue-router";
+import cloneDeep from "lodash.clonedeep";
+import { usePermission } from "@/utils/permission";
+
+const { currentUserHasPermission } = usePermission();
+
+const singlePages = ref<ListedSinglePageList>({
+  page: 1,
+  size: 50,
+  total: 0,
+  items: [],
+  first: true,
+  last: false,
+  hasNext: false,
+  hasPrevious: false,
+});
+const loading = ref(false);
+const selectedPageNames = ref<string[]>([]);
+const checkedAll = ref(false);
+const refreshInterval = ref();
+const keyword = ref("");
+
+const handleFetchSinglePages = async () => {
+  try {
+    clearInterval(refreshInterval.value);
+
+    loading.value = true;
+
+    const { data } = await apiClient.singlePage.listSinglePages({
+      labelSelector: [`content.halo.run/deleted=true`],
+      page: singlePages.value.page,
+      size: singlePages.value.size,
+      keyword: keyword.value,
+    });
+
+    singlePages.value = data;
+
+    const deletedSinglePages = singlePages.value.items.filter(
+      (singlePage) =>
+        !!singlePage.page.metadata.deletionTimestamp ||
+        !singlePage.page.spec.deleted
+    );
+
+    if (deletedSinglePages.length) {
+      refreshInterval.value = setInterval(() => {
+        handleFetchSinglePages();
+      }, 3000);
+    }
+  } catch (error) {
+    console.error("Failed to fetch deleted single pages", error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+onBeforeRouteLeave(() => {
+  clearInterval(refreshInterval.value);
+});
+
+const handlePaginationChange = ({
+  page,
+  size,
+}: {
+  page: number;
+  size: number;
+}) => {
+  singlePages.value.page = page;
+  singlePages.value.size = size;
+  handleFetchSinglePages();
+};
+
+const checkSelection = (singlePage: SinglePage) => {
+  return selectedPageNames.value.includes(singlePage.metadata.name);
+};
+
+const handleCheckAllChange = (e: Event) => {
+  const { checked } = e.target as HTMLInputElement;
+
+  if (checked) {
+    selectedPageNames.value =
+      singlePages.value.items.map((singlePage) => {
+        return singlePage.page.metadata.name;
+      }) || [];
+  } else {
+    selectedPageNames.value = [];
+  }
+};
+
+const handleDeletePermanently = async (singlePage: SinglePage) => {
+  Dialog.warning({
+    title: "是否确认永久删除该自定义页面?",
+    description: "删除之后将无法恢复",
+    confirmType: "danger",
+    onConfirm: async () => {
+      await apiClient.extension.singlePage.deletecontentHaloRunV1alpha1SinglePage(
+        {
+          name: singlePage.metadata.name,
+        }
+      );
+      await handleFetchSinglePages();
+    },
+  });
+};
+
+const handleDeletePermanentlyInBatch = async () => {
+  Dialog.warning({
+    title: "是否确认永久删除选中的自定义页面?",
+    description: "删除之后将无法恢复",
+    confirmType: "danger",
+    onConfirm: async () => {
+      await Promise.all(
+        selectedPageNames.value.map((name) => {
+          return apiClient.extension.singlePage.deletecontentHaloRunV1alpha1SinglePage(
+            {
+              name,
+            }
+          );
+        })
+      );
+      await handleFetchSinglePages();
+      selectedPageNames.value = [];
+    },
+  });
+};
+
+const handleRecovery = async (singlePage: SinglePage) => {
+  Dialog.warning({
+    title: "是否确认恢复该自定义页面?",
+    description: "此操作会将自定义页面恢复到被删除之前的状态",
+    onConfirm: async () => {
+      const singlePageToUpdate = cloneDeep(singlePage);
+      singlePageToUpdate.spec.deleted = false;
+      await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
+        {
+          name: singlePageToUpdate.metadata.name,
+          singlePage: singlePageToUpdate,
+        }
+      );
+      await handleFetchSinglePages();
+    },
+  });
+};
+
+const handleRecoveryInBatch = async () => {
+  Dialog.warning({
+    title: "是否确认恢复选中的自定义页面?",
+    description: "此操作会将自定义页面恢复到被删除之前的状态",
+    onConfirm: async () => {
+      await Promise.all(
+        selectedPageNames.value.map((name) => {
+          const singlePage = singlePages.value.items.find(
+            (item) => item.page.metadata.name === name
+          )?.page;
+
+          if (!singlePage) {
+            return Promise.resolve();
+          }
+
+          singlePage.spec.deleted = false;
+          return apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
+            {
+              name: singlePage.metadata.name,
+              singlePage: singlePage,
+            }
+          );
+        })
+      );
+      await handleFetchSinglePages();
+      selectedPageNames.value = [];
+    },
+  });
+};
+
+watch(selectedPageNames, (newValue) => {
+  checkedAll.value = newValue.length === singlePages.value.items?.length;
+});
+
+onMounted(handleFetchSinglePages);
+</script>
+
+<template>
+  <VPageHeader title="自定义页面回收站">
+    <template #icon>
+      <IconDeleteBin class="mr-2 self-center text-green-600" />
+    </template>
+    <template #actions>
+      <VSpace>
+        <VButton :route="{ name: 'SinglePages' }" size="sm">返回</VButton>
+        <VButton
+          v-permission="['system:singlepages:manage']"
+          :route="{ name: 'SinglePageEditor' }"
+          type="secondary"
+        >
+          <template #icon>
+            <IconAddCircle class="h-full w-full" />
+          </template>
+          新建
+        </VButton>
+      </VSpace>
+    </template>
+  </VPageHeader>
+  <div class="m-0 md:m-4">
+    <VCard :body-class="['!p-0']">
+      <template #header>
+        <div class="block w-full bg-gray-50 px-4 py-3">
+          <div
+            class="relative flex flex-col items-start sm:flex-row sm:items-center"
+          >
+            <div
+              v-permission="['system:singlepages:manage']"
+              class="mr-4 hidden items-center sm:flex"
+            >
+              <input
+                v-model="checkedAll"
+                class="h-4 w-4 rounded border-gray-300 text-indigo-600"
+                type="checkbox"
+                @change="handleCheckAllChange"
+              />
+            </div>
+            <div class="flex w-full flex-1 items-center sm:w-auto">
+              <div
+                v-if="!selectedPageNames.length"
+                class="flex items-center gap-2"
+              >
+                <FormKit
+                  v-model="keyword"
+                  placeholder="输入关键词搜索"
+                  type="text"
+                  @keyup.enter="handleFetchSinglePages"
+                ></FormKit>
+              </div>
+              <VSpace v-else>
+                <VButton type="danger" @click="handleDeletePermanentlyInBatch">
+                  永久删除
+                </VButton>
+                <VButton type="default" @click="handleRecoveryInBatch">
+                  恢复
+                </VButton>
+              </VSpace>
+            </div>
+            <div class="mt-4 flex sm:mt-0">
+              <VSpace spacing="lg">
+                <div class="flex flex-row gap-2">
+                  <div
+                    class="group cursor-pointer rounded p-1 hover:bg-gray-200"
+                    @click="handleFetchSinglePages"
+                  >
+                    <IconRefreshLine
+                      :class="{ 'animate-spin text-gray-900': loading }"
+                      class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
+                    />
+                  </div>
+                </div>
+              </VSpace>
+            </div>
+          </div>
+        </div>
+      </template>
+      <VEmpty
+        v-if="!singlePages.items.length && !loading"
+        message="你可以尝试刷新或者返回自定义页面管理"
+        title="没有自定义页面被放入回收站"
+      >
+        <template #actions>
+          <VSpace>
+            <VButton @click="handleFetchSinglePages">刷新</VButton>
+            <VButton
+              v-permission="['system:singlepages:view']"
+              :route="{ name: 'SinglePages' }"
+              type="primary"
+            >
+              返回
+            </VButton>
+          </VSpace>
+        </template>
+      </VEmpty>
+      <ul
+        v-else
+        class="box-border h-full w-full divide-y divide-gray-100"
+        role="list"
+      >
+        <li v-for="(singlePage, index) in singlePages.items" :key="index">
+          <VEntity :is-selected="checkSelection(singlePage.page)">
+            <template
+              v-if="currentUserHasPermission(['system:singlepages:manage'])"
+              #checkbox
+            >
+              <input
+                v-model="selectedPageNames"
+                :value="singlePage.page.metadata.name"
+                class="h-4 w-4 rounded border-gray-300 text-indigo-600"
+                type="checkbox"
+              />
+            </template>
+            <template #start>
+              <VEntityField :title="singlePage.page.spec.title">
+                <template #description>
+                  <VSpace>
+                    <span class="text-xs text-gray-500">
+                      {{ singlePage.page.status?.permalink }}
+                    </span>
+                    <span class="text-xs text-gray-500">
+                      访问量 {{ singlePage.stats.visit || 0 }}
+                    </span>
+                    <span class="text-xs text-gray-500">
+                      评论 {{ singlePage.stats.totalComment || 0 }}
+                    </span>
+                  </VSpace>
+                </template>
+              </VEntityField>
+            </template>
+            <template #end>
+              <VEntityField>
+                <template #description>
+                  <RouterLink
+                    v-for="(
+                      contributor, contributorIndex
+                    ) in singlePage.contributors"
+                    :key="contributorIndex"
+                    :to="{
+                      name: 'UserDetail',
+                      params: { name: contributor.name },
+                    }"
+                    class="flex items-center"
+                  >
+                    <VAvatar
+                      v-tooltip="contributor.displayName"
+                      size="xs"
+                      :src="contributor.avatar"
+                      :alt="contributor.displayName"
+                      circle
+                    ></VAvatar>
+                  </RouterLink>
+                </template>
+              </VEntityField>
+              <VEntityField v-if="!singlePage?.page?.spec.deleted">
+                <template #description>
+                  <VStatusDot v-tooltip="`恢复中`" state="success" animate />
+                </template>
+              </VEntityField>
+              <VEntityField v-if="singlePage?.page?.metadata.deletionTimestamp">
+                <template #description>
+                  <VStatusDot v-tooltip="`删除中`" state="warning" animate />
+                </template>
+              </VEntityField>
+              <VEntityField>
+                <template #description>
+                  <span class="truncate text-xs tabular-nums text-gray-500">
+                    {{ formatDatetime(singlePage.page.spec.publishTime) }}
+                  </span>
+                </template>
+              </VEntityField>
+            </template>
+            <template
+              v-if="currentUserHasPermission(['system:singlepages:manage'])"
+              #dropdownItems
+            >
+              <VButton
+                v-close-popper
+                block
+                type="danger"
+                @click="handleDeletePermanently(singlePage.page)"
+              >
+                永久删除
+              </VButton>
+              <VButton
+                v-close-popper
+                block
+                type="default"
+                @click="handleRecovery(singlePage.page)"
+              >
+                恢复
+              </VButton>
+            </template>
+          </VEntity>
+        </li>
+      </ul>
+
+      <template #footer>
+        <div class="bg-white sm:flex sm:items-center sm:justify-end">
+          <VPagination
+            :page="singlePages.page"
+            :size="singlePages.size"
+            :total="singlePages.total"
+            :size-options="[20, 30, 50, 100]"
+            @change="handlePaginationChange"
+          />
+        </div>
+      </template>
+    </VCard>
+  </div>
+</template>
diff --git a/src/modules/contents/pages/SinglePageList.vue b/src/modules/contents/pages/SinglePageList.vue
index ab95da76..3e17aeed 100644
--- a/src/modules/contents/pages/SinglePageList.vue
+++ b/src/modules/contents/pages/SinglePageList.vue
@@ -22,7 +22,7 @@ import {
 } from "@halo-dev/components";
 import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
 import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
-import { onMounted, ref, watchEffect } from "vue";
+import { onMounted, ref, watch, watchEffect } from "vue";
 import type {
   ListedSinglePageList,
   SinglePage,
@@ -57,7 +57,8 @@ const loading = ref(false);
 const settingModal = ref(false);
 const selectedSinglePage = ref<SinglePage>();
 const selectedSinglePageWithContent = ref<SinglePageRequest>();
-const checkAll = ref(false);
+const selectedPageNames = ref<string[]>([]);
+const checkedAll = ref(false);
 const refreshInterval = ref();
 
 const handleFetchSinglePages = async () => {
@@ -73,6 +74,7 @@ const handleFetchSinglePages = async () => {
     }
 
     const { data } = await apiClient.singlePage.listSinglePages({
+      labelSelector: [`content.halo.run/deleted=false`],
       page: singlePages.value.page,
       size: singlePages.value.size,
       visible: selectedVisibleItem.value.value,
@@ -85,7 +87,7 @@ const handleFetchSinglePages = async () => {
     singlePages.value = data;
 
     const deletedSinglePages = singlePages.value.items.filter(
-      (singlePage) => !!singlePage.page.metadata.deletionTimestamp
+      (singlePage) => singlePage.page.spec.deleted
     );
 
     if (deletedSinglePages.length) {
@@ -192,9 +194,30 @@ const handleSelectNext = async () => {
   }
 };
 
+const checkSelection = (singlePage: SinglePage) => {
+  return (
+    singlePage.metadata.name === selectedSinglePage.value?.metadata.name ||
+    selectedPageNames.value.includes(singlePage.metadata.name)
+  );
+};
+
+const handleCheckAllChange = (e: Event) => {
+  const { checked } = e.target as HTMLInputElement;
+
+  if (checked) {
+    selectedPageNames.value =
+      singlePages.value.items.map((singlePage) => {
+        return singlePage.page.metadata.name;
+      }) || [];
+  } else {
+    selectedPageNames.value = [];
+  }
+};
+
 const handleDelete = async (singlePage: SinglePage) => {
   Dialog.warning({
     title: "是否确认删除该自定义页面?",
+    description: "此操作会将自定义页面放入回收站,后续可以从回收站恢复",
     confirmType: "danger",
     onConfirm: async () => {
       const singlePageToUpdate = cloneDeep(singlePage);
@@ -210,6 +233,37 @@ const handleDelete = async (singlePage: SinglePage) => {
   });
 };
 
+const handleDeleteInBatch = async () => {
+  Dialog.warning({
+    title: "是否确认删除选中的自定义页面?",
+    description: "此操作会将自定义页面放入回收站,后续可以从回收站恢复",
+    confirmType: "danger",
+    onConfirm: async () => {
+      await Promise.all(
+        selectedPageNames.value.map((name) => {
+          const page = singlePages.value.items.find(
+            (item) => item.page.metadata.name === name
+          )?.page;
+
+          if (!page) {
+            return Promise.resolve();
+          }
+
+          page.spec.deleted = true;
+          return apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
+            {
+              name: page.metadata.name,
+              singlePage: page,
+            }
+          );
+        })
+      );
+      await handleFetchSinglePages();
+      selectedPageNames.value = [];
+    },
+  });
+};
+
 const finalStatus = (singlePage: SinglePage) => {
   if (singlePage.status?.phase) {
     return SinglePagePhase[singlePage.status.phase];
@@ -217,6 +271,28 @@ const finalStatus = (singlePage: SinglePage) => {
   return "";
 };
 
+watch(selectedPageNames, (newValue) => {
+  checkedAll.value = newValue.length === singlePages.value.items?.length;
+});
+
+watchEffect(async () => {
+  if (
+    !selectedSinglePage.value ||
+    !selectedSinglePage.value.spec.headSnapshot
+  ) {
+    return;
+  }
+
+  const { data: content } = await apiClient.content.obtainSnapshotContent({
+    snapshotName: selectedSinglePage.value.spec.headSnapshot,
+  });
+
+  selectedSinglePageWithContent.value = {
+    page: selectedSinglePage.value,
+    content: content,
+  };
+});
+
 onMounted(handleFetchSinglePages);
 
 // Filters
@@ -351,15 +427,20 @@ function handleSortItemChange(sortItem?: SortItem) {
             class="mr-4 hidden items-center sm:flex"
           >
             <input
-              v-model="checkAll"
+              v-model="checkedAll"
               class="h-4 w-4 rounded border-gray-300 text-indigo-600"
               type="checkbox"
+              @change="handleCheckAllChange"
             />
           </div>
           <div class="flex w-full flex-1 items-center sm:w-auto">
-            <div v-if="!checkAll" class="flex items-center gap-2">
+            <div
+              v-if="!selectedPageNames.length"
+              class="flex items-center gap-2"
+            >
               <FormKit
                 v-model="keyword"
+                outer-class="!p-0"
                 placeholder="输入关键词搜索"
                 type="text"
                 @keyup.enter="handleFetchSinglePages"
@@ -414,8 +495,7 @@ function handleSortItemChange(sortItem?: SortItem) {
               </div>
             </div>
             <VSpace v-else>
-              <VButton type="default">设置</VButton>
-              <VButton type="danger">删除</VButton>
+              <VButton type="danger" @click="handleDeleteInBatch">删除</VButton>
             </VSpace>
           </div>
           <div class="mt-4 flex sm:mt-0">
@@ -561,13 +641,14 @@ function handleSortItemChange(sortItem?: SortItem) {
       role="list"
     >
       <li v-for="(singlePage, index) in singlePages.items" :key="index">
-        <VEntity :is-selected="checkAll">
+        <VEntity :is-selected="checkSelection(singlePage.page)">
           <template
             v-if="currentUserHasPermission(['system:singlepages:manage'])"
             #checkbox
           >
             <input
-              v-model="checkAll"
+              v-model="selectedPageNames"
+              :value="singlePage.page.metadata.name"
               class="h-4 w-4 rounded border-gray-300 text-indigo-600"
               type="checkbox"
             />
@@ -652,6 +733,11 @@ function handleSortItemChange(sortItem?: SortItem) {
                 />
               </template>
             </VEntityField>
+            <VEntityField v-if="singlePage?.page?.spec.deleted">
+              <template #description>
+                <VStatusDot v-tooltip="`删除中`" state="warning" animate />
+              </template>
+            </VEntityField>
             <VEntityField>
               <template #description>
                 <span class="truncate text-xs tabular-nums text-gray-500">
diff --git a/src/modules/contents/pages/layouts/PageLayout.vue b/src/modules/contents/pages/layouts/PageLayout.vue
index ba339592..7d36e276 100644
--- a/src/modules/contents/pages/layouts/PageLayout.vue
+++ b/src/modules/contents/pages/layouts/PageLayout.vue
@@ -7,6 +7,7 @@ import {
   VTabbar,
   IconPages,
   VButton,
+  VSpace,
 } from "@halo-dev/components";
 import BasicLayout from "@/layouts/BasicLayout.vue";
 import { useRoute, useRouter } from "vue-router";
@@ -70,16 +71,21 @@ watchEffect(() => {
         <IconPages class="mr-2 self-center" />
       </template>
       <template #actions>
-        <VButton
-          v-permission="['system:singlepages:manage']"
-          :route="{ name: 'SinglePageEditor' }"
-          type="secondary"
-        >
-          <template #icon>
-            <IconAddCircle class="h-full w-full" />
-          </template>
-          新建
-        </VButton>
+        <VSpace>
+          <VButton :route="{ name: 'DeletedSinglePages' }" size="sm">
+            回收站
+          </VButton>
+          <VButton
+            v-permission="['system:singlepages:manage']"
+            :route="{ name: 'SinglePageEditor' }"
+            type="secondary"
+          >
+            <template #icon>
+              <IconAddCircle class="h-full w-full" />
+            </template>
+            新建
+          </VButton>
+        </VSpace>
       </template>
     </VPageHeader>
     <div class="m-0 md:m-4">
diff --git a/src/modules/contents/pages/module.ts b/src/modules/contents/pages/module.ts
index 016f31b7..6ea57704 100644
--- a/src/modules/contents/pages/module.ts
+++ b/src/modules/contents/pages/module.ts
@@ -4,6 +4,7 @@ import BlankLayout from "@/layouts/BlankLayout.vue";
 import PageLayout from "./layouts/PageLayout.vue";
 import FunctionalPageList from "./FunctionalPageList.vue";
 import SinglePageList from "./SinglePageList.vue";
+import DeletedSinglePageList from "./DeletedSinglePageList.vue";
 import SinglePageEditor from "./SinglePageEditor.vue";
 import { IconPages } from "@halo-dev/components";
 import { markRaw } from "vue";
@@ -63,6 +64,22 @@ export default definePlugin({
                 },
               ],
             },
+            {
+              path: "deleted",
+              component: BasicLayout,
+              children: [
+                {
+                  path: "",
+                  name: "DeletedSinglePages",
+                  component: DeletedSinglePageList,
+                  meta: {
+                    title: "自定义页面回收站",
+                    searchable: true,
+                    permissions: ["system:singlepages:view"],
+                  },
+                },
+              ],
+            },
             {
               path: "editor",
               component: BasicLayout,
diff --git a/src/modules/contents/posts/DeletedPostList.vue b/src/modules/contents/posts/DeletedPostList.vue
new file mode 100644
index 00000000..aea76693
--- /dev/null
+++ b/src/modules/contents/posts/DeletedPostList.vue
@@ -0,0 +1,421 @@
+<script lang="ts" setup>
+import {
+  IconAddCircle,
+  IconDeleteBin,
+  IconRefreshLine,
+  Dialog,
+  VButton,
+  VCard,
+  VEmpty,
+  VPageHeader,
+  VPagination,
+  VSpace,
+  VAvatar,
+  VStatusDot,
+  VEntity,
+  VEntityField,
+} from "@halo-dev/components";
+import PostTag from "./tags/components/PostTag.vue";
+import { onMounted, ref, watch } from "vue";
+import type { ListedPostList, Post } from "@halo-dev/api-client";
+import { apiClient } from "@/utils/api-client";
+import { formatDatetime } from "@/utils/date";
+import { usePermission } from "@/utils/permission";
+import { onBeforeRouteLeave } from "vue-router";
+import cloneDeep from "lodash.clonedeep";
+
+const { currentUserHasPermission } = usePermission();
+
+const posts = ref<ListedPostList>({
+  page: 1,
+  size: 50,
+  total: 0,
+  items: [],
+  first: true,
+  last: false,
+  hasNext: false,
+  hasPrevious: false,
+});
+const loading = ref(false);
+const checkedAll = ref(false);
+const selectedPostNames = ref<string[]>([]);
+const refreshInterval = ref();
+const keyword = ref("");
+
+const handleFetchPosts = async () => {
+  try {
+    clearInterval(refreshInterval.value);
+
+    loading.value = true;
+
+    const { data } = await apiClient.post.listPosts({
+      labelSelector: [`content.halo.run/deleted=true`],
+      page: posts.value.page,
+      size: posts.value.size,
+      keyword: keyword.value,
+    });
+    posts.value = data;
+
+    const deletedPosts = posts.value.items.filter(
+      (post) =>
+        !!post.post.metadata.deletionTimestamp || !post.post.spec.deleted
+    );
+
+    if (deletedPosts.length) {
+      refreshInterval.value = setInterval(() => {
+        handleFetchPosts();
+      }, 3000);
+    }
+  } catch (e) {
+    console.error("Failed to fetch deleted posts", e);
+  } finally {
+    loading.value = false;
+  }
+};
+
+onBeforeRouteLeave(() => {
+  clearInterval(refreshInterval.value);
+});
+
+const handlePaginationChange = ({
+  page,
+  size,
+}: {
+  page: number;
+  size: number;
+}) => {
+  posts.value.page = page;
+  posts.value.size = size;
+  handleFetchPosts();
+};
+
+const checkSelection = (post: Post) => {
+  return selectedPostNames.value.includes(post.metadata.name);
+};
+
+const handleCheckAllChange = (e: Event) => {
+  const { checked } = e.target as HTMLInputElement;
+
+  if (checked) {
+    selectedPostNames.value =
+      posts.value.items.map((post) => {
+        return post.post.metadata.name;
+      }) || [];
+  } else {
+    selectedPostNames.value = [];
+  }
+};
+
+const handleDeletePermanently = async (post: Post) => {
+  Dialog.warning({
+    title: "是否确认永久删除该文章?",
+    description: "删除之后将无法恢复",
+    confirmType: "danger",
+    onConfirm: async () => {
+      await apiClient.extension.post.deletecontentHaloRunV1alpha1Post({
+        name: post.metadata.name,
+      });
+      await handleFetchPosts();
+    },
+  });
+};
+
+const handleDeletePermanentlyInBatch = async () => {
+  Dialog.warning({
+    title: "是否确认永久删除选中的文章?",
+    description: "删除之后将无法恢复",
+    confirmType: "danger",
+    onConfirm: async () => {
+      await Promise.all(
+        selectedPostNames.value.map((name) => {
+          return apiClient.extension.post.deletecontentHaloRunV1alpha1Post({
+            name,
+          });
+        })
+      );
+      await handleFetchPosts();
+      selectedPostNames.value = [];
+    },
+  });
+};
+
+const handleRecovery = async (post: Post) => {
+  Dialog.warning({
+    title: "是否确认恢复该文章?",
+    description: "此操作会将文章恢复到被删除之前的状态",
+    onConfirm: async () => {
+      const postToUpdate = cloneDeep(post);
+      postToUpdate.spec.deleted = false;
+      await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
+        name: postToUpdate.metadata.name,
+        post: postToUpdate,
+      });
+      await handleFetchPosts();
+    },
+  });
+};
+
+const handleRecoveryInBatch = async () => {
+  Dialog.warning({
+    title: "是否确认恢复选中的文章?",
+    description: "此操作会将文章恢复到被删除之前的状态",
+    onConfirm: async () => {
+      await Promise.all(
+        selectedPostNames.value.map((name) => {
+          const post = posts.value.items.find(
+            (item) => item.post.metadata.name === name
+          )?.post;
+
+          if (!post) {
+            return Promise.resolve();
+          }
+
+          post.spec.deleted = false;
+          return apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
+            name: post.metadata.name,
+            post: post,
+          });
+        })
+      );
+      await handleFetchPosts();
+      selectedPostNames.value = [];
+    },
+  });
+};
+
+watch(selectedPostNames, (newValue) => {
+  checkedAll.value = newValue.length === posts.value.items?.length;
+});
+
+onMounted(() => {
+  handleFetchPosts();
+});
+</script>
+<template>
+  <VPageHeader title="文章回收站">
+    <template #icon>
+      <IconDeleteBin class="mr-2 self-center text-green-600" />
+    </template>
+    <template #actions>
+      <VSpace>
+        <VButton :route="{ name: 'Posts' }" size="sm">返回</VButton>
+        <VButton
+          v-permission="['system:posts:manage']"
+          :route="{ name: 'PostEditor' }"
+          type="secondary"
+        >
+          <template #icon>
+            <IconAddCircle class="h-full w-full" />
+          </template>
+          新建
+        </VButton>
+      </VSpace>
+    </template>
+  </VPageHeader>
+
+  <div class="m-0 md:m-4">
+    <VCard :body-class="['!p-0']">
+      <template #header>
+        <div class="block w-full bg-gray-50 px-4 py-3">
+          <div
+            class="relative flex flex-col items-start sm:flex-row sm:items-center"
+          >
+            <div
+              v-permission="['system:posts:manage']"
+              class="mr-4 hidden items-center sm:flex"
+            >
+              <input
+                v-model="checkedAll"
+                class="h-4 w-4 rounded border-gray-300 text-indigo-600"
+                type="checkbox"
+                @change="handleCheckAllChange"
+              />
+            </div>
+            <div class="flex w-full flex-1 items-center sm:w-auto">
+              <div
+                v-if="!selectedPostNames.length"
+                class="flex items-center gap-2"
+              >
+                <FormKit
+                  v-model="keyword"
+                  outer-class="!p-0"
+                  placeholder="输入关键词搜索"
+                  type="text"
+                  @keyup.enter="handleFetchPosts"
+                ></FormKit>
+              </div>
+              <VSpace v-else>
+                <VButton type="danger" @click="handleDeletePermanentlyInBatch">
+                  永久删除
+                </VButton>
+                <VButton type="default" @click="handleRecoveryInBatch">
+                  恢复
+                </VButton>
+              </VSpace>
+            </div>
+            <div class="mt-4 flex sm:mt-0">
+              <VSpace spacing="lg">
+                <div class="flex flex-row gap-2">
+                  <div
+                    class="group cursor-pointer rounded p-1 hover:bg-gray-200"
+                    @click="handleFetchPosts"
+                  >
+                    <IconRefreshLine
+                      :class="{ 'animate-spin text-gray-900': loading }"
+                      class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
+                    />
+                  </div>
+                </div>
+              </VSpace>
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <VEmpty
+        v-if="!posts.items.length && !loading"
+        message="你可以尝试刷新或者返回文章管理"
+        title="没有文章被放入回收站"
+      >
+        <template #actions>
+          <VSpace>
+            <VButton @click="handleFetchPosts">刷新</VButton>
+            <VButton :route="{ name: 'Posts' }" type="primary"> 返回 </VButton>
+          </VSpace>
+        </template>
+      </VEmpty>
+
+      <ul
+        v-else
+        class="box-border h-full w-full divide-y divide-gray-100"
+        role="list"
+      >
+        <li v-for="(post, index) in posts.items" :key="index">
+          <VEntity :is-selected="checkSelection(post.post)">
+            <template
+              v-if="currentUserHasPermission(['system:posts:manage'])"
+              #checkbox
+            >
+              <input
+                v-model="selectedPostNames"
+                :value="post.post.metadata.name"
+                class="h-4 w-4 rounded border-gray-300 text-indigo-600"
+                name="post-checkbox"
+                type="checkbox"
+              />
+            </template>
+            <template #start>
+              <VEntityField :title="post.post.spec.title">
+                <template #extra>
+                  <VSpace class="mt-1 sm:mt-0">
+                    <PostTag
+                      v-for="(tag, tagIndex) in post.tags"
+                      :key="tagIndex"
+                      :tag="tag"
+                      route
+                    ></PostTag>
+                  </VSpace>
+                </template>
+                <template #description>
+                  <VSpace>
+                    <p
+                      v-if="post.categories.length"
+                      class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
+                    >
+                      分类:<span
+                        v-for="(category, categoryIndex) in post.categories"
+                        :key="categoryIndex"
+                        class="cursor-pointer hover:text-gray-900"
+                      >
+                        {{ category.spec.displayName }}
+                      </span>
+                    </p>
+                    <span class="text-xs text-gray-500">
+                      访问量 {{ post.stats.visit || 0 }}
+                    </span>
+                    <span class="text-xs text-gray-500">
+                      评论 {{ post.stats.totalComment || 0 }}
+                    </span>
+                  </VSpace>
+                </template>
+              </VEntityField>
+            </template>
+            <template #end>
+              <VEntityField>
+                <template #description>
+                  <RouterLink
+                    v-for="(contributor, contributorIndex) in post.contributors"
+                    :key="contributorIndex"
+                    :to="{
+                      name: 'UserDetail',
+                      params: { name: contributor.name },
+                    }"
+                    class="flex items-center"
+                  >
+                    <VAvatar
+                      v-tooltip="contributor.displayName"
+                      size="xs"
+                      :src="contributor.avatar"
+                      :alt="contributor.displayName"
+                      circle
+                    ></VAvatar>
+                  </RouterLink>
+                </template>
+              </VEntityField>
+              <VEntityField v-if="!post?.post?.spec.deleted">
+                <template #description>
+                  <VStatusDot v-tooltip="`恢复中`" state="success" animate />
+                </template>
+              </VEntityField>
+              <VEntityField v-if="post?.post?.metadata.deletionTimestamp">
+                <template #description>
+                  <VStatusDot v-tooltip="`删除中`" state="warning" animate />
+                </template>
+              </VEntityField>
+              <VEntityField>
+                <template #description>
+                  <span class="truncate text-xs tabular-nums text-gray-500">
+                    {{ formatDatetime(post.post.spec.publishTime) }}
+                  </span>
+                </template>
+              </VEntityField>
+            </template>
+            <template
+              v-if="currentUserHasPermission(['system:posts:manage'])"
+              #dropdownItems
+            >
+              <VButton
+                v-close-popper
+                block
+                type="danger"
+                @click="handleDeletePermanently(post.post)"
+              >
+                永久删除
+              </VButton>
+              <VButton
+                v-close-popper
+                block
+                type="default"
+                @click="handleRecovery(post.post)"
+              >
+                恢复
+              </VButton>
+            </template>
+          </VEntity>
+        </li>
+      </ul>
+
+      <template #footer>
+        <div class="bg-white sm:flex sm:items-center sm:justify-end">
+          <VPagination
+            :page="posts.page"
+            :size="posts.size"
+            :total="posts.total"
+            :size-options="[20, 30, 50, 100]"
+            @change="handlePaginationChange"
+          />
+        </div>
+      </template>
+    </VCard>
+  </div>
+</template>
diff --git a/src/modules/contents/posts/PostList.vue b/src/modules/contents/posts/PostList.vue
index d87aff76..feb673d3 100644
--- a/src/modules/contents/posts/PostList.vue
+++ b/src/modules/contents/posts/PostList.vue
@@ -40,6 +40,7 @@ import { usePostCategory } from "@/modules/contents/posts/categories/composables
 import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
 import { usePermission } from "@/utils/permission";
 import { onBeforeRouteLeave } from "vue-router";
+import cloneDeep from "lodash.clonedeep";
 
 const { currentUserHasPermission } = usePermission();
 
@@ -93,6 +94,7 @@ const handleFetchPosts = async () => {
     }
 
     const { data } = await apiClient.post.listPosts({
+      labelSelector: [`content.halo.run/deleted=false`],
       page: posts.value.page,
       size: posts.value.size,
       visible: selectedVisibleItem.value?.value,
@@ -107,7 +109,7 @@ const handleFetchPosts = async () => {
     posts.value = data;
 
     const deletedPosts = posts.value.items.filter(
-      (post) => !!post.post.metadata.deletionTimestamp
+      (post) => post.post.spec.deleted
     );
 
     if (deletedPosts.length) {
@@ -217,17 +219,21 @@ const handleCheckAllChange = (e: Event) => {
         return post.post.metadata.name;
       }) || [];
   } else {
-    selectedPostNames.value.length = 0;
+    selectedPostNames.value = [];
   }
 };
 
 const handleDelete = async (post: Post) => {
   Dialog.warning({
     title: "是否确认删除该文章?",
+    description: "此操作会将文章放入回收站,后续可以从回收站恢复",
     confirmType: "danger",
     onConfirm: async () => {
-      await apiClient.extension.post.deletecontentHaloRunV1alpha1Post({
-        name: post.metadata.name,
+      const postToUpdate = cloneDeep(post);
+      postToUpdate.spec.deleted = true;
+      await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
+        name: postToUpdate.metadata.name,
+        post: postToUpdate,
       });
       await handleFetchPosts();
     },
@@ -237,17 +243,28 @@ const handleDelete = async (post: Post) => {
 const handleDeleteInBatch = async () => {
   Dialog.warning({
     title: "是否确认删除选中的文章?",
+    description: "此操作会将文章放入回收站,后续可以从回收站恢复",
     confirmType: "danger",
     onConfirm: async () => {
       await Promise.all(
         selectedPostNames.value.map((name) => {
-          return apiClient.extension.post.deletecontentHaloRunV1alpha1Post({
-            name,
+          const post = posts.value.items.find(
+            (item) => item.post.metadata.name === name
+          )?.post;
+
+          if (!post) {
+            return Promise.resolve();
+          }
+
+          post.spec.deleted = true;
+          return apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
+            name: post.metadata.name,
+            post: post,
           });
         })
       );
       await handleFetchPosts();
-      selectedPostNames.value.length = 0;
+      selectedPostNames.value = [];
     },
   });
 };
@@ -418,6 +435,7 @@ function handleContributorChange(user?: User) {
       <VSpace>
         <VButton :route="{ name: 'Categories' }" size="sm">分类</VButton>
         <VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
+        <VButton :route="{ name: 'DeletedPosts' }" size="sm">回收站</VButton>
         <VButton
           v-permission="['system:posts:manage']"
           :route="{ name: 'PostEditor' }"
@@ -457,6 +475,7 @@ function handleContributorChange(user?: User) {
               >
                 <FormKit
                   v-model="keyword"
+                  outer-class="!p-0"
                   placeholder="输入关键词搜索"
                   type="text"
                   @keyup.enter="handleFetchPosts"
@@ -936,7 +955,7 @@ function handleContributorChange(user?: User) {
                   />
                 </template>
               </VEntityField>
-              <VEntityField v-if="post?.post?.metadata.deletionTimestamp">
+              <VEntityField v-if="post?.post?.spec.deleted">
                 <template #description>
                   <VStatusDot v-tooltip="`删除中`" state="warning" animate />
                 </template>
diff --git a/src/modules/contents/posts/module.ts b/src/modules/contents/posts/module.ts
index ca427a3c..6254703b 100644
--- a/src/modules/contents/posts/module.ts
+++ b/src/modules/contents/posts/module.ts
@@ -3,6 +3,7 @@ import BasicLayout from "@/layouts/BasicLayout.vue";
 import BlankLayout from "@/layouts/BlankLayout.vue";
 import { IconBookRead } from "@halo-dev/components";
 import PostList from "./PostList.vue";
+import DeletedPostList from "./DeletedPostList.vue";
 import PostEditor from "./PostEditor.vue";
 import CategoryList from "./categories/CategoryList.vue";
 import TagList from "./tags/TagList.vue";
@@ -33,6 +34,16 @@ export default definePlugin({
             },
           },
         },
+        {
+          path: "deleted",
+          name: "DeletedPosts",
+          component: DeletedPostList,
+          meta: {
+            title: "文章回收站",
+            searchable: true,
+            permissions: ["system:posts:view"],
+          },
+        },
         {
           path: "editor",
           name: "PostEditor",