feat: 高级筛选&缺陷管理基础逻辑

This commit is contained in:
RubyLiu 2023-11-20 19:34:46 +08:00 committed by Craftsman
parent a93aa618c3
commit 1ecf35b325
16 changed files with 754 additions and 20 deletions

View File

@ -0,0 +1,42 @@
import MSR from '@/api/http/index';
import * as bugURL from '@/api/requrls/bug-management';
import { BugListItem } from '@/models/bug-management';
import { CommonList, TableQueryParams } from '@/models/common';
/**
*
* @param data
* @returns
*/
export function getBugList(data: TableQueryParams) {
return MSR.post<CommonList<BugListItem>>({ url: bugURL.postTableListUrl, data });
}
export function updateBug(data: TableQueryParams) {
return MSR.post({ url: bugURL.postUpdateBugUrl, data });
}
export function updateBatchBug(data: TableQueryParams) {
return MSR.post({ url: bugURL.postBatchUpdateBugUrl, data });
}
export function createBug(data: TableQueryParams) {
return MSR.post({ url: bugURL.postCreateBugUrl, data });
}
export function deleteBug(data: TableQueryParams) {
return MSR.get({ url: bugURL.getDeleteBugUrl, data });
}
export function deleteBatchBug(data: TableQueryParams) {
return MSR.post({ url: bugURL.postBatchDeleteBugUrl, data });
}
export function getTemplageOption(data: TableQueryParams) {
return MSR.get({ url: bugURL.getTemplageOption, data });
}
export function getTemplateById(data: TableQueryParams) {
return MSR.get({ url: bugURL.getTemplateUrl, data });
}

View File

@ -0,0 +1,8 @@
export const postTableListUrl = '/bug/page';
export const postUpdateBugUrl = '/bug/update';
export const postBatchUpdateBugUrl = '/bug/batch-update';
export const postCreateBugUrl = '/bug/add';
export const getDeleteBugUrl = '/bug/delete/';
export const postBatchDeleteBugUrl = '/bug/batch-delete';
export const getTemplateUrl = '/bug/template';
export const getTemplageOption = '/bug/template-option';

View File

@ -92,7 +92,6 @@
import { useI18n } from '@/hooks/useI18n';
import { TEST_PLAN_TEST_CASE } from './caseUtils';
import type { SearchKeyType } from './type';
import type { FormInstance } from '@arco-design/web-vue';
const { t } = useI18n();

View File

@ -0,0 +1,285 @@
<template>
<a-form ref="formRef" :model="formModel" layout="vertical">
<div class="w-full overflow-y-auto bg-[var(--color-text-n9)] px-[12px] py-[12px]">
<header class="flex flex-row items-center justify-between">
<div>{{ t('advanceFilter.setFilterCondition') }}</div>
<div class="flex flex-row items-center text-[var(--color-text-2)]">
<div>{{ t('advanceFilter.accordBelow') }}</div>
<div class="ml-[16px]">
<a-select v-model:model-value="accordBelow" size="small">
<a-option value="all">{{ t('advanceFilter.all') }}</a-option>
<a-option value="any">{{ t('advanceFilter.any') }}</a-option>
</a-select>
</div>
<div class="ml-[8px]">{{ t('advanceFilter.condition') }}</div>
</div>
</header>
<article class="overflow-auto-y mt-[12px] flex max-h-[300px] flex-col gap-[8px]">
<section
v-for="(item, idx) in formModel.list"
:key="item.dataIndex || `filter_item_${idx}`"
class="flex flex-row items-center gap-[8px]"
>
<div class="flex-1 grow">
<a-form-item
:field="`list[${idx}].dataIndex`"
hide-asterisk
class="hidden-item"
:rules="[{ required: true, message: t('advanceFilter.plaseSelectFilterDataIndex') }]"
>
<a-select v-model="item.dataIndex" allow-search @change="(v) => dataIndexChange(v, idx)">
<a-option
v-for="option in currentOptionArr"
:key="option.dataIndex"
:value="option.dataIndex"
:disabled="option.disabled"
>
{{ t(option.title as string) }}
</a-option>
</a-select>
</a-form-item>
</div>
<div class="flex-1 grow-0">
<a-form-item
:field="`list[${idx}].operator`"
hide-asterisk
class="hidden-item"
:rules="[{ required: true, message: t('advanceFilter.plaseSelectOperator') }]"
>
<a-select v-model="item.operator" class="w-[100px]" :disabled="!item.dataIndex">
<a-option value="equal">{{ t('advanceFilter.operator.equal') }}</a-option>
<a-option value="notEqual">{{ t('advanceFilter.operator.notEqual') }}</a-option>
</a-select>
</a-form-item>
</div>
<div class="flex-1 grow">
<a-form-item
:field="`list[${idx}].value`"
:rules="[{ required: true, message: t('advanceFilter.plaseInputFilterContent') }]"
hide-asterisk
class="hidden-item"
>
<a-input
v-if="item.type === FilterType.INPUT"
v-model:model-value="item.value"
class="w-full"
allow-clear
:disabled="!item.dataIndex"
:max-length="60"
/>
<a-select
v-else-if="item.type === FilterType.SELECT"
v-model:model-value="item.value"
class="w-full"
allow-clear
allow-search
:option="item.options"
:placeholder="t('advanceFilter.pleaseSelect')"
:disabled="!item.dataIndex"
></a-select>
<a-date-picker
v-else-if="item.type === FilterType.DATE_PICKER"
v-model:model-value="item.value"
class="w-full"
show-time
format="YYYY-MM-DD hh:mm"
:disabled="!item.dataIndex"
/>
<a-range-picker
v-else-if="item.type === FilterType.RANGE_PICKER"
v-model:model-value="item.value"
class="w-full"
show-time
format="YYYY-MM-DD HH:mm"
:disabled="!item.dataIndex"
/>
</a-form-item>
</div>
<div class="delete-btn" :class="{ 'delete-btn:disabled': idx === 0 }" @click="handleDeleteItem(idx)">
<icon-minus-circle />
</div>
</section>
</article>
<footer
class="mt-[12px] flex flex-row items-center justify-between"
:class="{ '!justify-end': !showAddCondition }"
>
<div
v-if="showAddCondition"
class="flex cursor-pointer items-center gap-[4px] text-[rgb(var(--primary-7))]"
@click="handleAddItem"
>
<icon-plus />
<span>{{ t('advanceFilter.addCondition') }}</span>
</div>
<div>
<a-button class="mr-[8px]" @click="handleReset">{{ t('advanceFilter.reset') }}</a-button>
<a-button type="primary" @click="handleFilter">{{ t('advanceFilter.filter') }}</a-button>
</div>
</footer>
</div>
</a-form>
</template>
<script lang="ts" setup>
import { FormInstance } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import { SelectValue } from '@/models/projectManagement/menuManagement';
import { AccordBelowType, BackEndEnum, FilterFormItem, FilterResult, FilterType } from './type';
const { t } = useI18n();
const accordBelow = ref<AccordBelowType>('all');
const formRef = ref<FormInstance | null>(null);
const formModel = reactive<{ list: FilterFormItem[] }>({
list: [],
});
const props = defineProps<{ configList: FilterFormItem[]; visible: boolean; count: number }>();
const emit = defineEmits<{
(e: 'onSearch', value: FilterResult): void;
(e: 'dataIndexChange', value: string): void;
(e: 'update:count', value: number): void;
}>();
//
const getCurrentOptionArr = () => {
const arr1 = props.configList;
const arr2 = formModel.list.map((item) => item.dataIndex);
const intersection = arr1.map((item1) => ({
...item1,
disabled: arr2.includes(item1.dataIndex),
}));
return intersection;
};
const currentOptionArr = computed(() => getCurrentOptionArr());
//
const showAddCondition = computed(() => {
return currentOptionArr.value.some((item) => !item.disabled);
});
const getInitItem = () => {
return {
dataIndex: '',
type: FilterType.INPUT,
operator: '',
value: '',
backendType: BackEndEnum.STRING,
};
};
/**
* @description 添加条件
*/
const handleAddItem = async () => {
formRef.value?.validate((errors) => {
if (!errors) {
formModel.list.push(getInitItem());
}
});
};
/**
* @description 删除条件
*/
const handleDeleteItem = (index: number) => {
if (index === 0) {
return;
}
formModel.list.splice(index, 1);
};
/**
* @description 重置
*/
const handleReset = () => {
formRef.value?.resetFields();
formModel.list = [getInitItem()];
};
/**
* @description 筛选
*/
const handleFilter = () => {
formRef.value?.validate((errors) => {
if (!errors) {
const tmpObj: FilterResult = {};
formModel.list.forEach((item) => {
tmpObj[item.dataIndex as string] = {
operator: item.operator,
value: item.value,
backendType: item.backendType,
};
});
tmpObj.accordBelow = accordBelow.value;
emit('onSearch', tmpObj);
}
});
};
const getAttributeByDataIndex = (dataIndex: string) => {
return props.configList.find((item) => item.dataIndex === dataIndex);
};
/**
* @description 筛选项变化
*/
const dataIndexChange = (dataIndex: SelectValue, idx: number) => {
if (!dataIndex) {
return;
}
const tmpObj = getAttributeByDataIndex(dataIndex as string);
if (!tmpObj) {
return;
}
const { type, backendType } = tmpObj;
formModel.list[idx].operator = '';
formModel.list[idx].backendType = backendType;
formModel.list[idx].type = type;
if (
formModel.list[idx].type === FilterType.RANGE_PICKER ||
formModel.list[idx].type === FilterType.MUTIPLE_SELECT
) {
formModel.list[idx].value = [];
} else {
formModel.list[idx].value = '';
}
emit('dataIndexChange', dataIndex as string);
};
onBeforeMount(() => {
formModel.list = [getInitItem()];
});
watch(
() => props.visible,
(val) => {
if (!val) {
emit('update:count', formModel.list.filter((item) => !!item.dataIndex).length);
}
}
);
</script>
<style lang="less" scoped>
.delete-btn {
padding: 8px;
width: 32px;
height: 32px;
color: var(--color-text-4);
cursor: pointer;
}
.delete-btn:hover {
color: rgb(var(--primary-5));
background-color: rgb(var(--primary-2));
}
.delete-btn:disabled {
@apply cursor-not-allowed hover:bg-transparent hover:text-[var(--color-text-4)];
}
:deep(.arco-form-item-layout-vertical > .arco-form-item-label-col) {
margin-bottom: 0;
}
:deep(.arco-form-item.arco-form-item-error, .arco-form-item.arco-form-item-has-help) {
position: relative;
top: 10px;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<MsTag
:type="props.visible ? 'primary' : 'default'"
:theme="props.visible ? 'lightOutLine' : 'outline'"
size="large"
class="mt-[3px] min-w-[64px] cursor-pointer"
>
<span :class="!props.visible ? 'text-[var(--color-text-4)]' : ''" @click="handleOpenFilter">
<icon-filter class="text-[16px]" />
<span class="ml-[4px]">
<span v-if="props.count">{{ props.count }}</span>
{{ t('common.filter') }}
</span>
</span>
</MsTag>
</template>
<script setup lang="ts">
import MsTag from '../ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
count?: number;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'update:count', value: number): void;
}>();
const handleOpenFilter = () => {
emit('update:visible', !props.visible);
};
</script>

View File

@ -0,0 +1,2 @@
export { default as FilterForm } from './FilterForm.vue';
export { default as FilterIcon } from './FilterIcon.vue';

View File

@ -0,0 +1,21 @@
export default {
'advanceFilter.operator.like': 'like',
'advanceFilter.operator.not_like': 'not like',
'advanceFilter.operator.in': 'in',
'advanceFilter.operator.not_in': 'not in',
'advanceFilter.operator.gt': 'gt',
'advanceFilter.operator.ge': 'ge',
'advanceFilter.operator.lt': 'lt',
'advanceFilter.operator.le': 'le',
'advanceFilter.operator.equals': 'equals',
'advanceFilter.operator.between': 'between',
'advanceFilter.setFilterCondition': 'Set Filter Condition',
'advanceFilter.accordBelow': 'Accord below',
'advanceFilter.all': 'All',
'advanceFilter.any': 'Any',
'advanceFilter.condition': 'Condition',
'advanceFilter.addCondition': 'Add Condition',
'advanceFilter.reset': 'Reset',
'advanceFilter.filter': 'Filter',
'advanceFilter.plaseInputFilterContent': 'Please input filter content',
};

View File

@ -0,0 +1,24 @@
export default {
'advanceFilter.operator.like': '包含',
'advanceFilter.operator.not_like': '不包含',
'advanceFilter.operator.in': '在列表中',
'advanceFilter.operator.not_in': '不在列表中',
'advanceFilter.operator.gt': '大于',
'advanceFilter.operator.ge': '大于等于',
'advanceFilter.operator.lt': '小于',
'advanceFilter.operator.le': '小于等于',
'advanceFilter.operator.equal': '等于',
'advanceFilter.operator.notEqual': '不等于',
'advanceFilter.operator.between': '介于',
'advanceFilter.setFilterCondition': '设置过滤条件',
'advanceFilter.accordBelow': '满足以下',
'advanceFilter.all': '所有',
'advanceFilter.any': '任意',
'advanceFilter.condition': '条件',
'advanceFilter.addCondition': '添加条件',
'advanceFilter.reset': '重置',
'advanceFilter.filter': '过滤',
'advanceFilter.plaseSelectFilterDataIndex': '请选择过滤条件',
'advanceFilter.plaseInputFilterContent': '请输入筛选内容',
'advanceFilter.plaseSelectOperator': '请选择运算符',
};

View File

@ -0,0 +1,34 @@
/* eslint-disable no-shadow */
export enum BackEndEnum {
STRING = 'string',
ARRAY = 'array',
TIME = 'time',
}
export enum FilterType {
INPUT = 'Input',
SELECT = 'Select',
DATE_PICKER = 'DatePicker',
RANGE_PICKER = 'RangePicker',
MUTIPLE_SELECT = 'MutiSelect',
}
export interface FilterFormItem {
dataIndex?: string; // 对应的row的数据key
title?: string; // 显示的label 国际化字符串定义在前端
type?: FilterType; // 类型Input,Select,DatePicker,RangePicker
value?: any; // 值 字符串 和 数组
operator?: string; // 运算符号
options?: any[]; // 下拉框的选项
backendType?: BackEndEnum; // 后端类型 string array time
}
export type AccordBelowType = 'all' | 'any';
export interface FilterResult {
[key: string]: Pick<FilterFormItem, 'value' | 'operator' | 'backendType'> | AccordBelowType;
}
export interface FilterFormProps {
configList: FilterFormItem[];
}

View File

@ -36,7 +36,7 @@
<a-table-column
v-for="(item, idx) in currentColumns"
:key="idx"
:width="item.width"
:width="item.isTag || item.isStringTag ? item.width || 360 : item.width"
:align="item.align"
:fixed="item.fixed"
:sortable="item.sortable"

View File

@ -28,6 +28,7 @@ export enum TableKeyEnum {
FILE_MANAGEMENT_VERSION = 'fileManagementVersion',
PROJECT_MANAGEMENT_MENU_FALSE_ALERT = 'projectManagementMenuFalseAlert',
ORGANIZATION_TEMPLATE_DEFECT_TABLE = 'organizationTemplateManagementDefect',
BUG_MANAGEMENT = 'bugManagement',
}
// 具有特殊功能的列

View File

@ -21,6 +21,7 @@ export default {
'common.operation': '操作',
'common.remove': '移除',
'common.revoked': '已撤销',
'common.filter': '筛选',
'common.createSuccess': '创建成功',
'common.createFailed': '创建失败',
'common.updateSuccess': '更新成功',

View File

@ -0,0 +1,17 @@
export interface BugListItem {
id: string; // 缺陷id
num: string; // 缺陷编号
name: string; // 缺陷名称
severity: string; // 缺陷严重程度
status: string; // 缺陷状态
handleUser: string; // 缺陷处理人
relationCaseCount: number; // 关联用例数
platform: string; // 所属平台
tag: string; // 缺陷标签
createUser: string; // 创建人
updateUser: string; // 更新人
createTime: string; // 创建时间
updateTime: string; // 更新时间
deleted: string; // 删除标志
}
export default {};

View File

@ -1,26 +1,254 @@
<template>
<MsCard simple>
<h1>BugManagement is waiting for development </h1>
<ms-pagination size="small" :total="100000" show-total show-jumper show-page-size />
<a-range-picker
style="margin: 0 24px 24px 0; width: 360px"
show-time
:time-picker-props="{ defaultValue: ['00:00:00', '09:09:06'] }"
format="YYYY-MM-DD HH:mm"
@change="onChange"
@select="onSelect"
@ok="onOk"
<div class="flex flex-row justify-between">
<div class="flex gap-[12px]">
<a-button type="primary" @click="handleCreate">
{{ t('bugManagement.createBug') }}
</a-button>
<a-button type="outline" @click="handleSync">
{{ t('bugManagement.syncBug') }}
</a-button>
</div>
<div class="flex flex-row gap-[8px]">
<a-input-search
v-model="keyword"
:placeholder="t('system.user.searchUser')"
class="w-[240px]"
allow-clear
@press-enter="fetchData"
@search="fetchData"
></a-input-search>
<FilterIcon v-model:visible="filterVisible" :count="filterCount" />
</div>
</div>
<FilterForm
v-show="filterVisible"
v-model:count="filterCount"
:visible="filterVisible"
:config-list="filterConfigList"
class="mt-[8px]"
@on-search="handleFilter"
@data-index-change="dataIndexChange"
/>
<!-- <MsRichText v-model="content" /> -->
<MsTimeSelector v-model="timeValue" />
<div>value: {{ timeValue }}</div>
<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 setup>
import MsCard from '@/components/pure/ms-card/index.vue';
import MsTimeSelector from '@/components/pure/ms-time-selector/MsTimeSelector.vue';
<script lang="ts" setup>
import { Message } from '@arco-design/web-vue';
const timeValue = ref('3M');
import { FilterForm, FilterIcon } from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterResult, 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 keyword = ref('');
const tableStore = useTableStore();
const appStore = useAppStore();
const projectId = computed(() => appStore.currentProjectId);
const filterVisible = ref(false);
const filterCount = ref(0);
const filterConfigList = reactive<FilterFormItem[]>([
{
title: 'bugManagement.ID',
dataIndex: 'num',
type: FilterType.INPUT,
},
{
title: 'bugManagement.bugName',
dataIndex: 'name',
type: FilterType.SELECT,
},
{
title: 'bugManagement.severity',
dataIndex: 'severity',
type: FilterType.MUTIPLE_SELECT,
},
]);
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 } = useTable(
postProjectTableByOrg,
{
tableKey: TableKeyEnum.BUG_MANAGEMENT,
selectable: false,
noDisable: false,
showJumpMethod: true,
showSetting: true,
scroll: { x: '1769px' },
},
undefined,
(record) => handleNameChange(record)
);
const fetchData = async () => {
setKeyword(keyword.value);
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 handleFilter = (filter: FilterResult) => {
// eslint-disable-next-line no-console
console.log('filter', filter);
};
const dataIndexChange = (dataIndex: string) => {
// eslint-disable-next-line no-console
console.log('dataIndexChange', dataIndex);
};
const jumpToTestPlan = (record: BugListItem) => {
router.push({
name: 'testPlan',
query: {
bugId: record.id,
projectId: projectId.value,
},
});
};
onMounted(() => {
setLoadListParams({ projectId: projectId.value });
fetchData();
});
</script>

View File

@ -0,0 +1,18 @@
export default {
bugManagement: {
createBug: 'Create Bug',
syncBug: 'Sync Bug',
ID: 'ID',
bugName: 'Bug Name',
severity: 'Severity',
status: 'Status',
handleMan: 'Handler',
numberOfCase: 'Number of Cases',
belongPlatform: 'Belong Platform',
tag: 'Tag',
creator: 'Creator',
updateUser: 'Updater',
createTime: 'Create Time',
updateTime: 'Update Time',
},
};

View File

@ -0,0 +1,18 @@
export default {
bugManagement: {
createBug: '创建缺陷',
syncBug: '同步缺陷',
ID: 'ID',
bugName: '缺陷名称',
severity: '严重程度',
status: '状态',
handleMan: '处理人',
numberOfCase: '用例数',
belongPlatform: '所属平台',
tag: '标签',
creator: '创建人',
updateUser: '更新人',
createTime: '创建时间',
updateTime: '更新时间',
},
};