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

View File

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

View File

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

View File

@ -27,7 +27,8 @@ export type AuthScopeType =
| 'CASE_MANAGEMENT'
| 'API_TEST'
| 'UI_TEST'
| 'LOAD_TEST';
| 'LOAD_TEST'
| 'PERSONAL';
export interface UserGroupItem {
// 组ID
@ -121,7 +122,7 @@ export interface AuthTableItem {
isApiTest?: boolean;
isUiTest?: boolean;
isLoadTest?: boolean;
isPersonal?: boolean;
indeterminate?: boolean;
}
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" />
</a-form-item>
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</div>
<MsUpload
v-model:file-list="fileList"
:auto-upload="false"
multiple
draggable
accept="unknown"
is-limit
size-unit="MB"
:max-size="500"
>
<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>
</MsUpload>
</a-dropdown>
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
<FileList
:show-tab="false"
@ -189,6 +196,8 @@
return Promise.resolve(fileItem);
};
const handleLineFile = () => {};
onBeforeMount(() => {
getTemplateOptions();
});

View File

@ -10,6 +10,9 @@
</template>
</MsAdvanceFilter>
<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 }">
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="jumpToTestPlan(record)">{{
record.memberCount
@ -67,6 +70,14 @@
</div>
</a-modal>
<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>
<script lang="ts" setup>
@ -81,6 +92,7 @@
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 BugDetailDrawer from './components/bug-detail-drawer.vue';
import { getBugList, getExportConfig } from '@/api/modules/bug-management';
import { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup';
@ -101,6 +113,10 @@
const syncVisible = ref(false);
const exportVisible = ref(false);
const exportOptionData = ref<MsExportDrawerMap>({});
const detailVisible = ref(false);
const activeDetailId = ref<string>('');
const activeCaseIndex = ref<number>(0);
const syncObject = reactive({
time: '',
operator: '',
@ -259,6 +275,12 @@
syncVisible.value = true;
};
const handleShowDetail = (id: string, rowIndex: number) => {
detailVisible.value = true;
activeDetailId.value = id;
activeCaseIndex.value = rowIndex;
};
const handleCopy = (record: BugListItem) => {
// eslint-disable-next-line no-console
console.log('create', record);

View File

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

View File

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