feat: 项目添加评论模块
This commit is contained in:
43
apps/web-antd/src/api/license/comment/index.ts
Normal file
43
apps/web-antd/src/api/license/comment/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace CommentApi {
|
||||||
|
/** 客户信息 */
|
||||||
|
export interface Comment {
|
||||||
|
id: number; // 主键
|
||||||
|
projectId?: number; // 项目ID
|
||||||
|
userId?: number; // 用户ID
|
||||||
|
parentId?: number; // 父评论ID
|
||||||
|
author?: string; // 评论人
|
||||||
|
avatar?: string; // 头像
|
||||||
|
replyUserId?: number; // 回复用户ID
|
||||||
|
replyUser?: string; // 回复用户
|
||||||
|
updateTime?: Dayjs; // 更新时间
|
||||||
|
depth?: number; // 评论深度
|
||||||
|
content?: string; // 评论内容
|
||||||
|
children?: Comment[]; // 子评论
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询评论树形列表 */
|
||||||
|
export function getCommentTree(projectId: number, sort: boolean = false) {
|
||||||
|
return requestClient.get<CommentApi.Comment[]>(
|
||||||
|
`/license/comment/tree?projectId=${projectId}&sort=${sort}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增评论 */
|
||||||
|
export function createComment(data: CommentApi.Comment) {
|
||||||
|
return requestClient.post('/license/comment/create', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改评论 */
|
||||||
|
export function updateComment(data: CommentApi.Comment) {
|
||||||
|
return requestClient.post('/license/comment/update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除客户 */
|
||||||
|
export function deleteComment(id: number) {
|
||||||
|
return requestClient.delete(`/license/comment/delete?id=${id}`);
|
||||||
|
}
|
||||||
11
apps/web-antd/src/locales/langs/en-US/comment.json
Normal file
11
apps/web-antd/src/locales/langs/en-US/comment.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"postComment": "Post Comment",
|
||||||
|
"replyComment": "Reply Comment",
|
||||||
|
"hotComment": "All Comments",
|
||||||
|
"reply": "Reply",
|
||||||
|
"cancelReply": "Cancel Reply",
|
||||||
|
"delete": "Delete",
|
||||||
|
"comment": "Comment",
|
||||||
|
"deleteCommentTitle": "Are you sure you want to delete this comment?",
|
||||||
|
"sortByTime": "Sort by Time"
|
||||||
|
}
|
||||||
11
apps/web-antd/src/locales/langs/zh-CN/comment.json
Normal file
11
apps/web-antd/src/locales/langs/zh-CN/comment.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"postComment": "发表评论",
|
||||||
|
"replyComment": "回复评论",
|
||||||
|
"hotComment": "全部评论",
|
||||||
|
"reply": "回复",
|
||||||
|
"cancelReply": "取消回复",
|
||||||
|
"delete": "删除",
|
||||||
|
"comment": "评论",
|
||||||
|
"deleteCommentTitle": "确定删除该条评论吗?",
|
||||||
|
"sortByTime": "按时间排序"
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
109
apps/web-antd/src/views/license/project/comment/index.vue
Normal file
109
apps/web-antd/src/views/license/project/comment/index.vue
Normal 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>
|
||||||
@@ -510,11 +510,11 @@ export function useGridColumns(
|
|||||||
code: 'edit',
|
code: 'edit',
|
||||||
show: hasAccessByCodes(['license:project:update']),
|
show: hasAccessByCodes(['license:project:update']),
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// code: 'progress',
|
code: 'progress',
|
||||||
// text: $t('project.progress'),
|
text: $t('project.progress'),
|
||||||
// show: hasAccessByCodes(['license:project:update']),
|
show: hasAccessByCodes(['license:project:update']),
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
code: 'delete',
|
code: 'delete',
|
||||||
show: hasAccessByCodes(['license:project:delete']),
|
show: hasAccessByCodes(['license:project:delete']),
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { CommentApi } from '#/api/license/comment';
|
||||||
import type { ProjectApi } from '#/api/license/project';
|
import type { ProjectApi } from '#/api/license/project';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenDrawer } from '@vben/common-ui';
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { getCommentTree } from '#/api/license/comment';
|
||||||
import { getProject } from '#/api/license/project';
|
import { getProject } from '#/api/license/project';
|
||||||
|
|
||||||
|
import Comment from '../comment/index.vue';
|
||||||
|
|
||||||
const projectData = ref<ProjectApi.Project>();
|
const projectData = ref<ProjectApi.Project>();
|
||||||
|
|
||||||
|
const comments = ref<CommentApi.Comment[]>([]);
|
||||||
|
|
||||||
const [Drawer, drawerApi] = useVbenDrawer({
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
async onConfirm() {},
|
async onConfirm() {},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
@@ -24,6 +30,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||||||
drawerApi.lock();
|
drawerApi.lock();
|
||||||
try {
|
try {
|
||||||
data = await getProject(data.id);
|
data = await getProject(data.id);
|
||||||
|
// comments.value = await getCommentTree(data.id);
|
||||||
|
getCommentByProjectId(data.id, false);
|
||||||
} finally {
|
} finally {
|
||||||
drawerApi.unlock();
|
drawerApi.unlock();
|
||||||
}
|
}
|
||||||
@@ -32,10 +40,17 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
|||||||
projectData.value = data;
|
projectData.value = data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getCommentByProjectId = async (projectId: number, sort: boolean) => {
|
||||||
|
comments.value = await getCommentTree(projectId, sort);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Drawer :title="projectData?.name" class="w-[800px]">
|
<Drawer :title="projectData?.name" class="w-[800px]" :footer="false">
|
||||||
<div>进展测试</div>
|
<Comment
|
||||||
<div>{{ projectData }}</div>
|
:comments="comments"
|
||||||
|
:project-id="projectData?.id"
|
||||||
|
@get-comment-by-project-id="getCommentByProjectId"
|
||||||
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user