feat(全局): 新增标签输入组件&部分组件调整&资源池/用户/系统参数页面支持地址栏参数跳转展示详情&日志支持名称跳转

This commit is contained in:
baiqi 2023-08-23 13:48:40 +08:00 committed by fit2-zhao
parent 3a77833613
commit e3b4dae516
26 changed files with 442 additions and 154 deletions

View File

@ -20,6 +20,7 @@ import type {
SystemRole,
ImportResult,
BatchAddUserGroupParams,
ResetUserPasswordParams,
} from '@/models/setting/user';
import type { CommonList, TableQueryParams } from '@/models/common';
@ -55,12 +56,12 @@ export function importUserInfo(data: ImportUserParams) {
// 获取系统用户组
export function getSystemRoles() {
return MSR.get<SystemRole>({ url: GetSystemRoleUrl });
return MSR.get<SystemRole[]>({ url: GetSystemRoleUrl });
}
// 重置用户密码
export function resetUserPassword(userIds: string[]) {
return MSR.post({ url: ResetPasswordUrl, data: userIds });
export function resetUserPassword(data: ResetUserPasswordParams) {
return MSR.post({ url: ResetPasswordUrl, data });
}
// 批量添加用户到多个用户组

View File

@ -15,4 +15,4 @@ export const GetSystemRoleUrl = '/system/user/get/global/system/role';
// 重置用户密码
export const ResetPasswordUrl = '/system/user/reset/password';
// 批量添加用户到多个用户组
export const BatchAddUserGroupUrl = '/user/role/relation/global/add/batch';
export const BatchAddUserGroupUrl = '/system/user/add/batch/user-role';

View File

@ -23,7 +23,7 @@
<style lang="less" scoped>
.ms-button {
@apply inline-block cursor-pointer align-middle;
@apply flex cursor-pointer items-center align-middle;
&:not(:last-child) {
@apply mr-4;
}

View File

@ -30,7 +30,7 @@
</div>
<div
v-if="!props.hideFooter && !props.simple"
class="fixed bottom-0 right-[16px] z-10 bg-white p-[24px] shadow-[0_-1px_4px_rgba(2,2,2,0.1)]"
class="fixed bottom-0 right-[16px] z-10 flex items-center bg-white p-[24px] shadow-[0_-1px_4px_rgba(2,2,2,0.1)]"
:style="{ width: `calc(100% - ${menuWidth + 16}px)` }"
>
<div class="ml-0 mr-auto">

View File

@ -0,0 +1,119 @@
<template>
<a-input-tag
v-model:model-value="innerModelValue"
v-model:input-value="innerInputValue"
:placeholder="t(props.placeholder || '')"
:allow-clear="props.allowClear"
:retain-input-value="props.retainInputValue"
:unique-value="props.uniqueValue"
@press-enter="tagInputEnter"
@blur="tagInputBlur"
>
<template v-if="props.customPrefix" #prefix>
<slot name="prefix"></slot>
</template>
<template v-if="props.customTag" #tag="{ data }">
<slot name="tag" :data="data"></slot>
</template>
<template v-if="props.customSuffix" #suffix>
<slot name="suffix"></slot>
</template>
</a-input-tag>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
const props = withDefaults(
defineProps<{
modelValue: string[]; // arco BUGa-form-item :validate-trigger="['blur', 'input']"
inputValue?: string;
placeholder?: string;
retainInputValue?: boolean;
uniqueValue?: boolean;
allowClear?: boolean;
tagsDuplicateText?: string;
customPrefix?: boolean;
customTag?: boolean;
customSuffix?: boolean;
}>(),
{
retainInputValue: true,
uniqueValue: true,
allowClear: true,
}
);
const emit = defineEmits(['update:modelValue', 'update:inputValue']);
const { t } = useI18n();
const innerModelValue = ref(props.modelValue);
const innerInputValue = ref(props.inputValue);
const tagsLength = ref(0); // tagstag
watch(
() => props.modelValue,
(val) => {
innerModelValue.value = val;
tagsLength.value = val.length;
}
);
watch(
() => innerModelValue.value,
(val) => {
if (val.length < tagsLength.value) {
// tagsLength tagsLength
tagsLength.value = val.length;
}
emit('update:modelValue', val);
}
);
watch(
() => props.inputValue,
(val) => {
innerInputValue.value = val;
}
);
watch(
() => innerInputValue.value,
(val) => {
emit('update:inputValue', val);
}
);
function validateUniqueValue() {
if (
props.uniqueValue &&
innerInputValue.value &&
tagsLength.value === innerModelValue.value.length &&
innerModelValue.value.includes(innerInputValue.value.trim())
) {
// tagsLength innerModelValue
Message.warning(t(props.tagsDuplicateText || 'ms.tagsInput.tagsDuplicateText'));
return false;
}
return true;
}
function tagInputBlur() {
if (innerInputValue.value && innerInputValue.value.trim() !== '' && validateUniqueValue()) {
innerModelValue.value.push(innerInputValue.value.trim());
innerInputValue.value = '';
tagsLength.value += 1;
}
}
function tagInputEnter() {
if (validateUniqueValue()) {
innerInputValue.value = '';
tagsLength.value += 1;
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,3 @@
export default {
'ms.tagsInput.tagsDuplicateText': 'Same label already exists',
};

View File

@ -0,0 +1,3 @@
export default {
'ms.tagsInput.tagsDuplicateText': '已存在相同的标签',
};

View File

@ -1,5 +1,4 @@
import { RouteEnum } from '@/enums/routeEnum';
import { TreeNode, mapTree } from '@/utils';
export const MENU_LEVEL = ['SYSTEM', 'ORGANIZATION', 'PROJECT'] as const; // 菜单级别
@ -8,8 +7,9 @@ export const MENU_LEVEL = ['SYSTEM', 'ORGANIZATION', 'PROJECT'] as const; // 菜
* key key
* locale key
* route name
* routeQuery routeParamKeys互斥 tab
* permission key
* level
* level /tab
* children /tab集合
*/
export const pathMap = [
@ -28,9 +28,9 @@ export const pathMap = [
level: MENU_LEVEL[0],
children: [
{
key: 'SETTING_SYSTEM_USER', // 系统设置-系统-用户
key: 'SETTING_SYSTEM_USER_SINGLE', // 系统设置-系统-用户
locale: 'menu.settings.system.user',
route: RouteEnum.SETTING_SYSTEM_USER,
route: RouteEnum.SETTING_SYSTEM_USER_SINGLE,
permission: [],
level: MENU_LEVEL[0],
},
@ -56,21 +56,30 @@ export const pathMap = [
level: MENU_LEVEL[0],
children: [
{
key: 'SETTING_SYSTEM_PARAMETER', // 系统设置-系统-系统参数-基础设置
key: 'SETTING_SYSTEM_PARAMETER_BASE_CONFIG', // 系统设置-系统-系统参数-基础设置
locale: 'system.config.baseConfig',
route: RouteEnum.SETTING_SYSTEM_PARAMETER,
permission: [],
level: MENU_LEVEL[0],
},
{
key: 'SETTING_SYSTEM_PARAMETER_PAGE_CONFIG', // 系统设置-系统-系统参数-界面设置
locale: 'system.config.pageConfig',
route: RouteEnum.SETTING_SYSTEM_PARAMETER,
permission: [],
routeQuery: {
tab: 'pageConfig',
},
level: MENU_LEVEL[0],
},
{
key: 'SETTING_SYSTEM_PARAMETER_AUTH_CONFIG', // 系统设置-系统-系统参数-认证设置
locale: 'system.config.authConfig',
route: RouteEnum.SETTING_SYSTEM_PARAMETER,
permission: [],
routeQuery: {
tab: 'authConfig',
},
level: MENU_LEVEL[0],
},
],
@ -136,40 +145,6 @@ export const pathMap = [
route: RouteEnum.PROJECT_MANAGEMENT,
permission: [],
level: MENU_LEVEL[2],
children: [
{
key: 'PROJECT_MANAGEMENT_LOG', // 项目管理-日志
locale: 'menu.projectManagement.log',
route: RouteEnum.PROJECT_MANAGEMENT_LOG,
permission: [],
level: MENU_LEVEL[2],
},
],
children: [],
},
];
/**
*
* @param level
* @param customNodeFn
* @returns
*/
export const getPathMapByLevel = <T>(
level: (typeof MENU_LEVEL)[number],
customNodeFn: (node: TreeNode<T>) => TreeNode<T> | null = (node) => node
) => {
return mapTree(pathMap, (e) => {
let isValid = true; // 默认是系统级别
if (level === MENU_LEVEL[1]) {
// 组织级别只展示组织、项目
isValid = e.level !== MENU_LEVEL[0];
} else if (level === MENU_LEVEL[2]) {
// 项目级别只展示项目
isValid = e.level !== MENU_LEVEL[0] && e.level !== MENU_LEVEL[1];
}
if (isValid) {
return typeof customNodeFn === 'function' ? customNodeFn(e) : e;
}
return null;
});
};

View File

@ -35,7 +35,7 @@ export enum WorkbenchRouteEnum {
export enum SettingRouteEnum {
SETTING = 'setting',
SETTING_SYSTEM = 'settingSystem',
SETTING_SYSTEM_USER = 'settingSystemUser',
SETTING_SYSTEM_USER_SINGLE = 'settingSystemUser',
SETTING_SYSTEM_USER_GROUP = 'settingSystemUserGroup',
SETTING_SYSTEM_ORGANIZATION = 'settingSystemOrganization',
SETTING_SYSTEM_PARAMETER = 'settingSystemParameter',

View File

@ -0,0 +1,55 @@
import { useRouter } from 'vue-router';
import { MENU_LEVEL, pathMap } from '@/config/pathMap';
import { TreeNode, findNodeByKey, mapTree } from '@/utils';
import { RouteEnum } from '@/enums/routeEnum';
export default function usePathMap() {
const router = useRouter();
/**
*
* @param level
* @param customNodeFn
* @returns
*/
const getPathMapByLevel = <T>(
level: (typeof MENU_LEVEL)[number],
customNodeFn: (node: TreeNode<T>) => TreeNode<T> | null = (node) => node
) => {
return mapTree(pathMap, (e) => {
let isValid = true; // 默认是系统级别
if (level === MENU_LEVEL[1]) {
// 组织级别只展示组织、项目
isValid = e.level !== MENU_LEVEL[0];
} else if (level === MENU_LEVEL[2]) {
// 项目级别只展示项目
isValid = e.level !== MENU_LEVEL[0] && e.level !== MENU_LEVEL[1];
}
if (isValid) {
return typeof customNodeFn === 'function' ? customNodeFn(e) : e;
}
return null;
});
};
/**
* key routeQuery routeQuery
* @param key
*/
const jumpRouteByMapKey = (key: typeof RouteEnum, routeQuery?: Record<string, any>) => {
const pathNode = findNodeByKey(pathMap, key as unknown as string);
if (pathNode) {
router.push({
name: pathNode?.route,
query: {
...routeQuery,
...pathNode?.routeQuery,
},
});
}
};
return {
getPathMapByLevel,
jumpRouteByMapKey,
};
}

View File

@ -29,3 +29,10 @@ export interface CommonList<T> {
current: number;
list: T[];
}
export interface BatchApiParams {
selectIds: string[]; // 已选 ID 集合,当 selectAll 为 false 时接口会使用该字段
excludeIds?: string[]; // 需要忽略的用户 id 集合当selectAll为 true 时接口会使用该字段
selectAll: boolean; // 是否跨页全选,即选择当前筛选条件下的全部表格数据
condition: Record<string, any>; // 当前表格查询的筛选条件
}

View File

@ -1,3 +1,5 @@
import type { RouteEnum } from '@/enums/routeEnum';
export interface OptionsItem {
id: string;
name: string;
@ -16,7 +18,7 @@ export interface LogItem {
projectName: string;
organizationId: string;
organizationName: string;
module: string; // 操作对象
module: typeof RouteEnum; // 操作对象
type: string; // 操作类型
content: string; // 操作名称
createTime: number;

View File

@ -1,3 +1,5 @@
import type { BatchApiParams } from '@/models/common';
// 用户所属用户组模型
export interface UserRoleListItem {
id: string;
@ -71,8 +73,7 @@ export interface CreateUserParams {
userInfoList: SimpleUserInfo[];
userRoleIdList: string[];
}
export interface UpdateUserStatusParams {
userIdList: string[];
export interface UpdateUserStatusParams extends BatchApiParams {
enable: boolean;
}
@ -80,9 +81,8 @@ export interface ImportUserParams {
fileList: (File | undefined)[];
}
export interface DeleteUserParams {
userIdList: string[];
}
export type DeleteUserParams = BatchApiParams;
export type ResetUserPasswordParams = BatchApiParams;
export interface SystemRole {
id: string;
@ -97,7 +97,6 @@ export interface ImportResult {
errorMessages: Record<string, any>;
}
export interface BatchAddUserGroupParams {
userIds: string[]; // 用户 id 集合
export interface BatchAddUserGroupParams extends BatchApiParams {
roleIds: string[]; // 用户组 id 集合
}

View File

@ -26,7 +26,7 @@ const Setting: AppRouteRecordRaw = {
children: [
{
path: 'user',
name: SettingRouteEnum.SETTING_SYSTEM_USER,
name: SettingRouteEnum.SETTING_SYSTEM_USER_SINGLE,
component: () => import('@/views/setting/system/user/index.vue'),
meta: {
locale: 'menu.settings.system.user',

View File

@ -206,3 +206,28 @@ export function mapTree<T>(
})
.filter(Boolean);
}
/**
* key
* @param trees
* @param targetKey
* @param customKey key
* @returns /null
*/
export function findNodeByKey<T>(trees: TreeNode<T>[], targetKey: string, customKey = 'key'): TreeNode<T> | null {
for (let i = 0; i < trees.length; i++) {
const node = trees[i];
if (node[customKey] === targetKey) {
return node; // 如果当前节点的 key 与目标 key 匹配,则返回当前节点
}
if (Array.isArray(node.children) && node.children.length > 0) {
const _node = findNodeByKey(node.children, targetKey); // 递归在子节点中查找
if (_node) {
return _node; // 如果在子节点中找到了匹配的节点,则返回该节点
}
}
}
return null; // 如果在整个树形数组中都没有找到匹配的节点,则返回 null
}

View File

@ -8,7 +8,7 @@
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" @click="openAuthDetail(record)">{{ record.name }}</a-button>
<a-button type="text" @click="openAuthDetail(record.id)">{{ record.name }}</a-button>
</template>
<template #action="{ record }">
<MsButton @click="editAuth(record)">{{ t('system.config.auth.edit') }}</MsButton>
@ -682,13 +682,13 @@
/**
* 查看认证源
* @param record 表格项
* @param id 表格项 id
*/
async function openAuthDetail(record: AuthItem) {
async function openAuthDetail(id: string) {
try {
showDetailDrawer.value = true;
detailDrawerLoading.value = true;
const res = await getAuthDetail(record.id);
const res = await getAuthDetail(id);
activeAuthDetail.value = { ...res, configuration: JSON.parse(res.configuration || '{}') };
const { configuration } = activeAuthDetail.value;
let description: Description[] = [
@ -993,6 +993,22 @@
showDrawer.value = false;
authFormRef.value?.resetFields();
}
defineExpose({
openAuthDetail, // ID
});
declare const _default: import('vue').DefineComponent<
unknown,
unknown,
import('vue').ComponentOptionsMixin,
import('vue').ComponentOptionsMixin,
{
openAuthDetail: (id: string) => void;
}
>;
export declare type AuthConfigInstance = InstanceType<typeof _default>;
</script>
<style lang="less" scoped></style>

View File

@ -15,22 +15,25 @@
</MsCard>
<baseConfig v-show="activeTab === 'baseConfig'" />
<pageConfig v-if="isInitedPageConfig" v-show="activeTab === 'pageConfig'" />
<authConfig v-if="isInitedAuthConfig" v-show="activeTab === 'authConfig'" />
<authConfig v-if="isInitedAuthConfig" v-show="activeTab === 'authConfig'" ref="authConfigRef" />
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import MsCard from '@/components/pure/ms-card/index.vue';
import { useI18n } from '@/hooks/useI18n';
import baseConfig from './components/baseConfig.vue';
import pageConfig from './components/pageConfig.vue';
import authConfig from './components/authConfig.vue';
import authConfig, { AuthConfigInstance } from './components/authConfig.vue';
const { t } = useI18n();
const route = useRoute();
const activeTab = ref('baseConfig');
const activeTab = ref((route.query.tab as string) || 'baseConfig');
const isInitedPageConfig = ref(activeTab.value === 'pageConfig');
const isInitedAuthConfig = ref(activeTab.value === 'authConfig');
const authConfigRef = ref<AuthConfigInstance | null>();
watch(
() => activeTab.value,
@ -40,8 +43,17 @@
} else if (val === 'authConfig' && !isInitedAuthConfig.value) {
isInitedAuthConfig.value = true;
}
},
{
immediate: true,
}
);
onMounted(() => {
if (route.query.tab === 'authConfig' && route.query.id) {
authConfigRef.value?.openAuthDetail(route.query.id as string);
}
});
</script>
<style lang="less" scoped>

View File

@ -45,7 +45,7 @@
<a-option v-for="opt of typeOptions" :key="opt.value" :value="opt.value">{{ t(opt.label) }}</a-option>
</a-select>
<MsCascader
v-model="_module"
v-model:model-value="_module"
:options="moduleOptions"
mode="native"
:prefix="t('system.log.operateTarget')"
@ -78,6 +78,12 @@
<template #range="{ record }">
{{ `${record.organizationName}${record.projectName ? `/${record.projectName}` : ''}` }}
</template>
<template #module="{ record }">
{{ getModuleLocale(record.module) }}
</template>
<template #type="{ record }">
{{ t(typeOptions.find((e) => e.value === record.type)?.label || '') }}
</template>
<template #content="{ record }">
<MsButton @click="handleNameClick(record)">{{ record.content }}</MsButton>
</template>
@ -90,6 +96,7 @@
import dayjs from 'dayjs';
import MsCard from '@/components/pure/ms-card/index.vue';
import { useI18n } from '@/hooks/useI18n';
import usePathMap from '@/hooks/usePathMap';
import { getLogList, getLogOptions, getLogUsers } from '@/api/modules/setting/log';
import MsCascader from '@/components/business/ms-cascader/index.vue';
import useTableStore from '@/store/modules/ms-table';
@ -97,12 +104,12 @@
import useTable from '@/components/pure/ms-table/useTable';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import { MENU_LEVEL, getPathMapByLevel } from '@/config/pathMap';
import { MENU_LEVEL } from '@/config/pathMap';
import MsSearchSelect from '@/components/business/ms-search-select/index';
import type { CascaderOption, SelectOptionData } from '@arco-design/web-vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import type { MsTimeLineListItem } from '@/components/pure/ms-timeline/types';
import type { LogItem } from '@/models/setting/log';
const props = defineProps<{
mode: (typeof MENU_LEVEL)[number]; // //
@ -228,6 +235,7 @@
const moduleOptions = ref<CascaderOption[]>([]);
const moduleLocaleMap = ref<Record<string, string>>({});
const { getPathMapByLevel, jumpRouteByMapKey } = usePathMap();
function initModuleOptions() {
moduleOptions.value = getPathMapByLevel(props.mode, (e) => {
@ -241,6 +249,18 @@
});
}
/**
* 获取操作对象映射的国际化文本并处理可能不存在的 key 导致报错的情况
* @param module 操作对象 key
*/
function getModuleLocale(module: string) {
try {
return t(moduleLocaleMap.value[module] || '') || module;
} catch (error) {
return module;
}
}
const typeOptions = [
{
label: 'system.log.operateType.all',
@ -330,10 +350,12 @@
{
title: 'system.log.operateTarget',
dataIndex: 'module',
slotName: 'module',
},
{
title: 'system.log.operateType',
dataIndex: 'type',
slotName: 'type',
width: 120,
},
{
@ -353,20 +375,12 @@
];
const tableStore = useTableStore();
tableStore.initColumn(TableKeyEnum.SYSTEM_LOG, columns, 'drawer');
const { propsRes, propsEvent, loadList, setLoadListParams, resetPagination } = useTable(
getLogList,
{
tableKey: TableKeyEnum.SYSTEM_LOG,
columns,
selectable: false,
showSelectAll: false,
},
(record) => ({
...record,
type: t(typeOptions.find((e) => e.value === record.type)?.label || ''),
module: t(moduleLocaleMap.value[record.module] || ''),
})
);
const { propsRes, propsEvent, loadList, setLoadListParams, resetPagination } = useTable(getLogList, {
tableKey: TableKeyEnum.SYSTEM_LOG,
columns,
selectable: false,
showSelectAll: false,
});
function searchLog() {
const ranges = operateRange.value.map((e) => e);
@ -385,8 +399,8 @@
loadList();
}
function handleNameClick(record: MsTimeLineListItem) {
console.log(record);
function handleNameClick(record: LogItem) {
jumpRouteByMapKey(record.module, record.sourceId ? { id: record.sourceId } : {});
}
onBeforeMount(() => {

View File

@ -3,7 +3,7 @@
v-model:visible="showJobDrawer"
:width="680"
:title="t('system.resourcePool.customJobTemplate')"
:footer="false"
:footer="!props.readOnly"
@close="handleClose"
>
<MsCodeEditor
@ -14,6 +14,9 @@
theme="MS-text"
:read-only="props.readOnly"
/>
<template v-if="!props.readOnly" #footer>
<a-button type="secondary" @click="resetTemplate">{{ t('system.resourcePool.jobTemplateReset') }}</a-button>
</template>
</MsDrawer>
</template>
@ -22,6 +25,7 @@
import { useI18n } from '@/hooks/useI18n';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { job } from '../template';
const props = defineProps<{
visible: boolean;
@ -68,6 +72,10 @@
}
);
function resetTemplate() {
jobDefinition.value = job;
}
function handleClose() {
emit('update:value', jobDefinition.value);
}

View File

@ -15,13 +15,13 @@
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" @click="showPoolDetail(record)">{{ record.name }}</a-button>
<a-button type="text" @click="showPoolDetail(record.id)">{{ record.name }}</a-button>
</template>
<template #action="{ record }">
<MsButton @click="editPool(record)">{{ t('system.resourcePool.editPool') }}</MsButton>
<MsButton v-if="record.enable" @click="disabledPool(record)">{{
t('system.resourcePool.tableDisable')
}}</MsButton>
<MsButton v-if="record.enable" @click="disabledPool(record)">
{{ t('system.resourcePool.tableDisable') }}
</MsButton>
<MsButton v-else @click="enablePool(record)">{{ t('system.resourcePool.tableEnable') }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleSelect($event, record)"></MsTableMoreAction>
</template>
@ -54,7 +54,7 @@
<script setup lang="ts">
import { onMounted, Ref, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import { getPoolList, delPoolInfo, togglePoolStatus, getPoolInfo } from '@/api/modules/setting/resourcePool';
@ -77,6 +77,7 @@
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const columns: MsTableColumn = [
{
@ -242,16 +243,16 @@
const drawerLoading = ref(false);
/**
* 查看资源池详情
* @param record
* @param id 资源池 id
*/
async function showPoolDetail(record: any) {
if (activePool.value?.id === record.id && showDetailDrawer.value) {
async function showPoolDetail(id: string) {
if (activePool.value?.id === id && showDetailDrawer.value) {
return;
}
drawerLoading.value = true;
showDetailDrawer.value = true;
try {
const res = await getPoolInfo(record.id);
const res = await getPoolInfo(id);
if (res) {
activePool.value = res;
const poolUses = [
@ -400,6 +401,13 @@
}
}
onMounted(() => {
if (route.query.id) {
// id
showPoolDetail(route.query.id as string);
}
});
/**
* 编辑资源池
* @param record

View File

@ -118,4 +118,7 @@ export default {
'system.resourcePool.jobTemplate': 'Job Templates',
'system.resourcePool.jobTemplateTip':
'A Kubernetes job template is a text in YAML format, which is used to define the running parameters of the job. You can edit the job template here.',
'system.resourcePool.jobTemplateReset': 'Reset Template',
'system.resourcePool.addSuccess': 'Added resource pool successfully',
'system.resourcePool.updateSuccess': 'Resource pool updated successfully',
};

View File

@ -112,6 +112,7 @@ export default {
'system.resourcePool.jobTemplate': 'Job 模版',
'system.resourcePool.jobTemplateTip':
'Kubernetes Job 模版是一个YAML格式的文本用于定义Job的运行参数您可以在此处编辑Job模版。',
'system.resourcePool.jobTemplateReset': '重置 Job 模版',
'system.resourcePool.addSuccess': '添加资源池成功',
'system.resourcePool.updateSuccess': '更新资源池成功',
};

View File

@ -11,8 +11,16 @@
field="emails"
:label="t('system.user.inviteEmail')"
:rules="[{ required: true, message: t('system.user.createUserEmailNotNull') }]"
:validate-trigger="['blur', 'input']"
asterisk-position="end"
>
<a-input-tag v-model="emailForm.emails" :placeholder="t('system.user.inviteEmailPlaceholder')" allow-clear />
<MsTagsInput
v-model:model-value="emailForm.emails"
placeholder="system.user.inviteEmailPlaceholder"
allow-clear
unique-value
retain-input-value
/>
</a-form-item>
<a-form-item class="mb-0" field="userGroup" :label="t('system.user.createUserUserGroup')">
<a-select
@ -21,7 +29,15 @@
:placeholder="t('system.user.createUserUserGroupPlaceholder')"
allow-clear
>
<a-option v-for="item of userGroupOptions" :key="item.value">{{ item.label }}</a-option>
<a-option
v-for="item of userGroupOptions"
:key="item.id"
:tag-props="{ closable: emailForm.userGroup.length > 1 }"
:value="item.id"
:disabled="emailForm.userGroup.includes(item.id) && emailForm.userGroup.length === 1"
>
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
</a-form>
@ -37,13 +53,17 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { cloneDeep } from 'lodash-es';
import { useI18n } from '@/hooks/useI18n';
import { FormInstance, Message, ValidatedError } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import type { SystemRole } from '@/models/setting/user';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
userGroupOptions: SystemRole[];
}>();
const emit = defineEmits(['update:visible']);
@ -52,24 +72,10 @@
const inviteLoading = ref(false);
const inviteFormRef = ref<FormInstance | null>(null);
const defaultInviteForm = {
emails: [],
userGroup: [],
emails: [] as string[],
userGroup: [] as string[],
};
const emailForm = ref(cloneDeep(defaultInviteForm));
const userGroupOptions = ref([
{
label: 'Beijing',
value: 'Beijing',
},
{
label: 'Shanghai',
value: 'Shanghai',
},
{
label: 'Guangzhou',
value: 'Guangzhou',
},
]);
watch(
() => props.visible,
@ -85,6 +91,15 @@
}
);
watch(
() => props.userGroupOptions,
(arr) => {
if (arr.length) {
emailForm.value.userGroup = arr.filter((e: SystemRole) => e.selected === true).map((e: SystemRole) => e.id);
}
}
);
function cancelInvite() {
inviteVisible.value = false;
inviteFormRef.value?.resetFields();

View File

@ -26,10 +26,10 @@
@batch-action="handleTableBatch"
>
<template #organization="{ record }">
<a-tooltip :content="record.organizationList.filter((e: any) => e).map((e: any) => e.name).join(',')">
<a-tooltip :content="record.organizationList.map((e: any) => e.name).join(',')">
<div>
<a-tag
v-for="org of record.organizationList.filter((e: any) => e).slice(0, 2)"
v-for="org of record.organizationList.slice(0, 2)"
:key="org.id"
class="mr-[4px] bg-transparent"
bordered
@ -46,19 +46,19 @@
<a-tooltip :content="record.userRoleList.map((e: any) => e.name).join(',')">
<div>
<a-tag
v-for="org of record.userRoleList.slice(0, 2)"
:key="org.id"
v-for="role of record.userRoleList.slice(0, 2)"
:key="role.id"
:class="['mr-[4px]', 'bg-transparent', record.enable ? 'enableTag' : 'disableTag']"
bordered
>
{{ org.name }}
{{ role.name }}
</a-tag>
<a-tag
v-show="record.organizationList.length > 2"
v-show="record.userRoleList.length > 2"
:class="['mr-[4px]', 'bg-transparent', record.enable ? 'enableTag' : 'disableTag']"
bordered
>
+{{ record.organizationList.length - 2 }}
+{{ record.userRoleList.length - 2 }}
</a-tag>
</div>
</a-tooltip>
@ -92,7 +92,13 @@
:default-vals="userForm.list"
max-height="250px"
></MsBatchForm>
<a-form-item class="mb-0" field="userGroup" :label="t('system.user.createUserUserGroup')">
<a-form-item
class="mb-0"
field="userGroup"
:label="t('system.user.createUserUserGroup')"
required
asterisk-position="end"
>
<a-select
v-model="userForm.userGroup"
multiple
@ -102,9 +108,9 @@
<a-option
v-for="item of userGroupOptions"
:key="item.id"
:tag-props="{ closable: item.closeable }"
:tag-props="{ closable: userForm.userGroup.length > 1 }"
:value="item.id"
:disabled="item.selected"
:disabled="userForm.userGroup.includes(item.id) && userForm.userGroup.length === 1"
>
{{ item.name }}
</a-option>
@ -197,7 +203,7 @@
</a-button>
</template>
</a-modal>
<inviteModal v-model:visible="inviteVisible"></inviteModal>
<inviteModal v-model:visible="inviteVisible" :user-group-options="userGroupOptions"></inviteModal>
<batchModal
v-model:visible="showBatchModal"
:table-selected="tableSelected"
@ -248,7 +254,6 @@
{
title: 'system.user.tableColumnEmail',
dataIndex: 'email',
width: 200,
showInTable: true,
},
{
@ -283,18 +288,25 @@
title: 'system.user.tableColumnActions',
slotName: 'action',
fixed: 'right',
width: 120,
showInTable: true,
},
];
const tableStore = useTableStore();
tableStore.initColumn(TableKeyEnum.SYSTEM_USER, columns, 'drawer');
const { propsRes, propsEvent, loadList, setKeyword } = useTable(getUserList, {
tableKey: TableKeyEnum.SYSTEM_USER,
columns,
scroll: { y: 'auto' },
selectable: true,
});
const { propsRes, propsEvent, loadList, setKeyword } = useTable(
getUserList,
{
tableKey: TableKeyEnum.SYSTEM_USER,
columns,
scroll: { y: 'auto' },
selectable: true,
},
(record) => ({
...record,
organizationList: record.organizationList.filter((e) => e),
userRoleList: record.userRoleList.filter((e) => e),
})
);
const keyword = ref('');
@ -316,12 +328,12 @@
/**
* 重置密码
*/
function resetPassword(record: any, isbatch?: boolean) {
function resetPassword(record: any, isBatch?: boolean) {
let title = t('system.user.resetPswTip', { name: characterLimit(record?.name) });
let userIdList = [record?.id];
if (isbatch) {
let selectIds = [record?.id];
if (isBatch) {
title = t('system.user.batchResetPswTip', { count: tableSelected.value.length });
userIdList = tableSelected.value as string[];
selectIds = tableSelected.value as string[];
}
openModal({
type: 'warning',
@ -331,7 +343,11 @@
cancelText: t('system.user.resetPswCancel'),
onBeforeOk: async () => {
try {
await resetUserPassword(userIdList);
await resetUserPassword({
selectIds,
selectAll: false,
condition: {},
});
Message.success(t('system.user.resetPswSuccess'));
} catch (error) {
console.log(error);
@ -344,12 +360,12 @@
/**
* 禁用用户
*/
function disabledUser(record: any, isbatch?: boolean) {
function disabledUser(record: any, isBatch?: boolean) {
let title = t('system.user.disableUserTip', { name: characterLimit(record?.name) });
let userIdList = [record?.id];
if (isbatch) {
let selectIds = [record?.id];
if (isBatch) {
title = t('system.user.batchDisableUserTip', { count: tableSelected.value.length });
userIdList = tableSelected.value as string[];
selectIds = tableSelected.value as string[];
}
openModal({
type: 'warning',
@ -361,7 +377,9 @@
onBeforeOk: async () => {
try {
await toggleUserStatus({
userIdList,
selectIds,
selectAll: false,
condition: {},
enable: false,
});
Message.success(t('system.user.disableUserSuccess'));
@ -377,12 +395,12 @@
/**
* 启用用户
*/
function enableUser(record: any, isbatch?: boolean) {
function enableUser(record: any, isBatch?: boolean) {
let title = t('system.user.enableUserTip', { name: characterLimit(record?.name) });
let userIdList = [record?.id];
if (isbatch) {
let selectIds = [record?.id];
if (isBatch) {
title = t('system.user.batchEnableUserTip', { count: tableSelected.value.length });
userIdList = tableSelected.value as string[];
selectIds = tableSelected.value as string[];
}
openModal({
type: 'info',
@ -394,7 +412,9 @@
onBeforeOk: async () => {
try {
await toggleUserStatus({
userIdList,
selectIds,
selectAll: false,
condition: {},
enable: true,
});
Message.success(t('system.user.enableUserSuccess'));
@ -410,12 +430,12 @@
/**
* 删除用户
*/
function deleteUser(record: any, isbatch?: boolean) {
function deleteUser(record: any, isBatch?: boolean) {
let title = t('system.user.deleteUserTip', { name: characterLimit(record?.name) });
let userIdList = [record?.id];
if (isbatch) {
let selectIds = [record?.id];
if (isBatch) {
title = t('system.user.batchDeleteUserTip', { count: tableSelected.value.length });
userIdList = tableSelected.value as string[];
selectIds = tableSelected.value as string[];
}
openModal({
type: 'error',
@ -430,7 +450,9 @@
onBeforeOk: async () => {
try {
await deleteUserInfo({
userIdList,
selectIds,
selectAll: false,
condition: {},
});
Message.success(t('system.user.deleteUserSuccess'));
loadList();
@ -601,7 +623,7 @@
userGroup: [],
};
const userForm = ref<UserForm>(cloneDeep(defaultUserForm));
const userGroupOptions = ref();
const userGroupOptions = ref<SystemRole[]>([]);
async function init() {
try {

View File

@ -63,7 +63,7 @@ export default {
'system.user.invite': 'Email invite',
'system.user.inviteEmail': 'Email',
'system.user.inviteCancel': 'Cancel',
'system.user.inviteEmailPlaceholder': 'Enter multiple email addresses, separated by spaces or carriage returns',
'system.user.inviteEmailPlaceholder': 'Enter multiple email addresses, separated by carriage returns',
'system.user.inviteSendEmail': 'SendEmail',
'system.user.inviteSuccess': 'Invitation successfully',
'system.user.importModalTitle': 'Import user',

View File

@ -62,7 +62,7 @@ export default {
'system.user.invite': '邮箱邀请',
'system.user.inviteEmail': '邮箱',
'system.user.inviteCancel': '取消',
'system.user.inviteEmailPlaceholder': '可输入多个邮箱地址,空格或回车分隔',
'system.user.inviteEmailPlaceholder': '可输入多个邮箱地址,回车分隔',
'system.user.inviteSendEmail': '发送邮件',
'system.user.inviteSuccess': '邀请成功',
'system.user.importModalTitle': '导入用户',