feat(缺陷管理): 编辑缺陷假页面

This commit is contained in:
RubyLiu 2023-11-28 18:40:15 +08:00 committed by 刘瑞斌
parent 730583ecf9
commit 349fa22764
13 changed files with 543 additions and 28 deletions

View File

@ -2,7 +2,7 @@ import MSR from '@/api/http/index';
import * as bugURL from '@/api/requrls/bug-management'; import * as bugURL from '@/api/requrls/bug-management';
import { BugListItem } from '@/models/bug-management'; import { BugListItem } from '@/models/bug-management';
import { CommonList, TableQueryParams } from '@/models/common'; import { CommonList, TableQueryParams, TemplateOption } from '@/models/common';
/** /**
* *
@ -33,8 +33,8 @@ export function deleteBatchBug(data: TableQueryParams) {
return MSR.post({ url: bugURL.postBatchDeleteBugUrl, data }); return MSR.post({ url: bugURL.postBatchDeleteBugUrl, data });
} }
export function getTemplageOption(data: TableQueryParams) { export function getTemplageOption(params: { projectId: string }) {
return MSR.get({ url: bugURL.getTemplageOption, data }); return MSR.get<TemplateOption[]>({ url: bugURL.getTemplageOption, params });
} }
export function getTemplateById(data: TableQueryParams) { export function getTemplateById(data: TableQueryParams) {

View File

@ -5,4 +5,4 @@ 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';
export const getTemplageOption = '/bug/template-option'; export const getTemplageOption = '/bug/template/option';

View File

@ -26,7 +26,9 @@
</div> </div>
</div> </div>
</a-scrollbar> </a-scrollbar>
<div :class="{ 'px-[24px]': props.dividerHasPX }">
<a-divider v-if="!props.simple && !props.hideDivider" class="mb-[16px] mt-0" /> <a-divider v-if="!props.simple && !props.hideDivider" class="mb-[16px] mt-0" />
</div>
<div class="ms-card-container"> <div class="ms-card-container">
<a-scrollbar :class="props.noContentPadding ? '' : 'pr-[5px]'" :style="getComputedContentStyle"> <a-scrollbar :class="props.noContentPadding ? '' : 'pr-[5px]'" :style="getComputedContentStyle">
<div class="relative h-full w-full" :style="{ minWidth: `${props.minWidth || 1000}px` }"> <div class="relative h-full w-full" :style="{ minWidth: `${props.minWidth || 1000}px` }">
@ -86,6 +88,7 @@
isFullscreen?: boolean; // isFullscreen?: boolean; //
hideDivider?: boolean; // 线 hideDivider?: boolean; // 线
handleBack: () => void; // handleBack: () => void; //
dividerHasPX: boolean; // 线padding;
}> }>
>(), >(),
{ {
@ -99,6 +102,7 @@
hasBreadcrumb: false, hasBreadcrumb: false,
noContentPadding: false, noContentPadding: false,
noBottomRadius: false, noBottomRadius: false,
dividerHasPX: false,
} }
); );

View File

@ -1,10 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
// import { unified } from 'unified'; /**
// import rehypeParse from 'rehype-parse'; *
// import rehypeFormat from 'rehype-format'; * @name: MsRichText.vue
// import rehypeStringify from 'rehype-stringify'; * @param {string} modelValue v-model绑定的值
import { useLocalStorage } from '@vueuse/core'; * @return {string} 返回编辑器内容
* @description: 富文本编辑器
* @example:
* import { unified } from 'unified';
* import rehypeParse from 'rehype-parse';
* import rehypeFormat from 'rehype-format';
* import rehypeStringify from 'rehype-stringify';
* return unified().use(rehypeParse).use(rehypeFormat).use(rehypeStringify).processSync(content.value);
*/
import useLocale from '@/locale/useLocale'; import useLocale from '@/locale/useLocale';
import '@halo-dev/richtext-editor/dist/style.css'; import '@halo-dev/richtext-editor/dist/style.css';
@ -57,7 +64,7 @@
}>(); }>();
const emit = defineEmits(['update:model-value']); const emit = defineEmits(['update:model-value']);
const content = useLocalStorage('content', ''); const content = ref('');
const editor = useEditor({ const editor = useEditor({
content: content.value, content: content.value,
@ -122,22 +129,11 @@
], ],
onUpdate: () => { onUpdate: () => {
content.value = `${editor.value?.getHTML()}`; content.value = `${editor.value?.getHTML()}`;
console.log(content.value);
}, },
}); });
// const formatContent = computed(() => {
// return unified().use(rehypeParse).use(rehypeFormat).use(rehypeStringify).processSync(content.value);
// });
// watchEffect(() => {
// console.log(String(formatContent.value));
// });
const { currentLocale } = useLocale(); const { currentLocale } = useLocale();
// const locale = useLocalStorage('locale', 'zh-CN');
const locale = computed(() => currentLocale.value as 'zh-CN' | 'en-US'); const locale = computed(() => currentLocale.value as 'zh-CN' | 'en-US');
watch( watch(
() => props.modelValue, () => props.modelValue,
(val) => { (val) => {
@ -156,13 +152,19 @@
</script> </script>
<template> <template>
<div style="height: 140px" class="rich-wrapper flex w-full"> <div class="rich-wrapper flex w-full">
<RichTextEditor v-if="editor" :editor="editor" :locale="locale" /> <RichTextEditor v-if="editor" :editor="editor" :locale="locale" />
</div> </div>
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.rich-wrapper { .rich-wrapper {
position: relative;
border: 1px solid var(--color-text-n8); border: 1px solid var(--color-text-n8);
:deep(.halo-rich-text-editor .ProseMirror) {
p:first-child {
margin-top: 0;
}
}
} }
</style> </style>

View File

@ -1,5 +1,8 @@
<template> <template>
<div v-if="props.mode === 'remote'" class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white"> <div
v-if="props.mode === 'remote' && props.showTab"
class="sticky top-[0] z-[9999] mb-[8px] flex justify-between bg-white"
>
<a-radio-group v-model:model-value="fileListTab" type="button" size="small"> <a-radio-group v-model:model-value="fileListTab" type="button" size="small">
<a-radio value="all">{{ `${t('ms.upload.all')} (${innerFileList.length})` }}</a-radio> <a-radio value="all">{{ `${t('ms.upload.all')} (${innerFileList.length})` }}</a-radio>
<a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}</a-radio> <a-radio value="waiting">{{ `${t('ms.upload.uploading')} (${totalWaitingFileList.length})` }}</a-radio>
@ -116,11 +119,13 @@
requestParams?: Record<string, any>; // requestParams?: Record<string, any>; //
route?: string; // route?: string; //
routeQuery?: Record<string, string>; // routeQuery?: Record<string, string>; //
showTab?: boolean; // tab
handleDelete?: (item: MsFileItem) => void; handleDelete?: (item: MsFileItem) => void;
handleReupload?: (item: MsFileItem) => void; handleReupload?: (item: MsFileItem) => void;
}>(), }>(),
{ {
mode: 'remote', mode: 'remote',
showTab: true,
} }
); );
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -73,4 +73,5 @@ export default {
'common.copy': 'Copy', 'common.copy': 'Copy',
'common.fork': 'Fork', 'common.fork': 'Fork',
'common.more': 'More', 'common.more': 'More',
'common.recycle': 'Recycle Bin',
}; };

View File

@ -73,4 +73,5 @@ export default {
'common.copy': '复制', 'common.copy': '复制',
'common.fork': '关注', 'common.fork': '关注',
'common.more': '更多', 'common.more': '更多',
'common.recycle': '回收站',
}; };

View File

@ -30,6 +30,12 @@ export interface CommonList<T> {
list: T[]; list: T[];
} }
export interface TemplateOption {
id: string;
name: string;
enableDefault: boolean;
}
export interface BatchApiParams { export interface BatchApiParams {
selectIds: string[]; // 已选 ID 集合,当 selectAll 为 false 时接口会使用该字段 selectIds: string[]; // 已选 ID 集合,当 selectAll 为 false 时接口会使用该字段
excludeIds?: string[]; // 需要忽略的用户 id 集合当selectAll为 true 时接口会使用该字段 excludeIds?: string[]; // 需要忽略的用户 id 集合当selectAll为 true 时接口会使用该字段

View File

@ -15,12 +15,47 @@ const BugManagement: AppRouteRecordRaw = {
hideChildrenInMenu: true, hideChildrenInMenu: true,
}, },
children: [ children: [
// 缺陷管理-首页
{ {
path: 'index', path: 'index',
name: 'bugManagementIndex', name: 'bugManagementIndex',
component: () => import('@/views/bug-management/index.vue'), component: () => import('@/views/bug-management/index.vue'),
meta: { meta: {
locale: 'bugManagement.index',
roles: ['*'], roles: ['*'],
isTopMenu: true,
},
},
// 缺陷管理-编辑缺陷
{
path: 'edit',
name: 'bugManagementBugEdit',
component: () => import('@/views/bug-management/edit.vue'),
meta: {
locale: 'bugManagement.editBug',
roles: ['*'],
breadcrumbs: [
{
name: 'bugManagementIndex',
locale: 'bugManagement.index',
},
{
name: 'bugManagementBugEdit',
locale: 'bugManagement.editBug',
editLocale: 'menu.settings.organization.templateFieldSetting',
},
],
},
},
// 回收站
{
path: 'recycle',
name: 'bugManagementRecycle',
component: () => import('@/views/bug-management/recycle.vue'),
meta: {
locale: 'bugManagement.recycle',
roles: ['*'],
isTopMenu: true,
}, },
}, },
], ],

View File

@ -0,0 +1,202 @@
<template>
<MsCard :special-height="-54" no-content-padding divider-has-p-x has-breadcrumb :title="title">
<template #headerRight>
<a-select
v-model="templateId"
class="w-[240px]"
:options="templateOption"
allow-search
:placeholder="t('bugManagement.edit.defaultSystemTemplate')"
/>
</template>
<a-form ref="formRef" :model="form" layout="vertical">
<div class="flex flex-row" style="height: calc(100vh - 224px)">
<div class="left mt-[16px] min-w-[732px] grow pl-[24px]">
<a-form-item
field="name"
:label="t('bugManagement.bugName')"
:rules="[{ required: true, message: t('bugManagement.edit.nameIsRequired') }]"
:placeholder="t('bugManagement.edit.pleaseInputBugName')"
>
<a-input v-model="form.name" :max-length="255" show-word-limit />
</a-form-item>
<a-form-item :label="t('bugManagement.edit.content')">
<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-button type="outline">
<template #icon>
<icon-plus />
</template>
{{ t('bugManagement.edit.uploadFile') }}
</a-button>
</MsUpload>
<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>
</div>
<a-divider class="ml-[16px]" direction="vertical" />
<div class="right mt-[16px] grow pr-[24px]">
<a-form-item
:label="t('bugManagement.handleMan')"
field="handleMan"
:rules="[{ required: true, message: t('bugManagement.edit.handleManIsRequired') }]"
>
<MsUserSelector
v-model:model-value="form.handleMan"
placeholder="bugManagement.edit.handleManPlaceholder"
/>
</a-form-item>
<a-form-item
field="status"
:label="t('bugManagement.status')"
:rules="[{ required: true, message: t('bugManagement.edit.statusIsRequired') }]"
>
<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-input-tag
v-model:model-value="form.tag"
:placeholder="t('bugManagement.edit.tagPlaceholder')"
allow-clear
/>
</a-form-item>
</div>
</div>
</a-form>
</MsCard>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { FileItem } from '@arco-design/web-vue';
import MsCard from '@/components/pure/ms-card/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 { MsUserSelector } from '@/components/business/ms-user-selector';
import { getTemplageOption } from '@/api/modules/bug-management';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
const { t } = useI18n();
interface TemplateOption {
label: string;
value: string;
}
const appStore = useAppStore();
const route = useRoute();
const templateOption = ref<TemplateOption[]>([]);
const form = ref({
name: '',
content: '',
templateId: '',
handleMan: [],
status: '',
severity: '',
tag: [],
});
const formRef = ref<any>(null);
const fileList = ref<FileItem[]>([]);
// id
const templateId = ref<string>('');
const isEdit = computed(() => !!route.query.id);
const title = computed(() => {
return isEdit.value ? t('bugManagement.editBug') : t('bugManagement.createBug');
});
const getTemplateOptions = async () => {
try {
const res = await getTemplageOption({ projectId: appStore.currentProjectId });
templateOption.value = res.map((item) => {
if (item.enableDefault && !isEdit.value) {
templateId.value = item.id;
}
return {
label: item.name,
value: item.id,
};
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
};
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 uploadFile = (file: File) => {
const fileItem: FileItem = {
uid: `${Date.now()}`,
name: file.name,
status: 'init',
file,
};
fileList.value.push(fileItem);
return Promise.resolve(fileItem);
};
onBeforeMount(() => {
getTemplateOptions();
});
</script>
<style lang="less" scoped>
:deep(.arco-form-item-extra) {
font-size: 14px;
color: var(--color-text-4);
}
</style>

View File

@ -4,7 +4,7 @@
<template #left> <template #left>
<div class="flex gap-[12px]"> <div class="flex gap-[12px]">
<a-button type="primary" @click="handleCreate">{{ t('bugManagement.createBug') }} </a-button> <a-button type="primary" @click="handleCreate">{{ t('bugManagement.createBug') }} </a-button>
<a-button type="primary" @click="handleSync">{{ t('bugManagement.syncBug') }} </a-button> <a-button type="outline" @click="handleSync">{{ t('bugManagement.syncBug') }} </a-button>
</div> </div>
</template> </template>
</MsAdvanceFilter> </MsAdvanceFilter>
@ -199,8 +199,9 @@
}; };
const handleCreate = () => { const handleCreate = () => {
// eslint-disable-next-line no-console router.push({
console.log('create'); name: 'bugManagementBugEdit',
});
}; };
const handleSync = () => { const handleSync = () => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -1,5 +1,8 @@
export default { export default {
bugManagement: { bugManagement: {
index: '缺陷管理',
editBug: '编辑缺陷',
recycle: '回收站',
createBug: '创建缺陷', createBug: '创建缺陷',
syncBug: '同步缺陷', syncBug: '同步缺陷',
ID: 'ID', ID: 'ID',
@ -14,5 +17,21 @@ export default {
updateUser: '更新人', updateUser: '更新人',
createTime: '创建时间', createTime: '创建时间',
updateTime: '更新时间', updateTime: '更新时间',
edit: {
defaultSystemTemplate: '默认为系统模板',
content: '缺陷内容',
file: '附件',
fileExtra: '支持任意类型文件,单个文件大小不超过 500MB',
pleaseInputBugName: '请输入缺陷名称',
nameIsRequired: '缺陷名称不能为空',
pleaseInputBugContent: '请输入缺陷内容',
tagPlaceholder: '输入内容后回车可直接添加标签',
handleManPlaceholder: '请选择处理人',
handleManIsRequired: '处理人不能为空',
statusPlaceholder: '请选择缺陷状态',
statusIsRequired: '状态不能为空',
severityPlaceholder: '请选择严重程度',
uploadFile: '添加附件',
},
}, },
}; };

View File

@ -0,0 +1,239 @@
<template>
<MsCard simple>
<MsAdvanceFilter :filter-config-list="filterConfigList" :row-count="filterRowCount">
<template #left>
<div class="flex gap-[12px]">
<a-button type="primary" @click="handleCreate">{{ t('bugManagement.createBug') }} </a-button>
<a-button type="primary" @click="handleSync">{{ t('bugManagement.syncBug') }} </a-button>
</div>
</template>
</MsAdvanceFilter>
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
<template #numberOfCase="{ record }">
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="jumpToTestPlan(record)">{{
record.memberCount
}}</span>
</template>
<template #operation="{ record }">
<div class="flex flex-row flex-nowrap">
<MsButton class="!mr-0" @click="handleCopy(record)">{{ t('common.copy') }}</MsButton>
<a-divider direction="vertical" />
<MsButton class="!mr-0" @click="handleEdit(record)">{{ t('common.edit') }}</MsButton>
<a-divider direction="vertical" />
<MsButton class="!mr-0" status="danger" @click="handleDelete(record)">{{ t('common.delete') }}</MsButton>
</div>
</template>
<template #empty> </template>
</MsBaseTable>
</MsCard>
</template>
<script lang="ts" setup>
import { Message } from '@arco-design/web-vue';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterType } from '@/components/pure/ms-advance-filter/type';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/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 { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup';
import { postProjectTableByOrg } from '@/api/modules/setting/organizationAndProject';
import { useI18n } from '@/hooks/useI18n';
import router from '@/router';
import { useAppStore, useTableStore } from '@/store';
import { BugListItem } from '@/models/bug-management';
import { OrgProjectTableItem } from '@/models/setting/system/orgAndProject';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const tableStore = useTableStore();
const appStore = useAppStore();
const projectId = computed(() => appStore.currentProjectId);
const filterVisible = ref(false);
const filterRowCount = ref(0);
const filterConfigList = reactive<FilterFormItem[]>([
{
title: 'bugManagement.ID',
dataIndex: 'num',
type: FilterType.INPUT,
},
{
title: 'bugManagement.bugName',
dataIndex: 'name',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
},
},
{
title: 'bugManagement.severity',
dataIndex: 'severity',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
multiple: true,
},
},
{
title: 'bugManagement.createTime',
dataIndex: 'createTime',
type: FilterType.DATE_PICKER,
},
]);
const heightUsed = computed(() => 286 + (filterVisible.value ? 160 + (filterRowCount.value - 1) * 60 : 0));
const columns: MsTableColumn = [
{
title: 'bugManagement.ID',
dataIndex: 'num',
showTooltip: true,
},
{
title: 'bugManagement.bugName',
editType: ColumnEditTypeEnum.INPUT,
dataIndex: 'name',
showTooltip: true,
},
{
title: 'bugManagement.severity',
slotName: 'memberCount',
showDrag: true,
dataIndex: 'severity',
},
{
title: 'bugManagement.status',
dataIndex: 'status',
showDrag: true,
},
{
title: 'bugManagement.handleMan',
dataIndex: 'handleUser',
showTooltip: true,
showDrag: true,
},
{
title: 'bugManagement.numberOfCase',
dataIndex: 'relationCaseCount',
slotName: 'numberOfCase',
showDrag: true,
},
{
title: 'bugManagement.belongPlatform',
width: 180,
showDrag: true,
dataIndex: 'platform',
},
{
title: 'bugManagement.tag',
showDrag: true,
isStringTag: true,
dataIndex: 'tag',
},
{
title: 'bugManagement.creator',
dataIndex: 'createUser',
showDrag: true,
},
{
title: 'bugManagement.updateUser',
dataIndex: 'updateUser',
showDrag: true,
},
{
title: 'bugManagement.createTime',
dataIndex: 'createTime',
showDrag: true,
},
{
title: 'bugManagement.updateTime',
dataIndex: 'updateTime',
showDrag: true,
},
{
title: 'common.operation',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 230,
},
];
await tableStore.initColumn(TableKeyEnum.BUG_MANAGEMENT, columns, 'drawer');
const handleNameChange = async (record: OrgProjectTableItem) => {
try {
await updateOrAddProjectUserGroup(record);
Message.success(t('common.updateSuccess'));
return true;
} catch (error) {
return false;
}
};
const { propsRes, propsEvent, loadList, setKeyword, setLoadListParams, setProps } = useTable(
postProjectTableByOrg,
{
tableKey: TableKeyEnum.BUG_MANAGEMENT,
selectable: false,
noDisable: false,
showJumpMethod: true,
showSetting: true,
scroll: { x: '1769px' },
},
undefined,
(record) => handleNameChange(record)
);
watchEffect(() => {
setProps({ heightUsed: heightUsed.value });
});
const fetchData = async (v = '') => {
setKeyword(v);
await loadList();
};
const handleCreate = () => {
// eslint-disable-next-line no-console
console.log('create');
};
const handleSync = () => {
// eslint-disable-next-line no-console
console.log('sync');
};
const handleCopy = (record: BugListItem) => {
// eslint-disable-next-line no-console
console.log('create', record);
};
const handleEdit = (record: BugListItem) => {
// eslint-disable-next-line no-console
console.log('create', record);
};
const handleDelete = (record: BugListItem) => {
// eslint-disable-next-line no-console
console.log('create', record);
};
const jumpToTestPlan = (record: BugListItem) => {
router.push({
name: 'testPlan',
query: {
bugId: record.id,
projectId: projectId.value,
},
});
};
onMounted(() => {
setLoadListParams({ projectId: projectId.value });
fetchData();
});
</script>