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 MSR from '@/api/http/index';
import * as bugURL from '@/api/requrls/bug-management'; 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 { AssociatedList } from '@/models/caseManagement/featureCase';
import { CommonList, TableQueryParams, TemplateOption } from '@/models/common'; 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}` }); 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 getBugDetailUrl = '/bug/detail/';
export const getFollowBugUrl = '/bug/follow/'; export const getFollowBugUrl = '/bug/follow/';
export const getUnFollowBugUrl = '/bug/unfollow/'; 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> <template>
<div class="flex flex-col"> <div class="flex flex-row gap-[8px]">
<MsAvatar avatar="default" /> <MsAvatar avatar="word" />
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-[var(--color-text-1)]">{{ props.element.createUser }}</div> <div class="text-[var(--color-text-1)]">{{ props.element.createUser }}</div>
<div v-dompurify-html="props.element.content" class="mt-[4px]"></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') dayjs(props.element.updateTime).format('YYYY-MM-DD HH:mm:ss')
}}</div> }}</div>
<div class="ml-[24px] flex flex-row gap-[16px]"> <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" /> <MsIconfont type="icon-icon_comment_outlined" />
<span>{{ !expendComment ? t('comment.expendComment') : t('comment.collapseComment') }}</span> <span>{{ !expendComment ? t('ms.comment.expendComment') : t('ms.comment.collapseComment') }}</span>
<span class="text-[var(--color-text-4)]">({{ element.children?.length }})</span> <span class="text-[var(--color-text-4)]">({{ element.childComments?.length }})</span>
</div> </div>
<div class="comment-btn" @click="replyClick"> <div class="comment-btn" @click="replyClick">
<MsIconfont type="icon-icon_reply" /> <MsIconfont type="icon-icon_reply" />
<span>{{ t('comment.reply') }}</span> <span>{{ t('ms.comment.reply') }}</span>
</div> </div>
<div v-if="hasEditAuth" class="comment-btn" @click="editClick"> <div v-if="hasEditAuth" class="comment-btn" @click="editClick">
<MsIconfont type="icon-icon_edit_outlined" /> <MsIconfont type="icon-icon_edit_outlined" />
<span>{{ t('comment.edit') }}</span> <span>{{ t('ms.comment.edit') }}</span>
</div> </div>
<div v-if="hasEditAuth" class="comment-btn" @click="deleteClick"> <div v-if="hasEditAuth" class="comment-btn" @click="deleteClick">
<MsIconfont type="icon-icon_delete-trash_outlined" /> <MsIconfont type="icon-icon_delete-trash_outlined" />
<span>{{ t('comment.delete') }}</span> <span>{{ t('ms.comment.delete') }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -40,18 +44,21 @@
import MsIconfont from '@/components/pure/ms-icon-font/index.vue'; import MsIconfont from '@/components/pure/ms-icon-font/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user/index';
import { CommentItem } from './types'; import { CommentItem } from './types';
const userStore = useUserStore();
const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
element: CommentItem; // element: CommentItem; //
mode: 'parent' | 'child'; // mode: 'parent' | 'child'; //
currentUserId: string; // id
}>(); }>();
// //
const hasEditAuth = computed(() => { const hasEditAuth = computed(() => {
return props.element.commentUserInfo.id === props.currentUserId; return props.element.commentUserInfo.id === userStore.id;
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@ -78,8 +85,6 @@
const deleteClick = () => { const deleteClick = () => {
emit('delete'); emit('delete');
}; };
const { t } = useI18n();
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,3 +1,5 @@
// eslint-disable-next-line no-shadow
import Item from './comment-item.vue'; import Item from './comment-item.vue';
import CommentInput from './input.vue'; import CommentInput from './input.vue';
@ -10,11 +12,6 @@ import message from '@arco-design/web-vue/es/message';
export default defineComponent({ export default defineComponent({
name: 'MsComment', name: 'MsComment',
props: { props: {
currentUserId: {
type: String,
default: '',
},
commentList: { commentList: {
type: Array as PropType<CommentItem[]>, type: Array as PropType<CommentItem[]>,
default: () => [], default: () => [],
@ -22,12 +19,11 @@ export default defineComponent({
}, },
emits: { emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
updateOrAdd: (value: CommentParams) => true, // 更新或者新增评论 updateOrAdd: (value: CommentParams, cb: (result: boolean) => void) => true, // 更新或者新增评论
delete: (value: string, cb: (result: boolean) => void) => true, // 删除评论 delete: (value: string) => true, // 删除评论
}, },
setup(props, { emit }) { setup(props, { emit }) {
const { currentUserId } = toRefs(props); const { commentList } = toRefs(props);
const commentList = ref<CommentItem[]>([]);
const currentItem = reactive<{ id: string; parentId: string }>({ id: '', parentId: '' }); const currentItem = reactive<{ id: string; parentId: string }>({ id: '', parentId: '' });
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
@ -38,27 +34,27 @@ export default defineComponent({
content, content,
event: 'REPLAY', 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) => { const handleDelete = (item: CommentItem) => {
openModal({ openModal({
type: 'error', type: 'error',
title: t('comment.deleteConfirm'), title: t('ms.comment.deleteConfirm'),
content: t('comment.deleteContent'), content: t('ms.comment.deleteContent'),
okText: t('common.confirmClose'), okText: t('common.confirmDelete'),
cancelText: t('common.cancel'), cancelText: t('common.cancel'),
okButtonProps: { okButtonProps: {
status: 'danger', status: 'danger',
}, },
onBeforeOk: async () => { onBeforeOk: async () => {
emit('delete', item.id, (result: boolean) => { emit('delete', item.id);
if (result) {
message.success(t('common.deleteSuccess'));
} else {
message.error(t('common.deleteFail'));
}
});
}, },
hideCancel: false, hideCancel: false,
}); });
@ -81,7 +77,7 @@ export default defineComponent({
} }
return list.map((item) => { return list.map((item) => {
return ( return (
<div class="flex flex-col gap-[24px]"> <div class="flex flex-col">
<Item <Item
onReply={() => { onReply={() => {
currentItem.id = item.id; currentItem.id = item.id;
@ -93,7 +89,6 @@ export default defineComponent({
}} }}
onDelete={() => handleDelete(item)} onDelete={() => handleDelete(item)}
mode={'child'} mode={'child'}
currentUserId={currentUserId.value}
element={item} element={item}
/> />
{item.id === currentItem.id && renderInput(item)} {item.id === currentItem.id && renderInput(item)}
@ -105,13 +100,15 @@ export default defineComponent({
const renderParentList = (list: CommentItem[]) => { const renderParentList = (list: CommentItem[]) => {
return list.map((item) => { return list.map((item) => {
return ( return (
<Item mode={'parent'} onDelete={() => handleDelete(item)} currentUserId={currentUserId.value} element={item}> <Item mode={'parent'} onDelete={() => handleDelete(item)} element={item}>
<div class="rounded border border-[var(--color-text-7)] p-[16px]">{renderChildrenList(item.children)}</div> <div class="rounded border border-[var(--color-text-7)] p-[16px]">
{renderChildrenList(item.childComments)}
</div>
</Item> </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 type CommentInstance = InstanceType<typeof _Comment>;
export { default as CommentInput } from './input.vue';
export default MsComment; 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; // 评论人用户信息 commentUserInfo: CommentUserInfo; // 评论人用户信息
replyUser?: string; // 回复人 replyUser?: string; // 回复人
notifier?: string; // 通知人 notifier?: string; // 通知人
children?: CommentItem[]; childComments?: CommentItem[];
} }
// 仅评论: COMMENT; 评论并@: AT; 回复评论/回复并@: REPLAY;) // 仅评论: COMMENT; 评论并@: AT; 回复评论/回复并@: REPLAY;)
@ -24,9 +24,11 @@ export interface WriteCommentProps {
id?: string; // 评论id id?: string; // 评论id
parentId?: string; // 父级评论id parentId?: string; // 父级评论id
event: commentEvent; // 评论事件 event: commentEvent; // 评论事件
bugId: string; // bug id bugId?: string; // bug id
caseId?: string; // 用例id
} }
export interface CommentParams extends WriteCommentProps { export interface CommentParams extends WriteCommentProps {
content: string; content: string;
replyUser?: string; // 回复人 replyUser?: string; // 回复人
notifiers?: string; // 通知人
} }

View File

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

View File

@ -78,15 +78,25 @@
<template #first> <template #first>
<div class="leftWrapper h-full"> <div class="leftWrapper h-full">
<div class="header h-[50px]"> <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"> <a-tab-pane key="detail">
<template #title>
{{ t('bugManagement.detail.detail') }}
</template>
<BugDetailTab :detail-info="detailInfo" /> <BugDetailTab :detail-info="detailInfo" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="case"> <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" /> <BugCaseTab :detail-info="detailInfo" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="comment"> <a-tab-pane key="comment">
<MsComment /> <template #title>
{{ t('bugManagement.detail.comment') }}
</template>
<CommentTab ref="commentRef" bug-id="1070838426116099" />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
@ -126,6 +136,13 @@
</template> </template>
</MsSplitBox> </MsSplitBox>
</div> </div>
<CommentInput
v-if="activeTab === 'comment'"
:content="commentContent"
is-show-avatar
is-use-bottom
@publish="publishHandler"
/>
</template> </template>
</MsDetailDrawer> </MsDetailDrawer>
</template> </template>
@ -143,19 +160,20 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type'; 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 MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import { MsUserSelector } from '@/components/business/ms-user-selector'; import { MsUserSelector } from '@/components/business/ms-user-selector';
import BugCaseTab from './bugCaseTab.vue'; import BugCaseTab from './bugCaseTab.vue';
import BugDetailTab from './bugDetailTab.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 { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase'; import useFeatureCaseStore from '@/store/modules/case/featureCase';
import useUserStore from '@/store/modules/user'; import { characterLimit } from '@/utils';
import { characterLimit, findNodeByKey } from '@/utils';
import type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase'; import type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase';
import { RouteEnum } from '@/enums/routeEnum'; import { RouteEnum } from '@/enums/routeEnum';
@ -165,7 +183,6 @@
const wrapperRef = ref(); const wrapperRef = ref();
const { isFullscreen, toggle } = useFullscreen(wrapperRef); const { isFullscreen, toggle } = useFullscreen(wrapperRef);
const featureCaseStore = useFeatureCaseStore(); const featureCaseStore = useFeatureCaseStore();
const userStore = useUserStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
@ -180,8 +197,9 @@
const emit = defineEmits(['update:visible']); const emit = defineEmits(['update:visible']);
const userId = computed(() => userStore.userInfo.id);
const appStore = useAppStore(); const appStore = useAppStore();
const commentContent = ref('');
const commentRef = ref();
const currentProjectId = computed(() => appStore.currentProjectId); const currentProjectId = computed(() => appStore.currentProjectId);
@ -200,13 +218,9 @@
function loadedBug(detail: CaseManagementTable) { function loadedBug(detail: CaseManagementTable) {
detailInfo.value = { ...detail }; detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields; 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); const editLoading = ref<boolean>(false);
function updateSuccess() { function updateSuccess() {
@ -331,6 +345,32 @@
}) as FormItem[]; }) 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( watch(
() => customFields.value, () => 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> <template #headerRight>
<a-select <a-select
v-if="!isEdit"
v-model="form.templateId" v-model="form.templateId"
class="w-[240px]" class="w-[240px]"
:options="templateOption" :options="templateOption"

View File

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