mirror of https://github.com/halo-dev/halo
refactor: simplify the code of reply creation modal (#5972)
#### What type of PR is this? /area ui /kind improvement /milestone 2.16.x #### What this PR does / why we need it: 简化评论回复组件的代码。 #### Does this PR introduce a user-facing change? ```release-note None ```pull/5975/head
parent
89b20bf5a3
commit
99eae2f31b
|
@ -1,18 +1,18 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
Dialog,
|
||||
VAvatar,
|
||||
VButton,
|
||||
VEntity,
|
||||
VEntityField,
|
||||
VStatusDot,
|
||||
VSpace,
|
||||
VEmpty,
|
||||
IconAddCircle,
|
||||
IconExternalLinkLine,
|
||||
VLoading,
|
||||
Toast,
|
||||
VAvatar,
|
||||
VButton,
|
||||
VDropdownItem,
|
||||
VEmpty,
|
||||
VEntity,
|
||||
VEntityField,
|
||||
VLoading,
|
||||
VSpace,
|
||||
VStatusDot,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import ReplyCreationModal from "./ReplyCreationModal.vue";
|
||||
|
@ -24,7 +24,7 @@ import type {
|
|||
SinglePage,
|
||||
} from "@halo-dev/api-client";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { computed, provide, ref, onMounted, type Ref } from "vue";
|
||||
import { computed, onMounted, provide, ref, type Ref } from "vue";
|
||||
import ReplyListItem from "./ReplyListItem.vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
@ -33,9 +33,9 @@ import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type {
|
||||
PluginModule,
|
||||
CommentSubjectRefProvider,
|
||||
CommentSubjectRefResult,
|
||||
PluginModule,
|
||||
} from "@halo-dev/console-shared";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
@ -48,12 +48,10 @@ const props = withDefaults(
|
|||
isSelected?: boolean;
|
||||
}>(),
|
||||
{
|
||||
comment: undefined,
|
||||
isSelected: false,
|
||||
}
|
||||
);
|
||||
|
||||
const selectedReply = ref<ListedReply>();
|
||||
const hoveredReply = ref<ListedReply>();
|
||||
const showReplies = ref(false);
|
||||
const replyModal = ref(false);
|
||||
|
@ -182,23 +180,14 @@ const handleToggleShowReplies = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleTriggerReply = () => {
|
||||
replyModal.value = true;
|
||||
};
|
||||
|
||||
const onTriggerReply = (reply: ListedReply) => {
|
||||
selectedReply.value = reply;
|
||||
replyModal.value = true;
|
||||
};
|
||||
|
||||
const onReplyCreationModalClose = () => {
|
||||
selectedReply.value = undefined;
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["comments"] });
|
||||
|
||||
if (showReplies.value) {
|
||||
refetch();
|
||||
}
|
||||
|
||||
replyModal.value = false;
|
||||
};
|
||||
|
||||
// Subject ref processing
|
||||
|
@ -287,10 +276,8 @@ const subjectRefResult = computed(() => {
|
|||
|
||||
<template>
|
||||
<ReplyCreationModal
|
||||
:key="comment?.comment.metadata.name"
|
||||
v-model:visible="replyModal"
|
||||
v-if="replyModal"
|
||||
:comment="comment"
|
||||
:reply="selectedReply"
|
||||
@close="onReplyCreationModalClose"
|
||||
/>
|
||||
<VEntity :is-selected="isSelected" :class="{ 'hover:bg-white': showReplies }">
|
||||
|
@ -364,7 +351,7 @@ const subjectRefResult = computed(() => {
|
|||
/>
|
||||
<span
|
||||
class="select-none text-gray-700 hover:text-gray-900"
|
||||
@click="handleTriggerReply"
|
||||
@click="replyModal = true"
|
||||
>
|
||||
{{ $t("core.comment.operations.reply.button") }}
|
||||
</span>
|
||||
|
@ -458,8 +445,8 @@ const subjectRefResult = computed(() => {
|
|||
:key="reply.reply.metadata.name"
|
||||
:class="{ 'hover:bg-white': showReplies }"
|
||||
:reply="reply"
|
||||
:comment="comment"
|
||||
:replies="replies"
|
||||
@reply="onTriggerReply"
|
||||
></ReplyListItem>
|
||||
</div>
|
||||
</Transition>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
VModal,
|
||||
VSpace,
|
||||
VButton,
|
||||
IconMotionLine,
|
||||
Toast,
|
||||
VButton,
|
||||
VDropdown,
|
||||
VModal,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||
import type {
|
||||
|
@ -16,9 +16,7 @@ import type {
|
|||
// @ts-ignore
|
||||
import { Picker } from "emoji-mart";
|
||||
import i18n from "@emoji-mart/data/i18n/zh.json";
|
||||
import { computed, nextTick, ref, watch, watchEffect } from "vue";
|
||||
import { reset } from "@formkit/core";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
@ -27,7 +25,6 @@ const { t } = useI18n();
|
|||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible?: boolean;
|
||||
comment?: ListedComment;
|
||||
reply?: ListedReply;
|
||||
}>(),
|
||||
|
@ -39,76 +36,38 @@ const props = withDefaults(
|
|||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:visible", visible: boolean): void;
|
||||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const initialFormState: ReplyRequest = {
|
||||
const modal = ref();
|
||||
const formState = ref<ReplyRequest>({
|
||||
raw: "",
|
||||
content: "",
|
||||
allowNotification: true,
|
||||
quoteReply: undefined,
|
||||
};
|
||||
|
||||
const formState = ref<ReplyRequest>(cloneDeep(initialFormState));
|
||||
});
|
||||
const saving = ref(false);
|
||||
|
||||
watch(
|
||||
() => formState.value.raw,
|
||||
(newValue) => {
|
||||
formState.value.content = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const formId = computed(() => {
|
||||
return `comment-reply-form-${[
|
||||
props.comment?.comment.metadata.name,
|
||||
props.reply?.reply.metadata.name,
|
||||
].join("-")}`;
|
||||
onMounted(() => {
|
||||
setFocus("content-input");
|
||||
});
|
||||
|
||||
const contentInputId = computed(() => {
|
||||
return `content-input-${[
|
||||
props.comment?.comment.metadata.name,
|
||||
props.reply?.reply.metadata.name,
|
||||
].join("-")}`;
|
||||
});
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit("update:visible", visible);
|
||||
if (!visible) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetForm = () => {
|
||||
formState.value = cloneDeep(initialFormState);
|
||||
reset(formId.value);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await nextTick();
|
||||
setFocus(contentInputId.value);
|
||||
} else {
|
||||
handleResetForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleCreateReply = async () => {
|
||||
try {
|
||||
saving.value = true;
|
||||
|
||||
if (props.reply) {
|
||||
formState.value.quoteReply = props.reply.reply.metadata.name;
|
||||
}
|
||||
|
||||
formState.value.content = formState.value.raw;
|
||||
|
||||
await apiClient.comment.createReply({
|
||||
name: props.comment?.comment.metadata.name as string,
|
||||
replyRequest: formState.value,
|
||||
});
|
||||
onVisibleChange(false);
|
||||
|
||||
modal.value.close();
|
||||
|
||||
Toast.success(
|
||||
t("core.comment.reply_modal.operations.submit.toast_success")
|
||||
|
@ -123,57 +82,56 @@ const handleCreateReply = async () => {
|
|||
// Emoji picker
|
||||
const emojiPickerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const handleCreateEmojiPicker = () => {
|
||||
const handleCreateEmojiPicker = async () => {
|
||||
if (emojiPickerRef.value?.childElementCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await import("@emoji-mart/data");
|
||||
|
||||
const emojiPicker = new Picker({
|
||||
data: async () => {
|
||||
const data = await import("@emoji-mart/data");
|
||||
return Object.assign({}, data);
|
||||
},
|
||||
data: Object.assign({}, data),
|
||||
theme: "light",
|
||||
autoFocus: true,
|
||||
i18n: i18n,
|
||||
onEmojiSelect: onEmojiSelect,
|
||||
});
|
||||
|
||||
emojiPickerRef.value?.appendChild(emojiPicker as unknown as Node);
|
||||
};
|
||||
|
||||
const onEmojiSelect = (emoji: { native: string }) => {
|
||||
formState.value.raw += emoji.native;
|
||||
setFocus(contentInputId.value);
|
||||
setFocus("content-input");
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (emojiPickerRef.value) {
|
||||
handleCreateEmojiPicker();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VModal
|
||||
ref="modal"
|
||||
:title="$t('core.comment.reply_modal.title')"
|
||||
:visible="visible"
|
||||
:width="500"
|
||||
@update:visible="onVisibleChange"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<FormKit
|
||||
:id="formId"
|
||||
:name="formId"
|
||||
id="create-reply-form"
|
||||
name="create-reply-form"
|
||||
type="form"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="handleCreateReply"
|
||||
>
|
||||
<FormKit
|
||||
:id="contentInputId"
|
||||
id="content-input"
|
||||
v-model="formState.raw"
|
||||
type="textarea"
|
||||
:validation-label="$t('core.comment.reply_modal.fields.content.label')"
|
||||
:rows="6"
|
||||
value=""
|
||||
validation="required|length:0,1024"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<VDropdown :classes="['!p-0']">
|
||||
<VDropdown :classes="['!p-0']" @show="handleCreateEmojiPicker">
|
||||
<IconMotionLine
|
||||
class="h-5 w-5 cursor-pointer text-gray-500 transition-all hover:text-gray-900"
|
||||
/>
|
||||
|
@ -185,14 +143,13 @@ watchEffect(() => {
|
|||
<template #footer>
|
||||
<VSpace>
|
||||
<SubmitButton
|
||||
v-if="visible"
|
||||
:loading="saving"
|
||||
type="secondary"
|
||||
:text="$t('core.common.buttons.submit')"
|
||||
@submit="$formkit.submit(formId)"
|
||||
@submit="$formkit.submit('create-reply-form')"
|
||||
>
|
||||
</SubmitButton>
|
||||
<VButton @click="onVisibleChange(false)">
|
||||
<VButton @click="modal.close()">
|
||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
VAvatar,
|
||||
VTag,
|
||||
VEntityField,
|
||||
VEntity,
|
||||
Dialog,
|
||||
VStatusDot,
|
||||
VDropdownItem,
|
||||
IconReplyLine,
|
||||
Toast,
|
||||
VAvatar,
|
||||
VDropdownItem,
|
||||
VEntity,
|
||||
VEntityField,
|
||||
VStatusDot,
|
||||
VTag,
|
||||
} from "@halo-dev/components";
|
||||
import type { ListedReply } from "@halo-dev/api-client";
|
||||
import type { ListedComment, ListedReply } from "@halo-dev/api-client";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { computed, inject, type Ref } from "vue";
|
||||
import { computed, inject, ref, type Ref } from "vue";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import ReplyCreationModal from "./ReplyCreationModal.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
comment: ListedComment;
|
||||
reply: ListedReply;
|
||||
replies?: ListedReply[];
|
||||
}>(),
|
||||
|
@ -32,10 +34,6 @@ const props = withDefaults(
|
|||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "reply", reply: ListedReply): void;
|
||||
}>();
|
||||
|
||||
const quoteReply = computed(() => {
|
||||
const { quoteReply: replyName } = props.reply.reply.spec;
|
||||
|
||||
|
@ -90,10 +88,6 @@ const handleApprove = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleTriggerReply = () => {
|
||||
emit("reply", props.reply);
|
||||
};
|
||||
|
||||
// Show hovered reply
|
||||
const hoveredReply = inject<Ref<ListedReply | undefined>>("hoveredReply");
|
||||
|
||||
|
@ -108,9 +102,25 @@ const isHoveredReply = computed(() => {
|
|||
hoveredReply?.value?.reply.metadata.name === props.reply.reply.metadata.name
|
||||
);
|
||||
});
|
||||
|
||||
// Create reply
|
||||
const replyModal = ref(false);
|
||||
|
||||
function onReplyCreationModalClose() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["comment-replies", props.comment.comment.metadata.name],
|
||||
});
|
||||
replyModal.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReplyCreationModal
|
||||
v-if="replyModal"
|
||||
:comment="comment"
|
||||
:reply="reply"
|
||||
@close="onReplyCreationModalClose"
|
||||
/>
|
||||
<VEntity class="!px-0 !py-2" :class="{ 'animate-breath': isHoveredReply }">
|
||||
<template #start>
|
||||
<VEntityField>
|
||||
|
@ -150,7 +160,7 @@ const isHoveredReply = computed(() => {
|
|||
<div class="flex items-center gap-3 text-xs">
|
||||
<span
|
||||
class="select-none text-gray-700 hover:text-gray-900"
|
||||
@click="handleTriggerReply"
|
||||
@click="replyModal = true"
|
||||
>
|
||||
{{ $t("core.comment.operations.reply.button") }}
|
||||
</span>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"@codemirror/legacy-modes": "^6.3.0",
|
||||
"@codemirror/state": "^6.1.4",
|
||||
"@codemirror/view": "^6.5.1",
|
||||
"@emoji-mart/data": "^1.0.8",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@formkit/core": "^1.5.9",
|
||||
"@formkit/i18n": "^1.5.9",
|
||||
"@formkit/inputs": "^1.5.9",
|
||||
|
@ -81,7 +81,7 @@
|
|||
"colorjs.io": "^0.4.3",
|
||||
"cropperjs": "^1.5.13",
|
||||
"dayjs": "^1.11.7",
|
||||
"emoji-mart": "^5.3.3",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"floating-vue": "^5.2.2",
|
||||
"fuse.js": "^6.6.2",
|
||||
"jsencrypt": "^3.3.2",
|
||||
|
|
|
@ -36,8 +36,8 @@ importers:
|
|||
specifier: ^6.5.1
|
||||
version: 6.5.1
|
||||
'@emoji-mart/data':
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
'@formkit/core':
|
||||
specifier: ^1.5.9
|
||||
version: 1.5.9
|
||||
|
@ -138,8 +138,8 @@ importers:
|
|||
specifier: ^1.11.7
|
||||
version: 1.11.7
|
||||
emoji-mart:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
floating-vue:
|
||||
specifier: ^5.2.2
|
||||
version: 5.2.2(vue@3.4.27(typescript@5.3.3))
|
||||
|
@ -1923,8 +1923,8 @@ packages:
|
|||
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@emoji-mart/data@1.0.8':
|
||||
resolution: {integrity: sha512-AMpqLrR80dHfj8ZA6xaf8/t9reBy88vz07fBdwKVVoUX6X2Wi+R2p2uhIZFqNZe8zRd6kga3hKheqK+deElEZw==}
|
||||
'@emoji-mart/data@1.2.1':
|
||||
resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==}
|
||||
|
||||
'@emotion/use-insertion-effect-with-fallbacks@1.0.1':
|
||||
resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==}
|
||||
|
@ -5721,8 +5721,8 @@ packages:
|
|||
element-resize-detector@1.2.4:
|
||||
resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==}
|
||||
|
||||
emoji-mart@5.3.3:
|
||||
resolution: {integrity: sha512-rr3wXUimYFQ5Mf50P/5UOsRibr5JSJE3Nj4zw0aDglb3GSHzn/wGKBoXoSkjtWaji8UcmXcYn3cdilD2Eix6iQ==}
|
||||
emoji-mart@5.6.0:
|
||||
resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==}
|
||||
|
||||
emoji-regex@10.3.0:
|
||||
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
|
||||
|
@ -12690,7 +12690,7 @@ snapshots:
|
|||
|
||||
'@discoveryjs/json-ext@0.5.7': {}
|
||||
|
||||
'@emoji-mart/data@1.0.8': {}
|
||||
'@emoji-mart/data@1.2.1': {}
|
||||
|
||||
'@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0)':
|
||||
dependencies:
|
||||
|
@ -17198,7 +17198,7 @@ snapshots:
|
|||
dependencies:
|
||||
batch-processor: 1.0.0
|
||||
|
||||
emoji-mart@5.3.3: {}
|
||||
emoji-mart@5.6.0: {}
|
||||
|
||||
emoji-regex@10.3.0: {}
|
||||
|
||||
|
|
Loading…
Reference in New Issue