feat: 评论组件优化

This commit is contained in:
RubyLiu 2023-12-06 19:09:23 +08:00 committed by 刘瑞斌
parent 098ebaed1c
commit b44e170c90
9 changed files with 222 additions and 23 deletions

View File

@ -1,11 +1,6 @@
<template>
<div class="flex flex-col">
<div class="h-[40px] w-[40px] gap-[8px] rounded-full">
<img
src="https://p6-passport.byteacctimg.com/img/user-avatar/9a6e39ea689600e70175649a8cd14913~200x200.awebp"
alt="User avatar"
/>
</div>
<MsAvatar avatar="default" />
<div class="flex flex-col">
<div class="text-[var(--color-text-1)]">{{ props.element.createUser }}</div>
<div v-dompurify-html="props.element.content" class="mt-[4px]"></div>
@ -14,7 +9,7 @@
dayjs(props.element.updateTime).format('YYYY-MM-DD HH:mm:ss')
}}</div>
<div class="ml-[24px] flex flex-row gap-[16px]">
<div class="comment-btn" @click="expendChange">
<div v-if="props.mode === 'parent'" class="comment-btn" @click="expendChange">
<MsIconfont type="icon-icon_comment_outlined" />
<span>{{ !expendComment ? t('comment.expendComment') : t('comment.collapseComment') }}</span>
<span class="text-[var(--color-text-4)]">({{ element.children?.length }})</span>
@ -23,11 +18,11 @@
<MsIconfont type="icon-icon_reply" />
<span>{{ t('comment.reply') }}</span>
</div>
<div class="comment-btn" @click="editClick">
<div v-if="hasEditAuth" class="comment-btn" @click="editClick">
<MsIconfont type="icon-icon_edit_outlined" />
<span>{{ t('comment.edit') }}</span>
</div>
<div class="comment-btn" @click="deleteClick">
<div v-if="hasEditAuth" class="comment-btn" @click="deleteClick">
<MsIconfont type="icon-icon_delete-trash_outlined" />
<span>{{ t('comment.delete') }}</span>
</div>
@ -41,27 +36,24 @@
import { ref } from 'vue';
import dayjs from 'dayjs';
import MsAvatar from '@/components/pure/ms-avatar/index.vue';
import MsIconfont from '@/components/pure/ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n';
export interface CommentItem {
id: string; // id
bugId: string; // bug id
createUser: string; //
updateTime: number; //
content: string;
replyUser?: string; //
notifier?: string; //
children?: CommentItem[];
}
// : COMMENT; @: AT; /@: REPLAY;)
export type commentEvent = 'COMMENT' | 'AT' | 'REPLAY';
import { CommentItem } from './types';
const props = defineProps<{
element: CommentItem;
element: CommentItem; //
mode: 'parent' | 'child'; //
currentUserId: string; // id
}>();
//
const hasEditAuth = computed(() => {
return props.element.commentUserInfo.id === props.currentUserId;
});
const emit = defineEmits<{
(event: 'reply'): void;
(event: 'edit'): void;

View File

@ -0,0 +1,110 @@
import CommentInput from './input.vue';
import Item from './item.vue';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { CommentItem, CommentParams } from './types';
import message from '@arco-design/web-vue/es/message';
export default defineComponent({
name: 'MsComment',
props: {
currentUserId: {
type: String,
default: '',
},
commentList: {
type: Array as PropType<CommentItem[]>,
default: () => [],
},
},
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */
updateOrAdd: (value: CommentParams) => true, // 更新或者新增评论
delete: (value: string, cb: (result: boolean) => void) => true, // 删除评论
},
setup(props, { emit }) {
const { currentUserId } = toRefs(props);
const commentList = ref<CommentItem[]>([]);
const currentItem = reactive<{ id: string; parentId: string }>({ id: '', parentId: '' });
const { t } = useI18n();
const { openModal } = useModal();
const handlePublish = (content: string, item: CommentItem) => {
const params: CommentParams = {
...item,
content,
event: 'REPLAY',
};
emit('updateOrAdd', params);
};
const handleDelete = (item: CommentItem) => {
openModal({
type: 'error',
title: t('comment.deleteConfirm'),
content: t('comment.deleteContent'),
okText: t('common.confirmClose'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
emit('delete', item.id, (result: boolean) => {
if (result) {
message.success(t('common.deleteSuccess'));
} else {
message.error(t('common.deleteFail'));
}
});
},
hideCancel: false,
});
};
const renderInput = (item: CommentItem) => {
return <CommentInput onPublish={(content: string) => handlePublish(content, item)} {...item} />;
};
const renderChildrenList = (list?: CommentItem[]) => {
if (!list || list.length === 0) {
return null;
}
return list.map((item) => {
return (
<div class="flex flex-col gap-[24px]">
<Item
onReply={() => {
currentItem.id = item.id;
currentItem.parentId = item.parentId || '';
}}
onEdit={() => {
currentItem.id = item.id;
currentItem.parentId = item.parentId || '';
}}
onDelete={() => handleDelete(item)}
mode={'child'}
currentUserId={currentUserId.value}
element={item}
/>
{item.id === currentItem.id && renderInput(item)}
</div>
);
});
};
const renderParentList = (list: CommentItem[]) => {
return list.map((item) => {
return (
<Item mode={'parent'} onDelete={() => handleDelete(item)} currentUserId={currentUserId.value} element={item}>
<div class="rounded border border-[var(--color-text-7)] p-[16px]">{renderChildrenList(item.children)}</div>
</Item>
);
});
};
return () => <div class="ms-comment">{renderParentList(commentList.value)}</div>;
},
});

View File

@ -0,0 +1,12 @@
import _Comment from './comment';
import type { App } from 'vue';
const MsComment = Object.assign(_Comment, {
install: (app: App) => {
app.component(_Comment.name, _Comment);
},
});
export type CommentInstance = InstanceType<typeof _Comment>;
export default MsComment;

View File

@ -0,0 +1,49 @@
<template>
<div class="w-full">
<a-input v-if="!isActive" class="w-full" @click="isActive = true"></a-input>
<div v-else class="flex flex-col">
<MsRichText v-model="currentContent" class="w-full" />
<div class="flex flex-row justify-end gap-[12px]">
<a-button @click="cancelClick">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :disabled="!content" @click="publish">{{ t('common.publish') }}</a-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{ content: string }>();
const emit = defineEmits<{
(event: 'publish', value: string): void;
}>();
const isActive = ref(false);
const currentContent = ref('');
watchEffect(() => {
if (props.content) {
currentContent.value = props.content;
}
});
const publish = () => {
emit('publish', currentContent.value);
isActive.value = false;
currentContent.value = '';
};
const cancelClick = () => {
isActive.value = false;
currentContent.value = '';
};
</script>
<style scoped lang="less"></style>

View File

@ -4,4 +4,6 @@ export default {
'comment.edit': '编辑',
'comment.reply': '回复',
'comment.delete': '删除',
'comment.deleteConfirm': '确认删除该评论吗?',
'comment.deleteContent': '删除后,评论无法回复,请谨慎操作',
};

View File

@ -0,0 +1,32 @@
export interface CommentUserInfo {
id: string;
name: string;
email: string;
}
export interface CommentItem {
id: string; // 评论id
parentId?: string; // 父级评论id
bugId: string; // bug id
createUser: string; // 创建人
updateTime: number; // 更新时间
content: string;
commentUserInfo: CommentUserInfo; // 评论人用户信息
replyUser?: string; // 回复人
notifier?: string; // 通知人
children?: CommentItem[];
}
// 仅评论: COMMENT; 评论并@: AT; 回复评论/回复并@: REPLAY;)
export type commentEvent = 'COMMENT' | 'AT' | 'REPLAY';
export interface WriteCommentProps {
id?: string; // 评论id
parentId?: string; // 父级评论id
event: commentEvent; // 评论事件
bugId: string; // bug id
}
export interface CommentParams extends WriteCommentProps {
content: string;
replyUser?: string; // 回复人
}

View File

@ -39,7 +39,7 @@
</a-select>
</a-form-item>
</div>
<div class="flex-1 grow-0">
<div class="grow-0">
<a-form-item
:field="`list[${idx}].operator`"
hide-asterisk

View File

@ -76,4 +76,5 @@ export default {
'common.recycle': 'Recycle Bin',
'common.new': 'New',
'common.newSuccess': 'Added successfully',
'common.publish': 'Publish',
};

View File

@ -76,4 +76,5 @@ export default {
'common.recycle': '回收站',
'common.new': '新增',
'common.newSuccess': '新增成功',
'common.publish': '发布',
};