feat: 项目添加评论模块

This commit is contained in:
caiyuchao
2025-07-15 15:38:21 +08:00
parent 5d475c24c7
commit e467d6d01b
8 changed files with 411 additions and 8 deletions

View File

@@ -0,0 +1,150 @@
<script lang="ts" setup>
import type { CommentApi } from '#/api/license/comment';
import { computed, ref } from 'vue';
import { useUserStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { deleteComment } from '#/api/license/comment';
import { $t } from '#/locales';
import CreateComment from './create-comment.vue';
const props = withDefaults(
defineProps<{
comments?: CommentApi.Comment[];
data?: CommentApi.Comment;
projectId?: number;
}>(),
{
data: () => ({
id: 0,
author: '',
avatar: '',
updateTime: dayjs(),
depth: 1,
content: '',
children: [],
}),
projectId: 0,
comments: () => [],
},
);
const emit = defineEmits(['getCommentByProjectId']);
const userStore = useUserStore();
const isShowReply = ref<boolean>();
const parentId = ref<number>();
const reply = (id: number) => {
isShowReply.value = !isShowReply.value;
parentId.value = id;
};
const showReply = (flag: boolean) => {
isShowReply.value = flag;
};
const onDelete = async (id: number) => {
try {
await deleteComment(id);
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
emit('getCommentByProjectId', props.projectId);
}
};
dayjs.extend(relativeTime);
const formatContent = computed(() => {
let dataContent = props.data.content || '';
if ((props.data?.depth ?? 0) > 2 && dataContent) {
const position = dataContent.indexOf('>') + 1;
const originalString = dataContent;
const stringToInsert = `<span style="font-size: 12px; color: #32363973">回复 ${props.data?.replyUser}</span>`;
dataContent =
originalString.slice(0, position) +
stringToInsert +
originalString.slice(position);
}
return dataContent;
});
</script>
<template>
<a-comment :author="data.author">
<template #avatar>
<a-avatar :src="data.avatar" :alt="data.author">
{{ data.author?.substring(0, 2) }}
</a-avatar>
</template>
<template #datetime>
<a-tooltip :title="dayjs(data.updateTime).format('YYYY-MM-DD HH:mm:ss')">
<span>{{ dayjs(data.updateTime).fromNow() }}</span>
</a-tooltip>
</template>
<template #content>
<div>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="formatContent"></span>
</div>
</template>
<template #actions>
<span key="comment-basic-reply-to">
<template v-if="!isShowReply">
<a-tooltip :title="$t('comment.reply')">
<span
class="icon-[mdi--comment-processing-outline] size-3.5"
@click="reply(data.id)"
></span>
</a-tooltip>
</template>
<template v-else>
<a-tooltip :title="$t('comment.cancelReply')">
<span
class="icon-[mdi--comment-processing] size-3.5"
@click="reply(data.id)"
></span>
</a-tooltip>
</template>
</span>
<span
v-if="userStore.userInfo?.id === data.userId"
key="comment-basic-delete"
>
<a-popconfirm
:title="$t('comment.deleteCommentTitle')"
@confirm="onDelete(data.id)"
>
<a-tooltip :title="$t('ui.actionTitle.delete')">
<span class="icon-[ri--delete-bin-line] size-3.5"></span>
</a-tooltip>
</a-popconfirm>
</span>
</template>
<div v-show="isShowReply">
<CreateComment
:project-id="projectId"
:comments="comments"
:parent-id="parentId"
@show-reply="showReply"
@get-comment-by-project-id="emit('getCommentByProjectId', projectId)"
/>
</div>
<template v-if="(data?.depth ?? 0) < 2">
<child-comment
v-for="(item, index) of data.children"
:data="item"
:key="index"
:project-id="projectId"
:comments="comments"
:parent-id="parentId"
@get-comment-by-project-id="emit('getCommentByProjectId', projectId)"
/>
</template>
</a-comment>
</template>

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import type { CommentApi } from '#/api/license/comment';
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { createComment } from '#/api/license/comment';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { $t } from '#/locales';
const props = defineProps<{
comments?: CommentApi.Comment[];
parentId?: number;
projectId?: number;
}>();
const emit = defineEmits(['getCommentByProjectId', 'showReply']);
const submitting = ref<boolean>(false);
const value = ref<string>('');
const handleSubmit = async () => {
if (!value.value) {
return;
}
// 提交表单
const data = {
parentId: props.parentId,
projectId: props.projectId,
content: value.value,
} as CommentApi.Comment;
try {
submitting.value = true;
await createComment(data);
submitting.value = false;
emit('showReply', false);
message.success($t('ui.actionMessage.operationSuccess'));
emit('getCommentByProjectId', props.projectId);
} finally {
value.value = '';
}
};
</script>
<template>
<div>
<RichTextarea v-model="value" />
<a-button
html-type="submit"
:loading="submitting"
type="primary"
class="mt-2"
@click="handleSubmit"
>
{{
(props.parentId ?? 0) === 0
? $t('comment.postComment')
: $t('comment.replyComment')
}}
</a-button>
</div>
</template>

View File

@@ -0,0 +1,109 @@
<script lang="ts" setup>
import type { CommentApi } from '#/api/license/comment';
import { ref } from 'vue';
import { useUserStore } from '@vben/stores';
import ChildComment from './child-comment.vue';
import CreateComment from './create-comment.vue';
const props = defineProps<{
comments?: CommentApi.Comment[];
projectId?: number;
}>();
const emit = defineEmits(['getCommentByProjectId']);
const userStore = useUserStore();
const isSortAsc = ref<boolean>();
const sortByTime = () => {
isSortAsc.value = !isSortAsc.value;
emit('getCommentByProjectId', props.projectId, isSortAsc.value);
};
</script>
<template>
<div id="article-comment" style="padding: 5px">
<div class="commentText">
<span style="margin-right: auto">
<span class="text">
{{ $t('comment.hotComment') }}
</span>
<span style="padding: 0 15px 0 5px; color: #9499a0">{{
comments?.length ?? 0
}}</span>
</span>
<span
v-if="comments?.length ?? 0 > 0"
style="padding: 5px; margin-left: auto; cursor: pointer"
>
<template v-if="isSortAsc">
<a-tooltip :title="$t('comment.sortByTime')">
<span
class="icon-[solar--sort-from-bottom-to-top-outline] size-5"
@click="sortByTime"
></span>
</a-tooltip>
</template>
<template v-else>
<a-tooltip :title="$t('comment.sortByTime')">
<span
class="icon-[solar--sort-from-top-to-bottom-outline] size-5"
@click="sortByTime"
></span>
</a-tooltip>
</template>
</span>
</div>
<a-empty v-if="(comments?.length ?? 0) === 0" :description="false" />
<ChildComment
v-for="(item, index) in comments"
:key="index"
:data="item"
:project-id="projectId"
:comments="comments"
@get-comment-by-project-id="emit('getCommentByProjectId', projectId)"
/>
<a-comment>
<template #avatar>
<a-avatar
:src="userStore.userInfo?.avatar"
:alt="userStore.userInfo?.nickname"
>
{{ userStore.userInfo?.nickname?.slice(-2).toUpperCase() }}
</a-avatar>
</template>
<template #content>
<CreateComment
:project-id="projectId"
:comments="comments"
@get-comment-by-project-id="emit('getCommentByProjectId', projectId)"
/>
</template>
</a-comment>
</div>
</template>
<style lang="less" scoped>
#article-comment {
.title {
font-size: 18px;
font-weight: 600;
color: #252933;
line-height: 30px;
}
.commentText {
display: flex;
justify-content: space-between;
text-align: center;
line-height: 30px;
.text {
font-size: 18px;
font-weight: 600;
color: #252933;
}
}
}
</style>