mirror of https://github.com/halo-dev/halo
feat: make field items of post data list extendable (#4528)
#### What type of PR is this? /area console /kind feature /milestone 2.9.x #### What this PR does / why we need it: 文章数据列表的显示字段支持通过插件扩展。 <img width="717" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/3b3b7e1f-4626-4878-a234-48915dd34e8d"> #### Special notes for your reviewer: 需要测试文章管理功能是否正常。 #### Does this PR introduce a user-facing change? ```release-note Console 端文章数据列表的显示字段支持通过插件扩展。 ```pull/4533/head v2.9.0
parent
6dd77af7f8
commit
272e279891
|
@ -9,6 +9,7 @@
|
||||||
目前支持扩展的数据列表:
|
目前支持扩展的数据列表:
|
||||||
|
|
||||||
- 插件:`"plugin:list-item:field:create"?: (plugin: Ref<Plugin>) => | EntityFieldItem[] | Promise<EntityFieldItem[]>`
|
- 插件:`"plugin:list-item:field:create"?: (plugin: Ref<Plugin>) => | EntityFieldItem[] | Promise<EntityFieldItem[]>`
|
||||||
|
- 文章:`"post:list-item:field:create"?: (post: Ref<ListedPost>) => | EntityFieldItem[] | Promise<EntityFieldItem[]>`
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,10 @@ export interface ExtensionPoint {
|
||||||
plugin: Ref<Plugin>
|
plugin: Ref<Plugin>
|
||||||
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
|
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
|
||||||
|
|
||||||
|
"post:list-item:field:create"?: (
|
||||||
|
post: Ref<ListedPost>
|
||||||
|
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
|
||||||
|
|
||||||
"theme:list:tabs:create"?: () => ThemeListTab[] | Promise<ThemeListTab[]>;
|
"theme:list:tabs:create"?: () => ThemeListTab[] | Promise<ThemeListTab[]>;
|
||||||
|
|
||||||
"theme:list-item:operation:create"?: (
|
"theme:list-item:operation:create"?: (
|
||||||
|
|
|
@ -32,11 +32,11 @@ import { usePermission } from "@/utils/permission";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
import type { PluginModule } from "@halo-dev/console-shared";
|
|
||||||
import type {
|
import type {
|
||||||
|
PluginModule,
|
||||||
CommentSubjectRefProvider,
|
CommentSubjectRefProvider,
|
||||||
CommentSubjectRefResult,
|
CommentSubjectRefResult,
|
||||||
} from "packages/shared/dist";
|
} from "@halo-dev/console-shared";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
|
@ -472,7 +472,7 @@ watch(selectedPostNames, (newValue) => {
|
||||||
class="box-border h-full w-full divide-y divide-gray-100"
|
class="box-border h-full w-full divide-y divide-gray-100"
|
||||||
role="list"
|
role="list"
|
||||||
>
|
>
|
||||||
<li v-for="(post, index) in posts" :key="index">
|
<li v-for="post in posts" :key="post.post.metadata.name">
|
||||||
<PostListItem
|
<PostListItem
|
||||||
:post="post"
|
:post="post"
|
||||||
:is-selected="checkSelection(post.post)"
|
:is-selected="checkSelection(post.post)"
|
||||||
|
|
|
@ -1,36 +1,31 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
IconExternalLinkLine,
|
|
||||||
IconEye,
|
|
||||||
IconEyeOff,
|
|
||||||
Toast,
|
Toast,
|
||||||
VAvatar,
|
|
||||||
VDropdownDivider,
|
VDropdownDivider,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VEntity,
|
VEntity,
|
||||||
VEntityField,
|
VEntityField,
|
||||||
VSpace,
|
|
||||||
VStatusDot,
|
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import PostTag from "../tags/components/PostTag.vue";
|
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import type { ListedPost, Post } from "@halo-dev/api-client";
|
import type { ListedPost, Post } from "@halo-dev/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { postLabels } from "@/constants/labels";
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/vue-query";
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
import { inject } from "vue";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { ref } from "vue";
|
import { computed, toRefs, markRaw, ref, inject } from "vue";
|
||||||
import { computed } from "vue";
|
|
||||||
import { markRaw } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import { useEntityFieldItemExtensionPoint } from "@/composables/use-entity-extension-points";
|
||||||
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
|
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
|
||||||
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
|
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
|
||||||
import { toRefs } from "vue";
|
import type { EntityFieldItem, OperationItem } from "@halo-dev/console-shared";
|
||||||
import type { OperationItem } from "packages/shared/dist";
|
import TitleField from "./entity-fields/TitleField.vue";
|
||||||
|
import EntityFieldItems from "@/components/entity-fields/EntityFieldItems.vue";
|
||||||
|
import ContributorsField from "./entity-fields/ContributorsField.vue";
|
||||||
|
import PublishStatusField from "./entity-fields/PublishStatusField.vue";
|
||||||
|
import VisibleField from "./entity-fields/VisibleField.vue";
|
||||||
|
import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -55,56 +50,6 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const selectedPostNames = inject<Ref<string[]>>("selectedPostNames", ref([]));
|
const selectedPostNames = inject<Ref<string[]>>("selectedPostNames", ref([]));
|
||||||
|
|
||||||
const externalUrl = computed(() => {
|
|
||||||
const { status, metadata } = props.post.post;
|
|
||||||
if (metadata.labels?.[postLabels.PUBLISHED] === "true") {
|
|
||||||
return status?.permalink;
|
|
||||||
}
|
|
||||||
return `/preview/posts/${metadata.name}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const publishStatus = computed(() => {
|
|
||||||
const { labels } = props.post.post.metadata;
|
|
||||||
return labels?.[postLabels.PUBLISHED] === "true"
|
|
||||||
? t("core.post.filters.status.items.published")
|
|
||||||
: t("core.post.filters.status.items.draft");
|
|
||||||
});
|
|
||||||
|
|
||||||
const isPublishing = computed(() => {
|
|
||||||
const { spec, status, metadata } = props.post.post;
|
|
||||||
return (
|
|
||||||
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
|
|
||||||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: changeVisibleMutation } = useMutation({
|
|
||||||
mutationFn: async (post: Post) => {
|
|
||||||
const { data } =
|
|
||||||
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
|
||||||
name: post.metadata.name,
|
|
||||||
});
|
|
||||||
data.spec.visible = data.spec.visible === "PRIVATE" ? "PUBLIC" : "PRIVATE";
|
|
||||||
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post(
|
|
||||||
{
|
|
||||||
name: post.metadata.name,
|
|
||||||
post: data,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mute: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["posts"] });
|
|
||||||
},
|
|
||||||
retry: 3,
|
|
||||||
onSuccess: () => {
|
|
||||||
Toast.success(t("core.common.toast.operation_success"));
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
Toast.error(t("core.common.toast.operation_failed"));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
Dialog.warning({
|
Dialog.warning({
|
||||||
title: t("core.post.operations.delete.title"),
|
title: t("core.post.operations.delete.title"),
|
||||||
|
@ -164,6 +109,64 @@ const { operationItems } = useOperationItemExtensionPoint<ListedPost>(
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { startFields, endFields } = useEntityFieldItemExtensionPoint<ListedPost>(
|
||||||
|
"post:list-item:field:create",
|
||||||
|
post,
|
||||||
|
computed((): EntityFieldItem[] => [
|
||||||
|
{
|
||||||
|
priority: 10,
|
||||||
|
position: "start",
|
||||||
|
component: markRaw(TitleField),
|
||||||
|
props: {
|
||||||
|
post: props.post,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 10,
|
||||||
|
position: "end",
|
||||||
|
component: markRaw(ContributorsField),
|
||||||
|
props: {
|
||||||
|
post: props.post,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 20,
|
||||||
|
position: "end",
|
||||||
|
component: markRaw(PublishStatusField),
|
||||||
|
props: {
|
||||||
|
post: props.post,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 30,
|
||||||
|
position: "end",
|
||||||
|
component: markRaw(VisibleField),
|
||||||
|
props: {
|
||||||
|
post: props.post,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 40,
|
||||||
|
position: "end",
|
||||||
|
component: markRaw(StatusDotField),
|
||||||
|
props: {
|
||||||
|
tooltip: t("core.common.status.deleting"),
|
||||||
|
state: "warning",
|
||||||
|
animate: true,
|
||||||
|
},
|
||||||
|
hidden: !props.post.post.spec.deleted,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
priority: 50,
|
||||||
|
position: "end",
|
||||||
|
component: markRaw(VEntityField),
|
||||||
|
props: {
|
||||||
|
description: formatDatetime(props.post.post.spec.publishTime),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -181,144 +184,10 @@ const { operationItems } = useOperationItemExtensionPoint<ListedPost>(
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #start>
|
<template #start>
|
||||||
<VEntityField
|
<EntityFieldItems :fields="startFields" />
|
||||||
:title="post.post.spec.title"
|
|
||||||
:route="{
|
|
||||||
name: 'PostEditor',
|
|
||||||
query: { name: post.post.metadata.name },
|
|
||||||
}"
|
|
||||||
width="27rem"
|
|
||||||
>
|
|
||||||
<template #extra>
|
|
||||||
<VSpace class="mt-1 sm:mt-0">
|
|
||||||
<RouterLink
|
|
||||||
v-if="post.post.status?.inProgress"
|
|
||||||
v-tooltip="$t('core.common.tooltips.unpublished_content_tip')"
|
|
||||||
:to="{
|
|
||||||
name: 'PostEditor',
|
|
||||||
query: { name: post.post.metadata.name },
|
|
||||||
}"
|
|
||||||
class="flex items-center"
|
|
||||||
>
|
|
||||||
<VStatusDot state="success" animate />
|
|
||||||
</RouterLink>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
:href="externalUrl"
|
|
||||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
|
||||||
>
|
|
||||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
|
||||||
</a>
|
|
||||||
</VSpace>
|
|
||||||
</template>
|
|
||||||
<template #description>
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<VSpace class="flex-wrap !gap-y-1">
|
|
||||||
<p
|
|
||||||
v-if="post.categories.length"
|
|
||||||
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
|
|
||||||
>
|
|
||||||
{{ $t("core.post.list.fields.categories") }}
|
|
||||||
<a
|
|
||||||
v-for="(category, categoryIndex) in post.categories"
|
|
||||||
:key="categoryIndex"
|
|
||||||
:href="category.status?.permalink"
|
|
||||||
:title="category.status?.permalink"
|
|
||||||
target="_blank"
|
|
||||||
class="cursor-pointer hover:text-gray-900"
|
|
||||||
>
|
|
||||||
{{ category.spec.displayName }}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<span class="text-xs text-gray-500">
|
|
||||||
{{
|
|
||||||
$t("core.post.list.fields.visits", {
|
|
||||||
visits: post.stats.visit,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-gray-500">
|
|
||||||
{{
|
|
||||||
$t("core.post.list.fields.comments", {
|
|
||||||
comments: post.stats.totalComment || 0,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span v-if="post.post.spec.pinned" class="text-xs text-gray-500">
|
|
||||||
{{ $t("core.post.list.fields.pinned") }}
|
|
||||||
</span>
|
|
||||||
</VSpace>
|
|
||||||
<VSpace v-if="post.tags.length" class="flex-wrap">
|
|
||||||
<PostTag
|
|
||||||
v-for="(tag, tagIndex) in post.tags"
|
|
||||||
:key="tagIndex"
|
|
||||||
:tag="tag"
|
|
||||||
route
|
|
||||||
></PostTag>
|
|
||||||
</VSpace>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</VEntityField>
|
|
||||||
</template>
|
</template>
|
||||||
<template #end>
|
<template #end>
|
||||||
<VEntityField>
|
<EntityFieldItems :fields="endFields" />
|
||||||
<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 :description="publishStatus">
|
|
||||||
<template v-if="isPublishing" #description>
|
|
||||||
<VStatusDot :text="$t('core.common.tooltips.publishing')" animate />
|
|
||||||
</template>
|
|
||||||
</VEntityField>
|
|
||||||
<VEntityField>
|
|
||||||
<template #description>
|
|
||||||
<IconEye
|
|
||||||
v-if="post.post.spec.visible === 'PUBLIC'"
|
|
||||||
v-tooltip="$t('core.post.filters.visible.items.public')"
|
|
||||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
|
||||||
@click="changeVisibleMutation(post.post)"
|
|
||||||
/>
|
|
||||||
<IconEyeOff
|
|
||||||
v-if="post.post.spec.visible === 'PRIVATE'"
|
|
||||||
v-tooltip="$t('core.post.filters.visible.items.private')"
|
|
||||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
|
||||||
@click="changeVisibleMutation(post.post)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</VEntityField>
|
|
||||||
<VEntityField v-if="post?.post?.spec.deleted">
|
|
||||||
<template #description>
|
|
||||||
<VStatusDot
|
|
||||||
v-tooltip="$t('core.common.status.deleting')"
|
|
||||||
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>
|
||||||
<template
|
<template
|
||||||
v-if="currentUserHasPermission(['system:posts:manage'])"
|
v-if="currentUserHasPermission(['system:posts:manage'])"
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VAvatar, VEntityField } from "@halo-dev/components";
|
||||||
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
post: ListedPost;
|
||||||
|
}>(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
</template>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VEntityField, VStatusDot } from "@halo-dev/components";
|
||||||
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
|
import { postLabels } from "@/constants/labels";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
post: ListedPost;
|
||||||
|
}>(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const publishStatus = computed(() => {
|
||||||
|
const { labels } = props.post.post.metadata;
|
||||||
|
return labels?.[postLabels.PUBLISHED] === "true"
|
||||||
|
? t("core.post.filters.status.items.published")
|
||||||
|
: t("core.post.filters.status.items.draft");
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPublishing = computed(() => {
|
||||||
|
const { spec, status, metadata } = props.post.post;
|
||||||
|
return (
|
||||||
|
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
|
||||||
|
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VEntityField :description="publishStatus">
|
||||||
|
<template v-if="isPublishing" #description>
|
||||||
|
<VStatusDot :text="$t('core.common.tooltips.publishing')" animate />
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
</template>
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
IconExternalLinkLine,
|
||||||
|
VEntityField,
|
||||||
|
VSpace,
|
||||||
|
VStatusDot,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import PostTag from "../../tags/components/PostTag.vue";
|
||||||
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
|
import { postLabels } from "@/constants/labels";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
post: ListedPost;
|
||||||
|
}>(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const externalUrl = computed(() => {
|
||||||
|
const { status, metadata } = props.post.post;
|
||||||
|
if (metadata.labels?.[postLabels.PUBLISHED] === "true") {
|
||||||
|
return status?.permalink;
|
||||||
|
}
|
||||||
|
return `/preview/posts/${metadata.name}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VEntityField
|
||||||
|
:title="post.post.spec.title"
|
||||||
|
:route="{
|
||||||
|
name: 'PostEditor',
|
||||||
|
query: { name: post.post.metadata.name },
|
||||||
|
}"
|
||||||
|
width="27rem"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<VSpace class="mt-1 sm:mt-0">
|
||||||
|
<RouterLink
|
||||||
|
v-if="post.post.status?.inProgress"
|
||||||
|
v-tooltip="$t('core.common.tooltips.unpublished_content_tip')"
|
||||||
|
:to="{
|
||||||
|
name: 'PostEditor',
|
||||||
|
query: { name: post.post.metadata.name },
|
||||||
|
}"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<VStatusDot state="success" animate />
|
||||||
|
</RouterLink>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
:href="externalUrl"
|
||||||
|
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||||
|
>
|
||||||
|
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||||
|
</a>
|
||||||
|
</VSpace>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<VSpace class="flex-wrap !gap-y-1">
|
||||||
|
<p
|
||||||
|
v-if="post.categories.length"
|
||||||
|
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
{{ $t("core.post.list.fields.categories") }}
|
||||||
|
<a
|
||||||
|
v-for="(category, categoryIndex) in post.categories"
|
||||||
|
:key="categoryIndex"
|
||||||
|
:href="category.status?.permalink"
|
||||||
|
:title="category.status?.permalink"
|
||||||
|
target="_blank"
|
||||||
|
class="cursor-pointer hover:text-gray-900"
|
||||||
|
>
|
||||||
|
{{ category.spec.displayName }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{
|
||||||
|
$t("core.post.list.fields.visits", {
|
||||||
|
visits: post.stats.visit,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{
|
||||||
|
$t("core.post.list.fields.comments", {
|
||||||
|
comments: post.stats.totalComment || 0,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-if="post.post.spec.pinned" class="text-xs text-gray-500">
|
||||||
|
{{ $t("core.post.list.fields.pinned") }}
|
||||||
|
</span>
|
||||||
|
</VSpace>
|
||||||
|
<VSpace v-if="post.tags.length" class="flex-wrap">
|
||||||
|
<PostTag
|
||||||
|
v-for="(tag, tagIndex) in post.tags"
|
||||||
|
:key="tagIndex"
|
||||||
|
:tag="tag"
|
||||||
|
route
|
||||||
|
></PostTag>
|
||||||
|
</VSpace>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
</template>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import type { ListedPost, Post } from "@halo-dev/api-client";
|
||||||
|
import { IconEye, IconEyeOff, Toast, VEntityField } from "@halo-dev/components";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/vue-query";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
post: ListedPost;
|
||||||
|
}>(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutate: changeVisibleMutation } = useMutation({
|
||||||
|
mutationFn: async (post: Post) => {
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
||||||
|
name: post.metadata.name,
|
||||||
|
});
|
||||||
|
data.spec.visible = data.spec.visible === "PRIVATE" ? "PUBLIC" : "PRIVATE";
|
||||||
|
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post(
|
||||||
|
{
|
||||||
|
name: post.metadata.name,
|
||||||
|
post: data,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mute: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["posts"] });
|
||||||
|
},
|
||||||
|
retry: 3,
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.success(t("core.common.toast.operation_success"));
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
Toast.error(t("core.common.toast.operation_failed"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VEntityField>
|
||||||
|
<template #description>
|
||||||
|
<IconEye
|
||||||
|
v-if="post.post.spec.visible === 'PUBLIC'"
|
||||||
|
v-tooltip="$t('core.post.filters.visible.items.public')"
|
||||||
|
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||||
|
@click="changeVisibleMutation(post.post)"
|
||||||
|
/>
|
||||||
|
<IconEyeOff
|
||||||
|
v-if="post.post.spec.visible === 'PRIVATE'"
|
||||||
|
v-tooltip="$t('core.post.filters.visible.items.private')"
|
||||||
|
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||||
|
@click="changeVisibleMutation(post.post)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
</template>
|
|
@ -22,7 +22,7 @@ import { markRaw } from "vue";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import UninstallOperationItem from "./operation/UninstallOperationItem.vue";
|
import UninstallOperationItem from "./operation/UninstallOperationItem.vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import type { OperationItem } from "packages/shared/dist";
|
import type { OperationItem } from "@halo-dev/console-shared";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { useI18n } from "vue-i18n";
|
||||||
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
|
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
|
||||||
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
|
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
|
||||||
import { toRefs } from "vue";
|
import { toRefs } from "vue";
|
||||||
import type { OperationItem } from "packages/shared/dist";
|
import type { OperationItem } from "@halo-dev/console-shared";
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
|
@ -26,7 +26,7 @@ import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
|
||||||
import AuthorField from "./entity-fields/AuthorField.vue";
|
import AuthorField from "./entity-fields/AuthorField.vue";
|
||||||
import SwitchField from "./entity-fields/SwitchField.vue";
|
import SwitchField from "./entity-fields/SwitchField.vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import type { EntityFieldItem, OperationItem } from "packages/shared/dist";
|
import type { EntityFieldItem, OperationItem } from "@halo-dev/console-shared";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
Loading…
Reference in New Issue