feat: 缺陷管理评论组件

This commit is contained in:
RubyLiu 2023-12-05 18:31:48 +08:00 committed by rubylliu
parent 3a42e95796
commit 8b54ddab04
17 changed files with 985 additions and 33 deletions

View File

@ -60,6 +60,7 @@
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-dompurify-html": "^4.1.4",
"vue-draggable-plus": "^0.2.7", "vue-draggable-plus": "^0.2.7",
"vue-echarts": "^6.6.1", "vue-echarts": "^6.6.1",
"vue-i18n": "^9.3.0", "vue-i18n": "^9.3.0",

View File

@ -0,0 +1,107 @@
<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>
<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>
<div class="mt-[16px] flex flex-row items-center">
<div class="text-[var(--color-text-4)]">{{
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">
<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>
</div>
<div class="comment-btn" @click="replyClick">
<MsIconfont type="icon-icon_reply" />
<span>{{ t('comment.reply') }}</span>
</div>
<div class="comment-btn" @click="editClick">
<MsIconfont type="icon-icon_edit_outlined" />
<span>{{ t('comment.edit') }}</span>
</div>
<div class="comment-btn" @click="deleteClick">
<MsIconfont type="icon-icon_delete-trash_outlined" />
<span>{{ t('comment.delete') }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import dayjs from 'dayjs';
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';
const props = defineProps<{
element: CommentItem;
}>();
const emit = defineEmits<{
(event: 'reply'): void;
(event: 'edit'): void;
(event: 'delete'): void;
}>();
const expendComment = ref(false);
const isEdit = ref(false);
const expendChange = () => {
expendComment.value = !expendComment.value;
};
const replyClick = () => {
emit('reply');
};
const editClick = () => {
isEdit.value = true;
emit('edit');
};
const deleteClick = () => {
emit('delete');
};
const { t } = useI18n();
</script>
<style lang="less" scoped>
.comment-btn {
display: flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
color: var(--color-text-1);
flex-direction: row;
gap: 4px;
cursor: pointer;
:hover {
background-color: var(--color-bg-2);
}
}
</style>

View File

@ -0,0 +1,7 @@
export default {
'comment.expendComment': 'Expand comment',
'comment.collapseComment': 'Collapse comment',
'comment.reply': 'Reply',
'comment.delete': 'Delete',
'comment.edit': 'Edit',
};

View File

@ -0,0 +1,7 @@
export default {
'comment.expendComment': '展开评论',
'comment.collapseComment': '收起评论',
'comment.edit': '编辑',
'comment.reply': '回复',
'comment.delete': '删除',
};

View File

@ -132,6 +132,7 @@
const uiTestSpan = ref(1); const uiTestSpan = ref(1);
const apiTestSpan = ref(1); const apiTestSpan = ref(1);
const loadTestSpan = ref(1); const loadTestSpan = ref(1);
const personalSpan = ref(1);
// //
const allChecked = ref(false); const allChecked = ref(false);
@ -203,6 +204,11 @@
rowspan: loadTestSpan.value, rowspan: loadTestSpan.value,
}; };
} }
if (record.isPersonal) {
return {
rowspan: personalSpan.value,
};
}
} }
}; };
@ -235,7 +241,7 @@
permissions: child?.permissions, permissions: child?.permissions,
indeterminate, indeterminate,
perChecked, perChecked,
ability: index === 0 ? t(`system.userGroup.${type}`) : undefined, ability: index === 0 ? item.name : undefined,
operationObject: t(child.name), operationObject: t(child.name),
isSystem: index === 0 && type === 'SYSTEM', isSystem: index === 0 && type === 'SYSTEM',
isOrganization: index === 0 && type === 'ORGANIZATION', isOrganization: index === 0 && type === 'ORGANIZATION',
@ -247,6 +253,7 @@
isUiTest: index === 0 && type === 'UI_TEST', isUiTest: index === 0 && type === 'UI_TEST',
isLoadTest: index === 0 && type === 'LOAD_TEST', isLoadTest: index === 0 && type === 'LOAD_TEST',
isApiTest: index === 0 && type === 'API_TEST', isApiTest: index === 0 && type === 'API_TEST',
isPersonal: index === 0 && type === 'PERSONAL',
}); });
}); });
return result; return result;
@ -255,26 +262,28 @@
const transformData = (data: UserGroupAuthSetting[]) => { const transformData = (data: UserGroupAuthSetting[]) => {
const result: AuthTableItem[] = []; const result: AuthTableItem[] = [];
data.forEach((item) => { data.forEach((item) => {
if (item.type === 'SYSTEM') { if (item.id === 'SYSTEM') {
systemSpan.value = item.children?.length || 0; systemSpan.value = item.children?.length || 0;
} else if (item.type === 'PROJECT') { } else if (item.id === 'PROJECT') {
projectSpan.value = item.children?.length || 0; projectSpan.value = item.children?.length || 0;
} else if (item.type === 'ORGANIZATION') { } else if (item.id === 'ORGANIZATION') {
organizationSpan.value = item.children?.length || 0; organizationSpan.value = item.children?.length || 0;
} else if (item.type === 'WORKSTATION') { } else if (item.id === 'WORKSTATION') {
workstationSpan.value = item.children?.length || 0; workstationSpan.value = item.children?.length || 0;
} else if (item.type === 'TEST_PLAN') { } else if (item.id === 'TEST_PLAN') {
testPlanSpan.value = item.children?.length || 0; testPlanSpan.value = item.children?.length || 0;
} else if (item.type === 'BUG_MANAGEMENT') { } else if (item.id === 'BUG_MANAGEMENT') {
bugManagementSpan.value = item.children?.length || 0; bugManagementSpan.value = item.children?.length || 0;
} else if (item.type === 'CASE_MANAGEMENT') { } else if (item.id === 'CASE_MANAGEMENT') {
caseManagementSpan.value = item.children?.length || 0; caseManagementSpan.value = item.children?.length || 0;
} else if (item.type === 'UI_TEST') { } else if (item.id === 'UI_TEST') {
uiTestSpan.value = item.children?.length || 0; uiTestSpan.value = item.children?.length || 0;
} else if (item.type === 'API_TEST') { } else if (item.id === 'API_TEST') {
apiTestSpan.value = item.children?.length || 0; apiTestSpan.value = item.children?.length || 0;
} else if (item.type === 'LOAD_TEST') { } else if (item.id === 'LOAD_TEST') {
loadTestSpan.value = item.children?.length || 0; loadTestSpan.value = item.children?.length || 0;
} else if (item.id === 'PERSONAL') {
personalSpan.value = item.children?.length || 0;
} }
result.push(...makeData(item, item.id)); result.push(...makeData(item, item.id));
}); });

View File

@ -30,6 +30,7 @@
:key="item.key" :key="item.key"
:value="item.key" :value="item.key"
class="mt-[8px] w-[95px] pl-[0px]" class="mt-[8px] w-[95px] pl-[0px]"
:disabled="item.key === 'name'"
> >
<a-tooltip :content="item.text"> <a-tooltip :content="item.text">
<span class="one-line-text">{{ item.text }}</span> <span class="one-line-text">{{ item.text }}</span>
@ -77,10 +78,10 @@
</a-checkbox-group> </a-checkbox-group>
</div> </div>
</div> </div>
<div class="w-[270px]"> <div>
<div class="optional-header"> <div class="optional-header min-w-[270px]">
<div class="font-medium">{{ t('system.orgTemplate.selectedField') }}</div> <div class="font-medium">{{ t('system.orgTemplate.selectedField') }}</div>
<MsButton @click="clearHandler">{{ t('system.orgTemplate.clear') }}</MsButton> <MsButton @click="handleReset">{{ t('system.orgTemplate.clear') }}</MsButton>
</div> </div>
<div class="p-[16px]"> <div class="p-[16px]">
<VueDraggable v-model="selectedList" ghost-class="ghost"> <VueDraggable v-model="selectedList" ghost-class="ghost">
@ -94,6 +95,7 @@
<div class="one-line-text ml-2 w-[178px]">{{ element.text }}</div> <div class="one-line-text ml-2 w-[178px]">{{ element.text }}</div>
</a-tooltip> </a-tooltip>
<icon-close <icon-close
v-if="element.key !== 'name'"
:style="{ 'font-size': '14px' }" :style="{ 'font-size': '14px' }"
class="cursor-pointer text-[var(--color-text-3)]" class="cursor-pointer text-[var(--color-text-3)]"
@click="removeSelectedField(element.key)" @click="removeSelectedField(element.key)"
@ -132,9 +134,14 @@
interface MsExportDrawerProps { interface MsExportDrawerProps {
visible: boolean; visible: boolean;
allData: MsExportDrawerMap; allData: MsExportDrawerMap;
// keys
defaultSelectedKeys?: string[];
} }
const props = defineProps<MsExportDrawerProps>(); const props = withDefaults(defineProps<MsExportDrawerProps>(), {
visible: false,
defaultSelectedKeys: () => ['name', 'id', 'title', 'status', 'handle_user', 'content'],
});
const selectedList = ref<MsExportDrawerOption[]>([]); // const selectedList = ref<MsExportDrawerOption[]>([]); //
const selectedIds = ref<string[]>([]); // id const selectedIds = ref<string[]>([]); // id
@ -199,13 +206,17 @@
return [...systemList.value, ...customList.value, ...otherList.value]; return [...systemList.value, ...customList.value, ...otherList.value];
}); });
const handleReset = () => {
selectedList.value = allList.value.filter((item) => props.defaultSelectedKeys.includes(item.key));
};
const handleDrawerConfirm = () => { const handleDrawerConfirm = () => {
emit('confirm', selectedList.value); emit('confirm', selectedList.value);
}; };
const handleDrawerCancel = () => { const handleDrawerCancel = () => {
visible.value = false; visible.value = false;
selectedList.value = []; handleReset();
}; };
const isCheckedAll = computed(() => { const isCheckedAll = computed(() => {
@ -216,10 +227,6 @@
return selectedList.value.length > 0 && selectedList.value.length < allList.value.length; return selectedList.value.length > 0 && selectedList.value.length < allList.value.length;
}); });
const clearHandler = () => {
selectedList.value = [];
};
const handleChangeAll = (value: boolean | (string | number | boolean)[]) => { const handleChangeAll = (value: boolean | (string | number | boolean)[]) => {
if (value) { if (value) {
selectedList.value = allList.value; selectedList.value = allList.value;
@ -239,6 +246,10 @@
watchEffect(() => { watchEffect(() => {
selectedIds.value = selectedList.value.map((item) => item.key); selectedIds.value = selectedList.value.map((item) => item.key);
}); });
watchEffect(() => {
handleReset();
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -16,6 +16,7 @@ import store from './store';
import ArcoVueIcon from '@arco-design/web-vue/es/icon'; import ArcoVueIcon from '@arco-design/web-vue/es/icon';
import '@/assets/style/global.less'; import '@/assets/style/global.less';
import localforage from 'localforage'; import localforage from 'localforage';
import VueDOMPurifyHTML from 'vue-dompurify-html';
async function bootstrap() { async function bootstrap() {
const app = createApp(App); const app = createApp(App);
@ -26,6 +27,7 @@ async function bootstrap() {
app.use(router); app.use(router);
app.use(ArcoVue); app.use(ArcoVue);
app.use(ArcoVueIcon); app.use(ArcoVueIcon);
app.use(VueDOMPurifyHTML);
app.component('SvgIcon', SvgIcon); app.component('SvgIcon', SvgIcon);
app.component('MsIcon', MsIcon); app.component('MsIcon', MsIcon);

View File

@ -27,7 +27,8 @@ export type AuthScopeType =
| 'CASE_MANAGEMENT' | 'CASE_MANAGEMENT'
| 'API_TEST' | 'API_TEST'
| 'UI_TEST' | 'UI_TEST'
| 'LOAD_TEST'; | 'LOAD_TEST'
| 'PERSONAL';
export interface UserGroupItem { export interface UserGroupItem {
// 组ID // 组ID
@ -121,7 +122,7 @@ export interface AuthTableItem {
isApiTest?: boolean; isApiTest?: boolean;
isUiTest?: boolean; isUiTest?: boolean;
isLoadTest?: boolean; isLoadTest?: boolean;
isPersonal?: boolean;
indeterminate?: boolean; indeterminate?: boolean;
} }
export interface SavePermissions { export interface SavePermissions {

View File

@ -0,0 +1,417 @@
<template>
<MsDetailDrawer
ref="detailDrawerRef"
v-model:visible="showDrawerVisible"
:width="1200"
:footer="false"
:title="t('caseManagement.featureCase.caseDetailTitle', { id: detailInfo?.id, name: detailInfo?.name })"
:detail-id="props.detailId"
:detail-index="props.detailIndex"
:get-detail-func="getCaseDetail"
:pagination="props.pagination"
:table-data="props.tableData"
:page-change="props.pageChange"
@loaded="loadedCase"
>
<template #titleRight="{ loading }">
<div class="rightButtons flex items-center">
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="editLoading"
@click="updateHandler('edit')"
>
<MsIcon type="icon-icon_edit_outlined" class="mr-1 font-[16px]" />
{{ t('common.edit') }}
</MsButton>
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="shareLoading"
@click="shareHandler"
>
<MsIcon type="icon-icon_share1" class="mr-1 font-[16px]" />
{{ t('caseManagement.featureCase.share') }}
</MsButton>
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="followLoading"
@click="followHandler"
>
<MsIcon
:type="detailInfo.followFlag ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
class="mr-1 font-[16px]"
:class="[detailInfo.followFlag ? 'text-[rgb(var(--warning-6))]' : '']"
/>
{{ t('caseManagement.featureCase.follow') }}
</MsButton>
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]">
<a-dropdown position="br" :hide-on-select="false">
<div>
<icon-more class="mr-1" />
<span> {{ t('caseManagement.featureCase.more') }}</span>
</div>
<template #content>
<a-doption class="error-6 text-[rgb(var(--danger-6))]" @click="deleteHandler()">
<MsIcon type="icon-icon_delete-trash_outlined" class="font-[16px] text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }}
</a-doption>
</template>
</a-dropdown>
</MsButton>
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]" @click="toggle">
<MsIcon :type="isFullscreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'" class="mr-1" size="16" />
{{ t('caseManagement.featureCase.fullScreen') }}
</MsButton>
</div>
</template>
<template #default>
<div ref="wrapperRef" class="h-full bg-white">
<MsSplitBox ref="wrapperRef" expand-direction="right" :max="0.7" :min="0.7" :size="900">
<template #left>
<div class="leftWrapper h-full">
<div class="header h-[50px]">
<a-tabs>
<a-tab-pane key="detail">
<BugDetailTab :detail-info="detailInfo" />
</a-tab-pane>
<a-tab-pane key="case">
<BugCaseTab :detail-info="detailInfo" />
</a-tab-pane>
<a-tab-pane key="comment">
<BugCommentTab :detail-info="detailInfo" />
</a-tab-pane>
<a-tab-pane key="history">
<BugHistoryTab :detail-info="detailInfo" />
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
<template #right>
<div class="rightWrapper p-[24px]">
<div class="mb-4 font-medium">{{ t('caseManagement.featureCase.basicInfo') }}</div>
<div class="baseItem">
<span class="label"> {{ t('caseManagement.featureCase.tableColumnModule') }}</span>
<span>{{ moduleName }}</span>
</div>
<!-- 自定义字段开始 -->
<MsFormCreate
v-if="formRules.length"
ref="formCreateRef"
class="w-full"
:option="options"
:form-rule="formRules"
:form-create-key="FormCreateKeyEnum.CASE_CUSTOM_ATTRS_DETAIL"
/>
<!-- 自定义字段结束 -->
<div class="baseItem">
<span class="label"> {{ t('caseManagement.featureCase.tableColumnCreateUser') }}</span>
<span>{{ detailInfo?.createUser }}</span>
</div>
<div class="baseItem">
<span class="label"> {{ t('caseManagement.featureCase.tableColumnCreateTime') }}</span>
<span>{{ dayjs(detailInfo?.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div>
</template>
</MsSplitBox>
</div>
</template>
</MsDetailDrawer>
<SettingDrawer v-model:visible="showSettingDrawer" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/form-create.vue';
import type { FormItem } from '@/components/pure/ms-form-create/types';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import BugDetailTab from './bugDetailTab.vue';
import { deleteCaseRequest, followerCaseRequest, getCaseDetail } from '@/api/modules/case-management/featureCase';
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 type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
const router = useRouter();
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
const wrapperRef = ref();
const { isFullscreen, toggle } = useFullscreen(wrapperRef);
const featureCaseStore = useFeatureCaseStore();
const userStore = useUserStore();
const { t } = useI18n();
const { openModal } = useModal();
const props = defineProps<{
visible: boolean;
detailId: string; // id
detailIndex: number; //
tableData: any[]; //
pagination: MsPaginationI; //
pageChange: (page: number) => Promise<void>; //
}>();
const emit = defineEmits(['update:visible']);
const userId = computed(() => userStore.userInfo.id);
const appStore = useAppStore();
const currentProjectId = computed(() => appStore.currentProjectId);
const showDrawerVisible = ref<boolean>(false);
const showSettingDrawer = ref<boolean>(false);
function showMenuSetting() {
showSettingDrawer.value = true;
}
const tabSettingList = computed(() => {
return featureCaseStore.tabSettingList;
});
const tabSetting = ref<TabItemType[]>([...tabSettingList.value]);
const activeTab = ref<string | number>('detail');
function changeTabs(key: string | number) {
activeTab.value = key;
switch (activeTab.value) {
case 'setting':
showMenuSetting();
break;
default:
break;
}
}
const detailInfo = ref<Record<string, any>>({});
const customFields = ref<CustomAttributes[]>([]);
function loadedCase(detail: CaseManagementTable) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
}
const moduleName = computed(() => {
return findNodeByKey<Record<string, any>>(featureCaseStore.caseTree, detailInfo.value?.moduleId as string, 'id')
?.name;
});
const editLoading = ref<boolean>(false);
function updateSuccess() {
detailDrawerRef.value?.initDetail();
}
function updateHandler(type: string) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
query: {
id: detailInfo.value.id,
},
params: {
mode: type,
},
});
}
const shareLoading = ref<boolean>(false);
function shareHandler() {}
const followLoading = ref<boolean>(false);
//
async function followHandler() {
followLoading.value = true;
try {
await followerCaseRequest({ userId: userId.value as string, functionalCaseId: detailInfo.value.id });
updateSuccess();
Message.success(
detailInfo.value.followFlag
? t('caseManagement.featureCase.cancelFollowSuccess')
: t('caseManagement.featureCase.followSuccess')
);
} catch (error) {
console.log(error);
} finally {
followLoading.value = false;
}
}
//
function deleteHandler() {
const { id, name } = detailInfo.value;
openModal({
type: 'error',
title: t('caseManagement.featureCase.deleteCaseTitle', { name: characterLimit(name) }),
content: t('caseManagement.featureCase.beforeDeleteCase'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
const params = {
id,
deleteAll: false,
projectId: currentProjectId.value,
};
await deleteCaseRequest(params);
Message.success(t('common.deleteSuccess'));
updateSuccess();
detailDrawerRef.value?.openPrevDetail();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
const formRules = ref<FormItem[]>([]);
const isDisabled = ref<boolean>(false);
//
const options = {
resetBtn: false, //
submitBtn: false,
on: false, // on
form: {
layout: 'horizontal',
labelAlign: 'left',
labelColProps: {
span: 9,
},
wrapperColProps: {
span: 15,
},
},
//
row: {
gutter: 0,
},
wrap: {
'asterisk-position': 'end',
'validate-trigger': ['change'],
'hide-asterisk': true,
},
};
//
function initForm() {
formRules.value = customFields.value.map((item: any) => {
return {
type: item.type,
name: item.fieldId,
label: item.fieldName,
value: JSON.parse(item.defaultValue),
required: item.required,
options: item.options || [],
props: {
modelValue: JSON.parse(item.defaultValue),
disabled: isDisabled.value,
options: item.options || [],
},
};
}) as FormItem[];
}
watch(
() => customFields.value,
() => {
initForm();
},
{ deep: true }
);
watch(
() => props.visible,
(val) => {
showDrawerVisible.value = val;
}
);
watch(
() => showDrawerVisible.value,
(val) => {
emit('update:visible', val);
}
);
watch(
() => tabSettingList.value,
() => {
tabSetting.value = featureCaseStore.getTab();
},
{ deep: true, immediate: true }
);
</script>
<style scoped lang="less">
.leftWrapper {
.header {
padding: 0 16px;
border-bottom: 1px solid var(--color-text-n8);
}
}
.rightWrapper {
.baseItem {
margin-bottom: 16px;
height: 32px;
line-height: 32px;
@apply flex;
.label {
width: 38%;
color: var(--color-text-3);
}
}
:deep(.arco-form-item-layout-horizontal) {
margin-bottom: 16px !important;
}
:deep(.arco-form-item-label-col > .arco-form-item-label) {
color: var(--color-text-3) !important;
}
}
.rightButtons {
:deep(.ms-button--secondary):hover,
:hover > .arco-icon {
color: rgb(var(--primary-5)) !important;
background: var(--color-bg-3);
.arco-icon:hover {
color: rgb(var(--primary-5)) !important;
}
}
}
.error-6 {
color: rgb(var(--danger-6));
&:hover {
color: rgb(var(--danger-6));
}
}
:deep(.active .arco-badge-number) {
background: rgb(var(--primary-5));
}
</style>

View File

@ -0,0 +1,186 @@
<template>
<MsDrawer
:width="680"
:visible="currentVisible"
unmount-on-close
:footer="false"
:title="t('system.organization.addMember')"
:mask="false"
@cancel="handleCancel"
>
<div>
<div class="flex flex-row justify-between">
<a-dropdown trigger="hover">
<template #content>
<a-doption @click="showRelatedDrawer('api')">{{ t('bugManagement.detail.apiCase') }}</a-doption>
<a-doption @click="showRelatedDrawer('scenario')">{{ t('bugManagement.detail.scenarioCase') }}</a-doption>
<a-doption @click="showRelatedDrawer('ui')">{{ t('bugManagement.detail.uiCase') }}</a-doption>
<a-doption @click="showRelatedDrawer('performance')">{{
t('bugManagement.detail.performanceCase')
}}</a-doption>
</template>
<a-button type="primary">{{ t('bugManagement.edit.linkCase') }}</a-button>
</a-dropdown>
<a-input-search
v-model:model-value="keyword"
allow-clear
:placeholder="t('bugManagement.detail.searchCase')"
class="w-[230px]"
@search="searchUser"
@press-enter="searchUser"
></a-input-search>
</div>
<ms-base-table class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<span>{{ record.name }}</span>
<span v-if="record.adminFlag" class="ml-[4px] text-[var(--color-text-4)]">{{
`(${t('common.admin')})`
}}</span>
</template>
<template #operation="{ record }">
<MsRemoveButton
:title="t('system.organization.removeName', { name: record.name })"
:sub-title-tip="t('system.organization.removeTip')"
@ok="handleRemove(record)"
/>
</template>
</ms-base-table>
</div>
<MsDrawer
:width="680"
:visible="relatedVisible"
unmount-on-close
:footer="false"
:mask="false"
@cancel="relatedVisible = false"
>
<template #title>
<div class="flex flex-row items-center gap-[4px]">
<div>{{ t('bugManagement.detail.relatedCase') }}</div>
<a-select>
<a-option></a-option>
<a-option></a-option>
<a-option></a-option>
</a-select>
</div>
</template>
</MsDrawer>
</MsDrawer>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { Message, TableData } from '@arco-design/web-vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
import { deleteProjectMemberByOrg, postProjectMemberByProjectId } from '@/api/modules/setting/organizationAndProject';
import { useI18n } from '@/hooks/useI18n';
export interface projectDrawerProps {
visible: boolean;
organizationId?: string;
projectId?: string;
}
const { t } = useI18n();
const props = defineProps<projectDrawerProps>();
const emit = defineEmits<{
(e: 'cancel'): void;
(e: 'requestFetchData'): void;
}>();
const relatedVisible = ref(false);
const relatedType = ref('api');
const showRelatedDrawer = (type: string) => {
relatedType.value = type;
};
const currentVisible = ref(props.visible);
const keyword = ref('');
const projectColumn: MsTableColumn = [
{
title: 'system.organization.userName',
slotName: 'name',
dataIndex: 'name',
showTooltip: true,
width: 200,
},
{
title: 'system.organization.email',
dataIndex: 'email',
showTooltip: true,
width: 200,
},
{
title: 'system.organization.phone',
dataIndex: 'phone',
},
{ title: 'system.organization.operation', slotName: 'operation' },
];
const { propsRes, propsEvent, loadList, setLoadListParams, setKeyword } = useTable(postProjectMemberByProjectId, {
heightUsed: 240,
columns: projectColumn,
scroll: { x: '100%' },
selectable: false,
noDisable: false,
pageSimple: true,
});
async function searchUser() {
setKeyword(keyword.value);
await loadList();
}
const handleCancel = () => {
keyword.value = '';
emit('cancel');
};
const fetchData = async () => {
await loadList();
};
const handleRemove = async (record: TableData) => {
try {
if (props.projectId) {
await deleteProjectMemberByOrg(props.projectId, record.id);
}
Message.success(t('common.removeSuccess'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
watch(
() => props.projectId,
() => {
setLoadListParams({ projectId: props.projectId });
fetchData();
}
);
watch(
() => props.visible,
(visible) => {
currentVisible.value = visible;
if (visible) {
fetchData();
}
}
);
</script>
<style lang="less" scoped>
:deep(.custom-height) {
height: 100vh !important;
border: 1px solid red;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="bug-comment">
<div class="header-icon">
<img :src="comment.user.avatar" alt="User avatar" />
</div>
<div class="content">
<h3>{{ comment.user.name }}</h3>
<MsRichText :text="comment.content" />
</div>
<div class="footer">
<span>{{ comment.date }}</span>
<button @click="toggleShowComment">{{ showComment ? '收起评论' : '展开评论' }}</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
const showComment = ref(false);
const toggleShowComment = () => {
showComment.value = !showComment.value;
};
</script>
<style scoped>
.bug-comment {
display: flex;
flex-direction: column;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
</style>

View File

@ -0,0 +1,124 @@
<template>
<div class="p-[16px]">
<div class="header">
<div class="header-title">{{ t('bugManagement.edit.content') }}</div>
<div class="header-action">
<a-button>
<template #icon> <MsIconfont type="icon-icon_edit_outlined" /> </template>
{{ t('bugManagement.edit.contentEdit') }}
</a-button>
</div>
</div>
<div class="header">
<div class="header-title">{{ t('bugManagement.edit.content') }}</div>
</div>
<div class="mt-[8]" :class="{ 'max-h-[260px]': contentEditAble }">
<MsRichText
v-if="form.content"
v-model:model-value="form.content"
:disabled="!contentEditAble"
:placeholder="t('bugManagement.edit.contentPlaceholder')"
/>
<div v-else>-</div>
</div>
</div>
<a-dropdown trigger="hover">
<template #content>
<MsUpload
v-model:file-list="fileList"
:auto-upload="false"
multiple
draggable
accept="unknown"
is-limit
size-unit="MB"
:max-size="500"
>
<a-doption>{{ t('bugManagement.edit.localUpload') }}</a-doption>
</MsUpload>
<a-doption @click="handleLineFile">{{ t('bugManagement.edit.linkFile') }}</a-doption>
</template>
<a-button type="outline">
<template #icon>
<icon-plus />
</template>
{{ t('bugManagement.edit.uploadFile') }}
</a-button>
</a-dropdown>
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
<FileList
:show-tab="false"
:file-list="fileList"
:upload-func="uploadFile"
@delete-file="deleteFile"
@reupload="reupload"
@handle-preview="handlePreview"
>
</FileList>
</template>
<script setup lang="ts">
import { FileItem } from '@arco-design/web-vue';
import MsIconfont from '@/components/pure/ms-icon-font/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import FileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const contentEditAble = ref(false);
const fileList = ref<FileItem[]>([]);
const form = ref({
content: '',
fileList: [],
});
const uploadFile = (file: File) => {
const fileItem: FileItem = {
uid: `${Date.now()}`,
name: file.name,
status: 'init',
file,
};
fileList.value.push(fileItem);
return Promise.resolve(fileItem);
};
const handlePreview = (item: FileItem) => {
const { url } = item;
window.open(url);
};
const deleteFile = (item: FileItem) => {
fileList.value = fileList.value.filter((e) => e.uid !== item.uid);
};
const reupload = (item: FileItem) => {
fileList.value = fileList.value.map((e) => {
if (e.uid === item.uid) {
return {
...e,
status: 'init',
};
}
return e;
});
};
const handleLineFile = () => {};
</script>
<style lang="less" scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
&-title {
color: var(--color-text-1);
}
&-action {
color: rgb(var(--primary-7));
}
}
</style>

View File

@ -24,23 +24,30 @@
<MsRichText v-model="form.content" /> <MsRichText v-model="form.content" />
</a-form-item> </a-form-item>
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</div> <div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</div>
<MsUpload
v-model:file-list="fileList" <a-dropdown trigger="hover">
:auto-upload="false" <template #content>
multiple <MsUpload
draggable v-model:file-list="fileList"
accept="unknown" :auto-upload="false"
is-limit multiple
size-unit="MB" draggable
:max-size="500" accept="unknown"
> is-limit
size-unit="MB"
:max-size="500"
>
<a-doption>{{ t('bugManagement.edit.localUpload') }}</a-doption>
</MsUpload>
<a-doption @click="handleLineFile">{{ t('bugManagement.edit.linkFile') }}</a-doption>
</template>
<a-button type="outline"> <a-button type="outline">
<template #icon> <template #icon>
<icon-plus /> <icon-plus />
</template> </template>
{{ t('bugManagement.edit.uploadFile') }} {{ t('bugManagement.edit.uploadFile') }}
</a-button> </a-button>
</MsUpload> </a-dropdown>
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div> <div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
<FileList <FileList
:show-tab="false" :show-tab="false"
@ -189,6 +196,8 @@
return Promise.resolve(fileItem); return Promise.resolve(fileItem);
}; };
const handleLineFile = () => {};
onBeforeMount(() => { onBeforeMount(() => {
getTemplateOptions(); getTemplateOptions();
}); });

View File

@ -10,6 +10,9 @@
</template> </template>
</MsAdvanceFilter> </MsAdvanceFilter>
<MsBaseTable v-bind="propsRes" v-on="propsEvent"> <MsBaseTable v-bind="propsRes" v-on="propsEvent">
<template #name="{ record, rowIndex }">
<a-button type="text" class="px-0" @click="handleShowDetail(record.id, rowIndex)">{{ record.name }}</a-button>
</template>
<template #numberOfCase="{ record }"> <template #numberOfCase="{ record }">
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="jumpToTestPlan(record)">{{ <span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="jumpToTestPlan(record)">{{
record.memberCount record.memberCount
@ -67,6 +70,14 @@
</div> </div>
</a-modal> </a-modal>
<MsExportDrawer v-model:visible="exportVisible" :all-data="exportOptionData" /> <MsExportDrawer v-model:visible="exportVisible" :all-data="exportOptionData" />
<BugDetailDrawer
v-model:visible="detailVisible"
:detail-id="activeDetailId"
:detail-index="activeCaseIndex"
:table-data="propsRes.data"
:page-change="propsEvent.pageChange"
:pagination="propsRes.msPagination!"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -81,6 +92,7 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type'; import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import BugDetailDrawer from './components/bug-detail-drawer.vue';
import { getBugList, getExportConfig } from '@/api/modules/bug-management'; import { getBugList, getExportConfig } from '@/api/modules/bug-management';
import { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup'; import { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup';
@ -101,6 +113,10 @@
const syncVisible = ref(false); const syncVisible = ref(false);
const exportVisible = ref(false); const exportVisible = ref(false);
const exportOptionData = ref<MsExportDrawerMap>({}); const exportOptionData = ref<MsExportDrawerMap>({});
const detailVisible = ref(false);
const activeDetailId = ref<string>('');
const activeCaseIndex = ref<number>(0);
const syncObject = reactive({ const syncObject = reactive({
time: '', time: '',
operator: '', operator: '',
@ -259,6 +275,12 @@
syncVisible.value = true; syncVisible.value = true;
}; };
const handleShowDetail = (id: string, rowIndex: number) => {
detailVisible.value = true;
activeDetailId.value = id;
activeCaseIndex.value = rowIndex;
};
const handleCopy = (record: BugListItem) => { const handleCopy = (record: BugListItem) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('create', record); console.log('create', record);

View File

@ -37,6 +37,17 @@ export default {
statusIsRequired: '状态不能为空', statusIsRequired: '状态不能为空',
severityPlaceholder: '请选择严重程度', severityPlaceholder: '请选择严重程度',
uploadFile: '添加附件', uploadFile: '添加附件',
localUpload: '本地上传',
linkFile: '关联文件',
contentEdit: '内容编辑',
linkCase: '关联用例',
},
detail: {
apiCase: '接口用例',
scenarioCase: '场景用例',
uiCase: 'UI用例',
performanceCase: '性能用例',
searchCase: '通过名称搜索',
}, },
}, },
}; };

View File

@ -47,6 +47,7 @@ export default {
UI_TEST: 'UI test', UI_TEST: 'UI test',
API_TEST: 'API test', API_TEST: 'API test',
LOAD_TEST: 'Performance test', LOAD_TEST: 'Performance test',
PERSONAL: 'Personal',
isDeleteUserGroup: 'Delete or not: {name}?', isDeleteUserGroup: 'Delete or not: {name}?',
beforeDeleteUserGroup: beforeDeleteUserGroup:
'After deletion, the project data under the organization will be deleted together. Please operate with caution!', 'After deletion, the project data under the organization will be deleted together. Please operate with caution!',

View File

@ -47,6 +47,7 @@ export default {
UI_TEST: 'UI测试', UI_TEST: 'UI测试',
API_TEST: 'API测试', API_TEST: 'API测试',
LOAD_TEST: '性能测试', LOAD_TEST: '性能测试',
PERSONAL: '我的设置',
isDeleteUserGroup: '是否删除: {name}?', isDeleteUserGroup: '是否删除: {name}?',
beforeDeleteUserGroup: '删除后,该组织下的项目数据将一起删除,请谨慎操作!', beforeDeleteUserGroup: '删除后,该组织下的项目数据将一起删除,请谨慎操作!',
confirmDelete: '确认删除', confirmDelete: '确认删除',