From 010a278207aaa54ffa3cee2c936e3eac89d3d406 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Wed, 13 Nov 2024 15:32:59 +0800 Subject: [PATCH] feat: updated permission management --- src/components/core/user/UserForm.vue | 82 +++++++- src/components/index.ts | 2 + src/components/ui/avatar/UserAvatar.vue | 114 +++++++++++ src/components/ui/button/LabelButton.vue | 2 +- .../ui/nav/NavActionGroupDetailCommon.vue | 4 - src/components/ui/nav/NavActionItem.vue | 55 +++--- src/components/ui/nav/NavLink.vue | 5 +- src/components/ui/table/Table.vue | 3 + src/i18n/lang/en/components/user.ts | 3 + src/i18n/lang/en/global.ts | 1 + src/i18n/lang/en/views/roles.ts | 2 + src/i18n/lang/en/views/users.ts | 1 + src/i18n/lang/zh/components/user.ts | 3 + src/i18n/lang/zh/global.ts | 1 + src/i18n/lang/zh/views/roles.ts | 2 + src/i18n/lang/zh/views/users.ts | 1 + src/interfaces/i18n/components/user.d.ts | 3 + src/interfaces/i18n/global.d.ts | 1 + src/interfaces/i18n/views/roles.d.ts | 3 +- src/interfaces/i18n/views/users.d.ts | 1 + src/interfaces/models/project.d.ts | 1 + src/interfaces/models/role.d.ts | 1 + src/interfaces/models/user.d.ts | 2 + src/interfaces/store/index.d.ts | 2 +- src/interfaces/store/modules/common.d.ts | 1 + src/layouts/components/Header.vue | 181 +++++++++++------- src/store/modules/common.ts | 8 +- src/utils/icon.ts | 2 +- src/utils/table.ts | 8 + src/utils/user.ts | 36 ++++ src/views/git/list/useGitList.tsx | 14 +- src/views/misc/MySettings.vue | 103 ++++------ src/views/project/list/useProjectList.tsx | 3 +- src/views/role/list/useRoleList.tsx | 27 ++- src/views/user/list/useUserList.tsx | 24 ++- 35 files changed, 501 insertions(+), 201 deletions(-) create mode 100644 src/components/ui/avatar/UserAvatar.vue create mode 100644 src/utils/user.ts diff --git a/src/components/core/user/UserForm.vue b/src/components/core/user/UserForm.vue index a3ee5644335e1..47a5ad096e60a 100644 --- a/src/components/core/user/UserForm.vue +++ b/src/components/core/user/UserForm.vue @@ -6,10 +6,19 @@ import useUser from '@/components/core/user/useUser'; import useUserDetail from '@/views/user/detail/useUserDetail'; import { isPro, translate } from '@/utils'; import { useRole } from '@/components'; +import { useI18n } from 'vue-i18n'; + +const props = defineProps<{ + form?: User; + isEdit?: boolean; + onChangePassword?: () => Promise; +}>(); // i18n const t = translate; +const { locale } = useI18n(); + // store const store = useStore(); @@ -17,12 +26,24 @@ const { activeId } = useUserDetail(); const { onChangePasswordFunc } = useUser(store); -const onChangePassword = () => onChangePasswordFunc(activeId.value); +const onChangePassword = + props.onChangePassword || + (() => onChangePasswordFunc(props.form?._id || activeId.value)); const isDetail = computed(() => !!activeId.value); -const { form, formRef, formRules, isSelectiveForm, isFormItemDisabled } = - useUser(store); +const { + form: userForm, + formRef, + formRules, + isSelectiveForm, + isFormItemDisabled, +} = useUser(store); + +const form = computed(() => { + if (props.form) return props.form; + return userForm.value; +}); const { allListSelectOptions: allRolesSelectOptions } = useRole(store); @@ -62,7 +83,7 @@ defineOptions({ name: 'ClUserForm' }); required > + + + + + +import { computed } from 'vue'; +import { getUserShortName } from '@/utils/user'; + +const props = withDefaults( + defineProps<{ + icon?: Icon; + size?: BasicSize | number; + shape?: 'circle' | 'square'; + src?: string; + alt?: string; + fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'; + color?: string; + user?: User; + tooltip?: string; + }>(), + { + size: 36, + } +); + +const emit = defineEmits<{ + (e: 'click', event: MouseEvent): void; +}>(); + +const slots = defineSlots<{ + default: any; +}>(); + +const userLabel = computed(() => { + const { user } = props; + if (user) { + return getUserShortName(user); + } + return ''; +}); + +const labelClass = computed(() => { + const length = userLabel.value.length; + const isChineseName = /[\u4e00-\u9fa5]/.test(userLabel.value); + + return { + label: true, + 'label--small': length === 3 || (isChineseName && length === 2), + 'label--smaller': length === 4 || (isChineseName && length === 3), + 'label--smallest': isChineseName && length === 4, + }; +}); + +defineOptions({ name: 'ClUserAvatar' }); + + + + + diff --git a/src/components/ui/button/LabelButton.vue b/src/components/ui/button/LabelButton.vue index 3f9ccd3260c90..4276e283ce5de 100644 --- a/src/components/ui/button/LabelButton.vue +++ b/src/components/ui/button/LabelButton.vue @@ -34,6 +34,6 @@ defineOptions({ name: 'ClLabelButton' }); diff --git a/src/components/ui/nav/NavActionGroupDetailCommon.vue b/src/components/ui/nav/NavActionGroupDetailCommon.vue index 5d7990c0f5a5d..c160e8cf686e3 100644 --- a/src/components/ui/nav/NavActionGroupDetailCommon.vue +++ b/src/components/ui/nav/NavActionGroupDetailCommon.vue @@ -57,10 +57,6 @@ defineOptions({ name: 'ClNavActionGroupDetailCommon' }); - diff --git a/src/components/ui/nav/NavLink.vue b/src/components/ui/nav/NavLink.vue index e8be068e7ebd6..ce4fc6f6550db 100644 --- a/src/components/ui/nav/NavLink.vue +++ b/src/components/ui/nav/NavLink.vue @@ -6,6 +6,7 @@ const props = defineProps<{ label?: string | number | boolean; icon?: Icon; external?: boolean; + tooltip?: string; }>(); const slots = defineSlots<{ @@ -36,11 +37,11 @@ defineOptions({ name: 'ClNavLink' }); - + diff --git a/src/store/modules/common.ts b/src/store/modules/common.ts index e14696d2a110c..69823c8f7a9be 100644 --- a/src/store/modules/common.ts +++ b/src/store/modules/common.ts @@ -1,7 +1,7 @@ import { plainClone } from '@/utils/object'; import useRequest from '@/services/request'; -const { get, put } = useRequest(); +const { get, put, post } = useRequest(); export default { namespaced: true, @@ -42,5 +42,11 @@ export default { putMe: async (_: StoreActionContext, me: User) => { await put(`/users/me`, me); }, + changeMyPassword: async ( + _: StoreActionContext, + { password }: { password: string } + ) => { + await post(`/users/me/change-password`, { password }); + }, } as CommonStoreActions, } as CommonStoreModule; diff --git a/src/utils/icon.ts b/src/utils/icon.ts index fe8dcdca273e9..193e7dc1feee6 100644 --- a/src/utils/icon.ts +++ b/src/utils/icon.ts @@ -265,7 +265,7 @@ export const getIconByRouteConcept = (concept: RouteConcept): Icon => { case 'system': return ['fa', 'cogs']; case 'disclaimer': - return ['fa', 'file-signature']; + return ['fa', 'info-circle']; case 'mySettings': return ['fa', 'user-cog']; default: diff --git a/src/utils/table.ts b/src/utils/table.ts index 22f11f4a241e7..e020dd8fbcb88 100644 --- a/src/utils/table.ts +++ b/src/utils/table.ts @@ -19,3 +19,11 @@ export const getColumnWidth = (column: TableColumn): number | undefined => { return column.width; } }; + +export const getPlaceholderColumn = (): TableColumn => { + return { + key: 'placeholder', + width: 'auto', + label: '', + }; +}; diff --git a/src/utils/user.ts b/src/utils/user.ts new file mode 100644 index 0000000000000..737d1acbdb86e --- /dev/null +++ b/src/utils/user.ts @@ -0,0 +1,36 @@ +export const isChineseName = (user: User) => { + return /[\u4e00-\u9fa5]/.test( + (user.first_name || '') + (user.last_name || '') + ); +}; + +export const getUserFullName = (user: User) => { + const firstName = user.first_name || ''; + const lastName = user.last_name || ''; + if (isChineseName(user)) { + return lastName + firstName; + } else { + return firstName + ' ' + lastName; + } +}; + +export const getUserShortName = (user: User) => { + // Get first and last name + const firstName = user.first_name || ''; + const lastName = user.last_name || ''; + + // Fallback to username if no name provided + if (!firstName && !lastName) { + return user.username || ''; + } + + // If Chinese name, return at most 4 characters + if (isChineseName(user)) { + return getUserFullName(user).slice(0, 4); + } + + // Otherwise, return first initial and last initial + const firstInitial = firstName ? firstName[0].toUpperCase() : ''; + const lastInitial = lastName ? lastName[0].toUpperCase() : ''; + return `${firstInitial}${lastInitial}`; +}; diff --git a/src/views/git/list/useGitList.tsx b/src/views/git/list/useGitList.tsx index 7752f72949f84..e84ba4e3224d6 100644 --- a/src/views/git/list/useGitList.tsx +++ b/src/views/git/list/useGitList.tsx @@ -10,22 +10,18 @@ import { ACTION_VIEW_CHANGES, ACTION_VIEW_COMMITS, ACTION_VIEW_FILES, - ACTION_VIEW_LOGS, ACTION_VIEW_SPIDERS, FILTER_OP_CONTAINS, TABLE_COLUMN_NAME_ACTIONS, } from '@/constants'; import { useList } from '@/layouts/content'; import { + getPlaceholderColumn, onListFilterChangeByKey, setupListComponent, translate, } from '@/utils'; import useGit from '@/components/core/git/useGit'; -// import NavLink from '@/components/ui/nav/NavLink.vue'; -// import GitStatus from '@/components/core/git/GitStatus.vue'; -// import ClTag from '@/components/ui/tag/Tag.vue'; -// import ClIcon from '@/components/ui/icon/Icon.vue'; import { ClNavLink, ClGitStatus, ClTag, ClIcon } from '@/components'; const useGitList = () => { @@ -171,11 +167,12 @@ const useGitList = () => { value: (row: Git) => ( ), hasSort: false, }, + getPlaceholderColumn(), { key: TABLE_COLUMN_NAME_ACTIONS, label: t('components.table.columns.actions'), @@ -221,10 +218,7 @@ const useGitList = () => { contextMenu: true, }, { - tooltip: row => - !row.spiders?.length - ? t('common.actions.delete') - : t('views.gits.table.actions.tooltip.deleteNotAllowed'), + tooltip: t('common.actions.delete'), disabled: row => row.spiders?.length > 0, onClick: deleteByIdConfirm, action: ACTION_DELETE, diff --git a/src/views/misc/MySettings.vue b/src/views/misc/MySettings.vue index 32b00038124ae..1faea48f8189d 100644 --- a/src/views/misc/MySettings.vue +++ b/src/views/misc/MySettings.vue @@ -1,34 +1,59 @@ @@ -48,65 +73,11 @@ defineOptions({ name: 'ClMySettings' }); - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/views/project/list/useProjectList.tsx b/src/views/project/list/useProjectList.tsx index d7ffcba92f8a7..f135ce88e2fc8 100644 --- a/src/views/project/list/useProjectList.tsx +++ b/src/views/project/list/useProjectList.tsx @@ -99,7 +99,7 @@ const useProjectList = () => { value: (row: Project) => ( ), width: '120', @@ -135,6 +135,7 @@ const useProjectList = () => { }, { tooltip: t('common.actions.delete'), + disabled: row => row.spiders > 0, onClick: deleteByIdConfirm, action: ACTION_DELETE, contextMenu: true, diff --git a/src/views/role/list/useRoleList.tsx b/src/views/role/list/useRoleList.tsx index 9cea956095b6b..48d6a5aadd705 100644 --- a/src/views/role/list/useRoleList.tsx +++ b/src/views/role/list/useRoleList.tsx @@ -1,4 +1,4 @@ -import { computed } from 'vue'; +import { computed, h } from 'vue'; import { useStore } from 'vuex'; import { useRouter } from 'vue-router'; import { TABLE_COLUMN_NAME_ACTIONS } from '@/constants/table'; @@ -96,6 +96,31 @@ const useRoleList = () => { hasFilter: true, allowFilterSearch: true, }, + { + className: 'pages', + key: 'routes', + label: t('views.roles.table.columns.pages'), + icon: ['fa', 'file-alt'], + value: (row: Role) => ( + + ), + width: '120', + }, + { + className: 'users', + key: 'users', + label: t('views.roles.table.columns.users'), + icon: ['fa', 'users'], + value: (row: Role) => ( + + ), + width: '120', + }, { key: 'description', label: t('views.roles.table.columns.description'), diff --git a/src/views/user/list/useUserList.tsx b/src/views/user/list/useUserList.tsx index 2a0574398b046..c524cb85484a3 100644 --- a/src/views/user/list/useUserList.tsx +++ b/src/views/user/list/useUserList.tsx @@ -20,6 +20,7 @@ import { import { getIconByAction, onListFilterChangeByKey } from '@/utils'; import useUser from '@/components/core/user/useUser'; import { ClNavLink } from '@/components'; +import { getUserFullName } from '@/utils/user'; // i18n const t = translate; @@ -122,19 +123,28 @@ const useUserList = () => { hasFilter: true, allowFilterSearch: true, }, + { + key: 'full_name', + label: t('views.users.table.columns.fullName'), + icon: ['fa', 'font'], + width: '180', + value: (row: User) => ( + + ), + }, { key: 'email', label: t('views.users.table.columns.email'), icon: ['fa', 'at'], width: '180', - hasSort: true, - hasFilter: true, - allowFilterSearch: true, + value: (row: User) => ( + + ), }, { key: 'role', label: t('views.users.table.columns.role'), - icon: ['fa', 'font'], + icon: ['fa', 'user-tag'], width: '150', value: (row: User) => ( { allowFilterItems: true, filterItems: rolesOptions, }, + { + key: 'placeholder', + width: 'auto', + }, { key: TABLE_COLUMN_NAME_ACTIONS, label: t('components.table.columns.actions'), @@ -174,7 +188,7 @@ const useUserList = () => { ); const selectableFunction = (row: User) => { - return row.username !== USERNAME_ADMIN; + return !row.root_admin; }; // options