Skip to content

Commit 28a4a13

Browse files
authored
refactor: redesign ui of comment management page (#7481)
* refactor: redesign ui of comment management page Signed-off-by: Ryan Wang <[email protected]> * Add comment detail modal Signed-off-by: Ryan Wang <[email protected]> * Add reply detail modal Signed-off-by: Ryan Wang <[email protected]> * Improve ui Signed-off-by: Ryan Wang <[email protected]> * Add pending comments widget Signed-off-by: Ryan Wang <[email protected]> * Improve ui Signed-off-by: Ryan Wang <[email protected]> * Improve ui Signed-off-by: Ryan Wang <[email protected]> --------- Signed-off-by: Ryan Wang <[email protected]>
1 parent 47e517d commit 28a4a13

18 files changed

+1307
-421
lines changed

ui/console-src/modules/contents/comments/CommentList.vue

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import {
1818
} from "@halo-dev/components";
1919
import { useQuery } from "@tanstack/vue-query";
2020
import { useRouteQuery } from "@vueuse/router";
21+
import { chunk } from "lodash-es";
2122
import { computed, ref, watch } from "vue";
2223
import { useI18n } from "vue-i18n";
2324
import CommentListItem from "./components/CommentListItem.vue";
2425
2526
const { t } = useI18n();
2627
2728
const checkAll = ref(false);
28-
const selectedComment = ref<ListedComment>();
2929
const selectedCommentNames = ref<string[]>([]);
3030
3131
const keyword = useRouteQuery<string>("keyword", "");
@@ -81,7 +81,7 @@ const {
8181
refetch,
8282
} = useQuery<ListedComment[]>({
8383
queryKey: [
84-
"comments",
84+
"core:comments",
8585
page,
8686
size,
8787
selectedApprovedStatus,
@@ -139,12 +139,8 @@ const handleCheckAllChange = (e: Event) => {
139139
}
140140
};
141141
142-
const checkSelection = (comment: ListedComment) => {
143-
return (
144-
comment.comment.metadata.name ===
145-
selectedComment.value?.comment.metadata.name ||
146-
selectedCommentNames.value.includes(comment.comment.metadata.name)
147-
);
142+
const isSelection = (comment: ListedComment) => {
143+
return selectedCommentNames.value.includes(comment.comment.metadata.name);
148144
};
149145
150146
watch(
@@ -165,12 +161,18 @@ const handleDeleteInBatch = async () => {
165161
cancelText: t("core.common.buttons.cancel"),
166162
onConfirm: async () => {
167163
try {
168-
const promises = selectedCommentNames.value.map((name) => {
169-
return coreApiClient.content.comment.deleteComment({
170-
name,
171-
});
172-
});
173-
await Promise.all(promises);
164+
const commentChunk = chunk(selectedCommentNames.value, 5);
165+
166+
for (const item of commentChunk) {
167+
await Promise.all(
168+
item.map((name) => {
169+
return coreApiClient.content.comment.deleteComment({
170+
name,
171+
});
172+
})
173+
);
174+
}
175+
174176
selectedCommentNames.value = [];
175177
176178
Toast.success(t("core.common.toast.delete_success"));
@@ -198,25 +200,34 @@ const handleApproveInBatch = async () => {
198200
);
199201
});
200202
201-
const promises = commentsToUpdate?.map((comment) => {
202-
return coreApiClient.content.comment.patchComment({
203-
name: comment.comment.metadata.name,
204-
jsonPatchInner: [
205-
{
206-
op: "add",
207-
path: "/spec/approved",
208-
value: true,
209-
},
210-
{
211-
op: "add",
212-
path: "/spec/approvedTime",
213-
// TODO: 暂时由前端设置发布时间。see https://github.com/halo-dev/halo/pull/2746
214-
value: new Date().toISOString(),
215-
},
216-
],
217-
});
218-
});
219-
await Promise.all(promises || []);
203+
if (!commentsToUpdate?.length) {
204+
return;
205+
}
206+
207+
const commentChunk = chunk(commentsToUpdate, 5);
208+
209+
for (const item of commentChunk) {
210+
await Promise.all(
211+
item.map((comment) => {
212+
return coreApiClient.content.comment.patchComment({
213+
name: comment.comment.metadata.name,
214+
jsonPatchInner: [
215+
{
216+
op: "add",
217+
path: "/spec/approved",
218+
value: true,
219+
},
220+
{
221+
op: "add",
222+
path: "/spec/approvedTime",
223+
value: new Date().toISOString(),
224+
},
225+
],
226+
});
227+
})
228+
);
229+
}
230+
220231
selectedCommentNames.value = [];
221232
222233
Toast.success(t("core.common.toast.operation_success"));
@@ -377,7 +388,7 @@ const handleApproveInBatch = async () => {
377388
v-for="comment in comments"
378389
:key="comment.comment.metadata.name"
379390
:comment="comment"
380-
:is-selected="checkSelection(comment)"
391+
:is-selected="isSelection(comment)"
381392
>
382393
<template #checkbox>
383394
<input
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<script setup lang="ts">
2+
import { formatDatetime, relativeTimeTo } from "@/utils/date";
3+
import {
4+
consoleApiClient,
5+
coreApiClient,
6+
type ListedComment,
7+
} from "@halo-dev/api-client";
8+
import {
9+
IconExternalLinkLine,
10+
Toast,
11+
VButton,
12+
VDescription,
13+
VDescriptionItem,
14+
VModal,
15+
VSpace,
16+
} from "@halo-dev/components";
17+
import { useQueryClient } from "@tanstack/vue-query";
18+
import { useUserAgent } from "@uc/modules/profile/tabs/composables/use-user-agent";
19+
import { computed, ref, useTemplateRef } from "vue";
20+
import { useI18n } from "vue-i18n";
21+
import { useSubjectRef } from "../composables/use-subject-ref";
22+
import OwnerButton from "./OwnerButton.vue";
23+
import ReplyFormItems from "./ReplyFormItems.vue";
24+
25+
const props = withDefaults(
26+
defineProps<{
27+
comment: ListedComment;
28+
}>(),
29+
{}
30+
);
31+
32+
const queryClient = useQueryClient();
33+
const { t } = useI18n();
34+
35+
const emit = defineEmits<{
36+
(e: "close"): void;
37+
}>();
38+
39+
const modal = useTemplateRef<InstanceType<typeof VModal> | null>("modal");
40+
41+
const { os, browser } = useUserAgent(props.comment.comment.spec.userAgent);
42+
43+
const creationTime = computed(() => {
44+
return (
45+
props.comment?.comment.spec.creationTime ||
46+
props.comment?.comment.metadata.creationTimestamp
47+
);
48+
});
49+
50+
const newReply = ref("");
51+
52+
async function handleApprove() {
53+
if (!newReply.value) {
54+
await coreApiClient.content.comment.patchComment({
55+
name: props.comment.comment.metadata.name,
56+
jsonPatchInner: [
57+
{
58+
op: "add",
59+
path: "/spec/approved",
60+
value: true,
61+
},
62+
{
63+
op: "add",
64+
path: "/spec/approvedTime",
65+
value: new Date().toISOString(),
66+
},
67+
],
68+
});
69+
} else {
70+
await consoleApiClient.content.comment.createReply({
71+
name: props.comment?.comment.metadata.name as string,
72+
replyRequest: {
73+
raw: newReply.value,
74+
content: newReply.value,
75+
allowNotification: true,
76+
quoteReply: undefined,
77+
},
78+
});
79+
}
80+
modal.value?.close();
81+
queryClient.invalidateQueries({ queryKey: ["core:comments"] });
82+
Toast.success(t("core.common.toast.operation_success"));
83+
}
84+
85+
const { subjectRefResult } = useSubjectRef(props.comment);
86+
87+
const websiteOfAnonymous = computed(() => {
88+
return props.comment.comment.spec.owner.annotations?.["website"];
89+
});
90+
</script>
91+
<template>
92+
<VModal
93+
ref="modal"
94+
:body-class="['!p-0']"
95+
:width="900"
96+
:title="$t('core.comment.comment_detail_modal.title')"
97+
mount-to-body
98+
:centered="false"
99+
@close="emit('close')"
100+
>
101+
<div>
102+
<VDescription>
103+
<VDescriptionItem :label="$t('core.comment.detail_modal.fields.owner')">
104+
<div class="flex items-center gap-3">
105+
<OwnerButton
106+
v-if="comment.comment.spec.owner.kind === 'User'"
107+
:owner="comment.comment.spec.owner"
108+
@click="
109+
$router.push({
110+
name: 'UserDetail',
111+
params: { name: comment.comment.spec.owner.name },
112+
})
113+
"
114+
/>
115+
<ul v-else class="space-y-1">
116+
<li>{{ comment.comment.spec.owner.displayName }}</li>
117+
<li>{{ comment.comment.spec.owner.name }}</li>
118+
<li v-if="websiteOfAnonymous">
119+
<a :href="websiteOfAnonymous" target="_blank">{{
120+
websiteOfAnonymous
121+
}}</a>
122+
</li>
123+
</ul>
124+
</div>
125+
</VDescriptionItem>
126+
<VDescriptionItem label="IP">
127+
{{ comment.comment.spec.ipAddress }}
128+
</VDescriptionItem>
129+
<VDescriptionItem
130+
:label="$t('core.comment.detail_modal.fields.user_agent')"
131+
>
132+
<span v-tooltip="comment.comment.spec.userAgent">
133+
{{ os }} {{ browser }}
134+
</span>
135+
</VDescriptionItem>
136+
<VDescriptionItem
137+
:label="$t('core.comment.detail_modal.fields.creation_time')"
138+
>
139+
<span v-tooltip="formatDatetime(creationTime)">
140+
{{ relativeTimeTo(creationTime) }}
141+
</span>
142+
</VDescriptionItem>
143+
<VDescriptionItem
144+
:label="$t('core.comment.detail_modal.fields.commented_on')"
145+
>
146+
<div class="flex items-center gap-2">
147+
<RouterLink
148+
v-tooltip="`${subjectRefResult.label}`"
149+
:to="subjectRefResult.route || $route"
150+
class="inline-block text-sm hover:text-gray-600"
151+
>
152+
{{ subjectRefResult.title }}
153+
</RouterLink>
154+
<a
155+
v-if="subjectRefResult.externalUrl"
156+
:href="subjectRefResult.externalUrl"
157+
target="_blank"
158+
class="text-gray-600 hover:text-gray-900"
159+
>
160+
<IconExternalLinkLine class="h-3.5 w-3.5" />
161+
</a>
162+
</div>
163+
</VDescriptionItem>
164+
<VDescriptionItem
165+
:label="$t('core.comment.comment_detail_modal.fields.content')"
166+
>
167+
<pre class="whitespace-pre-wrap break-words text-sm text-gray-900">{{
168+
comment.comment.spec.content
169+
}}</pre>
170+
</VDescriptionItem>
171+
<VDescriptionItem
172+
v-if="!comment.comment.spec.approved"
173+
:label="$t('core.comment.detail_modal.fields.new_reply')"
174+
>
175+
<ReplyFormItems
176+
:required="false"
177+
:auto-focus="false"
178+
@update="newReply = $event"
179+
/>
180+
</VDescriptionItem>
181+
</VDescription>
182+
</div>
183+
<template #footer>
184+
<VSpace>
185+
<VButton
186+
v-if="!comment.comment.spec.approved"
187+
type="secondary"
188+
@click="handleApprove"
189+
>
190+
{{
191+
newReply
192+
? $t("core.comment.operations.reply_and_approve.button")
193+
: $t("core.comment.operations.approve.button")
194+
}}
195+
</VButton>
196+
<VButton @click="modal?.close()">
197+
{{ $t("core.common.buttons.close") }}
198+
</VButton>
199+
</VSpace>
200+
</template>
201+
</VModal>
202+
</template>
203+
204+
<style scoped>
205+
:deep(.description-item__content) {
206+
@apply lg:col-span-5;
207+
}
208+
</style>

0 commit comments

Comments
 (0)