feat(缺陷管理): 评论接口对接

This commit is contained in:
RubyLiu 2024-01-02 20:05:33 +08:00 committed by Craftsman
parent 4bc05648fc
commit a81a8cba2b
15 changed files with 223 additions and 71 deletions

View File

@ -1,7 +1,9 @@
import { CommentParams } from '@/components/business/ms-comment/types';
import MSR from '@/api/http/index';
import * as bugURL from '@/api/requrls/bug-management';
import { BugEditFormObject, BugExportParams, BugListItem } from '@/models/bug-management';
import { BugEditFormObject, BugExportParams, BugListItem, CreateOrUpdateComment } from '@/models/bug-management';
import { AssociatedList } from '@/models/caseManagement/featureCase';
import { CommonList, TableQueryParams, TemplateOption } from '@/models/common';
@ -89,3 +91,19 @@ export function followBug(id: string, isFollow: boolean) {
}
return MSR.get({ url: `${bugURL.getFollowBugUrl}${id}` });
}
// 创建评论
export function createOrUpdateComment(data: CommentParams) {
if (data.id) {
return MSR.post({ url: bugURL.postUpdateCommentUrl, data });
}
return MSR.post({ url: bugURL.postCreateCommentUrl, data });
}
// 获取评论列表
export function getCommentList(bugId: string) {
return MSR.get({ url: `${bugURL.getCommentListUrl}${bugId}` });
}
// 删除评论
export function deleteComment(commentId: string) {
return MSR.get({ url: `${bugURL.getDeleteCommentUrl}${commentId}` });
}

View File

@ -14,3 +14,7 @@ export const postAssociatedFileListUrl = '/bug/relate/case/page';
export const getBugDetailUrl = '/bug/detail/';
export const getFollowBugUrl = '/bug/follow/';
export const getUnFollowBugUrl = '/bug/unfollow/';
export const postUpdateCommentUrl = '/bug/comment/update';
export const postCreateCommentUrl = '/bug/comment/add';
export const getCommentListUrl = '/bug/comment/get/';
export const getDeleteCommentUrl = '/bug/comment/delete/';

View File

@ -1,6 +1,6 @@
<template>
<div class="flex flex-col">
<MsAvatar avatar="default" />
<div class="flex flex-row gap-[8px]">
<MsAvatar avatar="word" />
<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>
@ -9,22 +9,26 @@
dayjs(props.element.updateTime).format('YYYY-MM-DD HH:mm:ss')
}}</div>
<div class="ml-[24px] flex flex-row gap-[16px]">
<div v-if="props.mode === 'parent'" class="comment-btn" @click="expendChange">
<div
v-if="props.mode === 'parent' && element.childComments?.length"
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>
<span>{{ !expendComment ? t('ms.comment.expendComment') : t('ms.comment.collapseComment') }}</span>
<span class="text-[var(--color-text-4)]">({{ element.childComments?.length }})</span>
</div>
<div class="comment-btn" @click="replyClick">
<MsIconfont type="icon-icon_reply" />
<span>{{ t('comment.reply') }}</span>
<span>{{ t('ms.comment.reply') }}</span>
</div>
<div v-if="hasEditAuth" class="comment-btn" @click="editClick">
<MsIconfont type="icon-icon_edit_outlined" />
<span>{{ t('comment.edit') }}</span>
<span>{{ t('ms.comment.edit') }}</span>
</div>
<div v-if="hasEditAuth" class="comment-btn" @click="deleteClick">
<MsIconfont type="icon-icon_delete-trash_outlined" />
<span>{{ t('comment.delete') }}</span>
<span>{{ t('ms.comment.delete') }}</span>
</div>
</div>
</div>
@ -40,18 +44,21 @@
import MsIconfont from '@/components/pure/ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user/index';
import { CommentItem } from './types';
const userStore = useUserStore();
const { t } = useI18n();
const props = defineProps<{
element: CommentItem; //
mode: 'parent' | 'child'; //
currentUserId: string; // id
}>();
//
const hasEditAuth = computed(() => {
return props.element.commentUserInfo.id === props.currentUserId;
return props.element.commentUserInfo.id === userStore.id;
});
const emit = defineEmits<{
@ -78,8 +85,6 @@
const deleteClick = () => {
emit('delete');
};
const { t } = useI18n();
</script>
<style lang="less" scoped>

View File

@ -1,3 +1,5 @@
// eslint-disable-next-line no-shadow
import Item from './comment-item.vue';
import CommentInput from './input.vue';
@ -10,11 +12,6 @@ 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: () => [],
@ -22,12 +19,11 @@ export default defineComponent({
},
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */
updateOrAdd: (value: CommentParams) => true, // 更新或者新增评论
delete: (value: string, cb: (result: boolean) => void) => true, // 删除评论
updateOrAdd: (value: CommentParams, cb: (result: boolean) => void) => true, // 更新或者新增评论
delete: (value: string) => true, // 删除评论
},
setup(props, { emit }) {
const { currentUserId } = toRefs(props);
const commentList = ref<CommentItem[]>([]);
const { commentList } = toRefs(props);
const currentItem = reactive<{ id: string; parentId: string }>({ id: '', parentId: '' });
const { t } = useI18n();
const { openModal } = useModal();
@ -38,27 +34,27 @@ export default defineComponent({
content,
event: 'REPLAY',
};
emit('updateOrAdd', params);
emit('updateOrAdd', params, (result: boolean) => {
if (result) {
message.success(t('common.publishSuccess'));
} else {
message.error(t('common.publishFail'));
}
});
};
const handleDelete = (item: CommentItem) => {
openModal({
type: 'error',
title: t('comment.deleteConfirm'),
content: t('comment.deleteContent'),
okText: t('common.confirmClose'),
title: t('ms.comment.deleteConfirm'),
content: t('ms.comment.deleteContent'),
okText: t('common.confirmDelete'),
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'));
}
});
emit('delete', item.id);
},
hideCancel: false,
});
@ -81,7 +77,7 @@ export default defineComponent({
}
return list.map((item) => {
return (
<div class="flex flex-col gap-[24px]">
<div class="flex flex-col">
<Item
onReply={() => {
currentItem.id = item.id;
@ -93,7 +89,6 @@ export default defineComponent({
}}
onDelete={() => handleDelete(item)}
mode={'child'}
currentUserId={currentUserId.value}
element={item}
/>
{item.id === currentItem.id && renderInput(item)}
@ -105,13 +100,15 @@ export default defineComponent({
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 mode={'parent'} onDelete={() => handleDelete(item)} element={item}>
<div class="rounded border border-[var(--color-text-7)] p-[16px]">
{renderChildrenList(item.childComments)}
</div>
</Item>
);
});
};
return () => <div class="ms-comment">{renderParentList(commentList.value)}</div>;
return () => <div class="ms-comment gap[24px] flex flex-col">{renderParentList(commentList.value)}</div>;
},
});

View File

@ -9,4 +9,5 @@ const MsComment = Object.assign(_Comment, {
export type CommentInstance = InstanceType<typeof _Comment>;
export { default as CommentInput } from './input.vue';
export default MsComment;

View File

@ -0,0 +1,10 @@
export default {
'ms.comment.expendComment': 'Expand comment',
'ms.comment.collapseComment': 'Collapse comment',
'ms.comment.edit': 'Edit',
'ms.comment.reply': 'Reply',
'ms.comment.delete': 'Delete',
'ms.comment.deleteConfirm': 'Are you sure you want to delete this comment?',
'ms.comment.deleteContent': 'After deletion, the comment cannot be replied to. Please proceed with caution.',
'ms.comment.enterPlaceHolderTip': 'Please enter a comment and press Enter to finish.',
};

View File

@ -1,8 +0,0 @@
export default {
'comment.expendComment': 'Expand comment',
'comment.collapseComment': 'Collapse comment',
'comment.reply': 'Reply',
'comment.delete': 'Delete',
'comment.edit': 'Edit',
'comment.enterPlaceHolderTip': 'Please enter a comment and press enter to end',
};

View File

@ -0,0 +1,10 @@
export default {
'ms.comment.expendComment': '展开评论',
'ms.comment.collapseComment': '收起评论',
'ms.comment.edit': '编辑',
'ms.comment.reply': '回复',
'ms.comment.delete': '删除',
'ms.comment.deleteConfirm': '确认删除该评论吗?',
'ms.comment.deleteContent': '删除后,评论无法回复,请谨慎操作',
'ms.comment.enterPlaceHolderTip': '请输入评论,回车结束',
};

View File

@ -1,10 +0,0 @@
export default {
'comment.expendComment': '展开评论',
'comment.collapseComment': '收起评论',
'comment.edit': '编辑',
'comment.reply': '回复',
'comment.delete': '删除',
'comment.deleteConfirm': '确认删除该评论吗?',
'comment.deleteContent': '删除后,评论无法回复,请谨慎操作',
'comment.enterPlaceHolderTip': '请输入评论,回车结束',
};

View File

@ -14,7 +14,7 @@ export interface CommentItem {
commentUserInfo: CommentUserInfo; // 评论人用户信息
replyUser?: string; // 回复人
notifier?: string; // 通知人
children?: CommentItem[];
childComments?: CommentItem[];
}
// 仅评论: COMMENT; 评论并@: AT; 回复评论/回复并@: REPLAY;)
@ -24,9 +24,11 @@ export interface WriteCommentProps {
id?: string; // 评论id
parentId?: string; // 父级评论id
event: commentEvent; // 评论事件
bugId: string; // bug id
bugId?: string; // bug id
caseId?: string; // 用例id
}
export interface CommentParams extends WriteCommentProps {
content: string;
replyUser?: string; // 回复人
notifiers?: string; // 通知人
}

View File

@ -52,4 +52,14 @@ export interface BugBatchUpdateFiledForm {
append: boolean;
inputValue: string;
}
export interface CreateOrUpdateComment {
id?: string;
bugId: string;
notifier: string;
replyUser: string;
parentId: string;
content: string;
event: string; // 任务事件(仅评论: COMMENT; 评论并@: AT; 回复评论/回复并@: REPLAY;)
}
export default {};

View File

@ -78,15 +78,25 @@
<template #first>
<div class="leftWrapper h-full">
<div class="header h-[50px]">
<a-tabs v-model:active-key="activeTab">
<a-tabs v-model:active-key="activeTab" lazy-load>
<a-tab-pane key="detail">
<template #title>
{{ t('bugManagement.detail.detail') }}
</template>
<BugDetailTab :detail-info="detailInfo" />
</a-tab-pane>
<a-tab-pane key="case">
<template #title>
{{ t('bugManagement.detail.case') }}
<a-badge class="relative top-1 ml-1" :count="1000" :max-count="99" />
</template>
<BugCaseTab :detail-info="detailInfo" />
</a-tab-pane>
<a-tab-pane key="comment">
<MsComment />
<template #title>
{{ t('bugManagement.detail.comment') }}
</template>
<CommentTab ref="commentRef" bug-id="1070838426116099" />
</a-tab-pane>
</a-tabs>
</div>
@ -126,6 +136,13 @@
</template>
</MsSplitBox>
</div>
<CommentInput
v-if="activeTab === 'comment'"
:content="commentContent"
is-show-avatar
is-use-bottom
@publish="publishHandler"
/>
</template>
</MsDetailDrawer>
</template>
@ -143,19 +160,20 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type';
import MsComment from '@/components/business/ms-comment';
import { CommentInput } from '@/components/business/ms-comment';
import { CommentParams } from '@/components/business/ms-comment/types';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import { MsUserSelector } from '@/components/business/ms-user-selector';
import BugCaseTab from './bugCaseTab.vue';
import BugDetailTab from './bugDetailTab.vue';
import CommentTab from './commentTab.vue';
import { deleteSingleBug, followBug, getBugDetail } from '@/api/modules/bug-management/index';
import { createOrUpdateComment, deleteSingleBug, followBug, getBugDetail } from '@/api/modules/bug-management/index';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import useUserStore from '@/store/modules/user';
import { characterLimit, findNodeByKey } from '@/utils';
import { characterLimit } from '@/utils';
import type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase';
import { RouteEnum } from '@/enums/routeEnum';
@ -165,7 +183,6 @@
const wrapperRef = ref();
const { isFullscreen, toggle } = useFullscreen(wrapperRef);
const featureCaseStore = useFeatureCaseStore();
const userStore = useUserStore();
const { t } = useI18n();
const { openModal } = useModal();
@ -180,8 +197,9 @@
const emit = defineEmits(['update:visible']);
const userId = computed(() => userStore.userInfo.id);
const appStore = useAppStore();
const commentContent = ref('');
const commentRef = ref();
const currentProjectId = computed(() => appStore.currentProjectId);
@ -200,13 +218,9 @@
function loadedBug(detail: CaseManagementTable) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
detailInfo.value.id = '1070838426116099';
}
const moduleName = computed(() => {
return findNodeByKey<Record<string, any>>(featureCaseStore.caseTree, detailInfo.value?.moduleId as string, 'id')
?.name;
});
const editLoading = ref<boolean>(false);
function updateSuccess() {
@ -331,6 +345,32 @@
}) as FormItem[];
}
async function publishHandler(currentContent: string) {
const regex = /data-id="([^"]*)"/g;
const matchesNotifier = currentContent.match(regex);
let notifiers = '';
if (matchesNotifier?.length) {
notifiers = matchesNotifier.map((match) => match.replace('data-id="', '').replace('"', '')).join(';');
}
try {
const params = {
// TODO
bugId: detailInfo.value.id || '1070838426116099',
notifier: notifiers,
replyUser: '',
parentId: '',
content: currentContent,
event: notifiers ? 'AT' : 'COMMENT', // (: COMMENT; @: AT; /@: REPLAY;)
};
await createOrUpdateComment(params as CommentParams);
Message.success(t('common.publishSuccessfully'));
commentRef.value?.initData(detailInfo.value.id || '1070838426116099');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watch(
() => customFields.value,
() => {

View File

@ -0,0 +1,69 @@
<template>
<MsComment :comment-list="commentList" @delete="handleDelete" @update-or-add="handleUpdate" />
</template>
<script lang="ts" setup>
import MsComment from '@/components/business/ms-comment';
import { CommentItem, CommentParams } from '@/components/business/ms-comment/types';
import { createOrUpdateComment, deleteComment, getCommentList } from '@/api/modules/bug-management/index';
import { useI18n } from '@/hooks/useI18n';
import message from '@arco-design/web-vue/es/message';
const props = defineProps<{
bugId: string;
}>();
const { t } = useI18n();
const commentList = ref<CommentItem[]>([]);
const initData = async (bugId: string) => {
try {
commentList.value = (await getCommentList(bugId)) || [];
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
const handleDelete = async (bugid: string) => {
try {
await deleteComment(bugid);
message.success(t('common.deleteSuccess'));
initData(bugid);
} catch (error) {
message.error(t('common.deleteFail'));
// eslint-disable-next-line no-console
console.error(error);
}
};
const handleUpdate = async (item: CommentParams, cb: (result: boolean) => void) => {
try {
await createOrUpdateComment(item);
if (item.bugId) {
initData(item.bugId);
}
cb(true);
} catch (error) {
cb(false);
// eslint-disable-next-line no-console
console.error(error);
}
};
onMounted(() => {
if (props.bugId) {
initData(props.bugId);
}
});
defineExpose({
initData,
});
</script>
<style lang="less" scoped>
/* Your component styles here */
</style>

View File

@ -11,6 +11,7 @@
>
<template #headerRight>
<a-select
v-if="!isEdit"
v-model="form.templateId"
class="w-[240px]"
:options="templateOption"

View File

@ -64,6 +64,9 @@ export default {
basicInfo: '基本信息',
handleUser: '处理人',
tag: '标签',
detail: '详情',
case: '用例',
comment: '评论',
},
batchUpdate: {
attribute: '选择属性',