feat(缺陷管理): 缺陷详情接口对接

feat(缺陷管理): 解决 ts 报错
This commit is contained in:
RubyLiu 2023-12-22 18:39:36 +08:00 committed by 刘瑞斌
parent 8d87368469
commit 89bd4836d2
15 changed files with 1225 additions and 83 deletions

View File

@ -1,7 +1,8 @@
import MSR from '@/api/http/index'; import MSR from '@/api/http/index';
import * as bugURL from '@/api/requrls/bug-management'; import * as bugURL from '@/api/requrls/bug-management';
import { BugExportParams, BugListItem, DefaultTemplate } from '@/models/bug-management'; import { BugEditFormObject, BugExportParams, BugListItem } from '@/models/bug-management';
import { AssociatedList } from '@/models/caseManagement/featureCase';
import { CommonList, TableQueryParams, TemplateOption } from '@/models/common'; import { CommonList, TableQueryParams, TemplateOption } from '@/models/common';
/** /**
@ -12,17 +13,35 @@ import { CommonList, TableQueryParams, TemplateOption } from '@/models/common';
export function getBugList(data: TableQueryParams) { export function getBugList(data: TableQueryParams) {
return MSR.post<CommonList<BugListItem>>({ url: bugURL.postTableListUrl, data }); return MSR.post<CommonList<BugListItem>>({ url: bugURL.postTableListUrl, data });
} }
/**
export function updateBug(data: TableQueryParams) { * Bug
return MSR.post({ url: bugURL.postUpdateBugUrl, data }); * @param data
* @returns
*/
export function updateBug(data: { request: BugEditFormObject; fileList: File[] }) {
return MSR.uploadFile({ url: bugURL.postUpdateBugUrl }, data, '', true);
} }
/**
*
* @param data
* @returns
*/
export function updateBatchBug(data: TableQueryParams) { export function updateBatchBug(data: TableQueryParams) {
return MSR.post({ url: bugURL.postBatchUpdateBugUrl, data }); return MSR.post({ url: bugURL.postBatchUpdateBugUrl, data });
} }
/**
export function createBug(data: TableQueryParams) { * Bug
return MSR.uploadFile({ url: bugURL.postCreateBugUrl }, { request: data.request, fileList: data.fileList }, ''); * @param data
* @returns
*/
export function createBug(data: { request: BugEditFormObject; fileList: File[] }) {
return MSR.uploadFile({ url: bugURL.postCreateBugUrl }, data, '', true);
}
/**
* bug
*/
export function getBugDetail(id: string) {
return MSR.get({ url: `${bugURL.getBugDetailUrl}${id}` });
} }
export function deleteSingleBug(data: TableQueryParams) { export function deleteSingleBug(data: TableQueryParams) {
@ -38,14 +57,14 @@ export function getTemplageOption(params: { projectId: string }) {
} }
export function getTemplateById(data: TableQueryParams) { export function getTemplateById(data: TableQueryParams) {
return MSR.get({ url: bugURL.getTemplateUrl, data }); return MSR.post({ url: bugURL.getTemplateUrl, data });
} }
// 获取导出字段配置 // 获取导出字段配置
export function getExportConfig(projectId: string) { export function getExportConfig(projectId: string) {
return MSR.get({ url: `${bugURL.getExportConfigUrl}${projectId}` }); return MSR.get({ url: `${bugURL.getExportConfigUrl}${projectId}` });
} }
// 获取模版详情 // 获取模版详情
export function getTemplateDetailInfo(data: DefaultTemplate) { export function getTemplateDetailInfo(data: { id: string; projectId: string }) {
return MSR.post({ url: `${bugURL.getTemplateDetailUrl}`, data }); return MSR.post({ url: `${bugURL.getTemplateDetailUrl}`, data });
} }
@ -58,3 +77,7 @@ export function syncBugOpenSource(params: { projectId: string }) {
export function exportBug(data: BugExportParams) { export function exportBug(data: BugExportParams) {
return MSR.post({ url: bugURL.postExportBugUrl, data }); return MSR.post({ url: bugURL.postExportBugUrl, data });
} }
// 获取关联文件列表
export function getAssociatedFileList(data: TableQueryParams) {
return MSR.post<CommonList<AssociatedList>>({ url: bugURL.postAssociatedFileListUrl, data });
}

View File

@ -4,9 +4,11 @@ export const postBatchUpdateBugUrl = '/bug/batch-update';
export const postCreateBugUrl = '/bug/add'; export const postCreateBugUrl = '/bug/add';
export const getDeleteBugUrl = '/bug/delete/'; export const getDeleteBugUrl = '/bug/delete/';
export const postBatchDeleteBugUrl = '/bug/batch-delete'; export const postBatchDeleteBugUrl = '/bug/batch-delete';
export const getTemplateUrl = '/bug/template'; export const getTemplateUrl = '/bug/template/detail';
export const getTemplageOption = '/bug/template/option'; export const getTemplageOption = '/bug/template/option';
export const getExportConfigUrl = '/bug/export/columns/'; export const getExportConfigUrl = '/bug/export/columns/';
export const getTemplateDetailUrl = '/bug/template/detail'; export const getTemplateDetailUrl = '/bug/template/detail';
export const getSyncBugOpenSourceUrl = '/bug/sync/'; export const getSyncBugOpenSourceUrl = '/bug/sync/';
export const postExportBugUrl = '/bug/export'; export const postExportBugUrl = '/bug/export';
export const postAssociatedFileListUrl = '/bug/relate/case/page';
export const getBugDetailUrl = '/bug/detail/';

View File

@ -140,6 +140,7 @@
couplingConfig: { couplingConfig: {
...item.props.couplingConfig, ...item.props.couplingConfig,
}, },
sourceType: item.sourceType || '',
}; };
return formItemRule; return formItemRule;
}); });

View File

@ -93,6 +93,15 @@ export interface FormItem {
[key: string]: any; [key: string]: any;
} }
export type FormValueType =
| string
| number
| boolean
| string[]
| number[]
| Record<string, any>
| Record<string, any>[];
interface FomItemSelect extends FormItemComplexCommonConfig { interface FomItemSelect extends FormItemComplexCommonConfig {
selectMultiple?: boolean; // 选择器是否多选 selectMultiple?: boolean; // 选择器是否多选
selectMultipleLimit?: [number, number]; // 选择器多选时最少和最多可选项数,如:[1, 3]表示最少选1项最多选3项[0, 3]表示最多选3项可不选[1, 0]表示最少选1项不限制最大可选数 selectMultipleLimit?: [number, number]; // 选择器多选时最少和最多可选项数,如:[1, 3]表示最少选1项最多选3项[0, 3]表示最多选3项可不选[1, 0]表示最少选1项不限制最大可选数

View File

@ -11,6 +11,8 @@ export enum FormCreateKeyEnum {
CASE_CUSTOM_ATTRS = 'caseCustomAttributes', CASE_CUSTOM_ATTRS = 'caseCustomAttributes',
// 用例tab详情字段 // 用例tab详情字段
CASE_CUSTOM_ATTRS_DETAIL = 'caseCustomAttributesDetail', CASE_CUSTOM_ATTRS_DETAIL = 'caseCustomAttributesDetail',
// bug 详情
BUG_DETAIL = 'bugDetail',
} }
export default {}; export default {};

View File

@ -1,3 +1,5 @@
import { FormItemType } from '@/components/pure/ms-form-create/types';
import { BatchApiParams } from './common'; import { BatchApiParams } from './common';
export interface BugListItem { export interface BugListItem {
@ -25,12 +27,21 @@ export interface BugExportColumn {
export interface BugExportParams extends BatchApiParams { export interface BugExportParams extends BatchApiParams {
bugExportColumns: BugExportColumn[]; // 导出字段 bugExportColumns: BugExportColumn[]; // 导出字段
} }
export interface BugEditCustomField {
// 获取默认模版缺陷 type: FormItemType; // 表单项类型
export interface DefaultTemplate { fieldId: string;
fieldName: string;
value: string;
platformOptionJson?: string; // 选项的 Json
required: boolean;
}
export interface BugEditFormObject {
[key: string]: any;
}
export interface BugEditCustomFieldItem {
id: string; id: string;
projectId: string; name: string;
fromStatusId?: string; type: string;
platformBugKey?: string; value: string;
} }
export default {}; export default {};

View File

@ -35,7 +35,12 @@ const useFormCreateStore = defineStore('form-create', {
(formItemType: any) => item.type?.toUpperCase() === formItemType (formItemType: any) => item.type?.toUpperCase() === formItemType
); );
if (currentTypeForm) { if (currentTypeForm) {
fieldType = FieldTypeFormRules[currentTypeForm].type; if (currentTypeForm === 'INPUT' && item.subDesc) {
// 如果是input类型并且有subDesc说明是JiraKey 类型
fieldType = 'JiraKey';
} else {
fieldType = FieldTypeFormRules[currentTypeForm].type;
}
const options = item?.options; const options = item?.options;
const currentOptions = options?.map((optionsItem) => { const currentOptions = options?.map((optionsItem) => {
return { return {
@ -55,6 +60,7 @@ const useFormCreateStore = defineStore('form-create', {
options: !item.optionMethod ? currentOptions : [], options: !item.optionMethod ? currentOptions : [],
link: item.couplingConfig?.cascade, link: item.couplingConfig?.cascade,
rule: item.validate || [], rule: item.validate || [],
sourceType: item.type, // 原始表单类型
// 梳理表单所需要属性 // 梳理表单所需要属性
props: { props: {
...FieldTypeFormRules[currentTypeForm].props, ...FieldTypeFormRules[currentTypeForm].props,

View File

@ -0,0 +1,155 @@
<template>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="mb-[16px]"
></a-input>
<a-spin class="min-h-[400px] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="props.selectedKeys"
:data="folderTree"
:keyword="moduleKeyword"
:expand-all="props.isExpandAll"
:empty-text="t('project.fileManagement.noFolder')"
:virtual-list-props="virtualListProps"
:draggable="false"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
block-node
title-tooltip-position="left"
@select="folderNodeSelect"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">
<MsIcon type="icon-icon_folder_filled1" size="14" class="mr-1 text-[var(--color-text-4)]" />{{
nodeData.name
}}</div
>
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div>
</template>
</MsTree>
</a-spin>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
isExpandAll: boolean;
selectedKeys?: Array<string | number>; // key
modulesCount?: Record<string, number>; //
showType?: string; //
getTreeRequest: (params: any) => Promise<ModuleTreeNode[]>; //
activeFolder: string | number;
}>();
const emit = defineEmits(['update:selectedKeys', 'init', 'folderNodeSelect', 'update:activeFolder']);
const moduleKeyword = ref('');
const folderTree = ref<ModuleTreeNode[]>([]);
const focusNodeKey = ref<string | number>('');
const loading = ref(false);
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 296px)',
};
});
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
emit('folderNodeSelect', _selectedKeys, offspringIds);
}
const selectedKeys = ref(props.selectedKeys || []);
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/
async function initModules(isSetDefaultKey = false) {
try {
loading.value = true;
const res = await props.getTreeRequest(appStore.currentProjectId);
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: false,
disabled: false,
count: props.modulesCount?.[e.id] || 0,
};
});
if (isSetDefaultKey) {
selectedKeys.value = [folderTree.value[0].id];
emit('update:activeFolder', folderTree.value[0].id);
}
emit('init', folderTree.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
watch(
() => props.showType,
(val) => {
if (val === 'Module') {
initModules(true);
}
},
{
immediate: true,
}
);
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,313 @@
<template>
<div class="pl-4">
<div class="header">
<div
><span class="one-line-text max-w-[300px]">{{ moduleInfo.name }}</span
><span class="ml-[4px] text-[var(--color-text-4)]">({{ moduleInfo.count }})</span></div
>
<div class="header-right">
<a-select v-model="tableFileType" class="w-[240px]" :loading="fileTypeLoading" @change="searchList">
<a-option key="" value="">{{ t('common.all') }}</a-option>
<a-option v-for="item of tableFileTypeOptions" :key="item" :value="item">
{{ item }}
</a-option>
</a-select>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="w-[240px]"
@search="searchList"
@press-enter="searchList"
/></div>
</div>
<ms-base-table v-bind="propsRes" ref="tableRef" no-disable v-on="propsEvent">
<template #name="{ record }">
<MsTag
v-if="record.fileType.toLowerCase() === 'jar'"
theme="light"
type="success"
:self-style="
record.enable
? {}
: {
color: 'var(--color-text-4)',
backgroundColor: 'var(--color-text-n9)',
}
"
>
{{ t(record.enable ? 'common.enable' : 'common.disable') }}
</MsTag>
<a-tooltip :content="record.name">
<div class="one-line-text max-w-[168px]">{{ record.name }}</div>
</a-tooltip>
</template>
<template #size="{ record }">
<span>{{ formatFileSize(record.size) }}</span>
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { debounce } from 'lodash-es';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { getFileTypes, getRepositoryFileTypes } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import { findNodeByKey, formatFileSize } from '@/utils';
import type { AssociatedList } from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common';
import type { FileListQueryParams, ModuleTreeNode } from '@/models/projectManagement/file';
import { Repository } from '@/models/projectManagement/file';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const props = defineProps<{
activeFolder: string;
activeFolderType: 'folder' | 'module' | 'storage';
offspringIds: string[]; // id
modulesCount: Record<string, any>; //
folderTree: ModuleTreeNode[];
selectFile: AssociatedList[]; //
getListRequest: (params: TableQueryParams) => Promise<CommonList<AssociatedList>>;
showType: 'Module' | 'Storage'; //
storageList: Repository[]; //
}>();
const emit = defineEmits<{
(e: 'init', params: FileListQueryParams): void;
(e: 'update:selectFile', val: AssociatedList[]): void;
}>();
const tableFileTypeOptions = ref<string[]>([]);
const tableFileType = ref(''); //
const keyword = ref('');
const fileTypeLoading = ref(false);
const fileType = ref('module'); // /
const appStore = useAppStore();
const userStore = useUserStore();
const combine = ref<Record<string, any>>({});
const isMyOrAllFolder = computed(() => ['my', 'all'].includes(props.activeFolder)); // /
const columns: MsTableColumn = [
{
title: 'project.fileManagement.name',
slotName: 'name',
dataIndex: 'name',
width: 270,
},
{
title: 'project.fileManagement.type',
dataIndex: 'fileType',
width: 90,
},
{
title: 'project.fileManagement.tag',
dataIndex: 'tags',
isTag: true,
},
{
title: 'project.fileManagement.creator',
dataIndex: 'creator',
showTooltip: true,
width: 120,
},
{
title: 'project.fileManagement.updater',
dataIndex: 'updateUser',
showTooltip: true,
width: 120,
},
{
title: 'project.fileManagement.updateTime',
dataIndex: 'updateTime',
width: 180,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
props.getListRequest,
{
columns,
tableKey: TableKeyEnum.FILE_MANAGEMENT_FILE,
showSetting: false,
selectable: true,
showSelectAll: true,
heightUsed: 300,
},
(item) => {
return {
...item,
tags: item.tags?.map((e: string) => ({ id: e, name: e })) || [],
};
}
);
function emitTableParams() {
emit('init', {
keyword: keyword.value,
fileType: tableFileType.value,
moduleIds: [],
projectId: appStore.currentProjectId,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
combine: combine.value,
});
}
function setTableParams() {
if (props.activeFolder === 'my') {
combine.value.createUser = userStore.id;
} else {
combine.value.createUser = '';
}
if (fileType.value === 'storage') {
combine.value.storage = 'git';
} else {
combine.value.storage = 'minio';
}
let moduleIds: string[] = [props.activeFolder, ...props.offspringIds];
if (isMyOrAllFolder.value) {
moduleIds = [];
}
setLoadListParams({
keyword: keyword.value,
fileType: tableFileType.value,
moduleIds,
projectId: appStore.currentProjectId,
combine: combine.value,
});
}
const searchList = debounce(() => {
setTableParams();
loadList();
emitTableParams();
}, 300);
/**
* 初始化文件类型筛选选项
*/
async function initFileTypes() {
try {
fileTypeLoading.value = true;
let res = null;
if (fileType.value === 'storage') {
res = await getRepositoryFileTypes(appStore.currentProjectId);
} else {
res = await getFileTypes(appStore.currentProjectId);
}
tableFileType.value = '';
tableFileTypeOptions.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
fileTypeLoading.value = false;
}
}
watch(
() => props.activeFolderType,
() => {
initFileTypes();
},
{
immediate: true,
}
);
watch(
() => props.activeFolderType,
(val) => {
if (val === 'folder') {
fileType.value = 'module';
} else {
fileType.value = val;
}
setTableParams();
}
);
watch(
() => props.activeFolder,
() => {
keyword.value = '';
searchList();
resetSelector();
},
{ immediate: true }
);
const moduleInfo = computed(() => {
if (props.showType === 'Module') {
return {
name: findNodeByKey<Record<string, any>>(props.folderTree, props.activeFolder, 'id')?.name,
count: props.modulesCount[props.activeFolder],
};
}
const storageItem = props.storageList.find((item) => item.id === props.activeFolder);
return {
name: storageItem?.name,
count: storageItem?.count,
};
});
const tableSelected = ref<AssociatedList[]>([]);
const selectedIds = computed(() => {
return [...propsRes.value.selectedKeys];
});
watch(
() => selectedIds.value,
() => {
tableSelected.value = propsRes.value.data.filter((item: any) => selectedIds.value.indexOf(item.id) > -1);
emit('update:selectFile', tableSelected.value);
}
);
defineExpose({
resetSelector,
});
onMounted(() => {
resetSelector();
});
onUnmounted(() => {
resetSelector();
});
</script>
<style scoped lang="less">
.header {
@apply flex items-center justify-between;
margin-bottom: 16px;
.header-right {
@apply ml-auto flex items-center justify-end;
width: 70%;
gap: 8px;
.show-type-icon {
:deep(.arco-radio-button-content) {
@apply flex;
padding: 4px;
line-height: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,273 @@
<template>
<MsDrawer
v-model:visible="showDrawer"
:mask="false"
:title="t('caseManagement.featureCase.associatedFile')"
:ok-text="t('caseManagement.featureCase.associated')"
:ok-loading="drawerLoading"
:ok-disabled="selectFile.length < 1"
:width="1200"
unmount-on-close
:show-continue="false"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<MsSplitBox>
<template #left>
<div class="p-[16px] pt-0">
<div class="folder">
<div class="folder-text">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('project.fileManagement.allFile') }}</div>
<div class="folder-count">({{ allFileCount }})</div>
</div>
<div class="ml-auto flex items-center">
<a-tooltip
:content="isExpandAll ? t('project.fileManagement.collapseAll') : t('project.fileManagement.expandAll')"
>
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
</div>
</div>
<a-divider class="my-[8px]" />
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="Module">{{ t('project.fileManagement.module') }}</a-radio>
<a-radio value="Storage">{{ t('project.fileManagement.storage') }}</a-radio>
</a-radio-group>
<div v-show="showType === 'Module'">
<FileTree
ref="folderTreeRef"
v-model:selected-keys="selectedKeys"
v-model:active-folder="activeFolder"
:is-expand-all="isExpandAll"
:modules-count="modulesCount"
:show-type="showType"
:get-tree-request="props.getTreeRequest"
@init="setRootModules"
@folder-node-select="folderNodeSelect"
/>
</div>
<div v-show="showType === 'Storage'">
<StorageList
v-model:drawer-visible="storageDrawerVisible"
v-model:active-folder="activeFolder"
:modules-count="modulesCount"
:show-type="showType"
@item-click="storageItemSelect"
/>
</div>
</div>
</template>
<template #right>
<LinkFileTable
v-model:selectFile="selectFile"
:active-folder="activeFolder"
:active-folder-type="activeFolderType"
:offspring-ids="offspringIds"
:modules-count="modulesCount"
:folder-tree="folderTree"
:storage-list="storageList"
:show-type="showType"
:get-list-request="props.getListRequest"
@init="handleModuleTableInit"
/>
</template>
</MsSplitBox>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import FileTree from './fileTree.vue';
import LinkFileTable from './linkFileTable.vue';
import StorageList from './storageList.vue';
import { useI18n } from '@/hooks/useI18n';
import type { AssociatedList } from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common';
import { FileListQueryParams, ModuleTreeNode, Repository } from '@/models/projectManagement/file';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
getTreeRequest: (params: any) => Promise<ModuleTreeNode[]>; //
getCountRequest: (params: any) => Promise<Record<string, any>>; //
getListRequest: (params: TableQueryParams) => Promise<CommonList<AssociatedList>>; //
}>();
const emit = defineEmits<{
(e: 'save', val: AssociatedList[]): void;
(e: 'update:visible', val: boolean): void;
}>();
const showDrawer = computed({
get() {
return props.visible;
},
set(val) {
emit('update:visible', val);
},
});
const drawerLoading = ref<boolean>(false);
const activeFolderType = ref<'folder' | 'module' | 'storage'>('module');
const activeFolder = ref<string>('root');
const selectedKeys = computed({
get: () => [activeFolder.value],
set: (val) => val,
});
const offspringIds = ref<string[]>([]);
const modulesCount = ref<Record<string, number>>({});
const myFileCount = ref(0);
const allFileCount = ref(0);
const isExpandAll = ref(false);
function changeExpand() {
isExpandAll.value = !isExpandAll.value;
}
type FileShowType = 'Module' | 'Storage';
const showType = ref<FileShowType>('Module');
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(keys: string[], _offspringIds: string[]) {
[activeFolder.value] = keys;
activeFolderType.value = 'module';
offspringIds.value = [..._offspringIds];
}
/**
* 设置根模块名称列表
* @param names 根模块名称列表
*/
const folderTree = ref<ModuleTreeNode[]>([]);
const rootModulesName = ref<string[]>([]); //
function setRootModules(treeNode: ModuleTreeNode[]) {
folderTree.value = treeNode;
rootModulesName.value = treeNode.map((e) => e.name);
}
/*
* 初始化模块文件数量
*/
async function initModulesCount(params: FileListQueryParams) {
try {
modulesCount.value = await props.getCountRequest(params);
myFileCount.value = modulesCount.value.my || 0;
allFileCount.value = modulesCount.value.all || 0;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const tableFilterParams = ref<FileListQueryParams>({
moduleIds: [],
fileType: '',
projectId: '',
});
function changeShowType(val: string | number | boolean) {
showType.value = val as FileShowType;
if (val === 'Storage') {
initModulesCount({
...tableFilterParams.value,
combine: {
...tableFilterParams.value.combine,
storage: 'git',
},
});
} else {
initModulesCount(tableFilterParams.value);
}
}
/**
* 右侧表格数据刷新后若当前展示的是模块则刷新模块树的统计数量
*/
function handleModuleTableInit(params: FileListQueryParams) {
initModulesCount(params);
tableFilterParams.value = { ...params };
}
const storageDrawerVisible = ref(false);
/**
* 处理存储库列表项选中事件
*/
const storageList = ref<Repository[]>([]);
function storageItemSelect(key: string, storages: Repository[]) {
storageList.value = storages;
activeFolder.value = key;
activeFolderType.value = 'storage';
}
const selectFile = ref<AssociatedList[]>([]);
function handleDrawerConfirm() {
emit('save', selectFile.value);
showDrawer.value = false;
}
function handleDrawerCancel() {
showDrawer.value = false;
}
</script>
<style lang="less" scoped>
.folder {
@apply flex cursor-pointer items-center justify-between;
padding: 8px 4px;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-text {
@apply flex cursor-pointer items-center;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.folder-text--active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
.file-show-type {
@apply grid grid-cols-2;
margin-bottom: 8px;
:deep(.arco-radio-button-content) {
@apply text-center;
}
}
:deep(.arco-drawer-body) {
padding: 0 16px !important;
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<a-input
v-model:model-value="storageKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="mb-[8px]"
></a-input>
<a-spin class="h-full w-full" :loading="loading">
<MsList
v-model:focus-item-key="focusItemKey"
:virtual-list-props="{
height: 'calc(100vh - 325px)',
}"
:data="storageList"
:bordered="false"
:split="false"
:empty-text="t('project.fileManagement.noStorage')"
item-key-field="id"
class="mr-[-6px]"
>
<template #title="{ item, index }">
<div :key="index" class="storage" @click="setActiveFolder(item.id)">
<div :class="activeStorageNode === item.id ? 'storage-text storage-text--active' : 'storage-text'">
<MsIcon type="icon-icon_git" class="storage-icon" />
<div class="storage-name">{{ item.name }}</div>
<div class="storage-count">({{ item.count }})</div>
</div>
</div>
</template>
</MsList>
</a-spin>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { debounce } from 'lodash-es';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsList from '@/components/pure/ms-list/index.vue';
import { getRepositories } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { Repository } from '@/models/projectManagement/file';
const props = defineProps<{
activeFolder: string | number;
drawerVisible: boolean;
showType: string;
modulesCount?: Record<string, number>; //
}>();
const emit = defineEmits(['update:drawerVisible', 'itemClick', 'update:activeFolder']);
const { t } = useI18n();
const appStore = useAppStore();
const activeStorageNode = computed({
get() {
return props.activeFolder;
},
set(val) {
emit('update:activeFolder', val);
},
});
const storageKeyword = ref('');
const originStorageList = ref<Repository[]>([]);
const storageList = ref(originStorageList.value);
const loading = ref(false);
const searchStorage = debounce(() => {
storageList.value = originStorageList.value.filter((item) => item.name.includes(storageKeyword.value));
}, 300);
watch(
() => storageKeyword.value,
() => {
if (storageKeyword.value === '') {
storageList.value = [...originStorageList.value];
}
searchStorage();
}
);
/**
* 初始化存储库列表
*/
async function initRepositories(setDefaultKeys = false) {
try {
loading.value = true;
const res = await getRepositories(appStore.currentProjectId);
originStorageList.value = res;
storageList.value = originStorageList.value.map((e) => ({
...e,
count: props.modulesCount?.[e.id] || 0,
}));
if (setDefaultKeys) {
activeStorageNode.value = storageList.value[0].id;
emit('itemClick', storageList.value[0].id, storageList.value);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
watch(
() => props.showType,
(val) => {
if (val === 'Storage') {
initRepositories(true);
}
}
);
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
storageList.value = originStorageList.value.map((e) => ({
...e,
count: obj?.[e.id] || 0,
}));
}
);
const focusItemKey = ref('');
function setActiveFolder(id: string) {
emit('itemClick', id, storageList.value);
}
</script>
<style lang="less" scoped>
.storage {
@apply flex cursor-pointer items-center justify-between;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.storage-text {
@apply flex cursor-pointer items-center;
.storage-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.storage-name {
color: var(--color-text-1);
}
.storage-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.storage-text--active {
.storage-icon,
.storage-name,
.storage-count {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -1,53 +1,71 @@
<template> <template>
<MsCard :special-height="-54" no-content-padding divider-has-p-x has-breadcrumb :title="title"> <MsCard
:special-height="-54"
no-content-padding
divider-has-p-x
has-breadcrumb
:title="title"
:loading="loading"
@save="saveHandler"
@save-and-continue="saveHandler"
>
<template #headerRight> <template #headerRight>
<a-select <a-select
v-model="templateId" v-model="form.templateId"
class="w-[240px]" class="w-[240px]"
:options="templateOption" :options="templateOption"
allow-search allow-search
:placeholder="t('bugManagement.edit.defaultSystemTemplate')" :placeholder="t('bugManagement.edit.defaultSystemTemplate')"
@change="templateChange"
/> />
</template> </template>
<a-form ref="formRef" :model="form" layout="vertical"> <a-form ref="formRef" :model="form" layout="vertical">
<div class="flex flex-row" style="height: calc(100vh - 224px)"> <div class="flex flex-row" style="height: calc(100vh - 224px)">
<div class="left mt-[16px] min-w-[732px] grow pl-[24px]"> <div class="left mt-[16px] min-w-[732px] grow pl-[24px]">
<a-form-item <a-form-item
field="name" field="title"
:label="t('bugManagement.bugName')" :label="t('bugManagement.bugName')"
:rules="[{ required: true, message: t('bugManagement.edit.nameIsRequired') }]" :rules="[{ required: true, message: t('bugManagement.edit.nameIsRequired') }]"
:placeholder="t('bugManagement.edit.pleaseInputBugName')" :placeholder="t('bugManagement.edit.pleaseInputBugName')"
> >
<a-input v-model="form.name" :max-length="255" show-word-limit /> <a-input v-model="form.title" :max-length="255" show-word-limit />
</a-form-item> </a-form-item>
<a-form-item :label="t('bugManagement.edit.content')"> <a-form-item field="description" :label="t('bugManagement.edit.content')">
<MsRichText v-model="form.content" /> <MsRichText v-model="form.description" />
</a-form-item>
<a-form-item field="attachment" :label="t('bugManagement.edit.file')">
<div class="flex flex-col">
<div class="mb-1">
<a-dropdown position="tr" trigger="hover">
<a-button type="outline">
<template #icon> <icon-plus class="text-[14px]" /> </template>
{{ t('bugManagement.edit.uploadFile') }}
</a-button>
<template #content>
<a-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:show-file-list="false"
:before-upload="beforeUpload"
@change="handleChange"
>
<template #upload-button>
<a-button type="text" class="!text-[var(--color-text-1)]">
<icon-upload />{{ t('bugManagement.edit.localUpload') }}</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('bugManagement.edit.linkFile')
}}</a-button
>
</template>
</a-dropdown>
</div>
</div>
</a-form-item> </a-form-item>
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</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> <div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
<FileList <FileList
:show-tab="false" :show-tab="false"
@ -60,33 +78,25 @@
</FileList> </FileList>
</div> </div>
<a-divider class="ml-[16px]" direction="vertical" /> <a-divider class="ml-[16px]" direction="vertical" />
<div class="right mt-[16px] grow pr-[24px]"> <div class="right mt-[16px] max-w-[433px] grow pr-[24px]">
<a-form-item <!-- <a-form-item
:label="t('bugManagement.handleMan')" :label="t('bugManagement.handleMan')"
field="handleMan" field="handleMan"
:rules="[{ required: true, message: t('bugManagement.edit.handleManIsRequired') }]" :rules="[{ required: true, message: t('bugManagement.edit.handleManIsRequired') }]"
> >
<MsUserSelector <MsUserSelector
v-model:model-value="form.handleMan" v-model:model-value="form.handleMan"
:type="UserRequestTypeEnum.PROJECT_PERMISSION_MEMBER"
:load-option-params="{ projectId: appStore.currentProjectId }"
placeholder="bugManagement.edit.handleManPlaceholder" placeholder="bugManagement.edit.handleManPlaceholder"
/> />
</a-form-item> </a-form-item> -->
<a-form-item <MsFormCreate
field="status" v-if="formRules.length"
:label="t('bugManagement.status')" ref="formCreateRef"
:rules="[{ required: true, message: t('bugManagement.edit.statusIsRequired') }]" :form-rule="formRules"
> :form-create-key="FormCreateKeyEnum.BUG_DETAIL"
<a-select />
v-model:model-value="form.status"
:placeholder="t('bugManagement.edit.statusPlaceholder')"
></a-select>
</a-form-item>
<a-form-item field="severity" :label="t('bugManagement.severity')">
<a-select
v-model:model-value="form.severity"
:placeholder="t('bugManagement.edit.severityPlaceholder')"
></a-select>
</a-form-item>
<a-form-item field="tag" :label="t('bugManagement.tag')"> <a-form-item field="tag" :label="t('bugManagement.tag')">
<a-input-tag <a-input-tag
v-model:model-value="form.tag" v-model:model-value="form.tag"
@ -98,21 +108,64 @@
</div> </div>
</a-form> </a-form>
</MsCard> </MsCard>
<div>
<MsUpload
v-model:file-list="fileList"
accept="none"
:auto-upload="false"
:sub-text="acceptType === 'jar' ? '' : t('project.fileManagement.normalFileSubText', { size: 50 })"
multiple
draggable
size-unit="MB"
:max-size="50"
:is-all-screen="true"
class="mb-[16px]"
@change="handleChange"
/>
</div>
<RelateFileDrawer
v-model:visible="associatedDrawer"
:get-tree-request="getModules"
:get-count-request="getModulesCount"
:get-list-request="getAssociatedFileList"
@save="saveSelectAssociatedFile"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { FileItem } from '@arco-design/web-vue'; import { FileItem, Message } from '@arco-design/web-vue';
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/form-create.vue';
import { FormItem } from '@/components/pure/ms-form-create/types';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue'; import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import FileList from '@/components/pure/ms-upload/fileList.vue'; import FileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue'; import MsUpload from '@/components/pure/ms-upload/index.vue';
import { MsUserSelector } from '@/components/business/ms-user-selector'; import { MsFileItem } from '@/components/pure/ms-upload/types';
import RelateFileDrawer from './components/relateFile/relateFileDrawer.vue';
import { getTemplageOption } from '@/api/modules/bug-management'; // import { MsUserSelector } from '@/components/business/ms-user-selector';
// import { UserRequestTypeEnum } from '@/components/business/ms-user-selector/utils';
import {
createBug,
getAssociatedFileList,
getBugDetail,
getTemplageOption,
getTemplateById,
} from '@/api/modules/bug-management';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import useFormCreateStore from '@/store/modules/form-create/form-create';
import { scrollIntoView } from '@/utils/dom';
import { BugEditCustomField, BugEditCustomFieldItem, BugEditFormObject } from '@/models/bug-management';
import { AssociatedList } from '@/models/caseManagement/featureCase';
import { SelectValue } from '@/models/projectManagement/menuManagement';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { convertToFile } from '../case-management/caseManagementFeature/components/utils';
const { t } = useI18n(); const { t } = useI18n();
@ -122,36 +175,73 @@
} }
const appStore = useAppStore(); const appStore = useAppStore();
const formCreateStore = useFormCreateStore();
const route = useRoute(); const route = useRoute();
const templateOption = ref<TemplateOption[]>([]); const templateOption = ref<TemplateOption[]>([]);
const form = ref({ const form = ref<BugEditFormObject>({
name: '', projectId: appStore.currentProjectId,
content: '', title: '',
description: '',
templateId: '', templateId: '',
handleMan: [],
status: '',
severity: '',
tag: [], tag: [],
}); });
const formRef = ref<any>(null); const formRef = ref();
const formCreateRef = ref();
const fileList = ref<FileItem[]>([]); const fileList = ref<FileItem[]>([]);
const formRules = ref<FormItem[]>([]);
const associatedDrawer = ref(false);
const loading = ref(false);
const acceptType = ref('none'); // -
// id
const templateId = ref<string>('');
const isEdit = computed(() => !!route.query.id); const isEdit = computed(() => !!route.query.id);
const title = computed(() => { const title = computed(() => {
return isEdit.value ? t('bugManagement.editBug') : t('bugManagement.createBug'); return isEdit.value ? t('bugManagement.editBug') : t('bugManagement.createBug');
}); });
//
const getFormRules = (arr: BugEditCustomField[]) => {
formRules.value = [];
if (Array.isArray(arr) && arr.length) {
formRules.value = arr.map((item) => {
return {
type: item.type,
name: item.fieldId,
label: item.fieldName,
value: item.value,
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : [],
required: item.required as boolean,
props: {
modelValue: item.value,
options: item.platformOptionJson ? JSON.parse(item.platformOptionJson) : [],
},
};
});
}
};
const templateChange = async (v: SelectValue) => {
if (v) {
try {
const res = await getTemplateById({ projectId: appStore.currentProjectId, id: v });
getFormRules(res.customFields);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
};
const getTemplateOptions = async () => { const getTemplateOptions = async () => {
try { try {
const res = await getTemplageOption({ projectId: appStore.currentProjectId }); const res = await getTemplageOption({ projectId: appStore.currentProjectId });
templateOption.value = res.map((item) => { templateOption.value = res.map((item) => {
if (item.enableDefault && !isEdit.value) { if (item.enableDefault && !isEdit.value) {
templateId.value = item.id; //
form.value.templateId = item.id;
templateChange(item.id);
} }
return { return {
label: item.name, label: item.name,
@ -196,10 +286,95 @@
return Promise.resolve(fileItem); return Promise.resolve(fileItem);
}; };
const handleLineFile = () => {}; 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 associatedFile() {
associatedDrawer.value = true;
}
function handleChange(_fileList: MsFileItem[]) {
fileList.value = _fileList.map((e) => {
return {
...e,
enable: true, //
local: true, //
};
});
}
//
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
fileList.value.push(...fileResultList);
}
//
const saveHandler = async () => {
formRef.value.validate((error: any) => {
if (!error) {
formCreateRef.value.formApi.validate(async (valid: any) => {
if (valid === true) {
try {
loading.value = true;
const customFields: BugEditCustomFieldItem[] = [];
const formRuleList = formCreateStore.formCreateRuleMap.get(FormCreateKeyEnum.BUG_DETAIL);
if (formRuleList && formRuleList.length) {
formRuleList.forEach((item) => {
customFields.push({
id: item.field as string,
name: item.title as string,
type: item.sourceType as string,
value: item.value as string,
});
});
}
const tmpObj = {
...form.value,
tag: form.value.tag.join(',') || '',
customFields,
};
await createBug({ request: tmpObj, fileList: fileList.value as unknown as File[] });
Message.success(t('common.createSuccess'));
} catch (err) {
// eslint-disable-next-line no-console
console.log(err);
} finally {
loading.value = false;
}
}
});
}
});
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
};
const getDetailInfo = async () => {
const id = route.query.id as string;
// TODO:
const res = await getBugDetail(id);
const { customFields, file } = res;
formRules.value = customFields;
fileList.value = file;
};
const initDefaultFields = () => {
getTemplateOptions();
};
onBeforeMount(() => { onBeforeMount(() => {
getTemplateOptions(); if (isEdit.value) {
//
getDetailInfo();
} else {
initDefaultFields();
}
}); });
</script> </script>

View File

@ -181,6 +181,7 @@
noDisable: false, noDisable: false,
size: 'default', size: 'default',
showSetting: true, showSetting: true,
heightUsed: 286,
}, },
undefined, undefined,
(record) => handleNameChange(record) (record) => handleNameChange(record)

View File

@ -166,6 +166,7 @@
noDisable: false, noDisable: false,
size: 'default', size: 'default',
showSetting: true, showSetting: true,
heightUsed: 286,
}, },
undefined, undefined,
handleNameChange handleNameChange

View File

@ -161,6 +161,7 @@
{ {
tableKey: TableKeyEnum.SYSTEM_PROJECT, tableKey: TableKeyEnum.SYSTEM_PROJECT,
scroll: { x: '1600px' }, scroll: { x: '1600px' },
heightUsed: 286,
selectable: false, selectable: false,
noDisable: false, noDisable: false,
size: 'default', size: 'default',