Add subject-based comment list modal (#7681)

pull/7685/head
Ryan Wang 2025-08-13 15:43:50 +08:00 committed by GitHub
parent 2bcfbbc371
commit f5af5a1550
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 461 additions and 84 deletions

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue"; import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
import type { ListedComment } from "@halo-dev/api-client"; import type { ListedComment } from "@halo-dev/api-client";
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client"; import { coreApiClient } from "@halo-dev/api-client";
import { import {
Dialog, Dialog,
IconMessage, IconMessage,
@ -16,12 +16,12 @@ import {
VPagination, VPagination,
VSpace, VSpace,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
import { useRouteQuery } from "@vueuse/router"; import { useRouteQuery } from "@vueuse/router";
import { chunk } from "lodash-es"; import { chunk } from "lodash-es";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import CommentListItem from "./components/CommentListItem.vue"; import CommentListItem from "./components/CommentListItem.vue";
import useCommentsFetch from "./composables/use-comments-fetch";
const { t } = useI18n(); const { t } = useI18n();
@ -72,58 +72,21 @@ const page = useRouteQuery<number>("page", 1, {
const size = useRouteQuery<number>("size", 20, { const size = useRouteQuery<number>("size", 20, {
transform: Number, transform: Number,
}); });
const total = ref(0);
const { const {
data: comments, data: comments,
isLoading, isLoading,
isFetching, isFetching,
refetch, refetch,
} = useQuery<ListedComment[]>({ } = useCommentsFetch(
queryKey: [ "core:comments",
"core:comments", page,
page, size,
size, selectedApprovedStatus,
selectedApprovedStatus, selectedSort,
selectedSort, selectedUser,
selectedUser, keyword
keyword, );
],
queryFn: async () => {
const fieldSelectorMap: Record<string, string | boolean | undefined> = {
"spec.approved": selectedApprovedStatus.value,
};
const fieldSelector = Object.entries(fieldSelectorMap)
.map(([key, value]) => {
if (value !== undefined) {
return `${key}=${value}`;
}
})
.filter(Boolean) as string[];
const { data } = await consoleApiClient.content.comment.listComments({
fieldSelector,
page: page.value,
size: size.value,
sort: [selectedSort.value].filter(Boolean) as string[],
keyword: keyword.value,
ownerName: selectedUser.value,
// TODO: email users are not supported at the moment.
ownerKind: selectedUser.value ? "User" : undefined,
});
total.value = data.total;
return data.items;
},
refetchInterval(data) {
const hasDeletingData = data?.some(
(comment) => !!comment.comment.metadata.deletionTimestamp
);
return hasDeletingData ? 1000 : false;
},
});
// Selection // Selection
const handleCheckAllChange = (e: Event) => { const handleCheckAllChange = (e: Event) => {
@ -131,7 +94,7 @@ const handleCheckAllChange = (e: Event) => {
if (checked) { if (checked) {
selectedCommentNames.value = selectedCommentNames.value =
comments.value?.map((comment) => { comments.value?.items.map((comment) => {
return comment.comment.metadata.name; return comment.comment.metadata.name;
}) || []; }) || [];
} else { } else {
@ -146,7 +109,7 @@ const isSelection = (comment: ListedComment) => {
watch( watch(
() => selectedCommentNames.value, () => selectedCommentNames.value,
(newValue) => { (newValue) => {
checkAll.value = newValue.length === comments.value?.length; checkAll.value = newValue.length === comments.value?.items.length;
} }
); );
@ -192,7 +155,7 @@ const handleApproveInBatch = async () => {
cancelText: t("core.common.buttons.cancel"), cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => { onConfirm: async () => {
try { try {
const commentsToUpdate = comments.value?.filter((comment) => { const commentsToUpdate = comments.value?.items.filter((comment) => {
return ( return (
selectedCommentNames.value.includes( selectedCommentNames.value.includes(
comment.comment.metadata.name comment.comment.metadata.name
@ -370,7 +333,7 @@ const handleApproveInBatch = async () => {
</div> </div>
</template> </template>
<VLoading v-if="isLoading" /> <VLoading v-if="isLoading" />
<Transition v-else-if="!comments?.length" appear name="fade"> <Transition v-else-if="!comments?.items.length" appear name="fade">
<VEmpty <VEmpty
:message="$t('core.comment.empty.message')" :message="$t('core.comment.empty.message')"
:title="$t('core.comment.empty.title')" :title="$t('core.comment.empty.title')"
@ -385,7 +348,7 @@ const handleApproveInBatch = async () => {
<Transition v-else appear name="fade"> <Transition v-else appear name="fade">
<VEntityContainer> <VEntityContainer>
<CommentListItem <CommentListItem
v-for="comment in comments" v-for="comment in comments.items"
:key="comment.comment.metadata.name" :key="comment.comment.metadata.name"
:comment="comment" :comment="comment"
:is-selected="isSelection(comment)" :is-selected="isSelection(comment)"
@ -409,9 +372,11 @@ const handleApproveInBatch = async () => {
:page-label="$t('core.components.pagination.page_label')" :page-label="$t('core.components.pagination.page_label')"
:size-label="$t('core.components.pagination.size_label')" :size-label="$t('core.components.pagination.size_label')"
:total-label=" :total-label="
$t('core.components.pagination.total_label', { total: total }) $t('core.components.pagination.total_label', {
total: comments?.total || 0,
})
" "
:total="total" :total="comments?.total || 0"
:size-options="[20, 30, 50, 100]" :size-options="[20, 30, 50, 100]"
/> />
</template> </template>

View File

@ -275,7 +275,9 @@ const { data: contentProvider } = useContentProviderExtensionPoint();
/> />
<VEntity :is-selected="isSelected"> <VEntity :is-selected="isSelected">
<template <template
v-if="currentUserHasPermission(['system:comments:manage'])" v-if="
currentUserHasPermission(['system:comments:manage']) && $slots.checkbox
"
#checkbox #checkbox
> >
<slot name="checkbox" /> <slot name="checkbox" />

View File

@ -0,0 +1,192 @@
<script lang="ts" setup>
import FilterCleanButton from "@/components/filter/FilterCleanButton.vue";
import FilterDropdown from "@/components/filter/FilterDropdown.vue";
import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
import SearchInput from "@/components/input/SearchInput.vue";
import HasPermission from "@/components/permission/HasPermission.vue";
import {
IconRefreshLine,
VButton,
VEmpty,
VEntityContainer,
VLoading,
VPagination,
VSpace,
} from "@halo-dev/components";
import { computed, ref, toRefs, watch } from "vue";
import useCommentsFetch from "../composables/use-comments-fetch";
import CommentListItem from "./CommentListItem.vue";
const props = defineProps<{
subjectRefKey: string;
}>();
const { subjectRefKey } = toRefs(props);
const selectedApprovedStatus = ref();
const selectedSort = ref();
const selectedUser = ref();
const page = ref(1);
const size = ref(20);
const keyword = ref("");
const {
data: comments,
isLoading,
isFetching,
refetch,
} = useCommentsFetch(
"core:comments:with-subject",
page,
size,
selectedApprovedStatus,
selectedSort,
selectedUser,
keyword,
subjectRefKey
);
watch(
() => [
selectedApprovedStatus.value,
selectedSort.value,
selectedUser.value,
keyword.value,
],
() => {
page.value = 1;
}
);
const hasFilters = computed(() => {
return (
selectedApprovedStatus.value !== undefined ||
selectedSort.value ||
selectedUser.value
);
});
function handleClearFilters() {
selectedApprovedStatus.value = undefined;
selectedSort.value = undefined;
selectedUser.value = undefined;
}
</script>
<template>
<div>
<div class="mb-4 flex flex-wrap items-center justify-between gap-4">
<SearchInput v-model="keyword" />
<VSpace spacing="lg" class="flex-wrap">
<FilterCleanButton v-if="hasFilters" @click="handleClearFilters" />
<FilterDropdown
v-model="selectedApprovedStatus"
:label="$t('core.common.filters.labels.status')"
:items="[
{
label: $t('core.common.filters.item_labels.all'),
},
{
label: $t('core.comment.filters.status.items.approved'),
value: true,
},
{
label: $t('core.comment.filters.status.items.pending_review'),
value: false,
},
]"
/>
<HasPermission :permissions="['system:users:view']">
<UserFilterDropdown
v-model="selectedUser"
:label="$t('core.comment.filters.owner.label')"
/>
</HasPermission>
<FilterDropdown
v-model="selectedSort"
:label="$t('core.common.filters.labels.sort')"
:items="[
{
label: $t('core.common.filters.item_labels.default'),
},
{
label: $t('core.comment.filters.sort.items.last_reply_time_desc'),
value: 'status.lastReplyTime,desc',
},
{
label: $t('core.comment.filters.sort.items.last_reply_time_asc'),
value: 'status.lastReplyTime,asc',
},
{
label: $t('core.comment.filters.sort.items.reply_count_desc'),
value: 'status.replyCount,desc',
},
{
label: $t('core.comment.filters.sort.items.reply_count_asc'),
value: 'status.replyCount,asc',
},
{
label: $t('core.comment.filters.sort.items.create_time_desc'),
value: 'metadata.creationTimestamp,desc',
},
{
label: $t('core.comment.filters.sort.items.create_time_asc'),
value: 'metadata.creationTimestamp,asc',
},
]"
/>
<div class="flex flex-row gap-2">
<div
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="refetch()"
>
<IconRefreshLine
v-tooltip="$t('core.common.buttons.refresh')"
:class="{ 'animate-spin text-gray-900': isFetching }"
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
/>
</div>
</div>
</VSpace>
</div>
<VLoading v-if="isLoading" />
<Transition v-else-if="!comments?.items.length" appear name="fade">
<VEmpty
:message="$t('core.comment.empty.message')"
:title="$t('core.comment.empty.title')"
>
<template #actions>
<VButton @click="refetch">
{{ $t("core.common.buttons.refresh") }}
</VButton>
</template>
</VEmpty>
</Transition>
<Transition v-else appear name="fade">
<div class="overflow-hidden rounded-base border">
<VEntityContainer>
<CommentListItem
v-for="comment in comments.items"
:key="comment.comment.metadata.name"
:comment="comment"
>
</CommentListItem>
</VEntityContainer>
</div>
</Transition>
<div class="mt-4">
<VPagination
v-model:page="page"
v-model:size="size"
:page-label="$t('core.components.pagination.page_label')"
:size-label="$t('core.components.pagination.size_label')"
:total-label="
$t('core.components.pagination.total_label', {
total: comments?.total || 0,
})
"
:total="comments?.total || 0"
:size-options="[20, 30, 50, 100]"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,33 @@
<script lang="ts" setup>
import { VButton, VModal } from "@halo-dev/components";
import { useTemplateRef } from "vue";
import SubjectQueryCommentList from "./SubjectQueryCommentList.vue";
const props = defineProps<{
subjectRefKey: string;
}>();
const emit = defineEmits<{
(event: "close"): void;
}>();
const modal = useTemplateRef<InstanceType<typeof VModal> | null>("modal");
</script>
<template>
<VModal
ref="modal"
:centered="false"
:width="1400"
:title="$t('core.comment.title')"
:layer-closable="true"
mount-to-body
@close="emit('close')"
>
<SubjectQueryCommentList :subject-ref-key="props.subjectRefKey" />
<template #footer>
<VButton @click="modal?.close()">
{{ $t("core.common.buttons.close") }}
</VButton>
</template>
</VModal>
</template>

View File

@ -0,0 +1,50 @@
import { consoleApiClient, type ListedCommentList } from "@halo-dev/api-client";
import { useQuery } from "@tanstack/vue-query";
import type { Ref } from "vue";
export default function useCommentsFetch(
queryKey: string,
page: Ref<number>,
size: Ref<number>,
approved: Ref<boolean | undefined>,
sort: Ref<string | undefined>,
user: Ref<string | undefined>,
keyword: Ref<string | undefined>,
subjectRefKey?: Ref<string | undefined>
) {
return useQuery<ListedCommentList>({
queryKey: [queryKey, page, size, approved, sort, user, keyword],
queryFn: async () => {
const fieldSelectorMap: Record<string, string | boolean | undefined> = {
"spec.approved": approved.value,
"spec.subjectRef": subjectRefKey?.value,
};
const fieldSelector = Object.entries(fieldSelectorMap)
.map(([key, value]) => {
if (value !== undefined) {
return `${key}=${value}`;
}
})
.filter(Boolean) as string[];
const { data } = await consoleApiClient.content.comment.listComments({
fieldSelector,
page: page.value,
size: size.value,
sort: [sort.value].filter(Boolean) as string[],
keyword: keyword.value,
ownerName: user.value,
ownerKind: user.value ? "User" : undefined,
});
return data;
},
refetchInterval(data) {
const hasDeletingData = data?.items.some(
(comment) => !!comment.comment.metadata.deletionTimestamp
);
return hasDeletingData ? 1000 : false;
},
});
}

View File

@ -3,8 +3,14 @@ import { IconMessage } from "@halo-dev/components";
import { definePlugin } from "@halo-dev/console-shared"; import { definePlugin } from "@halo-dev/console-shared";
import { markRaw } from "vue"; import { markRaw } from "vue";
import CommentList from "./CommentList.vue"; import CommentList from "./CommentList.vue";
import SubjectQueryCommentList from "./components/SubjectQueryCommentList.vue";
import SubjectQueryCommentListModal from "./components/SubjectQueryCommentListModal.vue";
export default definePlugin({ export default definePlugin({
components: {
SubjectQueryCommentList,
SubjectQueryCommentListModal,
},
routes: [ routes: [
{ {
path: "/comments", path: "/comments",

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { singlePageLabels } from "@/constants/labels"; import { singlePageLabels } from "@/constants/labels";
import SubjectQueryCommentListModal from "@console/modules/contents/comments/components/SubjectQueryCommentListModal.vue";
import type { ListedSinglePage } from "@halo-dev/api-client"; import type { ListedSinglePage } from "@halo-dev/api-client";
import { import {
IconExternalLinkLine, IconExternalLinkLine,
@ -7,7 +8,8 @@ import {
VSpace, VSpace,
VStatusDot, VStatusDot,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { computed } from "vue"; import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -16,6 +18,8 @@ const props = withDefaults(
{} {}
); );
const { t } = useI18n();
const externalUrl = computed(() => { const externalUrl = computed(() => {
const { metadata, status } = props.singlePage.page; const { metadata, status } = props.singlePage.page;
if (metadata.labels?.[singlePageLabels.PUBLISHED] === "true") { if (metadata.labels?.[singlePageLabels.PUBLISHED] === "true") {
@ -23,6 +27,30 @@ const externalUrl = computed(() => {
} }
return `/preview/singlepages/${metadata.name}`; return `/preview/singlepages/${metadata.name}`;
}); });
const commentSubjectRefKey = `content.halo.run/SinglePage/${props.singlePage.page.metadata.name}`;
const commentListVisible = ref(false);
const commentText = computed(() => {
const { totalComment, approvedComment } = props.singlePage.stats || {};
let text = t("core.page.list.fields.comments", {
comments: totalComment || 0,
});
if (!totalComment || !approvedComment) {
return text;
}
const pendingComments = totalComment - approvedComment;
if (pendingComments > 0) {
text += t("core.page.list.fields.comments-with-pending", {
count: pendingComments,
});
}
return text;
});
</script> </script>
<template> <template>
@ -66,15 +94,20 @@ const externalUrl = computed(() => {
}) })
}} }}
</span> </span>
<span class="text-xs text-gray-500"> <span
{{ class="cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline"
$t("core.page.list.fields.comments", { @click="commentListVisible = true"
comments: singlePage.stats.totalComment || 0, >
}) {{ commentText }}
}}
</span> </span>
</VSpace> </VSpace>
</div> </div>
<SubjectQueryCommentListModal
v-if="commentListVisible"
:subject-ref-key="commentSubjectRefKey"
@close="commentListVisible = false"
/>
</template> </template>
</VEntityField> </VEntityField>
</template> </template>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { postLabels } from "@/constants/labels"; import { postLabels } from "@/constants/labels";
import SubjectQueryCommentListModal from "@console/modules/contents/comments/components/SubjectQueryCommentListModal.vue";
import type { ListedPost } from "@halo-dev/api-client"; import type { ListedPost } from "@halo-dev/api-client";
import { import {
IconExternalLinkLine, IconExternalLinkLine,
@ -7,7 +8,8 @@ import {
VSpace, VSpace,
VStatusDot, VStatusDot,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { computed } from "vue"; import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import PostTag from "../../tags/components/PostTag.vue"; import PostTag from "../../tags/components/PostTag.vue";
const props = withDefaults( const props = withDefaults(
@ -17,6 +19,8 @@ const props = withDefaults(
{} {}
); );
const { t } = useI18n();
const externalUrl = computed(() => { const externalUrl = computed(() => {
const { status, metadata } = props.post.post; const { status, metadata } = props.post.post;
if (metadata.labels?.[postLabels.PUBLISHED] === "true") { if (metadata.labels?.[postLabels.PUBLISHED] === "true") {
@ -24,6 +28,30 @@ const externalUrl = computed(() => {
} }
return `/preview/posts/${metadata.name}`; return `/preview/posts/${metadata.name}`;
}); });
const commentSubjectRefKey = `content.halo.run/Post/${props.post.post.metadata.name}`;
const commentListVisible = ref(false);
const commentText = computed(() => {
const { totalComment, approvedComment } = props.post.stats || {};
let text = t("core.post.list.fields.comments", {
comments: totalComment || 0,
});
if (!totalComment || !approvedComment) {
return text;
}
const pendingComments = totalComment - approvedComment;
if (pendingComments > 0) {
text += t("core.post.list.fields.comments-with-pending", {
count: pendingComments,
});
}
return text;
});
</script> </script>
<template> <template>
@ -83,12 +111,11 @@ const externalUrl = computed(() => {
}) })
}} }}
</span> </span>
<span class="text-xs text-gray-500"> <span
{{ class="cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline"
$t("core.post.list.fields.comments", { @click="commentListVisible = true"
comments: post.stats.totalComment || 0, >
}) {{ commentText }}
}}
</span> </span>
<span v-if="post.post.spec.pinned" class="text-xs text-gray-500"> <span v-if="post.post.spec.pinned" class="text-xs text-gray-500">
{{ $t("core.post.list.fields.pinned") }} {{ $t("core.post.list.fields.pinned") }}
@ -103,6 +130,12 @@ const externalUrl = computed(() => {
></PostTag> ></PostTag>
</VSpace> </VSpace>
</div> </div>
<SubjectQueryCommentListModal
v-if="commentListVisible"
:subject-ref-key="commentSubjectRefKey"
@close="commentListVisible = false"
/>
</template> </template>
</VEntityField> </VEntityField>
</template> </template>

View File

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { postLabels } from "@/constants/labels"; import { postLabels } from "@/constants/labels";
import { formatDatetime, relativeTimeTo } from "@/utils/date"; import { formatDatetime, relativeTimeTo } from "@/utils/date";
import SubjectQueryCommentListModal from "@console/modules/contents/comments/components/SubjectQueryCommentListModal.vue";
import type { ListedPost } from "@halo-dev/api-client"; import type { ListedPost } from "@halo-dev/api-client";
import { import {
IconExternalLinkLine, IconExternalLinkLine,
@ -9,7 +10,10 @@ import {
VSpace, VSpace,
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { computed } from "vue"; import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
post: ListedPost; post: ListedPost;
@ -33,6 +37,33 @@ const datetime = computed(() => {
props.post.post.metadata.creationTimestamp props.post.post.metadata.creationTimestamp
); );
}); });
const commentSubjectRefKey = `content.halo.run/Post/${props.post.post.metadata.name}`;
const commentListVisible = ref(false);
const commentText = computed(() => {
const { totalComment, approvedComment } = props.post.stats || {};
let text = t("core.dashboard.widgets.presets.recent_published.comments", {
comments: totalComment || 0,
});
if (!totalComment || !approvedComment) {
return text;
}
const pendingComments = totalComment - approvedComment;
if (pendingComments > 0) {
text += t(
"core.dashboard.widgets.presets.recent_published.comments-with-pending",
{
count: pendingComments,
}
);
}
return text;
});
</script> </script>
<template> <template>
@ -54,14 +85,18 @@ const datetime = computed(() => {
}) })
}} }}
</span> </span>
<span class="text-xs text-gray-500"> <span
{{ class="cursor-pointer text-xs text-gray-500 hover:text-gray-900 hover:underline"
$t("core.dashboard.widgets.presets.recent_published.comments", { @click="commentListVisible = true"
comments: post.stats.totalComment || 0, >
}) {{ commentText }}
}}
</span> </span>
</VSpace> </VSpace>
<SubjectQueryCommentListModal
v-if="commentListVisible"
:subject-ref-key="commentSubjectRefKey"
@close="commentListVisible = false"
/>
</template> </template>
<template #extra> <template #extra>
<VSpace> <VSpace>

View File

@ -33,6 +33,7 @@ core:
recent_published: recent_published:
empty: empty:
title: No published posts title: No published posts
comments-with-pending: " ({count} pending comments)"
notification: notification:
title: Notifications title: Notifications
empty: empty:
@ -116,6 +117,7 @@ core:
fields: fields:
schedule_publish: schedule_publish:
tooltip: Schedule publish tooltip: Schedule publish
comments-with-pending: ({count} pending comments)
settings: settings:
fields: fields:
publish_time: publish_time:
@ -175,6 +177,10 @@ core:
fields: fields:
prevent_parent_post_cascade_query: Prevent parent category from including this category and its subcategories in cascade post queries prevent_parent_post_cascade_query: Prevent parent category from including this category and its subcategories in cascade post queries
hide_from_list: This category is hidden, This category and its subcategories, as well as its posts, will not be displayed in the front-end list. You need to actively visit the category archive page hide_from_list: This category is hidden, This category and its subcategories, as well as its posts, will not be displayed in the front-end list. You need to actively visit the category archive page
page:
list:
fields:
comments-with-pending: " ({count} pending comments)"
page_editor: page_editor:
actions: actions:
snapshots: Snapshots snapshots: Snapshots
@ -375,10 +381,16 @@ core:
operations: operations:
enable: enable:
title: Enable title: Enable
description: Are you sure you want to enable this user? description: Are you sure you want to enable this user? Once enabled, the user will be able to log back into the system.
enable_in_batch:
title: Enable
description: Are you sure you want to enable the selected users? Once enabled, these users will be able to log back into the system.
disable: disable:
title: Disable title: Disable
description: Are you sure you want to disable this user? This user will not be able to log in after being disabled description: Are you sure you want to disable this user? Once disabled, the user will no longer be able to log in.
disable_in_batch:
title: Disable
description: Are you sure you want to disable the selected users? Once disabled, these users will no longer be able to log in.
grant_permission_modal: grant_permission_modal:
roles_preview: roles_preview:
all: The currently selected role contains all permissions all: The currently selected role contains all permissions

View File

@ -73,6 +73,7 @@ core:
comments: "{comments} Comments" comments: "{comments} Comments"
empty: empty:
title: No published posts title: No published posts
comments-with-pending: " ({count} pending comments)"
notification: notification:
title: Notifications title: Notifications
empty: empty:
@ -238,6 +239,7 @@ core:
pinned: Pinned pinned: Pinned
schedule_publish: schedule_publish:
tooltip: Schedule publish tooltip: Schedule publish
comments-with-pending: " ({count} pending comments)"
settings: settings:
title: Settings title: Settings
groups: groups:
@ -404,7 +406,10 @@ core:
help: Theme adaptation is required to support help: Theme adaptation is required to support
description: description:
label: Description label: Description
help: "The description will be automatically added to the page's meta description tag for SEO; other display purposes require theme adaptation" help: >-
The description will be automatically added to the page's meta
description tag for SEO; other display purposes require theme
adaptation
prevent_parent_post_cascade_query: prevent_parent_post_cascade_query:
label: Prevent Parent Post Cascade Query label: Prevent Parent Post Cascade Query
help: >- help: >-
@ -473,6 +478,7 @@ core:
fields: fields:
visits: "{visits} Visits" visits: "{visits} Visits"
comments: "{comments} Comments" comments: "{comments} Comments"
comments-with-pending: " ({count} pending comments)"
settings: settings:
title: Settings title: Settings
groups: groups:
@ -1143,19 +1149,23 @@ core:
enable: enable:
title: Enable title: Enable
description: >- description: >-
Are you sure you want to enable this user? Once enabled, the user will be able to log back into the system. Are you sure you want to enable this user? Once enabled, the user will
be able to log back into the system.
enable_in_batch: enable_in_batch:
title: Enable title: Enable
description: >- description: >-
Are you sure you want to enable the selected users? Once enabled, these users will be able to log back into the system. Are you sure you want to enable the selected users? Once enabled,
these users will be able to log back into the system.
disable: disable:
title: Disable title: Disable
description: >- description: >-
Are you sure you want to disable this user? Once disabled, the user will no longer be able to log in. Are you sure you want to disable this user? Once disabled, the user
will no longer be able to log in.
disable_in_batch: disable_in_batch:
title: Disable title: Disable
description: >- description: >-
Are you sure you want to disable the selected users? Once disabled, these users will no longer be able to log in. Are you sure you want to disable the selected users? Once disabled,
these users will no longer be able to log in.
filters: filters:
role: role:
label: Role label: Role

View File

@ -67,6 +67,7 @@ core:
comments: 评论 {comments} comments: 评论 {comments}
empty: empty:
title: 暂无已发布文章 title: 暂无已发布文章
comments-with-pending: " ({count} 条待审核)"
notification: notification:
title: 通知 title: 通知
empty: empty:
@ -224,6 +225,7 @@ core:
pinned: 已置顶 pinned: 已置顶
schedule_publish: schedule_publish:
tooltip: 定时发布 tooltip: 定时发布
comments-with-pending: {count} 条待审核)
settings: settings:
title: 文章设置 title: 文章设置
groups: groups:
@ -451,6 +453,7 @@ core:
fields: fields:
visits: 访问量 {visits} visits: 访问量 {visits}
comments: 评论 {comments} comments: 评论 {comments}
comments-with-pending: {count} 条待审核)
settings: settings:
title: 页面设置 title: 页面设置
groups: groups:

View File

@ -67,6 +67,7 @@ core:
comments: 留言 {comments} comments: 留言 {comments}
empty: empty:
title: 沒有已發布文章 title: 沒有已發布文章
comments-with-pending: " ({count} 條待審核)"
notification: notification:
title: 通知 title: 通知
empty: empty:
@ -224,6 +225,7 @@ core:
pinned: 已置頂 pinned: 已置頂
schedule_publish: schedule_publish:
tooltip: 定時發佈 tooltip: 定時發佈
comments-with-pending: ({count} 條待審核)
settings: settings:
title: 文章設置 title: 文章設置
groups: groups:
@ -436,6 +438,7 @@ core:
fields: fields:
visits: 訪問量 {visits} visits: 訪問量 {visits}
comments: 留言 {comments} comments: 留言 {comments}
comments-with-pending: " ({count} 條待審核)"
settings: settings:
title: 頁面設置 title: 頁面設置
groups: groups: