feat(缺陷管理): 详情编辑
This commit is contained in:
parent
d6c1b9f55a
commit
01d8747468
|
@ -4,7 +4,7 @@ import MSR from '@/api/http/index';
|
|||
import * as bugURL from '@/api/requrls/bug-management';
|
||||
|
||||
import { BugEditFormObject, BugExportParams, BugListItem } from '@/models/bug-management';
|
||||
import { AssociatedList, OperationFile } from '@/models/caseManagement/featureCase';
|
||||
import { AssociatedList, CreateOrUpdateDemand, DemandItem, OperationFile } from '@/models/caseManagement/featureCase';
|
||||
import { CommonList, TableQueryParams, TemplateOption } from '@/models/common';
|
||||
|
||||
/**
|
||||
|
@ -181,3 +181,30 @@ export function deleteSingleByRecycle(id: string) {
|
|||
export function deleteBatchByRecycle(data: TableQueryParams) {
|
||||
return MSR.post({ url: bugURL.getBatchDeleteUrl, data });
|
||||
}
|
||||
|
||||
// ----------------------关联需求
|
||||
|
||||
// 已关联需求列表
|
||||
export function getAssociatedList(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<DemandItem[]>>({ url: bugURL.getDemandListUrl, data });
|
||||
}
|
||||
|
||||
// 缺陷管理-关联用例-未关联用例-列表分页
|
||||
export function getUnAssociatedList(data: TableQueryParams) {
|
||||
return MSR.post({ url: bugURL.getUnrelatedDemandListUrl, data });
|
||||
}
|
||||
|
||||
// 未关联用例-模块树
|
||||
export function getModuleTree(data: TableQueryParams) {
|
||||
return MSR.post({ url: `${bugURL.getUnrelatedModuleTreeUrl}`, data });
|
||||
}
|
||||
|
||||
// 批量关联需求
|
||||
export function batchAssociation(data: TableQueryParams) {
|
||||
return MSR.post({ url: bugURL.postAddDemandUrl, data });
|
||||
}
|
||||
|
||||
// 取消关联
|
||||
export function cancelAssociation(id: string) {
|
||||
return MSR.get({ url: `${bugURL.getCancelDemandUrl}/${id}` });
|
||||
}
|
||||
|
|
|
@ -51,3 +51,16 @@ export const getBatchRecoverUrl = '/bug/trash/batch-recover';
|
|||
export const getDeleteSingleUrl = '/bug/trash/delete/';
|
||||
// 批量删除
|
||||
export const getBatchDeleteUrl = '/bug/trash/batch-delete';
|
||||
|
||||
// 获取关联的需求列表
|
||||
export const getDemandListUrl = '/bug/case/page';
|
||||
// 批量添加关联
|
||||
export const postAddDemandUrl = '/bug/case/relate';
|
||||
// 单个取消关联
|
||||
export const getCancelDemandUrl = '/bug/case/un-relate/';
|
||||
// 未关联的用例列表
|
||||
export const getUnrelatedDemandListUrl = '/bug/case/un-relate/page';
|
||||
// 未关联的模块树
|
||||
export const getUnrelatedModuleTreeUrl = '/bug/case/un-relate/module/tree';
|
||||
// 未关联的模块树 数量
|
||||
export const getUnrelatedModuleTreeCountUrl = '/bug/case/un-relate/module/count';
|
||||
|
|
|
@ -88,20 +88,19 @@
|
|||
<template #title>
|
||||
{{ t('bugManagement.detail.detail') }}
|
||||
</template>
|
||||
<BugDetailTab />
|
||||
<BugDetailTab :allow-edit="true" :detail-info="detailInfo" @update-success="updateSuccess" />
|
||||
</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 />
|
||||
<BugCaseTab :bug-id="detailInfo.id" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="comment">
|
||||
<template #title>
|
||||
{{ t('bugManagement.detail.comment') }}
|
||||
</template>
|
||||
<CommentTab ref="commentRef" bug-id="detailInfo.id" />
|
||||
<CommentTab ref="commentRef" :bug-id="detailInfo.id" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
@ -119,6 +118,7 @@
|
|||
:form-rule="formRules"
|
||||
class="w-full"
|
||||
:option="options"
|
||||
@change="handleOK"
|
||||
/>
|
||||
<!-- 自定义字段结束 -->
|
||||
<div class="baseItem">
|
||||
|
@ -175,6 +175,7 @@
|
|||
followBug,
|
||||
getBugDetail,
|
||||
getTemplateById,
|
||||
updateBug,
|
||||
} from '@/api/modules/bug-management/index';
|
||||
import useFullScreen from '@/hooks/useFullScreen';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
@ -214,7 +215,7 @@
|
|||
|
||||
const activeTab = ref<string | number>('detail');
|
||||
|
||||
const detailInfo = ref<Record<string, any>>({}); // 存储当前详情信息,通过loadBug 获取
|
||||
const detailInfo = ref<Record<string, any>>({ match: [] }); // 存储当前详情信息,通过loadBug 获取
|
||||
const tags = ref([]);
|
||||
|
||||
// 处理表单格式
|
||||
|
@ -332,6 +333,25 @@
|
|||
});
|
||||
}
|
||||
|
||||
const handleOK = async () => {
|
||||
const values = await fApi.value.validate();
|
||||
if (values) {
|
||||
const params = {
|
||||
id: detailInfo.value.id,
|
||||
projectId: currentProjectId.value,
|
||||
...values,
|
||||
};
|
||||
try {
|
||||
await updateBug(params);
|
||||
Message.success(t('common.editSuccess'));
|
||||
detailDrawerRef.value?.initDetail();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 表单配置项
|
||||
const options = {
|
||||
resetBtn: false, // 不展示默认配置的重置和提交
|
||||
|
|
|
@ -1,148 +1,286 @@
|
|||
<template>
|
||||
<div class="p-[16px]">
|
||||
<div class="flex flex-row justify-between">
|
||||
<a-dropdown trigger="hover">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<a-dropdown @select="handleSelect">
|
||||
<a-button type="primary"> {{ t('caseManagement.featureCase.linkCase') }} </a-button>
|
||||
<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 v-for="item of caseTypeOptions" :key="item.value" :value="item.value">{{
|
||||
t(item.label)
|
||||
}}</a-doption>
|
||||
</template>
|
||||
<a-button type="primary">{{ t('bugManagement.edit.linkCase') }}</a-button>
|
||||
</a-dropdown>
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
|
||||
allow-clear
|
||||
:placeholder="t('bugManagement.detail.searchCase')"
|
||||
class="w-[230px]"
|
||||
@search="searchUser"
|
||||
@press-enter="searchUser"
|
||||
class="mx-[8px] w-[240px]"
|
||||
@search="searchCase"
|
||||
@press-enter="searchCase"
|
||||
></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>
|
||||
<ms-base-table v-bind="propsRes" v-on="propsEvent">
|
||||
<template #defectName="{ record }">
|
||||
<span class="one-line-text max-w[300px]"> {{ record.name }}</span
|
||||
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<MsRemoveButton
|
||||
:title="t('system.organization.removeName', { name: record.name })"
|
||||
:sub-title-tip="t('system.organization.removeTip')"
|
||||
@ok="handleRemove()"
|
||||
/>
|
||||
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
|
||||
</template>
|
||||
<template v-if="(keyword || '').trim() === ''" #empty>
|
||||
<div class="flex w-full items-center justify-center">
|
||||
{{ t('caseManagement.caseReview.tableNoData') }}
|
||||
<a-dropdown @select="handleSelect">
|
||||
<MsButton class="ml-[8px]">
|
||||
{{ t('caseManagement.featureCase.linkCase') }}
|
||||
</MsButton>
|
||||
<template #content>
|
||||
<a-doption v-for="item of caseTypeOptions" :key="item.value" :value="item.value">{{
|
||||
t(item.label)
|
||||
}}</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ms-base-table>
|
||||
</div>
|
||||
<MsDrawer
|
||||
:width="680"
|
||||
:visible="relatedVisible"
|
||||
unmount-on-close
|
||||
:footer="false"
|
||||
:mask="false"
|
||||
@cancel="relatedVisible = false"
|
||||
<MsCaseAssociate
|
||||
v-model:visible="innerVisible"
|
||||
v-model:project-id="innerProject"
|
||||
v-model:currentSelectCase="currentSelectCase"
|
||||
:ok-button-disabled="associateForm.reviewers.length === 0"
|
||||
:get-modules-func="getModuleTree"
|
||||
:modules-params="modulesTreeParams"
|
||||
:get-table-func="getUnAssociatedList"
|
||||
:table-params="getTableParams"
|
||||
:modules-count="modulesCount"
|
||||
:module-options="caseTypeOptions"
|
||||
:confirm-loading="confirmLoading"
|
||||
:case-id="props.bugId"
|
||||
:associated-ids="associatedIds"
|
||||
:type="RequestModuleEnum.API_CASE"
|
||||
@close="emit('close')"
|
||||
@save="saveHandler"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex flex-row items-center gap-[4px]">
|
||||
<div>{{ t('bugManagement.detail.relatedCase') }}</div>
|
||||
<a-select>
|
||||
<a-option>1</a-option>
|
||||
<a-option>2</a-option>
|
||||
<a-option>3</a-option>
|
||||
</a-select>
|
||||
</MsCaseAssociate>
|
||||
</div>
|
||||
</template>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import type { 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 MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
|
||||
import { RequestModuleEnum } from '@/components/business/ms-case-associate/utils';
|
||||
|
||||
import { postProjectMemberByProjectId } from '@/api/modules/setting/organizationAndProject';
|
||||
import {
|
||||
batchAssociation,
|
||||
cancelAssociation,
|
||||
getAssociatedList,
|
||||
getModuleTree,
|
||||
getUnAssociatedList,
|
||||
} from '@/api/modules/bug-management';
|
||||
import { postTabletList } from '@/api/modules/project-management/menuManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { formatPhoneNumber } from '@/utils';
|
||||
import { useAppStore } from '@/store';
|
||||
import useFeatureCaseStore from '@/store/modules/case/featureCase';
|
||||
|
||||
import type { TableQueryParams } from '@/models/common';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
import Message from '@arco-design/web-vue/es/message';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const featureCaseStore = useFeatureCaseStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const relatedVisible = ref(false);
|
||||
const relatedType = ref('api');
|
||||
const currentProjectId = computed(() => appStore.currentProjectId);
|
||||
|
||||
const showRelatedDrawer = (type: string) => {
|
||||
relatedType.value = type;
|
||||
};
|
||||
const props = defineProps<{
|
||||
bugId: string; // 缺陷id
|
||||
}>();
|
||||
|
||||
const keyword = ref('');
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'update:project', val: string): void;
|
||||
(e: 'success', val: string[]): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
const keyword = ref<string>('');
|
||||
|
||||
const projectColumn: MsTableColumn = [
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'system.organization.userName',
|
||||
title: 'caseManagement.featureCase.tableColumnID',
|
||||
dataIndex: 'id',
|
||||
width: 200,
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.tableColumnName',
|
||||
slotName: 'name',
|
||||
dataIndex: 'name',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 200,
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{
|
||||
title: 'system.organization.email',
|
||||
dataIndex: 'email',
|
||||
title: 'caseManagement.featureCase.projectName',
|
||||
slotName: 'projectName',
|
||||
dataIndex: 'projectName',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 200,
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{
|
||||
title: 'system.organization.phone',
|
||||
dataIndex: 'phone',
|
||||
title: 'caseManagement.featureCase.tableColumnVersion',
|
||||
slotName: 'version',
|
||||
dataIndex: 'version',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.changeType',
|
||||
slotName: 'type',
|
||||
dataIndex: 'type',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.tableColumnActions',
|
||||
slotName: 'operation',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
width: 140,
|
||||
showInTable: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{ title: 'system.organization.operation', slotName: 'operation' },
|
||||
];
|
||||
|
||||
const { propsRes, propsEvent, loadList, setKeyword } = useTable(
|
||||
postProjectMemberByProjectId,
|
||||
{
|
||||
heightUsed: 240,
|
||||
columns: projectColumn,
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, setKeyword } = useTable(getAssociatedList, {
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
selectable: false,
|
||||
noDisable: false,
|
||||
pageSimple: true,
|
||||
},
|
||||
(record) => {
|
||||
return {
|
||||
...record,
|
||||
phone: formatPhoneNumber(record.phone || ''),
|
||||
};
|
||||
}
|
||||
);
|
||||
heightUsed: 340,
|
||||
enableDrag: true,
|
||||
});
|
||||
|
||||
async function searchUser() {
|
||||
const innerVisible = ref(false);
|
||||
const innerProject = ref(currentProjectId.value);
|
||||
|
||||
const associateForm = ref({
|
||||
reviewers: [],
|
||||
});
|
||||
|
||||
const associatedIds = ref<string[]>([]);
|
||||
|
||||
const currentSelectCase = ref<string>('');
|
||||
|
||||
const modulesTreeParams = ref<TableQueryParams>({});
|
||||
|
||||
const getTableParams = ref<TableQueryParams>({});
|
||||
|
||||
function handleSelect(value: string | number | Record<string, any> | undefined) {
|
||||
currentSelectCase.value = value as string;
|
||||
innerVisible.value = true;
|
||||
}
|
||||
|
||||
async function getFetch() {
|
||||
setLoadListParams({
|
||||
keyword: keyword.value,
|
||||
bugId: props.bugId,
|
||||
});
|
||||
await loadList();
|
||||
const { msPagination } = propsRes.value;
|
||||
featureCaseStore.setListCount(featureCaseStore.activeTab, msPagination?.total || 0);
|
||||
}
|
||||
|
||||
async function cancelLink(record: any) {
|
||||
try {
|
||||
const { id } = record;
|
||||
await cancelAssociation(id);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const caseTypeOptions = ref<{ label: string; value: string }[]>([]);
|
||||
|
||||
const modulesCount = ref<Record<string, any>>({});
|
||||
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
|
||||
async function saveHandler(params: TableQueryParams) {
|
||||
try {
|
||||
confirmLoading.value = true;
|
||||
await batchAssociation(params);
|
||||
Message.success(t('caseManagement.featureCase.AssociatedSuccess'));
|
||||
innerVisible.value = false;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const moduleMaps: Record<string, { label: string; value: string }[]> = {
|
||||
apiTest: [
|
||||
{
|
||||
value: 'API',
|
||||
label: t('caseManagement.featureCase.apiCase'),
|
||||
},
|
||||
{
|
||||
value: 'SCENARIO',
|
||||
label: t('caseManagement.featureCase.sceneCase'),
|
||||
},
|
||||
],
|
||||
uiTest: [
|
||||
{
|
||||
value: 'UI',
|
||||
label: t('caseManagement.featureCase.uiCase'),
|
||||
},
|
||||
],
|
||||
loadTest: [
|
||||
{
|
||||
value: 'PERFORMANCE',
|
||||
label: t('caseManagement.featureCase.propertyCase'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function getEnabledModules() {
|
||||
const result = await postTabletList({ projectId: currentProjectId.value });
|
||||
const caseArr = result.filter((item) => Object.keys(moduleMaps).includes(item.module));
|
||||
caseArr.forEach((item: any) => {
|
||||
const currentModule = moduleMaps[item.module];
|
||||
caseTypeOptions.value.push(...currentModule);
|
||||
});
|
||||
currentSelectCase.value = caseTypeOptions.value[0].value;
|
||||
}
|
||||
|
||||
async function searchCase() {
|
||||
setKeyword(keyword.value);
|
||||
await loadList();
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
await loadList();
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
Message.success(t('common.removeSuccess'));
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
onMounted(async () => {
|
||||
getEnabledModules();
|
||||
getFetch();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.custom-height) {
|
||||
height: 100vh !important;
|
||||
border: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
@ -2,111 +2,479 @@
|
|||
<div class="p-[16px]">
|
||||
<div class="header">
|
||||
<div class="header-title">{{ t('bugManagement.edit.content') }}</div>
|
||||
<div class="header-action">
|
||||
<a-button>
|
||||
<div v-if="!contentEditAble" v-permission="['PROJECT_BUG:READ+UPDATE']" class="header-action">
|
||||
<a-button type="text" @click="contentEditAble = true">
|
||||
<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-if="contentEditAble"
|
||||
v-model:raw="form.content"
|
||||
v-model:filed-ids="fileIds"
|
||||
:disabled="!contentEditAble"
|
||||
:placeholder="t('bugManagement.edit.contentPlaceholder')"
|
||||
:upload-image="handleUploadImage"
|
||||
/>
|
||||
<div v-else>-</div>
|
||||
<div v-else v-dompurify-html="form?.content || '-'" class="text-[var(--color-text-3)]"></div>
|
||||
</div>
|
||||
<div v-if="contentEditAble" class="flex justify-end">
|
||||
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
|
||||
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading">
|
||||
{{ t('common.save') }}
|
||||
</a-button></div
|
||||
>
|
||||
<div v-if="props.allowEdit">
|
||||
<div class="font-medium text-[var(--color-text-1)]">
|
||||
{{ t('bugManagement.edit.file') }}
|
||||
</div>
|
||||
<a-dropdown trigger="hover">
|
||||
<div class="mb-1">
|
||||
<a-dropdown position="tr" trigger="hover">
|
||||
<a-button v-permission="['PROJECT_BUG:READ+UPDATE']" type="outline">
|
||||
<template #icon> <icon-plus class="text-[14px]" /> </template
|
||||
>{{ t('system.orgTemplate.addAttachment') }}</a-button
|
||||
>
|
||||
<template #content>
|
||||
<MsUpload
|
||||
<a-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="fileList"
|
||||
:auto-upload="false"
|
||||
multiple
|
||||
draggable
|
||||
accept="unknown"
|
||||
is-limit
|
||||
size-unit="MB"
|
||||
:max-size="500"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<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') }}
|
||||
<template #upload-button>
|
||||
<a-button type="text" class="!text-[var(--color-text-1)]">
|
||||
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
<a-button type="text" class="!text-[var(--color-text-1)]" @click="associatedFile">
|
||||
<MsIcon type="icon-icon_link-copy_outlined" size="16" />
|
||||
{{ t('caseManagement.featureCase.associatedFile') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
|
||||
<FileList
|
||||
<MsFileList
|
||||
ref="fileListRef"
|
||||
v-model:file-list="fileList"
|
||||
:show-tab="false"
|
||||
:file-list="fileList"
|
||||
:upload-func="uploadFile"
|
||||
@delete-file="deleteFile"
|
||||
@reupload="reupload"
|
||||
@handle-preview="handlePreview"
|
||||
:request-params="{
|
||||
bugId: bugId,
|
||||
projectId: currentProjectId,
|
||||
}"
|
||||
:upload-func="uploadOrAssociationFile"
|
||||
:handle-delete="deleteFileHandler"
|
||||
:show-delete="props.allowEdit"
|
||||
>
|
||||
</FileList>
|
||||
<template #actions="{ item }">
|
||||
<div v-if="props.allowEdit">
|
||||
<!-- 本地文件 -->
|
||||
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
|
||||
<MsButton
|
||||
v-if="item.status !== 'init'"
|
||||
type="button"
|
||||
status="primary"
|
||||
class="!mr-[4px]"
|
||||
@click="handlePreview(item)"
|
||||
>
|
||||
{{ t('ms.upload.preview') }}
|
||||
</MsButton>
|
||||
<MsButton type="button" status="primary" class="!mr-[4px]" @click="transferVisible = true">
|
||||
{{ t('caseManagement.featureCase.storage') }}
|
||||
</MsButton>
|
||||
<TransferModal
|
||||
v-model:visible="transferVisible"
|
||||
:request-fun="transferFileRequest"
|
||||
:params="{
|
||||
projectId: currentProjectId,
|
||||
bugId: bugId,
|
||||
fileId: item.uid,
|
||||
local: true,
|
||||
}"
|
||||
@success="emit('updateSuccess')"
|
||||
/>
|
||||
<MsButton
|
||||
v-if="item.status === 'done'"
|
||||
type="button"
|
||||
status="primary"
|
||||
class="!mr-[4px]"
|
||||
@click="downloadFile(item)"
|
||||
>
|
||||
{{ t('caseManagement.featureCase.download') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
<!-- 关联文件 -->
|
||||
<div v-else class="flex flex-nowrap">
|
||||
<MsButton
|
||||
v-if="item.status !== 'init'"
|
||||
type="button"
|
||||
status="primary"
|
||||
class="!mr-[4px]"
|
||||
@click="handlePreview(item)"
|
||||
>
|
||||
{{ t('ms.upload.preview') }}
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-if="item.status === 'done'"
|
||||
type="button"
|
||||
status="primary"
|
||||
class="!mr-[4px]"
|
||||
@click="downloadFile(item)"
|
||||
>
|
||||
{{ t('caseManagement.featureCase.download') }}
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-if="item.isUpdateFlag"
|
||||
type="button"
|
||||
status="primary"
|
||||
class="!mr-[4px]"
|
||||
@click="handleUpdateFile(item)"
|
||||
>
|
||||
{{ t('common.update') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #title="{ item }">
|
||||
<span v-if="item.isUpdateFlag" class="ml-4 flex items-center font-normal text-[rgb(var(--warning-6))]"
|
||||
><icon-exclamation-circle-fill /> <span>{{ t('caseManagement.featureCase.fileIsUpdated') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</MsFileList>
|
||||
</div>
|
||||
<RelateFileDrawer
|
||||
v-model:visible="associatedDrawer"
|
||||
:get-tree-request="getModules"
|
||||
:get-count-request="getModulesCount"
|
||||
:get-list-request="getAssociatedFileList"
|
||||
:get-list-fun-params="getListFunParams"
|
||||
@save="saveSelectAssociatedFile"
|
||||
/>
|
||||
<a-image-preview v-model:visible="previewVisible" :src="imageUrl" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileItem } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.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 MsFileList from '@/components/pure/ms-upload/fileList.vue';
|
||||
import { MsFileItem } from '@/components/pure/ms-upload/types';
|
||||
import RelateFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue';
|
||||
import TransferModal from '@/views/case-management/caseManagementFeature/components/tabContent/transferModal.vue';
|
||||
|
||||
import {
|
||||
checkFileIsUpdateRequest,
|
||||
deleteFileOrCancelAssociation,
|
||||
downloadFileRequest,
|
||||
editorUploadFile,
|
||||
getAssociatedFileList,
|
||||
previewFile,
|
||||
transferFileRequest,
|
||||
updateFile,
|
||||
uploadOrAssociationFile,
|
||||
} from '@/api/modules/bug-management';
|
||||
import { updateCaseRequest } from '@/api/modules/case-management/featureCase';
|
||||
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useAppStore } from '@/store';
|
||||
import { downloadByteFile, sleep } from '@/utils';
|
||||
import { scrollIntoView } from '@/utils/dom';
|
||||
|
||||
import { BugEditFormObject } from '@/models/bug-management';
|
||||
import { AssociatedList, AttachFileInfo } from '@/models/caseManagement/featureCase';
|
||||
import { TableQueryParams } from '@/models/common';
|
||||
|
||||
import { convertToFileByBug } from '@/views/bug-management/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
detailInfo: BugEditFormObject;
|
||||
allowEdit?: boolean; // 是否允许编辑
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateSuccess'): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const transferVisible = ref<boolean>(false);
|
||||
const previewVisible = ref<boolean>(false);
|
||||
// 富文本附件id
|
||||
const fileIds = ref<string[]>([]);
|
||||
const imageUrl = ref<string>('');
|
||||
const associatedDrawer = ref(false);
|
||||
const fileListRef = ref<InstanceType<typeof MsFileList>>();
|
||||
// 富文本编辑器是否可编辑
|
||||
const contentEditAble = ref(false);
|
||||
const fileList = ref<FileItem[]>([]);
|
||||
const currentProjectId = computed(() => appStore.currentProjectId);
|
||||
const fileList = ref<MsFileItem[]>([]);
|
||||
const bugId = computed(() => props.detailInfo.id);
|
||||
const attachmentsList = ref<AttachFileInfo[]>([]);
|
||||
const getListFunParams = ref<TableQueryParams>({
|
||||
combine: {
|
||||
hiddenIds: [],
|
||||
},
|
||||
});
|
||||
const form = ref({
|
||||
content: '',
|
||||
fileList: [],
|
||||
deleteLocalFileIds: [] as string[],
|
||||
unLinkRefIds: [] as string[],
|
||||
linkFileIds: [] as string[],
|
||||
});
|
||||
const uploadFile = (file: File) => {
|
||||
const fileItem: FileItem = {
|
||||
uid: `${Date.now()}`,
|
||||
name: file.name,
|
||||
status: 'init',
|
||||
file,
|
||||
|
||||
const handleFileFunc = async (attachments: AttachFileInfo[]) => {
|
||||
if (attachments && attachments.length) {
|
||||
attachmentsList.value = attachments;
|
||||
// 检查文件是否有更新
|
||||
const checkUpdateFileIds = await checkFileIsUpdateRequest(attachments.map((item: any) => item.fileId));
|
||||
// 处理文件列表
|
||||
fileList.value =
|
||||
attachments
|
||||
.map((fileInfo: any) => {
|
||||
return {
|
||||
...fileInfo,
|
||||
name: fileInfo.fileName,
|
||||
isUpdateFlag: checkUpdateFileIds.includes(fileInfo.id),
|
||||
};
|
||||
fileList.value.push(fileItem);
|
||||
return Promise.resolve(fileItem);
|
||||
};
|
||||
const handlePreview = (item: FileItem) => {
|
||||
const { url } = item;
|
||||
window.open(url);
|
||||
})
|
||||
.map((fileInfo: any) => {
|
||||
return convertToFileByBug(fileInfo);
|
||||
}) || [];
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFile = (item: FileItem) => {
|
||||
fileList.value = fileList.value.filter((e) => e.uid !== item.uid);
|
||||
function handleCancel() {
|
||||
contentEditAble.value = false;
|
||||
}
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
|
||||
// function handleOK() {
|
||||
// caseFormRef.value?.validate().then(async (res: any) => {
|
||||
// if (!res) {
|
||||
// try {
|
||||
// confirmLoading.value = true;
|
||||
// await updateCaseRequest();
|
||||
// Message.success(t('caseManagement.featureCase.editSuccess'));
|
||||
// handleCancel();
|
||||
// emit('updateSuccess');
|
||||
// } catch (error) {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log(error);
|
||||
// } finally {
|
||||
// confirmLoading.value = false;
|
||||
// }
|
||||
// }
|
||||
// return scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
|
||||
// });
|
||||
// }
|
||||
|
||||
const initCurrentDetail = async (detail: BugEditFormObject) => {
|
||||
const { attachments, content } = detail;
|
||||
form.value.content = content;
|
||||
handleFileFunc(attachments);
|
||||
};
|
||||
|
||||
const reupload = (item: FileItem) => {
|
||||
fileList.value = fileList.value.map((e) => {
|
||||
if (e.uid === item.uid) {
|
||||
// 删除本地文件
|
||||
async function deleteFileHandler(item: MsFileItem) {
|
||||
try {
|
||||
const params = {
|
||||
id: item.uid,
|
||||
local: item.local,
|
||||
bugId,
|
||||
projectId: currentProjectId.value,
|
||||
};
|
||||
await deleteFileOrCancelAssociation(params);
|
||||
Message.success(
|
||||
item.local ? t('caseManagement.featureCase.deleteSuccess') : t('caseManagement.featureCase.cancelLinkSuccess')
|
||||
);
|
||||
emit('updateSuccess');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function beforeUpload(file: File) {
|
||||
const _maxSize = 50 * 1024 * 1024;
|
||||
if (file.size > _maxSize) {
|
||||
Message.warning(t('ms.upload.overSize'));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
function handleChange(_fileList: MsFileItem[]) {
|
||||
fileList.value = _fileList.map((e) => {
|
||||
return {
|
||||
...e,
|
||||
status: 'init',
|
||||
enable: true, // 是否启用
|
||||
local: true, // 是否本地文件
|
||||
};
|
||||
}
|
||||
return e;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const handleLineFile = () => {};
|
||||
// 预览图片
|
||||
async function handlePreview(item: MsFileItem) {
|
||||
try {
|
||||
previewVisible.value = true;
|
||||
if (item.status !== 'init') {
|
||||
const res = await previewFile({
|
||||
projectId: currentProjectId.value,
|
||||
bugId: bugId.value as string,
|
||||
fileId: item.uid,
|
||||
associated: !item.local,
|
||||
});
|
||||
const blob = new Blob([res], { type: 'image/jpeg' });
|
||||
imageUrl.value = URL.createObjectURL(blob);
|
||||
} else {
|
||||
imageUrl.value = item.url || '';
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 下载
|
||||
async function downloadFile(item: MsFileItem) {
|
||||
try {
|
||||
const res = await downloadFileRequest({
|
||||
projectId: currentProjectId.value,
|
||||
bugId: bugId.value as string,
|
||||
fileId: item.uid,
|
||||
associated: !item.local,
|
||||
});
|
||||
downloadByteFile(res, `${item.name}`);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
function associatedFile() {
|
||||
associatedDrawer.value = true;
|
||||
}
|
||||
|
||||
// 处理关联文件
|
||||
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
|
||||
const fileResultList = fileData.map((fileInfo) => convertToFileByBug(fileInfo));
|
||||
fileList.value.push(...fileResultList);
|
||||
}
|
||||
|
||||
// 后台传过来的local文件的item列表
|
||||
const oldLocalFileList = computed(() => {
|
||||
return attachmentsList.value.filter((item) => item.local).map((item: any) => item.uid);
|
||||
});
|
||||
|
||||
// 已经关联过的id列表
|
||||
const associateFileIds = computed(() => {
|
||||
return attachmentsList.value.filter((item: any) => !item.local).map((item: any) => item.id);
|
||||
});
|
||||
|
||||
// 当前新增传过来的关联list
|
||||
const currentAlreadyAssociateFileList = computed(() => {
|
||||
return fileList.value
|
||||
.filter((item) => !item.local && !associateFileIds.value.includes(item.uid))
|
||||
.map((item: any) => item.uid);
|
||||
});
|
||||
|
||||
// 后台已保存本地文件的item列表
|
||||
const currentOldLocalFileList = computed(() => {
|
||||
return fileList.value.filter((item) => item.local && item.status !== 'init').map((item: any) => item.uid);
|
||||
});
|
||||
|
||||
// 新增关联文件ID列表
|
||||
const newAssociateFileListIds = computed(() => {
|
||||
return fileList.value
|
||||
.filter((item: any) => !item.local && !associateFileIds.value.includes(item.uid))
|
||||
.map((item: any) => item.uid);
|
||||
});
|
||||
|
||||
// 取消关联文件id TODO
|
||||
const unLinkFilesIds = computed(() => {
|
||||
const deleteAssociateFileIds = fileList.value
|
||||
.filter(
|
||||
(item: any) =>
|
||||
!currentAlreadyAssociateFileList.value.includes(item.uid) && associateFileIds.value.includes(item.uid)
|
||||
)
|
||||
.map((item) => item.uid);
|
||||
return associateFileIds.value.filter(
|
||||
(id: string) => !currentAlreadyAssociateFileList.value.includes(id) && !deleteAssociateFileIds.includes(id)
|
||||
);
|
||||
});
|
||||
|
||||
// 删除本地上传的文件id
|
||||
const deleteFileMetaIds = computed(() => {
|
||||
return oldLocalFileList.value
|
||||
.filter((item: any) => !currentOldLocalFileList.value.includes(item.id))
|
||||
.map((item: any) => item.id);
|
||||
});
|
||||
|
||||
// 处理关联文件和已关联文件本地文件和已上传文本文件
|
||||
function getFilesParams() {
|
||||
form.value.deleteLocalFileIds = deleteFileMetaIds.value;
|
||||
form.value.unLinkRefIds = unLinkFilesIds.value;
|
||||
form.value.linkFileIds = newAssociateFileListIds.value;
|
||||
}
|
||||
|
||||
async function startUpload() {
|
||||
await sleep(300);
|
||||
fileListRef.value?.startUpload();
|
||||
emit('updateSuccess');
|
||||
}
|
||||
|
||||
// 文件列表单个上传
|
||||
watch(
|
||||
() => fileList.value,
|
||||
async (val) => {
|
||||
const isNewFiles = val.filter((item) => item.status === 'init').length;
|
||||
if (val && isNewFiles) {
|
||||
startUpload();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 更新文件
|
||||
async function handleUpdateFile(item: MsFileItem) {
|
||||
try {
|
||||
await updateFile(currentProjectId.value, item.associationId);
|
||||
Message.success(t('common.updateSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUploadImage(file: File) {
|
||||
const { data } = await editorUploadFile({
|
||||
fileList: [file],
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 监视文件列表处理关联和本地文件
|
||||
watch(
|
||||
() => fileList.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getListFunParams.value.combine.hiddenIds = fileList.value.filter((item) => !item.local).map((item) => item.uid);
|
||||
getFilesParams();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
initCurrentDetail(props.detailInfo);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -115,6 +483,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&-title {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
&-action {
|
||||
|
|
|
@ -80,20 +80,20 @@
|
|||
]);
|
||||
|
||||
const isNextTip = ref<boolean>(false);
|
||||
const countDown = ref<number>(5);
|
||||
const countDown = ref<number>(15);
|
||||
const timer = ref<any>(null);
|
||||
function setCountdown() {
|
||||
// timer.value = setInterval(() => {
|
||||
// if (countDown.value > 1) {
|
||||
// --countDown.value;
|
||||
// } else {
|
||||
// clearInterval(timer.value);
|
||||
// router.push({
|
||||
// name: BugManagementRouteEnum.BUG_MANAGEMENT_INDEX,
|
||||
// });
|
||||
// }
|
||||
// }, 1000);
|
||||
timer.value = 5;
|
||||
timer.value = setInterval(() => {
|
||||
if (countDown.value > 1) {
|
||||
--countDown.value;
|
||||
} else {
|
||||
clearInterval(timer.value);
|
||||
router.push({
|
||||
name: BugManagementRouteEnum.BUG_MANAGEMENT_INDEX,
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
timer.value = 15;
|
||||
}
|
||||
|
||||
function isDoNotShowAgainChecked() {
|
||||
|
|
|
@ -218,6 +218,7 @@
|
|||
} from '@/api/modules/bug-management';
|
||||
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useVisit from '@/hooks/useVisit';
|
||||
import router from '@/router';
|
||||
import { useAppStore } from '@/store';
|
||||
import { downloadByteFile } from '@/utils';
|
||||
|
@ -274,9 +275,12 @@
|
|||
const isEdit = computed(() => !!route.query.id && route.params.mode === 'edit');
|
||||
const bugId = computed(() => route.query.id || '');
|
||||
const isEditOrCopy = computed(() => !!bugId.value);
|
||||
const isCopy = computed(() => route.params.mode === 'copy');
|
||||
const imageUrl = ref('');
|
||||
const previewVisible = ref<boolean>(false);
|
||||
const richTextFileIds = ref<string[]>([]);
|
||||
const visitedKey = 'doNotNextTipCreateBug';
|
||||
const { getIsVisited } = useVisit(visitedKey);
|
||||
|
||||
const title = computed(() => {
|
||||
return isEdit.value ? t('bugManagement.editBug') : t('bugManagement.createBug');
|
||||
|
@ -507,10 +511,13 @@
|
|||
});
|
||||
});
|
||||
}
|
||||
const tmpObj = {
|
||||
const tmpObj: BugEditFormObject = {
|
||||
...form.value,
|
||||
customFields,
|
||||
};
|
||||
if (isCopy.value) {
|
||||
delete tmpObj.id;
|
||||
}
|
||||
// 执行保存操作
|
||||
const res = await createOrUpdateBug({ request: tmpObj, fileList: fileList.value as unknown as File[] });
|
||||
if (isEdit.value) {
|
||||
|
@ -520,6 +527,7 @@
|
|||
});
|
||||
} else {
|
||||
Message.success(t('common.createSuccess'));
|
||||
|
||||
if (isContinue) {
|
||||
// 如果是保存并继续创建
|
||||
const { templateId } = form.value;
|
||||
|
@ -536,6 +544,12 @@
|
|||
fileList.value = [];
|
||||
} else {
|
||||
// 否则跳转到成功页
|
||||
if (getIsVisited()) {
|
||||
router.push({
|
||||
name: BugManagementRouteEnum.BUG_MANAGEMENT_INDEX,
|
||||
});
|
||||
return;
|
||||
}
|
||||
router.push({
|
||||
name: BugManagementRouteEnum.BUG_MANAGEMENT_CREATE_SUCCESS,
|
||||
query: {
|
||||
|
|
Loading…
Reference in New Issue