feat(系统设置): 添加项目管理员必填&用户选择器替换为MsSelect

This commit is contained in:
RubyLiu 2024-02-07 15:57:31 +08:00 committed by wxg0103
parent 5662e5abe7
commit ed9b95eba1
7 changed files with 104 additions and 132 deletions

View File

@ -11,7 +11,7 @@
</div> </div>
<div v-else> <div v-else>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<MsUserSelector v-bind="$attrs" v-model="memberList" class="w-[262px]" /> <MsUserSelector v-bind="$attrs" v-model="memberList" class="min-w-[262px]" />
<a-button <a-button
type="outline" type="outline"
:disabled="!memberList.length" :disabled="!memberList.length"
@ -22,12 +22,14 @@
> >
{{ t('common.confirm') }} {{ t('common.confirm') }}
</a-button> </a-button>
<div <a-button
class="ml-[8px] flex h-[24px] w-[48px] cursor-pointer items-center justify-center rounded-[4px] border border-[var(--color-text-input-border)] px-[11px] text-[12px] leading-[16px] text-[var(--color-text-1)] hover:bg-[rgb(var(--primary-1))]" type="outline"
size="mini"
class="ml-[12px] !border-[var(--color-text-input-border)] !text-[var(--color-text-1)]"
@click="handleCancel" @click="handleCancel"
> >
{{ t('common.cancel') }} {{ t('common.cancel') }}
</div> </a-button>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,36 +1,35 @@
<template> <template>
<a-select <MsSelect
:model-value="currentValue" v-model:model-value="currentValue"
:placeholder="t(props.placeholder || 'common.pleaseSelectMember')" mode="remote"
:options="[]"
:placeholder="props.placeholder || 'common.pleaseSelectMember'"
multiple multiple
:value-key="props.valueKey" :value-key="props.valueKey"
:disabled="props.disabled" :disabled="props.disabled"
:at-least-one="props.atLeastOne"
:label-key="props.firstLabelKey"
:filter-option="false" :filter-option="false"
allow-clear allow-clear
:loading="loading" :loading="loading"
@change="change" :search-keys="[props.firstLabelKey, props.secondLabelKey]"
@search="debouncedSearch" :remote-func="loadList"
:remote-extra-params="{ ...props.loadOptionParams, type: props.type }"
:option-label-render="optionLabelRender"
:should-calculate-max-tag="false"
> >
<template #label="{ data }"> </MsSelect>
<span class="text-[var(--color-text-1)]"> {{ data.value.name }} </span>
</template>
<a-option v-for="data in allOptions" :key="data.id" :disabled="data.disabled" :value="data">
<span :class="data.disabled ? 'text-[var(--color-text-4)]' : 'text-[var(--color-text-1)]'">
{{ data.name }}
</span>
<span v-if="data.email" class="text-[var(--color-text-4)]"> {{ `(${data.email})` }} </span>
</a-option>
</a-select>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { SelectOptionData } from '@arco-design/web-vue';
import { debounce } from 'lodash-es';
import { useI18n } from '@/hooks/useI18n'; import MsSelect from '@/components/business/ms-select/index';
import initOptionsFunc, { UserRequestTypeEnum } from './utils'; import initOptionsFunc, { UserRequestTypeEnum } from './utils';
defineOptions({ name: 'MsUserSelector' });
export interface MsUserSelectorOption { export interface MsUserSelectorOption {
id: string; id: string;
name: string; name: string;
@ -41,7 +40,6 @@
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue: string[] | string; //
disabled?: boolean; // disabled?: boolean; //
disabledKey?: string; // key disabledKey?: string; // key
valueKey?: string; // valuekey valueKey?: string; // valuekey
@ -50,6 +48,7 @@
secondLabelKey?: string; // key secondLabelKey?: string; // key
loadOptionParams?: Record<string, any>; // loadOptionParams?: Record<string, any>; //
type?: UserRequestTypeEnum; // type?: UserRequestTypeEnum; //
atLeastOne?: boolean; //
}>(), }>(),
{ {
disabled: false, disabled: false,
@ -58,27 +57,18 @@
firstLabelKey: 'name', firstLabelKey: 'name',
secondLabelKey: 'email', secondLabelKey: 'email',
type: UserRequestTypeEnum.SYSTEM_USER_GROUP, type: UserRequestTypeEnum.SYSTEM_USER_GROUP,
atLeastOne: false,
} }
); );
const emit = defineEmits<{ const currentValue = defineModel<string[] | string>({ default: [] });
(e: 'update:modelValue', value: string[]): void;
(e: 'select', value: string[]): void;
}>();
const { t } = useI18n();
const allOptions = ref<MsUserSelectorOption[]>([]);
const currentLoadParams = ref<Record<string, any>>(props.loadOptionParams || {});
const loading = ref(true); const loading = ref(true);
const currentValue = computed(() => { const loadList = async (params: Record<string, any>) => {
return allOptions.value.filter((item) => props.modelValue.includes(item.id)) || [];
});
const loadList = async () => {
try { try {
loading.value = true; loading.value = true;
const list = (await initOptionsFunc(props.type, currentLoadParams.value || {})) || []; const { type, keyword, ...rest } = params;
const list = (await initOptionsFunc(type, { keyword, ...rest })) || [];
const { firstLabelKey, secondLabelKey, disabledKey, valueKey } = props; const { firstLabelKey, secondLabelKey, disabledKey, valueKey } = props;
list.forEach((item: MsUserSelectorOption) => { list.forEach((item: MsUserSelectorOption) => {
if (firstLabelKey) { if (firstLabelKey) {
@ -94,55 +84,16 @@
item.id = item[valueKey] as string; item.id = item[valueKey] as string;
} }
}); });
allOptions.value = [...list]; return list;
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
allOptions.value = []; return [];
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
const optionLabelRender = (option: SelectOptionData) => {
const debouncedSearch = async (value: string) => { return `<span class='text-[var(--color-text-1)]'>${option.name}</span><span class='text-[var(--color-text-4)] ml-[4px]'>(${option.email})</span>`;
if (!value) {
currentLoadParams.value = {
...currentLoadParams.value,
keyword: value,
}; };
await loadList();
} else {
const fn = debounce(
() => {
currentLoadParams.value = {
...currentLoadParams.value,
keyword: value,
};
loadList();
},
300,
{ maxWait: 1000 }
);
fn();
}
};
const change = (
value: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) => {
const tmpArr = Array.isArray(value) ? value : [value];
const { valueKey } = props;
emit(
'update:modelValue',
tmpArr.map((item) => item[valueKey])
);
emit(
'select',
tmpArr.map((item) => item[valueKey])
);
};
onBeforeMount(async () => {
await loadList();
});
</script> </script>

View File

@ -194,6 +194,10 @@
import { convertToFileByBug } from '@/views/bug-management/utils'; import { convertToFileByBug } from '@/views/bug-management/utils';
import { convertToFile } from '@/views/case-management/caseManagementFeature/components/utils'; import { convertToFile } from '@/views/case-management/caseManagementFeature/components/utils';
defineOptions({
name: 'BugDetailTab',
});
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
@ -408,7 +412,7 @@
watch( watch(
() => fileList.value, () => fileList.value,
async (val) => { async (val) => {
const isNewFiles = val.filter((item) => item.status === 'init').length; const isNewFiles = val.filter((item) => item.status === 'init' || (!item.local && !item.associateId)).length;
if (val && isNewFiles) { if (val && isNewFiles) {
startUpload(); startUpload();
} }

View File

@ -43,14 +43,20 @@
> >
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item field="userIds" :label="t('system.project.projectAdmin')"> <a-form-item
field="userIds"
asterisk-position="end"
:rules="[{ required: true, message: t('system.project.projectAdminIsNotNull') }]"
:label="t('system.project.projectAdmin')"
>
<MsUserSelector <MsUserSelector
v-model="form.userIds" v-model="form.userIds"
:type="UserRequestTypeEnum.ORGANIZATION_PROJECT_ADMIN" :type="UserRequestTypeEnum.ORGANIZATION_PROJECT_ADMIN"
placeholder="system.project.projectAdminPlaceholder" placeholder="system.project.pleaseSelectAdmin"
:load-option-params="{ :load-option-params="{
organizationId: currentOrgId, organizationId: currentOrgId,
}" }"
:at-least-one="true"
/> />
</a-form-item> </a-form-item>
<a-form-item field="module" :label="t('system.project.moduleSetting')"> <a-form-item field="module" :label="t('system.project.moduleSetting')">
@ -115,7 +121,7 @@
import { createOrUpdateProjectByOrg, getSystemOrgOption } from '@/api/modules/setting/organizationAndProject'; import { createOrUpdateProjectByOrg, getSystemOrgOption } from '@/api/modules/setting/organizationAndProject';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import { useAppStore, useUserStore } from '@/store';
import useLicenseStore from '@/store/modules/setting/license'; import useLicenseStore from '@/store/modules/setting/license';
import { CreateOrUpdateSystemProjectParams, SystemOrgOption } from '@/models/setting/system/orgAndProject'; import { CreateOrUpdateSystemProjectParams, SystemOrgOption } from '@/models/setting/system/orgAndProject';
@ -128,7 +134,6 @@
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
visible: boolean;
currentProject?: CreateOrUpdateSystemProjectParams; currentProject?: CreateOrUpdateSystemProjectParams;
}>(); }>();
@ -137,6 +142,7 @@
const loading = ref(false); const loading = ref(false);
const isEdit = computed(() => !!(props.currentProject && props.currentProject.id)); const isEdit = computed(() => !!(props.currentProject && props.currentProject.id));
const affiliatedOrgOption = ref<SystemOrgOption[]>([]); const affiliatedOrgOption = ref<SystemOrgOption[]>([]);
const userStore = useUserStore();
const appStore = useAppStore(); const appStore = useAppStore();
const currentOrgId = computed(() => appStore.currentOrgId); const currentOrgId = computed(() => appStore.currentOrgId);
const licenseStore = useLicenseStore(); const licenseStore = useLicenseStore();
@ -180,20 +186,18 @@
moduleIds: allModuleIds, moduleIds: allModuleIds,
}); });
const currentVisible = ref(props.visible); const currentVisible = defineModel<boolean>('visible', {
default: false,
});
const showPool = computed(() => showPoolModuleIds.some((item) => form.moduleIds?.includes(item))); const showPool = computed(() => showPoolModuleIds.some((item) => form.moduleIds?.includes(item)));
const isXpack = computed(() => { const isXpack = computed(() => {
return licenseStore.hasLicense(); return licenseStore.hasLicense();
}); });
watchEffect(() => {
currentVisible.value = props.visible;
});
const formReset = () => { const formReset = () => {
form.name = ''; form.name = '';
form.userIds = []; form.userIds = userStore.id ? [userStore.id] : [];
form.organizationId = currentOrgId.value; form.organizationId = currentOrgId.value;
form.description = ''; form.description = '';
form.enable = true; form.enable = true;
@ -252,8 +256,12 @@
console.error(error); console.error(error);
} }
}; };
watchEffect(() => { watchEffect(() => {
if (isEdit.value && props.currentProject) { initAffiliatedOrgOption();
if (props.currentProject?.id) {
//
if (props.currentProject) {
form.id = props.currentProject.id; form.id = props.currentProject.id;
form.name = props.currentProject.name; form.name = props.currentProject.name;
form.description = props.currentProject.description; form.description = props.currentProject.description;
@ -263,11 +271,9 @@
form.moduleIds = props.currentProject.moduleIds; form.moduleIds = props.currentProject.moduleIds;
form.resourcePoolIds = props.currentProject.resourcePoolIds; form.resourcePoolIds = props.currentProject.resourcePoolIds;
} }
}); } else {
onMounted(() => { //
initAffiliatedOrgOption();
});
onUnmounted(() => {
formReset(); formReset();
}
}); });
</script> </script>

View File

@ -50,11 +50,17 @@
> >
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item field="userIds" :label="t('system.project.projectAdmin')"> <a-form-item
field="userIds"
asterisk-position="end"
:rules="[{ required: true, message: t('system.project.projectAdminIsNotNull') }]"
:label="t('system.project.projectAdmin')"
>
<MsUserSelector <MsUserSelector
v-model="form.userIds" v-model="form.userIds"
:type="UserRequestTypeEnum.SYSTEM_PROJECT_ADMIN" :type="UserRequestTypeEnum.SYSTEM_PROJECT_ADMIN"
placeholder="system.project.projectAdminPlaceholder" placeholder="system.project.pleaseSelectAdmin"
:at-least-one="true"
/> />
</a-form-item> </a-form-item>
<a-form-item field="module" :label="t('system.project.moduleSetting')"> <a-form-item field="module" :label="t('system.project.moduleSetting')">
@ -99,7 +105,7 @@
{{ t('common.cancel') }} {{ t('common.cancel') }}
</a-button> </a-button>
<a-button type="primary" :loading="loading" @click="handleBeforeOk"> <a-button type="primary" :loading="loading" @click="handleBeforeOk">
{{ isEdit ? t('common.confirm') : t('common.create') }} {{ isEdit ? t('common.update') : t('common.create') }}
</a-button> </a-button>
</div> </div>
</div> </div>
@ -119,6 +125,7 @@
import { createOrUpdateProject, getSystemOrgOption } from '@/api/modules/setting/organizationAndProject'; import { createOrUpdateProject, getSystemOrgOption } from '@/api/modules/setting/organizationAndProject';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useUserStore } from '@/store';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import useLicenseStore from '@/store/modules/setting/license'; import useLicenseStore from '@/store/modules/setting/license';
@ -127,9 +134,9 @@
import type { FormInstance, ValidatedError } from '@arco-design/web-vue'; import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
visible: boolean;
currentProject?: CreateOrUpdateSystemProjectParams; currentProject?: CreateOrUpdateSystemProjectParams;
}>(); }>();
@ -183,7 +190,9 @@
resourcePoolIds: [], resourcePoolIds: [],
}); });
const currentVisible = ref(props.visible); const currentVisible = defineModel<boolean>('visible', {
default: false,
});
const showPool = computed(() => showPoolModuleIds.some((item) => form.moduleIds?.includes(item))); const showPool = computed(() => showPoolModuleIds.some((item) => form.moduleIds?.includes(item)));
const isXpack = computed(() => { const isXpack = computed(() => {
@ -192,7 +201,7 @@
const formReset = () => { const formReset = () => {
form.name = ''; form.name = '';
form.userIds = []; form.userIds = userStore.id ? [userStore.id] : [];
form.organizationId = ''; form.organizationId = '';
form.description = ''; form.description = '';
form.enable = true; form.enable = true;
@ -256,6 +265,9 @@
} }
}; };
watchEffect(() => { watchEffect(() => {
initAffiliatedOrgOption();
if (props.currentProject?.id) {
//
if (props.currentProject) { if (props.currentProject) {
form.id = props.currentProject.id; form.id = props.currentProject.id;
form.name = props.currentProject.name; form.name = props.currentProject.name;
@ -266,16 +278,9 @@
form.moduleIds = props.currentProject.moduleIds; form.moduleIds = props.currentProject.moduleIds;
form.resourcePoolIds = props.currentProject.resourcePoolIds; form.resourcePoolIds = props.currentProject.resourcePoolIds;
} }
});
watch(
() => props.visible,
(val) => {
currentVisible.value = val;
if (!val) {
formReset();
} else { } else {
initAffiliatedOrgOption(); //
formReset();
} }
} });
);
</script> </script>

View File

@ -79,4 +79,6 @@ export default {
'system.project.searchPlaceholder': 'Search by name or id', 'system.project.searchPlaceholder': 'Search by name or id',
'system.project.afterModule': 'system.project.afterModule':
'After the module is canceled, users will be unable to access the specified module, and existing data will remain intact', 'After the module is canceled, users will be unable to access the specified module, and existing data will remain intact',
'system.project.projectAdminIsNotNull': 'Project administrator cannot be empty',
'system.project.pleaseSelectAdmin': 'Please select project administrator',
}; };

View File

@ -75,4 +75,6 @@ export default {
'system.project.deleteTip': '删除后,系统会在 30天 后执行删除项目 (含项目下所有业务数据),请谨慎操作!', 'system.project.deleteTip': '删除后,系统会在 30天 后执行删除项目 (含项目下所有业务数据),请谨慎操作!',
'system.project.searchPlaceholder': '通过ID或项目名称搜索', 'system.project.searchPlaceholder': '通过ID或项目名称搜索',
'system.project.afterModule': '取消模块后用户将无法进入指定模块,已存在的数据会继续保留', 'system.project.afterModule': '取消模块后用户将无法进入指定模块,已存在的数据会继续保留',
'system.project.projectAdminIsNotNull': '项目管理员不能为空',
'system.project.pleaseSelectAdmin': '请选择项目管理员',
}; };