From 0cb33dad5a37f449f7a98a8683c48b8dfb570bb4 Mon Sep 17 00:00:00 2001 From: wxg0103 <727495428@qq.com> Date: Thu, 5 Jun 2025 19:47:47 +0800 Subject: [PATCH] feat: ui add system settings --- apps/users/views/login.py | 8 +- apps/users/views/user.py | 12 +- ui/package.json | 3 + ui/src/api/systemSettings/auth-setting.ts | 38 ++ ui/src/api/systemSettings/email-setting.ts | 38 ++ ui/src/api/systemSettings/platform-source.ts | 27 ++ ui/src/api/systemSettings/theme.ts | 36 ++ ui/src/api/user/login.ts | 95 +++- ui/src/api/user/user.ts | 8 + ui/src/assets/scan/icon_qr_outlined.svg | 3 + ui/src/assets/scan/logo_dingtalk.svg | 3 + ui/src/assets/scan/logo_lark.svg | 12 + ui/src/assets/scan/logo_slack.svg | 1 + ui/src/assets/scan/logo_wechat-work.svg | 7 + ui/src/assets/scan/logo_wechat.svg | 3 + ui/src/components/app-icon/icons/menu.ts | 40 ++ ui/src/locales/index.ts | 11 +- ui/src/router/modules/system.ts | 50 +++ ui/src/stores/modules/login.ts | 8 + ui/src/stores/modules/theme.ts | 6 +- ui/src/stores/modules/user.ts | 232 +++++----- ui/src/utils/utils.ts | 167 +++++++ ui/src/views/authentication/component/CAS.vue | 127 ++++++ .../authentication/component/EditModal.vue | 230 ++++++++++ .../views/authentication/component/LDAP.vue | 184 ++++++++ .../views/authentication/component/OAuth2.vue | 210 +++++++++ .../views/authentication/component/OIDC.vue | 221 +++++++++ .../views/authentication/component/SCAN.vue | 236 ++++++++++ ui/src/views/authentication/index.vue | 76 ++++ ui/src/views/email/index.vue | 146 ++++++ ui/src/views/login/index.vue | 336 +++++++++++++- .../views/login/scanCompinents/QrCodeTab.vue | 74 +++ .../login/scanCompinents/dingtalkQrCode.vue | 141 ++++++ .../views/login/scanCompinents/larkQrCode.vue | 59 +++ .../login/scanCompinents/wecomQrCode.vue | 79 ++++ ui/src/views/theme/LoginPreview.vue | 115 +++++ ui/src/views/theme/index.vue | 420 ++++++++++++++++++ 37 files changed, 3318 insertions(+), 144 deletions(-) create mode 100644 ui/src/api/systemSettings/auth-setting.ts create mode 100644 ui/src/api/systemSettings/email-setting.ts create mode 100644 ui/src/api/systemSettings/platform-source.ts create mode 100644 ui/src/api/systemSettings/theme.ts create mode 100644 ui/src/assets/scan/icon_qr_outlined.svg create mode 100644 ui/src/assets/scan/logo_dingtalk.svg create mode 100644 ui/src/assets/scan/logo_lark.svg create mode 100644 ui/src/assets/scan/logo_slack.svg create mode 100644 ui/src/assets/scan/logo_wechat-work.svg create mode 100644 ui/src/assets/scan/logo_wechat.svg create mode 100644 ui/src/utils/utils.ts create mode 100644 ui/src/views/authentication/component/CAS.vue create mode 100644 ui/src/views/authentication/component/EditModal.vue create mode 100644 ui/src/views/authentication/component/LDAP.vue create mode 100644 ui/src/views/authentication/component/OAuth2.vue create mode 100644 ui/src/views/authentication/component/OIDC.vue create mode 100644 ui/src/views/authentication/component/SCAN.vue create mode 100644 ui/src/views/authentication/index.vue create mode 100644 ui/src/views/email/index.vue create mode 100644 ui/src/views/login/scanCompinents/QrCodeTab.vue create mode 100644 ui/src/views/login/scanCompinents/dingtalkQrCode.vue create mode 100644 ui/src/views/login/scanCompinents/larkQrCode.vue create mode 100644 ui/src/views/login/scanCompinents/wecomQrCode.vue create mode 100644 ui/src/views/theme/LoginPreview.vue create mode 100644 ui/src/views/theme/index.vue diff --git a/apps/users/views/login.py b/apps/users/views/login.py index 4cc03fc12..c34b21512 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -17,16 +17,18 @@ from common.utils.common import encryption from users.api.login import LoginAPI, CaptchaAPI from users.serializers.login import LoginSerializer, CaptchaSerializer + def _get_details(request): path = request.path body = request.data query = request.query_params return { - 'path':path, - 'body':{**body, 'password': encryption(body.get('password',''))}, + 'path': path, + 'body': {**body, 'password': encryption(body.get('password', ''))}, 'query': query } + class LoginView(APIView): @extend_schema(methods=['POST'], description=_("Log in"), @@ -37,7 +39,7 @@ class LoginView(APIView): responses=LoginAPI.get_response()) @log(menu='User management', operate='Log in', get_user=lambda r: {'username': r.data.get('username', None)}, get_details=_get_details, - get_operation_object=lambda r,k: {'name': r.data.get('username')}) + get_operation_object=lambda r, k: {'name': r.data.get('username')}) def post(self, request: Request): return result.success(LoginSerializer().login(request.data)) diff --git a/apps/users/views/user.py b/apps/users/views/user.py index c0cc55210..e12b69810 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -32,10 +32,11 @@ def get_user_operation_object(user_id): user_model = QuerySet(model=User).filter(id=user_id).first() if user_model is not None: return { - "name":user_model.name + "name": user_model.name } return {} + def get_re_password_details(request): path = request.path body = request.data @@ -43,12 +44,11 @@ def get_re_password_details(request): return { "path": path, "body": {**body, 'password': encryption(body.get('password', '')), - 're_password': encryption(body.get('re_password',''))}, + 're_password': encryption(body.get('re_password', ''))}, "query": query } - class UserProfileView(APIView): authentication_classes = [TokenAuth] @@ -145,7 +145,7 @@ class UserManage(APIView): responses=DefaultModelResponse.get_response()) @has_permissions(PermissionConstants.USER_DELETE) @log(menu='User management', operate='Delete user', - get_operation_object= lambda r,k: get_user_operation_object(k.get('user_id'))) + get_operation_object=lambda r, k: get_user_operation_object(k.get('user_id'))) def delete(self, request: Request, user_id): return result.success(UserManageSerializer.Operate(data={'id': user_id}).delete(with_valid=True)) @@ -185,7 +185,7 @@ class UserManage(APIView): responses=DefaultModelResponse.get_response()) @has_permissions(PermissionConstants.USER_DELETE) @log(menu='User management', operate='Batch delete user', - get_operation_object= lambda r,k: get_user_operation_object(k.get('user_id'))) + get_operation_object=lambda r, k: get_user_operation_object(k.get('user_id'))) def post(self, request: Request): return result.success(UserManageSerializer.BatchDelete(data=request.data).batch_delete(with_valid=True)) @@ -201,7 +201,7 @@ class UserManage(APIView): request=ChangeUserPasswordApi.get_request(), responses=DefaultModelResponse.get_response()) @log(menu='User management', operate='Change password', - get_operation_object= lambda r,k: get_user_operation_object(k.get('user_id')), + get_operation_object=lambda r, k: get_user_operation_object(k.get('user_id')), get_details=get_re_password_details) def put(self, request: Request, user_id): return result.success( diff --git a/ui/package.json b/ui/package.json index e74a8e006..df3f14d24 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,12 +16,15 @@ "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-python": "^6.2.1", "@codemirror/theme-one-dark": "^6.1.2", + "@wecom/jssdk": "^2.3.1", "axios": "^1.8.4", + "dingtalk-jsapi": "^3.1.0", "element-plus": "^2.9.10", "nprogress": "^0.2.0", "pinia": "^3.0.1", "use-element-plus-theme": "^0.0.5", "vue": "^3.5.13", + "vue-clipboard3": "^2.0.0", "vue-codemirror": "^6.1.1", "vue-i18n": "^11.1.3", "vue-router": "^4.5.0" diff --git a/ui/src/api/systemSettings/auth-setting.ts b/ui/src/api/systemSettings/auth-setting.ts new file mode 100644 index 000000000..552f82e12 --- /dev/null +++ b/ui/src/api/systemSettings/auth-setting.ts @@ -0,0 +1,38 @@ +import {Result} from '@/request/Result' +import {get, post, put} from '@/request/index' +import {type Ref} from 'vue' + +const prefix = '/auth' +/** + * 获取认证设置 + */ +const getAuthSetting: (auth_type: string, loading?: Ref) => Promise> = (auth_type, loading) => { + return get(`${prefix}/${auth_type}/detail`, undefined, loading) +} + +/** + * ldap连接测试 + */ +const postAuthSetting: (data: any, loading?: Ref) => Promise> = ( + data, + loading +) => { + return post(`${prefix}/connection`, data, undefined, loading) +} + +/** + * 修改邮箱设置 + */ +const putAuthSetting: (auth_type: string, data: any, loading?: Ref) => Promise> = ( + auth_type, + data, + loading +) => { + return put(`${prefix}/${auth_type}/info`, data, undefined, loading) +} + +export default { + getAuthSetting, + postAuthSetting, + putAuthSetting +} diff --git a/ui/src/api/systemSettings/email-setting.ts b/ui/src/api/systemSettings/email-setting.ts new file mode 100644 index 000000000..9fc8bf508 --- /dev/null +++ b/ui/src/api/systemSettings/email-setting.ts @@ -0,0 +1,38 @@ +import { Result } from '@/request/Result' +import { get, post, del, put } from '@/request/index' +import type { pageRequest } from '@/api/type/common' +import { type Ref } from 'vue' + +const prefix = '/email_setting' +/** + * 获取邮箱设置 + */ +const getEmailSetting: (loading?: Ref) => Promise> = (loading) => { + return get(`${prefix}`, undefined, loading) +} + +/** + * 邮箱测试 + */ +const postTestEmail: (data: any, loading?: Ref) => Promise> = ( + data, + loading +) => { + return post(`${prefix}`, data, undefined, loading) +} + +/** + * 修改邮箱设置 + */ +const putEmailSetting: (data: any, loading?: Ref) => Promise> = ( + data, + loading +) => { + return put(`${prefix}`, data, undefined, loading) +} + +export default { + getEmailSetting, + postTestEmail, + putEmailSetting +} diff --git a/ui/src/api/systemSettings/platform-source.ts b/ui/src/api/systemSettings/platform-source.ts new file mode 100644 index 000000000..fe234e71d --- /dev/null +++ b/ui/src/api/systemSettings/platform-source.ts @@ -0,0 +1,27 @@ +import { Result } from '@/request/Result' +import { get, post, del, put } from '@/request/index' +import { type Ref } from 'vue' + +const prefix = '/platform' +const getPlatformInfo: (loading?: Ref) => Promise> = (loading) => { + return get(`${prefix}/source`, undefined, loading) +} + +const updateConfig: (data: any, loading?: Ref) => Promise> = ( + data, + loading +) => { + return post(`${prefix}/source`, data, undefined, loading) +} + +const validateConnection: (data: any, loading?: Ref) => Promise> = ( + data, + loading +) => { + return put(`${prefix}/source`, data, undefined, loading) +} +export default { + getPlatformInfo, + updateConfig, + validateConnection +} diff --git a/ui/src/api/systemSettings/theme.ts b/ui/src/api/systemSettings/theme.ts new file mode 100644 index 000000000..cf192bfac --- /dev/null +++ b/ui/src/api/systemSettings/theme.ts @@ -0,0 +1,36 @@ +import {Result} from '@/request/Result' +import {get, post, del, put} from '@/request/index' +import type {Ref} from 'vue' + +const prefix = '/display' + +/** + * 查看外观设置 + */ +const getThemeInfo: (loading?: Ref) => Promise> = (loading) => { + return get(`${prefix}/info`, undefined, loading) +} + +/** + * 更新外观设置 + * @param 参数 + * * formData { + * theme + * icon + * loginLogo + * loginImage + * title + * slogan + * } + */ +const postThemeInfo: (data: any, loading?: Ref) => Promise> = ( + data, + loading +) => { + return put(`${prefix}/update`, data, undefined, loading) +} + +export default { + getThemeInfo, + postThemeInfo +} diff --git a/ui/src/api/user/login.ts b/ui/src/api/user/login.ts index b6cce715d..b224a0314 100644 --- a/ui/src/api/user/login.ts +++ b/ui/src/api/user/login.ts @@ -1,7 +1,9 @@ -import { Result } from '@/request/Result' -import { get, post } from '@/request/index' -import type { LoginRequest } from '@/api/type/login' -import type { Ref } from 'vue' +import {Result} from '@/request/Result' +import {get, post} from '@/request/index' +import type {LoginRequest} from '@/api/type/login' +import type {Ref} from 'vue' +import type {User} from "@/api/type/user.ts"; + /** * 登录 * @param request 登录接口请求表单 @@ -15,6 +17,23 @@ const login: (request: LoginRequest, loading?: Ref) => Promise) => Promise> = ( + request, + loading, +) => { + return post('/ldap/login', request, undefined, loading) +} + + +/** + * 登出 + * @param loading 接口加载器 + * @returns + */ +const logout: (loading?: Ref) => Promise> = (loading) => { + return post('/user/logout', undefined, undefined, loading) +} + /** * 获取验证码 * @param loading 接口加载器 @@ -23,7 +42,75 @@ const getCaptcha: (loading?: Ref) => Promise> = (loading) = return get('/user/captcha', undefined, loading) } +/** + * 获取登录方式 + */ +const getAuthType: (loading?: Ref) => Promise> = (loading) => { + return get('auth/types', undefined, loading) +} + +/** + * 获取二维码类型 + */ +const getQrType: (loading?: Ref) => Promise> = (loading) => { + return get('qr_type', undefined, loading) +} + +const getQrSource: (loading?: Ref) => Promise> = (loading) => { + return get('qr_type/source', undefined, loading) +} + +const getDingCallback: (code: string, loading?: Ref) => Promise> = ( + code, + loading +) => { + return get('dingtalk', {code}, loading) +} + +const getDingOauth2Callback: (code: string, loading?: Ref) => Promise> = ( + code, + loading +) => { + return get('dingtalk/oauth2', {code}, loading) +} + +const getWecomCallback: (code: string, loading?: Ref) => Promise> = ( + code, + loading +) => { + return get('wecom', {code}, loading) +} +const getLarkCallback: (code: string, loading?: Ref) => Promise> = ( + code, + loading +) => { + return get('lark/oauth2', {code}, loading) +} + +/** + * 设置语言 + * data: { + * "language": "string" + * } + */ +const postLanguage: (data: any, loading?: Ref) => Promise> = ( + data, + loading +) => { + return post('/user/language', data, undefined, loading) +} + export default { login, + logout, getCaptcha, + getAuthType, + getDingCallback, + getQrType, + getWecomCallback, + postLanguage, + getDingOauth2Callback, + getLarkCallback, + getQrSource, + ldapLogin } diff --git a/ui/src/api/user/user.ts b/ui/src/api/user/user.ts index 647d34e62..e8a99739e 100644 --- a/ui/src/api/user/user.ts +++ b/ui/src/api/user/user.ts @@ -12,6 +12,13 @@ const getUserProfile: (loading?: Ref) => Promise> = (loadi return get('/user/profile', undefined, loading) } +/** + * 获取profile + */ +const getProfile: (loading?: Ref) => Promise> = (loading) => { + return get('/profile', undefined, loading) +} + /** * 获取版本profile */ @@ -21,4 +28,5 @@ const getUserProfile: (loading?: Ref) => Promise> = (loadi export default { getUserProfile, + getProfile } diff --git a/ui/src/assets/scan/icon_qr_outlined.svg b/ui/src/assets/scan/icon_qr_outlined.svg new file mode 100644 index 000000000..1d3cf4343 --- /dev/null +++ b/ui/src/assets/scan/icon_qr_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/assets/scan/logo_dingtalk.svg b/ui/src/assets/scan/logo_dingtalk.svg new file mode 100644 index 000000000..64d957d4f --- /dev/null +++ b/ui/src/assets/scan/logo_dingtalk.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/assets/scan/logo_lark.svg b/ui/src/assets/scan/logo_lark.svg new file mode 100644 index 000000000..938c5055d --- /dev/null +++ b/ui/src/assets/scan/logo_lark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ui/src/assets/scan/logo_slack.svg b/ui/src/assets/scan/logo_slack.svg new file mode 100644 index 000000000..972c87fce --- /dev/null +++ b/ui/src/assets/scan/logo_slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/assets/scan/logo_wechat-work.svg b/ui/src/assets/scan/logo_wechat-work.svg new file mode 100644 index 000000000..ea8601249 --- /dev/null +++ b/ui/src/assets/scan/logo_wechat-work.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/assets/scan/logo_wechat.svg b/ui/src/assets/scan/logo_wechat.svg new file mode 100644 index 000000000..6c0e78de8 --- /dev/null +++ b/ui/src/assets/scan/logo_wechat.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/components/app-icon/icons/menu.ts b/ui/src/components/app-icon/icons/menu.ts index 8d89117bd..a2be9a7b3 100644 --- a/ui/src/components/app-icon/icons/menu.ts +++ b/ui/src/components/app-icon/icons/menu.ts @@ -54,4 +54,44 @@ export default { ]) }, }, + 'app-setting': { + iconReader: () => { + return h('i', [ + h( + 'svg', + { + viewBox: '0 0 20 20', + version: '1.1', + xmlns: 'http://www.w3.org/2000/svg' + }, + [ + h('path', { + d: 'M3.60734 16.4448L3.34807 16.1624C2.44036 15.1737 1.75935 13.9944 1.36011 12.7066L1.24756 12.3435L2.95427 10.0001L1.24756 7.65668L1.36011 7.29361C1.75935 6.00574 2.44036 4.82649 3.34807 3.83779L3.60734 3.55539L6.47552 3.86889L7.64049 1.21319L8.01405 1.12909C8.66134 0.983366 9.32633 0.90918 10.0004 0.90918C10.6744 0.90918 11.3394 0.983366 11.9867 1.12909L12.3603 1.21319L13.5252 3.86889L16.3934 3.55539L16.6527 3.83779C17.5604 4.82649 18.2414 6.00574 18.6406 7.29361L18.7532 7.65668L17.0465 10.0001L18.7532 12.3435L18.6406 12.7066C18.2414 13.9944 17.5604 15.1737 16.6527 16.1624L16.3934 16.4448L13.5252 16.1313L12.3603 18.787L11.9867 18.8711C11.3394 19.0168 10.6744 19.091 10.0004 19.091C9.32633 19.091 8.66134 19.0168 8.01405 18.8711L7.64049 18.787L6.47552 16.1313L3.60734 16.4448ZM6.51159 14.6031C7.05002 14.5443 7.56436 14.8417 7.78194 15.3377L8.71565 17.4662C9.13677 17.5389 9.56603 17.5758 10.0004 17.5758C10.4347 17.5758 10.864 17.5389 11.2851 17.4662L12.2188 15.3377C12.4364 14.8417 12.9507 14.5443 13.4892 14.6031L15.7844 14.854C16.3387 14.1868 16.7757 13.4286 17.0741 12.6116L15.7038 10.7301C15.3869 10.295 15.3869 9.70511 15.7038 9.26999L17.0741 7.38847C16.7757 6.57146 16.3387 5.81331 15.7844 5.14609L13.4892 5.39696C12.9507 5.45581 12.4364 5.1584 12.2188 4.66238L11.2851 2.53389C10.864 2.46117 10.4347 2.42429 10.0004 2.42429C9.56603 2.42429 9.13677 2.46117 8.71565 2.53389L7.78194 4.66238C7.56436 5.1584 7.05002 5.45581 6.51159 5.39696L4.21641 5.14609C3.66208 5.81331 3.22502 6.57146 2.92666 7.38847L4.29697 9.26999C4.61387 9.70511 4.61387 10.295 4.29697 10.7301L2.92666 12.6116C3.22502 13.4286 3.66208 14.1868 4.21641 14.854L6.51159 14.6031ZM10.0004 13.788C7.91555 13.788 6.22693 12.0913 6.22693 10.0001C6.22693 7.9089 7.91555 6.2122 10.0004 6.2122C12.0852 6.2122 13.7738 7.9089 13.7738 10.0001C13.7738 12.0913 12.0852 13.788 10.0004 13.788ZM10.0004 12.2729C11.2468 12.2729 12.2587 11.2561 12.2587 10.0001C12.2587 8.74413 11.2468 7.72741 10.0004 7.72741C8.75397 7.72741 7.74208 8.74413 7.74208 10.0001C7.74208 11.2561 8.75397 12.2729 10.0004 12.2729Z', + fill: 'currentColor' + }) + ] + ) + ]) + } + }, + 'app-setting-active': { + iconReader: () => { + return h('i', [ + h( + 'svg', + { + viewBox: '0 0 20 20', + version: '1.1', + xmlns: 'http://www.w3.org/2000/svg' + }, + [ + h('path', { + d: 'M3.26425 16.2151C2.35478 15.2292 1.65887 14.0432 1.25 12.7305L2.70785 10.7384C3.02952 10.2988 3.02952 9.70154 2.70785 9.26197L1.25 7.26979C1.65887 5.95714 2.35478 4.77112 3.26425 3.78522L5.71416 4.05172C6.25589 4.11065 6.77338 3.81185 6.99316 3.31321L7.98848 1.05505C8.63579 0.910018 9.30896 0.833496 10 0.833496C10.691 0.833496 11.3642 0.910018 12.0115 1.05505L13.0068 3.31321C13.2266 3.81185 13.7441 4.11065 14.2858 4.05172L16.7357 3.78522C17.6452 4.77112 18.3411 5.95714 18.75 7.26979L17.2921 9.26197C16.9705 9.70154 16.9705 10.2988 17.2921 10.7384L18.75 12.7305C18.3411 14.0432 17.6452 15.2292 16.7357 16.2151L14.2858 15.9486C13.7441 15.8897 13.2266 16.1885 13.0068 16.6871L12.0115 18.9453C11.3642 19.0903 10.691 19.1668 10 19.1668C9.30896 19.1668 8.63579 19.0903 7.98848 18.9453L6.99316 16.6871C6.77338 16.1885 6.25589 15.8897 5.71416 15.9486L3.26425 16.2151ZM10 13.3335C11.8409 13.3335 13.3333 11.8411 13.3333 10.0002C13.3333 8.15921 11.8409 6.66683 10 6.66683C8.15905 6.66683 6.66667 8.15921 6.66667 10.0002C6.66667 11.8411 8.15905 13.3335 10 13.3335Z', + fill: 'currentColor' + }) + ] + ) + ]) + } + }, } diff --git a/ui/src/locales/index.ts b/ui/src/locales/index.ts index a2c8b2e3a..86f0a8c4d 100644 --- a/ui/src/locales/index.ts +++ b/ui/src/locales/index.ts @@ -1,9 +1,9 @@ -import { useLocalStorage, usePreferredLanguages } from '@vueuse/core' -import { computed } from 'vue' -import { createI18n } from 'vue-i18n' +import {useLocalStorage, usePreferredLanguages} from '@vueuse/core' +import {computed} from 'vue' +import {createI18n} from 'vue-i18n' // 导入语言文件 -const langModules = import.meta.glob('./lang/*/index.ts', { eager: true }) as Record< +const langModules = import.meta.glob('./lang/*/index.ts', {eager: true}) as Record< string, () => Promise<{ default: Object }> > @@ -79,7 +79,6 @@ export const langList = computed(() => { return list }) -// @ts-ignore -export const { t } = i18n.global +export const {t} = i18n.global export default i18n diff --git a/ui/src/router/modules/system.ts b/ui/src/router/modules/system.ts index a54e0966e..ec4289406 100644 --- a/ui/src/router/modules/system.ts +++ b/ui/src/router/modules/system.ts @@ -32,6 +32,56 @@ const systemRouter = { }, component: () => import('@/views/resource-authorization/index.vue'), }, + { + path:'/system/setting', + name: 'setting', + meta: { + icon: 'app-setting', + iconActive: 'app-setting-active', + title: 'views.system.subTitle', + activeMenu: '/system', + parentPath: '/system', + parentName: 'system', + }, + children: [ + { + path: '/system/theme', + name: 'theme', + meta: { + title: 'views.system.theme.title', + activeMenu: '/setting', + parentPath: '/setting', + parentName: 'setting', + //permission: new ComplexPermission(['ADMIN'], ['x-pack'], 'AND') + }, + component: () => import('@/views/theme/index.vue'), + }, + { + path: '/system/authentication', + name: 'authentication', + meta: { + title: 'views.system.authentication.title', + activeMenu: '/setting', + parentPath: '/setting', + parentName: 'setting', + //permission: new ComplexPermission(['ADMIN'], ['x-pack'], 'AND') + }, + component: () => import('@/views/authentication/index.vue') + }, + { + path: '/system/email', + name: 'email', + meta: { + title: 'views.system.email.title', + activeMenu: '/setting', + parentPath: '/setting', + parentName: 'setting', + //permission: new Role('ADMIN') + }, + component: () => import('@/views/email/index.vue') + } + ] + } ], } diff --git a/ui/src/stores/modules/login.ts b/ui/src/stores/modules/login.ts index 7bc487f2b..636af77cc 100644 --- a/ui/src/stores/modules/login.ts +++ b/ui/src/stores/modules/login.ts @@ -37,6 +37,14 @@ const useLoginStore = defineStore('login', { return user.profile(loading) }) }, + async asyncLdapLogin(data: LoginRequest, loading?: Ref) { + return loginApi.ldapLogin(data).then((ok) => { + this.token = ok?.data?.token + localStorage.setItem('token', ok?.data?.token) + const user = useUserStore() + return user.profile(loading) + }) + }, }, }) diff --git a/ui/src/stores/modules/theme.ts b/ui/src/stores/modules/theme.ts index 991780010..82a444aa7 100644 --- a/ui/src/stores/modules/theme.ts +++ b/ui/src/stores/modules/theme.ts @@ -1,6 +1,8 @@ import { defineStore } from 'pinia' import { cloneDeep } from 'lodash' import { useElementPlusTheme } from 'use-element-plus-theme' +import ThemeApi from '@/api/systemSettings/theme' +import type {Ref} from "vue"; export interface themeStateTypes { themeInfo: any } @@ -18,8 +20,8 @@ const useThemeStore = defineStore('theme', { setTheme(data?: any) { const { changeTheme } = useElementPlusTheme(this.themeInfo?.theme || defalueColor) changeTheme(defalueColor) - // changeTheme(data?.['theme']) - // this.themeInfo = cloneDeep(data) + changeTheme(data?.['theme']) + this.themeInfo = cloneDeep(data) }, // async theme(loading?: Ref) { diff --git a/ui/src/stores/modules/user.ts b/ui/src/stores/modules/user.ts index c06489e42..4989a5eb5 100644 --- a/ui/src/stores/modules/user.ts +++ b/ui/src/stores/modules/user.ts @@ -1,20 +1,25 @@ -import { defineStore } from 'pinia' -import { type Ref } from 'vue' -import type { User } from '@/api/type/user' -import { cloneDeep } from 'lodash' +import {defineStore} from 'pinia' +import {type Ref} from 'vue' +import type {User} from '@/api/type/user' import UserApi from '@/api/user/user' -// import ThemeApi from '@/api/theme' -// import { useElementPlusTheme } from 'use-element-plus-theme' +import LoginApi from '@/api/user/login' +import {cloneDeep} from 'lodash' +import ThemeApi from '@/api/systemSettings/theme' // import { defaultPlatformSetting } from '@/utils/theme' -import { useLocalStorage } from '@vueuse/core' -import { localeConfigKey, getBrowserLang } from '@/locales/index' +import {useLocalStorage} from '@vueuse/core' +import {localeConfigKey, getBrowserLang} from '@/locales/index' import useThemeStore from './theme' +import {useElementPlusTheme} from "use-element-plus-theme"; +import {defaultPlatformSetting} from "@/utils/theme.ts"; + export interface userStateTypes { userType: number // 1 系统操作者 2 对话用户 userInfo: User | null version?: string - XPACK_LICENSE_IS_VALID: false - isXPack: false + XPACK_LICENSE_IS_VALID: true + isXPack: true + themeInfo: any + token: any } const useLoginStore = defineStore('user', { @@ -24,6 +29,8 @@ const useLoginStore = defineStore('user', { version: '', XPACK_LICENSE_IS_VALID: false, isXPack: false, + themeInfo: null, + token: '' }), actions: { getLanguage() { @@ -31,6 +38,14 @@ const useLoginStore = defineStore('user', { ? localStorage.getItem('MaxKB-locale') || getBrowserLang() : sessionStorage.getItem('language') || getBrowserLang() }, + isDefaultTheme() { + return !this.themeInfo?.theme || this.themeInfo?.theme === '#3370FF' + }, + setTheme(data: any) { + const {changeTheme} = useElementPlusTheme(this.themeInfo?.theme) + changeTheme(data?.['theme']) + this.themeInfo = cloneDeep(data) + }, async profile(loading?: Ref) { return UserApi.getUserProfile(loading).then((ok) => { this.userInfo = ok.data @@ -38,31 +53,31 @@ const useLoginStore = defineStore('user', { ok?.data?.language || this.getLanguage() const theme = useThemeStore() theme.setTheme() - // return this.asyncGetProfile() + return this.asyncGetProfile() }) }, - // async asyncGetProfile() { - // return new Promise((resolve, reject) => { - // UserApi.getProfile() - // .then(async (ok) => { - // this.version = ok.data?.version || '-' - // this.isXPack = ok.data?.IS_XPACK - // this.XPACK_LICENSE_IS_VALID = ok.data?.XPACK_LICENSE_IS_VALID + async asyncGetProfile() { + return new Promise((resolve, reject) => { + UserApi.getProfile() + .then(async (ok) => { + // this.version = ok.data?.version || '-' + this.isXPack = true + this.XPACK_LICENSE_IS_VALID = true - // if (this.isEnterprise()) { - // await this.theme() - // } else { - // this.themeInfo = { - // ...defaultPlatformSetting - // } - // } - // resolve(ok) - // }) - // .catch((error) => { - // reject(error) - // }) - // }) - // }, + if (this.isEnterprise()) { + await this.theme() + } else { + this.themeInfo = { + ...defaultPlatformSetting + } + } + resolve(ok) + }) + .catch((error) => { + reject(error) + }) + }) + }, getPermissions() { if (this.userInfo) { @@ -80,16 +95,26 @@ const useLoginStore = defineStore('user', { return '' } }, - // showXpack() { - // return this.isXPack - // }, + async theme(loading?: Ref) { + return await ThemeApi.getThemeInfo(loading).then((ok) => { + this.setTheme(ok.data) + // window.document.title = this.themeInfo['title'] || 'MaxKB' + // const link = document.querySelector('link[rel="icon"]') as any + // if (link) { + // link['href'] = this.themeInfo['icon'] || '/favicon.ico' + // } + }) + }, + showXpack() { + return this.isXPack + }, - // isExpire() { - // return this.isXPack && !this.XPACK_LICENSE_IS_VALID - // }, - // isEnterprise() { - // return this.isXPack && this.XPACK_LICENSE_IS_VALID - // }, + isExpire() { + return this.isXPack && !this.XPACK_LICENSE_IS_VALID + }, + isEnterprise() { + return this.isXPack && this.XPACK_LICENSE_IS_VALID + }, // changeUserType(num: number, token?: string) { // this.userType = num @@ -97,70 +122,69 @@ const useLoginStore = defineStore('user', { // }, + async dingCallback(code: string) { + return LoginApi.getDingCallback(code).then((ok) => { + this.token = ok.data + localStorage.setItem('token', ok.data) + return this.profile() + }) + }, + async dingOauth2Callback(code: string) { + return LoginApi.getDingOauth2Callback(code).then((ok) => { + this.token = ok.data + localStorage.setItem('token', ok.data) + return this.profile() + }) + }, + async wecomCallback(code: string) { + return LoginApi.getWecomCallback(code).then((ok) => { + this.token = ok.data + localStorage.setItem('token', ok.data) + return this.profile() + }) + }, + async larkCallback(code: string) { + return LoginApi.getLarkCallback(code).then((ok) => { + this.token = ok.data + localStorage.setItem('token', ok.data) + return this.profile() + }) + }, - // async dingCallback(code: string) { - // return UserApi.getDingCallback(code).then((ok) => { - // this.token = ok.data - // localStorage.setItem('token', ok.data) - // return this.profile() - // }) - // }, - // async dingOauth2Callback(code: string) { - // return UserApi.getDingOauth2Callback(code).then((ok) => { - // this.token = ok.data - // localStorage.setItem('token', ok.data) - // return this.profile() - // }) - // }, - // async wecomCallback(code: string) { - // return UserApi.getWecomCallback(code).then((ok) => { - // this.token = ok.data - // localStorage.setItem('token', ok.data) - // return this.profile() - // }) - // }, - // async larkCallback(code: string) { - // return UserApi.getlarkCallback(code).then((ok) => { - // this.token = ok.data - // localStorage.setItem('token', ok.data) - // return this.profile() - // }) - // }, - - // async logout() { - // return UserApi.logout().then(() => { - // localStorage.removeItem('token') - // return true - // }) - // }, - // async getAuthType() { - // return UserApi.getAuthType().then((ok) => { - // return ok.data - // }) - // }, - // async getQrType() { - // return UserApi.getQrType().then((ok) => { - // return ok.data - // }) - // }, - // async getQrSource() { - // return UserApi.getQrSource().then((ok) => { - // return ok.data - // }) - // }, - // async postUserLanguage(lang: string, loading?: Ref) { - // return new Promise((resolve, reject) => { - // UserApi.postLanguage({ language: lang }, loading) - // .then(async (ok) => { - // useLocalStorage(localeConfigKey, 'en-US').value = lang - // window.location.reload() - // resolve(ok) - // }) - // .catch((error) => { - // reject(error) - // }) - // }) - // } + async logout() { + return LoginApi.logout().then(() => { + localStorage.removeItem('token') + return true + }) + }, + async getAuthType() { + return LoginApi.getAuthType().then((ok) => { + return ok.data + }) + }, + async getQrType() { + return LoginApi.getQrType().then((ok) => { + return ok.data + }) + }, + async getQrSource() { + return LoginApi.getQrSource().then((ok) => { + return ok.data + }) + }, + async postUserLanguage(lang: string, loading?: Ref) { + return new Promise((resolve, reject) => { + LoginApi.postLanguage({ language: lang }, loading) + .then(async (ok) => { + useLocalStorage(localeConfigKey, 'en-US').value = lang + window.location.reload() + resolve(ok) + }) + .catch((error) => { + reject(error) + }) + }) + } }, }) diff --git a/ui/src/utils/utils.ts b/ui/src/utils/utils.ts new file mode 100644 index 000000000..44e68895c --- /dev/null +++ b/ui/src/utils/utils.ts @@ -0,0 +1,167 @@ +import { MsgError } from '@/utils/message' + +export function toThousands(num: any) { + return num?.toString().replace(/\d+/, function (n: any) { + return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') + }) +} +export function numberFormat(num: number) { + return num < 1000 ? toThousands(num) : toThousands((num / 1000).toFixed(1)) + 'k' +} + +export function filesize(size: number) { + if (!size) return '' + /* byte */ + const num = 1024.0 + + if (size < num) return size + 'B' + if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + 'K' //kb + if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + 'M' //M + if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + 'G' //G + return (size / Math.pow(num, 4)).toFixed(2) + 'T' //T +} + +/* + 随机id +*/ +export const randomId = function () { + return Math.floor(Math.random() * 10000) + '' +} + +/* + 获取文件后缀 +*/ +export function fileType(name: string) { + const suffix = name.split('.') + return suffix[suffix.length - 1] +} + +/* + 获得文件对应图片 +*/ +const typeList: any = { + txt: ['txt', 'pdf', 'docx', 'md', 'html', 'zip', 'xlsx', 'xls', 'csv'], + table: ['xlsx', 'xls', 'csv'], + QA: ['xlsx', 'csv', 'xls', 'zip'] +} + +export function getImgUrl(name: string) { + const list = Object.values(typeList).flat() + + const type = list.includes(fileType(name).toLowerCase()) ? fileType(name).toLowerCase() : 'unknown' + return new URL(`../assets/fileType/${type}-icon.svg`, import.meta.url).href +} +// 是否是白名单后缀 +export function isRightType(name: string, type: string) { + return typeList[type].includes(fileType(name).toLowerCase()) +} + +/* + 从指定数组中过滤出对应的对象 +*/ +export function relatedObject(list: any, val: any, attr: string) { + const filterData: any = list.filter((item: any) => item[attr] === val)?.[0] + return filterData || null +} + +// 排序 +export function arraySort(list: Array, property: any, desc?: boolean) { + return list.sort((a: any, b: any) => { + return desc ? b[property] - a[property] : a[property] - b[property] + }) +} + +// 判断对象里所有属性全部为空 +export function isAllPropertiesEmpty(obj: object) { + return Object.values(obj).every( + (value) => + value === null || typeof value === 'undefined' || (typeof value === 'string' && !value) + ) +} + +// 数组对象中某一属性值的集合 +export function getAttrsArray(array: Array, attr: string) { + return array.map((item) => { + return item[attr] + }) +} + +// 求和 +export function getSum(array: Array) { + return array.reduce((total, item) => total + item, 0) +} + +// 下载 +export function downloadByURL(url: string, name: string) { + const a = document.createElement('a') + a.setAttribute('href', url) + a.setAttribute('target', '_blank') + a.setAttribute('download', name) + document.body.appendChild(a) + a.click() + document.body.removeChild(a) +} + +// 截取文件名 +export function cutFilename(filename: string, num: number) { + const lastIndex = filename.lastIndexOf('.') + const suffix = lastIndex === -1 ? '' : filename.substring(lastIndex + 1) + return filename.substring(0, num - suffix.length - 1) + '.' + suffix +} + +export function getNormalizedUrl(url: string) { + if (url && !url.endsWith('/') && !/\.[^/]+$/.test(url)) { + return url + '/' + } + return url +} + +interface LoadScriptOptions { + jsId?: string // 自定义脚本 ID + forceReload?: boolean // 是否强制重新加载(默认 false) +} + +export const loadScript = (url: string, options: LoadScriptOptions = {}): Promise => { + const { jsId, forceReload = false } = options + const scriptId = jsId || `script-${btoa(url).slice(0, 12)}` // 生成唯一 ID + + return new Promise((resolve, reject) => { + // 检查是否已存在且无需强制加载 + const existingScript = document.getElementById(scriptId) as HTMLScriptElement | null + if (existingScript && !forceReload) { + if (existingScript.src === url) { + existingScript.onload = () => resolve() // 复用现有脚本 + return + } + // URL 不同则移除旧脚本 + existingScript.parentElement?.removeChild(existingScript) + } + + // 创建新脚本 + const script = document.createElement('script') + script.id = scriptId + script.src = url + script.async = true // 明确启用异步加载 + + // 成功回调 + script.onload = () => { + resolve() + } + + // 错误处理(兼容性增强) + script.onerror = () => { + reject(new Error(`Failed to load script: ${url}`)) + cleanupScript(script) + } + + // 插入到 确保加载顺序 + document.head.appendChild(script) + }) +} + +// 清理脚本(可选) +const cleanupScript = (script: HTMLScriptElement) => { + script.onload = null + script.onerror = null + script.parentElement?.removeChild(script) +} diff --git a/ui/src/views/authentication/component/CAS.vue b/ui/src/views/authentication/component/CAS.vue new file mode 100644 index 000000000..3a0381b84 --- /dev/null +++ b/ui/src/views/authentication/component/CAS.vue @@ -0,0 +1,127 @@ + + + diff --git a/ui/src/views/authentication/component/EditModal.vue b/ui/src/views/authentication/component/EditModal.vue new file mode 100644 index 000000000..c23e69867 --- /dev/null +++ b/ui/src/views/authentication/component/EditModal.vue @@ -0,0 +1,230 @@ +template + + + + + diff --git a/ui/src/views/authentication/component/LDAP.vue b/ui/src/views/authentication/component/LDAP.vue new file mode 100644 index 000000000..57e51ddae --- /dev/null +++ b/ui/src/views/authentication/component/LDAP.vue @@ -0,0 +1,184 @@ + + + diff --git a/ui/src/views/authentication/component/OAuth2.vue b/ui/src/views/authentication/component/OAuth2.vue new file mode 100644 index 000000000..ccc65a849 --- /dev/null +++ b/ui/src/views/authentication/component/OAuth2.vue @@ -0,0 +1,210 @@ + + + diff --git a/ui/src/views/authentication/component/OIDC.vue b/ui/src/views/authentication/component/OIDC.vue new file mode 100644 index 000000000..b8279235e --- /dev/null +++ b/ui/src/views/authentication/component/OIDC.vue @@ -0,0 +1,221 @@ + + + diff --git a/ui/src/views/authentication/component/SCAN.vue b/ui/src/views/authentication/component/SCAN.vue new file mode 100644 index 000000000..c0ea40655 --- /dev/null +++ b/ui/src/views/authentication/component/SCAN.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/ui/src/views/authentication/index.vue b/ui/src/views/authentication/index.vue new file mode 100644 index 000000000..178b9730f --- /dev/null +++ b/ui/src/views/authentication/index.vue @@ -0,0 +1,76 @@ + + + diff --git a/ui/src/views/email/index.vue b/ui/src/views/email/index.vue new file mode 100644 index 000000000..4ba86f837 --- /dev/null +++ b/ui/src/views/email/index.vue @@ -0,0 +1,146 @@ + + + diff --git a/ui/src/views/login/index.vue b/ui/src/views/login/index.vue index 6cd1c9b4d..39e652aba 100644 --- a/ui/src/views/login/index.vue +++ b/ui/src/views/login/index.vue @@ -1,8 +1,8 @@ - + diff --git a/ui/src/views/login/scanCompinents/QrCodeTab.vue b/ui/src/views/login/scanCompinents/QrCodeTab.vue new file mode 100644 index 000000000..a7781dd69 --- /dev/null +++ b/ui/src/views/login/scanCompinents/QrCodeTab.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/ui/src/views/login/scanCompinents/dingtalkQrCode.vue b/ui/src/views/login/scanCompinents/dingtalkQrCode.vue new file mode 100644 index 000000000..670084d38 --- /dev/null +++ b/ui/src/views/login/scanCompinents/dingtalkQrCode.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/ui/src/views/login/scanCompinents/larkQrCode.vue b/ui/src/views/login/scanCompinents/larkQrCode.vue new file mode 100644 index 000000000..fd4e4fcb4 --- /dev/null +++ b/ui/src/views/login/scanCompinents/larkQrCode.vue @@ -0,0 +1,59 @@ + + + + diff --git a/ui/src/views/login/scanCompinents/wecomQrCode.vue b/ui/src/views/login/scanCompinents/wecomQrCode.vue new file mode 100644 index 000000000..d0e639158 --- /dev/null +++ b/ui/src/views/login/scanCompinents/wecomQrCode.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/ui/src/views/theme/LoginPreview.vue b/ui/src/views/theme/LoginPreview.vue new file mode 100644 index 000000000..9de9cae3f --- /dev/null +++ b/ui/src/views/theme/LoginPreview.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/ui/src/views/theme/index.vue b/ui/src/views/theme/index.vue new file mode 100644 index 000000000..e425692b2 --- /dev/null +++ b/ui/src/views/theme/index.vue @@ -0,0 +1,420 @@ + + + + +