mirror of https://github.com/halo-dev/halo
834 lines
30 KiB
Vue
834 lines
30 KiB
Vue
<script lang="ts" setup>
|
|
import {
|
|
IconAddCircle,
|
|
IconArrowDown,
|
|
IconArrowLeft,
|
|
IconArrowRight,
|
|
IconBookRead,
|
|
IconEye,
|
|
IconEyeOff,
|
|
IconSettings,
|
|
IconTeam,
|
|
useDialog,
|
|
VButton,
|
|
VCard,
|
|
VEmpty,
|
|
VPageHeader,
|
|
VPagination,
|
|
VSpace,
|
|
VTag,
|
|
} from "@halo-dev/components";
|
|
import PostSettingModal from "./components/PostSettingModal.vue";
|
|
import PostTag from "../posts/tags/components/PostTag.vue";
|
|
import { onMounted, ref, watch, watchEffect } from "vue";
|
|
import type { ListedPostList, Post, PostRequest } from "@halo-dev/api-client";
|
|
import { apiClient } from "@halo-dev/admin-shared";
|
|
import { formatDatetime } from "@/utils/date";
|
|
import { useUserFetch } from "@/modules/system/users/composables/use-user";
|
|
import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category";
|
|
import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
|
|
import cloneDeep from "lodash.clonedeep";
|
|
import { postLabels } from "@/constants/labels";
|
|
|
|
enum PostPhase {
|
|
DRAFT = "未发布",
|
|
PENDING_APPROVAL = "待审核",
|
|
PUBLISHED = "已发布",
|
|
}
|
|
|
|
const posts = ref<ListedPostList>({
|
|
page: 1,
|
|
size: 20,
|
|
total: 0,
|
|
items: [],
|
|
first: true,
|
|
last: false,
|
|
hasNext: false,
|
|
hasPrevious: false,
|
|
});
|
|
const loading = ref(false);
|
|
const settingModal = ref(false);
|
|
const selectedPost = ref<Post | null>(null);
|
|
const selectedPostWithContent = ref<PostRequest | null>(null);
|
|
const checkedAll = ref(false);
|
|
const selectedPostNames = ref<string[]>([]);
|
|
|
|
const { users } = useUserFetch();
|
|
const { categories } = usePostCategory();
|
|
const { tags } = usePostTag();
|
|
const dialog = useDialog();
|
|
|
|
const handleFetchPosts = async () => {
|
|
try {
|
|
loading.value = true;
|
|
|
|
const labelSelector: string[] = [];
|
|
|
|
if (selectedVisibleFilterItem.value.value) {
|
|
labelSelector.push(
|
|
`${postLabels.VISIBLE}=${selectedVisibleFilterItem.value.value}`
|
|
);
|
|
}
|
|
|
|
if (selectedPhaseFilterItem.value.value) {
|
|
labelSelector.push(
|
|
`${postLabels.PHASE}=${selectedPhaseFilterItem.value.value}`
|
|
);
|
|
}
|
|
|
|
const { data } = await apiClient.post.listPosts({
|
|
page: posts.value.page,
|
|
size: posts.value.size,
|
|
labelSelector,
|
|
});
|
|
posts.value = data;
|
|
} catch (e) {
|
|
console.error("Failed to fetch posts", e);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const handlePaginationChange = ({
|
|
page,
|
|
size,
|
|
}: {
|
|
page: number;
|
|
size: number;
|
|
}) => {
|
|
posts.value.page = page;
|
|
posts.value.size = size;
|
|
handleFetchPosts();
|
|
};
|
|
|
|
const handleOpenSettingModal = async (post: Post) => {
|
|
const { data } = await apiClient.extension.post.getcontentHaloRunV1alpha1Post(
|
|
{
|
|
name: post.metadata.name,
|
|
}
|
|
);
|
|
selectedPost.value = data;
|
|
settingModal.value = true;
|
|
};
|
|
|
|
const onSettingModalClose = () => {
|
|
selectedPost.value = null;
|
|
selectedPostWithContent.value = null;
|
|
handleFetchPosts();
|
|
};
|
|
|
|
const handleSelectPrevious = async () => {
|
|
const { items, hasPrevious } = posts.value;
|
|
const index = items.findIndex(
|
|
(post) => post.post.metadata.name === selectedPost.value?.metadata.name
|
|
);
|
|
if (index > 0) {
|
|
const { data } =
|
|
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
|
name: items[index - 1].post.metadata.name,
|
|
});
|
|
selectedPost.value = data;
|
|
return;
|
|
}
|
|
if (index === 0 && hasPrevious) {
|
|
posts.value.page--;
|
|
await handleFetchPosts();
|
|
selectedPost.value = posts.value.items[posts.value.items.length - 1].post;
|
|
}
|
|
};
|
|
|
|
const handleSelectNext = async () => {
|
|
const { items, hasNext } = posts.value;
|
|
const index = items.findIndex(
|
|
(post) => post.post.metadata.name === selectedPost.value?.metadata.name
|
|
);
|
|
if (index < items.length - 1) {
|
|
const { data } =
|
|
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
|
name: items[index + 1].post.metadata.name,
|
|
});
|
|
selectedPost.value = data;
|
|
return;
|
|
}
|
|
if (index === items.length - 1 && hasNext) {
|
|
posts.value.page++;
|
|
await handleFetchPosts();
|
|
selectedPost.value = posts.value.items[0].post;
|
|
}
|
|
};
|
|
|
|
const checkSelection = (post: Post) => {
|
|
return (
|
|
post.metadata.name === selectedPost.value?.metadata.name ||
|
|
selectedPostNames.value.includes(post.metadata.name)
|
|
);
|
|
};
|
|
|
|
const finalStatus = (post: Post) => {
|
|
if (post.status?.phase) {
|
|
return PostPhase[post.status.phase];
|
|
}
|
|
return "";
|
|
};
|
|
|
|
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.length = 0;
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (post: Post) => {
|
|
dialog.warning({
|
|
title: "是否确认删除该文章?",
|
|
confirmType: "danger",
|
|
onConfirm: async () => {
|
|
const postToUpdate = cloneDeep(post);
|
|
postToUpdate.spec.deleted = true;
|
|
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
|
|
name: postToUpdate.metadata.name,
|
|
post: postToUpdate,
|
|
});
|
|
await handleFetchPosts();
|
|
},
|
|
});
|
|
};
|
|
|
|
watch(selectedPostNames, (newValue) => {
|
|
checkedAll.value = newValue.length === posts.value.items?.length;
|
|
});
|
|
|
|
watchEffect(async () => {
|
|
if (!selectedPost.value || !selectedPost.value.spec.headSnapshot) {
|
|
return;
|
|
}
|
|
|
|
const { data: content } = await apiClient.content.obtainSnapshotContent({
|
|
snapshotName: selectedPost.value.spec.headSnapshot,
|
|
});
|
|
|
|
selectedPostWithContent.value = {
|
|
post: selectedPost.value,
|
|
content: content,
|
|
};
|
|
});
|
|
|
|
onMounted(() => {
|
|
handleFetchPosts();
|
|
});
|
|
|
|
interface FilterItem {
|
|
label: string;
|
|
value: string | undefined;
|
|
}
|
|
|
|
const VisibleFilterItems: FilterItem[] = [
|
|
{
|
|
label: "全部",
|
|
value: "",
|
|
},
|
|
{
|
|
label: "公开",
|
|
value: "PUBLIC",
|
|
},
|
|
{
|
|
label: "内部成员可访问",
|
|
value: "INTERNAL",
|
|
},
|
|
{
|
|
label: "私有",
|
|
value: "PRIVATE",
|
|
},
|
|
];
|
|
|
|
const PhaseFilterItems: FilterItem[] = [
|
|
{
|
|
label: "全部",
|
|
value: "",
|
|
},
|
|
{
|
|
label: "已发布",
|
|
value: "PUBLISHED",
|
|
},
|
|
{
|
|
label: "未发布",
|
|
value: "DRAFT",
|
|
},
|
|
{
|
|
label: "待审核",
|
|
value: "PENDING_APPROVAL",
|
|
},
|
|
];
|
|
|
|
const selectedVisibleFilterItem = ref<FilterItem>(VisibleFilterItems[0]);
|
|
const selectedPhaseFilterItem = ref<FilterItem>(PhaseFilterItems[0]);
|
|
|
|
function handleVisibleFilterItemChange(filterItem: FilterItem) {
|
|
selectedVisibleFilterItem.value = filterItem;
|
|
handleFetchPosts();
|
|
}
|
|
|
|
function handlePhaseFilterItemChange(filterItem: FilterItem) {
|
|
selectedPhaseFilterItem.value = filterItem;
|
|
handleFetchPosts();
|
|
}
|
|
</script>
|
|
<template>
|
|
<PostSettingModal
|
|
v-model:visible="settingModal"
|
|
:post="selectedPostWithContent"
|
|
@close="onSettingModalClose"
|
|
>
|
|
<template #actions>
|
|
<div class="modal-header-action" @click="handleSelectPrevious">
|
|
<IconArrowLeft />
|
|
</div>
|
|
<div class="modal-header-action" @click="handleSelectNext">
|
|
<IconArrowRight />
|
|
</div>
|
|
</template>
|
|
</PostSettingModal>
|
|
<VPageHeader title="文章">
|
|
<template #icon>
|
|
<IconBookRead class="mr-2 self-center" />
|
|
</template>
|
|
<template #actions>
|
|
<VSpace>
|
|
<VButton :route="{ name: 'Categories' }" size="sm">分类</VButton>
|
|
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
|
|
<VButton :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 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 sm:w-auto">
|
|
<div v-if="!selectedPostNames.length">
|
|
<FormKit placeholder="输入关键词搜索" type="text"></FormKit>
|
|
</div>
|
|
<VSpace v-else>
|
|
<VButton type="default">设置</VButton>
|
|
<VButton type="danger">删除</VButton>
|
|
</VSpace>
|
|
</div>
|
|
<div class="mt-4 flex sm:mt-0">
|
|
<VSpace spacing="lg">
|
|
<FloatingDropdown>
|
|
<div
|
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
|
>
|
|
<span class="mr-0.5">状态</span>
|
|
<span>
|
|
<IconArrowDown />
|
|
</span>
|
|
</div>
|
|
<template #popper>
|
|
<div class="w-72 p-4">
|
|
<ul class="space-y-1">
|
|
<li
|
|
v-for="(filterItem, index) in PhaseFilterItems"
|
|
:key="index"
|
|
v-close-popper
|
|
:class="{
|
|
'bg-gray-100':
|
|
selectedPhaseFilterItem.value ===
|
|
filterItem.value,
|
|
}"
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
@click="handlePhaseFilterItemChange(filterItem)"
|
|
>
|
|
<span class="truncate">{{ filterItem.label }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
</FloatingDropdown>
|
|
<FloatingDropdown>
|
|
<div
|
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
|
>
|
|
<span class="mr-0.5"> 可见性 </span>
|
|
<span>
|
|
<IconArrowDown />
|
|
</span>
|
|
</div>
|
|
<template #popper>
|
|
<div class="w-72 p-4">
|
|
<ul class="space-y-1">
|
|
<li
|
|
v-for="(filterItem, index) in VisibleFilterItems"
|
|
:key="index"
|
|
v-close-popper
|
|
:class="{
|
|
'bg-gray-100':
|
|
selectedVisibleFilterItem.value ===
|
|
filterItem.value,
|
|
}"
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
@click="handleVisibleFilterItemChange(filterItem)"
|
|
>
|
|
<span class="truncate">
|
|
{{ filterItem.label }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
</FloatingDropdown>
|
|
<FloatingDropdown>
|
|
<div
|
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
|
>
|
|
<span class="mr-0.5">分类</span>
|
|
<span>
|
|
<IconArrowDown />
|
|
</span>
|
|
</div>
|
|
<template #popper>
|
|
<div class="h-96 w-80">
|
|
<div class="bg-white p-4">
|
|
<FormKit
|
|
placeholder="输入关键词搜索"
|
|
type="text"
|
|
></FormKit>
|
|
</div>
|
|
<div class="mt-2">
|
|
<ul
|
|
class="box-border h-full w-full divide-y divide-gray-100"
|
|
>
|
|
<li
|
|
v-for="(category, index) in categories"
|
|
:key="index"
|
|
v-close-popper
|
|
>
|
|
<div
|
|
class="group relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
|
>
|
|
<div class="relative flex flex-row items-center">
|
|
<div class="flex-1">
|
|
<div class="flex flex-col sm:flex-row">
|
|
<span
|
|
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
|
|
>
|
|
{{ category.spec.displayName }}
|
|
</span>
|
|
<VSpace class="mt-1 sm:mt-0"></VSpace>
|
|
</div>
|
|
<div class="mt-1 flex">
|
|
<span class="text-xs text-gray-500">
|
|
/categories/{{ category.spec.slug }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex">
|
|
<div
|
|
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
|
>
|
|
<div
|
|
class="cursor-pointer text-sm text-gray-500 hover:text-gray-900"
|
|
>
|
|
20 篇文章
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</FloatingDropdown>
|
|
<FloatingDropdown>
|
|
<div
|
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
|
>
|
|
<span class="mr-0.5">标签</span>
|
|
<span>
|
|
<IconArrowDown />
|
|
</span>
|
|
</div>
|
|
<template #popper>
|
|
<div class="h-96 w-80">
|
|
<div class="bg-white p-4">
|
|
<FormKit
|
|
placeholder="输入关键词搜索"
|
|
type="text"
|
|
></FormKit>
|
|
</div>
|
|
|
|
<div class="mt-2">
|
|
<ul
|
|
class="box-border h-full w-full divide-y divide-gray-100"
|
|
role="list"
|
|
>
|
|
<li
|
|
v-for="(tag, index) in tags"
|
|
:key="index"
|
|
v-close-popper
|
|
>
|
|
<div
|
|
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
|
>
|
|
<div class="relative flex flex-row items-center">
|
|
<div class="flex-1">
|
|
<div class="flex flex-col sm:flex-row">
|
|
<PostTag :tag="tag" />
|
|
</div>
|
|
<div class="mt-1 flex">
|
|
<span class="text-xs text-gray-500">
|
|
/tags/{{ tag.spec.slug }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex">
|
|
<div
|
|
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
|
>
|
|
<div
|
|
class="cursor-pointer text-sm text-gray-500 hover:text-gray-900"
|
|
>
|
|
20 篇文章
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</FloatingDropdown>
|
|
<FloatingDropdown>
|
|
<div
|
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
|
>
|
|
<span class="mr-0.5">作者</span>
|
|
<span>
|
|
<IconArrowDown />
|
|
</span>
|
|
</div>
|
|
<template #popper>
|
|
<div class="h-96 w-80">
|
|
<div class="bg-white p-4">
|
|
<!--TODO: Auto Focus-->
|
|
<FormKit
|
|
placeholder="输入关键词搜索"
|
|
type="text"
|
|
></FormKit>
|
|
</div>
|
|
<div class="mt-2">
|
|
<ul class="divide-y divide-gray-200" role="list">
|
|
<li
|
|
v-for="(user, index) in users"
|
|
:key="index"
|
|
v-close-popper
|
|
class="cursor-pointer hover:bg-gray-50"
|
|
>
|
|
<div class="flex items-center space-x-4 px-4 py-3">
|
|
<div class="flex-shrink-0">
|
|
<img
|
|
:alt="user.spec.displayName"
|
|
:src="user.spec.avatar"
|
|
class="h-10 w-10 rounded"
|
|
/>
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<p
|
|
class="truncate text-sm font-medium text-gray-900"
|
|
>
|
|
{{ user.spec.displayName }}
|
|
</p>
|
|
<p class="truncate text-sm text-gray-500">
|
|
@{{ user.metadata.name }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<VTag>{{ index + 1 }} 篇</VTag>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</FloatingDropdown>
|
|
<FloatingDropdown>
|
|
<div
|
|
class="flex cursor-pointer select-none items-center text-sm text-gray-700 hover:text-black"
|
|
>
|
|
<span class="mr-0.5">排序</span>
|
|
<span>
|
|
<IconArrowDown />
|
|
</span>
|
|
</div>
|
|
<template #popper>
|
|
<div class="w-72 p-4">
|
|
<ul class="space-y-1">
|
|
<li
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
>
|
|
<span class="truncate">较近发布</span>
|
|
</li>
|
|
<li
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
>
|
|
<span class="truncate">较晚发布</span>
|
|
</li>
|
|
<li
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
>
|
|
<span class="truncate">浏览量最多</span>
|
|
</li>
|
|
<li
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
>
|
|
<span class="truncate">浏览量最少</span>
|
|
</li>
|
|
<li
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
>
|
|
<span class="truncate">评论量最多</span>
|
|
</li>
|
|
<li
|
|
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
>
|
|
<span class="truncate">评论量最少</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
</FloatingDropdown>
|
|
</VSpace>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<VEmpty
|
|
v-if="!posts.items.length && !loading"
|
|
message="你可以尝试刷新或者新建文章"
|
|
title="当前没有文章"
|
|
>
|
|
<template #actions>
|
|
<VSpace>
|
|
<VButton @click="handleFetchPosts">刷新</VButton>
|
|
<VButton :route="{ name: 'PostEditor' }" type="primary">
|
|
<template #icon>
|
|
<IconAddCircle class="h-full w-full" />
|
|
</template>
|
|
新建文章
|
|
</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">
|
|
<div
|
|
:class="{
|
|
'bg-gray-100': checkSelection(post.post),
|
|
}"
|
|
class="relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
|
|
>
|
|
<div
|
|
v-show="checkSelection(post.post)"
|
|
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
|
></div>
|
|
<div class="relative flex flex-row items-center">
|
|
<div class="mr-4 hidden items-center sm:flex">
|
|
<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"
|
|
/>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex flex-col items-center sm:flex-row">
|
|
<RouterLink
|
|
:to="{
|
|
name: 'PostEditor',
|
|
query: { name: post.post.metadata.name },
|
|
}"
|
|
>
|
|
<span
|
|
class="mr-0 truncate text-sm font-medium text-gray-900 sm:mr-2"
|
|
>
|
|
{{ post.post.spec.title }}
|
|
</span>
|
|
</RouterLink>
|
|
<VSpace class="mt-1 sm:mt-0">
|
|
<FloatingTooltip
|
|
v-if="post.post.status?.inProgress"
|
|
class="hidden items-center sm:flex"
|
|
>
|
|
<RouterLink
|
|
:to="{
|
|
name: 'PostEditor',
|
|
query: { name: post.post.metadata.name },
|
|
}"
|
|
class="flex items-center"
|
|
>
|
|
<div
|
|
class="inline-flex h-1.5 w-1.5 rounded-full bg-orange-600"
|
|
>
|
|
<span
|
|
class="inline-block h-1.5 w-1.5 animate-ping rounded-full bg-orange-600"
|
|
></span>
|
|
</div>
|
|
</RouterLink>
|
|
<template #popper>
|
|
当前有内容已保存,但还未发布。
|
|
</template>
|
|
</FloatingTooltip>
|
|
<PostTag
|
|
v-for="(tag, tagIndex) in post.tags"
|
|
:key="tagIndex"
|
|
:tag="tag"
|
|
route
|
|
></PostTag>
|
|
</VSpace>
|
|
</div>
|
|
<div class="mt-1 flex">
|
|
<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">访问量 0</span>
|
|
<span class="text-xs text-gray-500"> 评论 0 </span>
|
|
</VSpace>
|
|
</div>
|
|
</div>
|
|
<div class="flex">
|
|
<div
|
|
class="inline-flex flex-col items-end gap-4 sm:flex-row sm:items-center sm:gap-6"
|
|
>
|
|
<RouterLink
|
|
v-for="(contributor, contributorIndex) in post.contributors"
|
|
:key="contributorIndex"
|
|
:to="{
|
|
name: 'UserDetail',
|
|
params: { name: contributor.name },
|
|
}"
|
|
>
|
|
<img
|
|
v-tooltip="contributor.displayName"
|
|
:alt="contributor.name"
|
|
:src="contributor.avatar"
|
|
:title="contributor.displayName"
|
|
class="hidden h-6 w-6 rounded-full ring-2 ring-white sm:inline-block"
|
|
/>
|
|
</RouterLink>
|
|
<span class="text-sm text-gray-500">
|
|
{{ finalStatus(post.post) }}
|
|
</span>
|
|
<span>
|
|
<IconEye
|
|
v-if="post.post.spec.visible === 'PUBLIC'"
|
|
v-tooltip="`公开访问`"
|
|
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
|
/>
|
|
<IconEyeOff
|
|
v-if="post.post.spec.visible === 'PRIVATE'"
|
|
v-tooltip="`私有访问`"
|
|
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
|
/>
|
|
<IconTeam
|
|
v-if="post.post.spec.visible === 'INTERNAL'"
|
|
v-tooltip="`内部成员可访问`"
|
|
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
|
/>
|
|
</span>
|
|
<time class="text-sm text-gray-500">
|
|
{{ formatDatetime(post.post.metadata.creationTimestamp) }}
|
|
</time>
|
|
<span>
|
|
<FloatingDropdown>
|
|
<IconSettings
|
|
class="cursor-pointer transition-all hover:text-blue-600"
|
|
/>
|
|
<template #popper>
|
|
<div class="w-48 p-2">
|
|
<VSpace class="w-full" direction="column">
|
|
<VButton
|
|
v-close-popper
|
|
block
|
|
type="secondary"
|
|
@click="handleOpenSettingModal(post.post)"
|
|
>
|
|
设置
|
|
</VButton>
|
|
<VButton
|
|
v-close-popper
|
|
block
|
|
type="danger"
|
|
@click="handleDelete(post.post)"
|
|
>
|
|
删除
|
|
</VButton>
|
|
</VSpace>
|
|
</div>
|
|
</template>
|
|
</FloatingDropdown>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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"
|
|
@change="handlePaginationChange"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</VCard>
|
|
</div>
|
|
</template>
|