From b041b8e7d7189851ba03bdf81083aeeadf234858 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Tue, 11 Nov 2025 13:42:13 +0800 Subject: [PATCH] feat: auth and route --- apps/web-tdesign/src/api/core/auth.ts | 162 +++++++++++--- apps/web-tdesign/src/api/core/index.ts | 2 - apps/web-tdesign/src/api/core/menu.ts | 10 - apps/web-tdesign/src/api/core/user.ts | 10 - .../src/locales/langs/en-US/demos.json | 12 - .../src/locales/langs/en-US/page.json | 19 ++ .../src/locales/langs/zh-CN/demos.json | 12 - .../src/locales/langs/zh-CN/page.json | 19 ++ apps/web-tdesign/src/router/access.ts | 16 +- apps/web-tdesign/src/router/guard.ts | 30 ++- apps/web-tdesign/src/router/index.ts | 3 + apps/web-tdesign/src/router/routes/core.ts | 17 ++ apps/web-tdesign/src/router/routes/index.ts | 12 +- .../src/router/routes/modules/demos.ts | 28 --- .../src/router/routes/modules/vben.ts | 94 -------- apps/web-tdesign/src/store/auth.ts | 76 +++++-- .../views/_core/authentication/code-login.vue | 113 +++++++++- .../_core/authentication/forget-password.vue | 190 +++++++++++++++- .../src/views/_core/authentication/login.vue | 209 +++++++++++++----- .../views/_core/authentication/register.vue | 151 +++++++++++-- .../src/views/demos/tdesign/index.vue | 67 ------ 21 files changed, 875 insertions(+), 377 deletions(-) delete mode 100644 apps/web-tdesign/src/api/core/menu.ts delete mode 100644 apps/web-tdesign/src/api/core/user.ts delete mode 100644 apps/web-tdesign/src/locales/langs/en-US/demos.json delete mode 100644 apps/web-tdesign/src/locales/langs/zh-CN/demos.json delete mode 100644 apps/web-tdesign/src/router/routes/modules/demos.ts delete mode 100644 apps/web-tdesign/src/router/routes/modules/vben.ts delete mode 100644 apps/web-tdesign/src/views/demos/tdesign/index.vue diff --git a/apps/web-tdesign/src/api/core/auth.ts b/apps/web-tdesign/src/api/core/auth.ts index 71d9f9943..604644a1f 100644 --- a/apps/web-tdesign/src/api/core/auth.ts +++ b/apps/web-tdesign/src/api/core/auth.ts @@ -1,3 +1,5 @@ +import type { AuthPermissionInfo } from '@vben/types'; + import { baseRequestClient, requestClient } from '#/api/request'; export namespace AuthApi { @@ -5,47 +7,155 @@ export namespace AuthApi { export interface LoginParams { password?: string; username?: string; + captchaVerification?: string; + // 绑定社交登录时,需要传递如下参数 + socialType?: number; + socialCode?: string; + socialState?: string; } /** 登录接口返回值 */ export interface LoginResult { accessToken: string; + refreshToken: string; + userId: number; + expiresTime: number; } - export interface RefreshTokenResult { - data: string; - status: number; + /** 租户信息返回值 */ + export interface TenantResult { + id: number; + name: string; + } + + /** 手机验证码获取接口参数 */ + export interface SmsCodeParams { + mobile: string; + scene: number; + } + + /** 手机验证码登录接口参数 */ + export interface SmsLoginParams { + mobile: string; + code: string; + } + + /** 注册接口参数 */ + export interface RegisterParams { + username: string; + password: string; + captchaVerification: string; + } + + /** 重置密码接口参数 */ + export interface ResetPasswordParams { + password: string; + mobile: string; + code: string; + } + + /** 社交快捷登录接口参数 */ + export interface SocialLoginParams { + type: number; + code: string; + state: string; } } -/** - * 登录 - */ +/** 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/auth/login', data); -} - -/** - * 刷新accessToken - */ -export async function refreshTokenApi() { - return baseRequestClient.post('/auth/refresh', { - withCredentials: true, + return requestClient.post('/system/auth/login', data, { + headers: { + isEncrypt: false, + }, }); } -/** - * 退出登录 - */ -export async function logoutApi() { - return baseRequestClient.post('/auth/logout', { - withCredentials: true, +/** 刷新 accessToken */ +export async function refreshTokenApi(refreshToken: string) { + return baseRequestClient.post( + `/system/auth/refresh-token?refreshToken=${refreshToken}`, + ); +} + +/** 退出登录 */ +export async function logoutApi(accessToken: string) { + return baseRequestClient.post( + '/system/auth/logout', + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); +} + +/** 获取权限信息 */ +export async function getAuthPermissionInfoApi() { + return requestClient.get( + '/system/auth/get-permission-info', + ); +} + +/** 获取租户列表 */ +export async function getTenantSimpleList() { + return requestClient.get( + `/system/tenant/simple-list`, + ); +} + +/** 使用租户域名,获得租户信息 */ +export async function getTenantByWebsite(website: string) { + return requestClient.get( + `/system/tenant/get-by-website?website=${website}`, + ); +} + +/** 获取验证码 */ +export async function getCaptcha(data: any) { + return baseRequestClient.post('/system/captcha/get', data); +} + +/** 校验验证码 */ +export async function checkCaptcha(data: any) { + return baseRequestClient.post('/system/captcha/check', data); +} + +/** 获取登录验证码 */ +export async function sendSmsCode(data: AuthApi.SmsCodeParams) { + return requestClient.post('/system/auth/send-sms-code', data); +} + +/** 短信验证码登录 */ +export async function smsLogin(data: AuthApi.SmsLoginParams) { + return requestClient.post('/system/auth/sms-login', data); +} + +/** 注册 */ +export async function register(data: AuthApi.RegisterParams) { + return requestClient.post('/system/auth/register', data); +} + +/** 通过短信重置密码 */ +export async function smsResetPassword(data: AuthApi.ResetPasswordParams) { + return requestClient.post('/system/auth/reset-password', data); +} + +/** 社交授权的跳转 */ +export async function socialAuthRedirect(type: number, redirectUri: string) { + return requestClient.get('/system/auth/social-auth-redirect', { + params: { + type, + redirectUri, + }, }); } -/** - * 获取用户权限码 - */ -export async function getAccessCodesApi() { - return requestClient.get('/auth/codes'); +/** 社交快捷登录 */ +export async function socialLogin(data: AuthApi.SocialLoginParams) { + return requestClient.post( + '/system/auth/social-login', + data, + ); } diff --git a/apps/web-tdesign/src/api/core/index.ts b/apps/web-tdesign/src/api/core/index.ts index 28a5aef47..269586ee8 100644 --- a/apps/web-tdesign/src/api/core/index.ts +++ b/apps/web-tdesign/src/api/core/index.ts @@ -1,3 +1 @@ export * from './auth'; -export * from './menu'; -export * from './user'; diff --git a/apps/web-tdesign/src/api/core/menu.ts b/apps/web-tdesign/src/api/core/menu.ts deleted file mode 100644 index 9ef60b11c..000000000 --- a/apps/web-tdesign/src/api/core/menu.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RouteRecordStringComponent } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户所有菜单 - */ -export async function getAllMenusApi() { - return requestClient.get('/menu/all'); -} diff --git a/apps/web-tdesign/src/api/core/user.ts b/apps/web-tdesign/src/api/core/user.ts deleted file mode 100644 index 7e28ea848..000000000 --- a/apps/web-tdesign/src/api/core/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { UserInfo } from '@vben/types'; - -import { requestClient } from '#/api/request'; - -/** - * 获取用户信息 - */ -export async function getUserInfoApi() { - return requestClient.get('/user/info'); -} diff --git a/apps/web-tdesign/src/locales/langs/en-US/demos.json b/apps/web-tdesign/src/locales/langs/en-US/demos.json deleted file mode 100644 index e7bcea129..000000000 --- a/apps/web-tdesign/src/locales/langs/en-US/demos.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Demos", - "tdesign": "TDesign Vue", - "vben": { - "title": "Project", - "about": "About", - "document": "Document", - "antdv": "Ant Design Vue Version", - "naive-ui": "Naive UI Version", - "element-plus": "Element Plus Version" - } -} diff --git a/apps/web-tdesign/src/locales/langs/en-US/page.json b/apps/web-tdesign/src/locales/langs/en-US/page.json index 618a258c0..00a8c90b2 100644 --- a/apps/web-tdesign/src/locales/langs/en-US/page.json +++ b/apps/web-tdesign/src/locales/langs/en-US/page.json @@ -10,5 +10,24 @@ "title": "Dashboard", "analytics": "Analytics", "workspace": "Workspace" + }, + "action": { + "action": "Action", + "add": "Add", + "edit": "Edit", + "delete": "Delete", + "save": "Save", + "import": "Import", + "export": "Export", + "submit": "Submit", + "cancel": "Cancel", + "confirm": "Confirm", + "reset": "Reset", + "search": "Search", + "more": "More" + }, + "tenant": { + "placeholder": "Please select tenant", + "success": "Switch tenant success" } } diff --git a/apps/web-tdesign/src/locales/langs/zh-CN/demos.json b/apps/web-tdesign/src/locales/langs/zh-CN/demos.json deleted file mode 100644 index 843a1f30d..000000000 --- a/apps/web-tdesign/src/locales/langs/zh-CN/demos.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "演示", - "tdesign": "TDesign Vue", - "vben": { - "title": "项目", - "about": "关于", - "document": "文档", - "antdv": "Ant Design Vue 版本", - "naive-ui": "Naive UI 版本", - "element-plus": "Element Plus 版本" - } -} diff --git a/apps/web-tdesign/src/locales/langs/zh-CN/page.json b/apps/web-tdesign/src/locales/langs/zh-CN/page.json index 4cb67081c..eefc4924b 100644 --- a/apps/web-tdesign/src/locales/langs/zh-CN/page.json +++ b/apps/web-tdesign/src/locales/langs/zh-CN/page.json @@ -10,5 +10,24 @@ "title": "概览", "analytics": "分析页", "workspace": "工作台" + }, + "action": { + "action": "操作", + "add": "新增", + "edit": "编辑", + "delete": "删除", + "save": "保存", + "import": "导入", + "export": "导出", + "submit": "提交", + "cancel": "取消", + "confirm": "确认", + "reset": "重置", + "search": "搜索", + "more": "更多" + }, + "tenant": { + "placeholder": "请选择租户", + "success": "切换租户成功" } } diff --git a/apps/web-tdesign/src/router/access.ts b/apps/web-tdesign/src/router/access.ts index 08cb4b0bd..c4b161a5d 100644 --- a/apps/web-tdesign/src/router/access.ts +++ b/apps/web-tdesign/src/router/access.ts @@ -1,20 +1,21 @@ import type { + AppRouteRecordRaw, ComponentRecordType, GenerateMenuAndRoutesOptions, } from '@vben/types'; import { generateAccessible } from '@vben/access'; import { preferences } from '@vben/preferences'; +import { useAccessStore } from '@vben/stores'; +import { convertServerMenuToRouteRecordStringComponent } from '@vben/utils'; -import { message } from '#/adapter/tdesign'; -import { getAllMenusApi } from '#/api'; import { BasicLayout, IFrameView } from '#/layouts'; -import { $t } from '#/locales'; const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue'); async function generateAccess(options: GenerateMenuAndRoutesOptions) { const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); + const accessStore = useAccessStore(); const layoutMap: ComponentRecordType = { BasicLayout, @@ -24,11 +25,10 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { return await generateAccessible(preferences.app.accessMode, { ...options, fetchMenuListAsync: async () => { - message.loading({ - content: `${$t('common.loadingMenu')}...`, - duration: 1500, - }); - return await getAllMenusApi(); + // 由于 yudao 通过 accessStore 读取,所以不在进行 message.loading 提示 + // 补充说明:accessStore.accessMenus 一开始是 AppRouteRecordRaw 类型(后端加载),后面被赋值成 MenuRecordRaw 类型(前端转换) + const accessMenus = accessStore.accessMenus as AppRouteRecordRaw[]; + return convertServerMenuToRouteRecordStringComponent(accessMenus); }, // 可以指定没有权限跳转403页面 forbiddenComponent, diff --git a/apps/web-tdesign/src/router/guard.ts b/apps/web-tdesign/src/router/guard.ts index a1ad6d88c..593867acf 100644 --- a/apps/web-tdesign/src/router/guard.ts +++ b/apps/web-tdesign/src/router/guard.ts @@ -1,10 +1,13 @@ import type { Router } from 'vue-router'; import { LOGIN_PATH } from '@vben/constants'; +import { $t } from '@vben/locales'; import { preferences } from '@vben/preferences'; -import { useAccessStore, useUserStore } from '@vben/stores'; +import { useAccessStore, useDictStore, useUserStore } from '@vben/stores'; import { startProgress, stopProgress } from '@vben/utils'; +import { message } from '#/adapter/tdesign'; +import { getSimpleDictDataList } from '#/api/system/dict/data'; import { accessRoutes, coreRouteNames } from '#/router/routes'; import { useAuthStore } from '#/store'; @@ -49,6 +52,7 @@ function setupAccessGuard(router: Router) { const accessStore = useAccessStore(); const userStore = useUserStore(); const authStore = useAuthStore(); + const dictStore = useDictStore(); // 基本路由,这些路由不需要进入权限拦截 if (coreRouteNames.includes(to.name as string)) { @@ -90,10 +94,27 @@ function setupAccessGuard(router: Router) { return true; } + // 加载字典数据(不阻塞加载) + dictStore.setDictCacheByApi(getSimpleDictDataList); + // 生成路由表 // 当前登录用户拥有的角色标识列表 - const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); - const userRoles = userInfo.roles ?? []; + let userInfo = userStore.userInfo; + if (!userInfo) { + // add by 芋艿:由于 yudao 是 fetchUserInfo 统一加载用户 + 权限信息,所以将 fetchMenuListAsync + const loading = message.loading({ + content: `${$t('common.loadingMenu')}...`, + }); + try { + const authPermissionInfo = await authStore.fetchUserInfo(); + if (authPermissionInfo) { + userInfo = authPermissionInfo.user; + } + } finally { + message.close(loading); + } + } + const userRoles = userStore.userRoles ?? []; // 生成菜单和路由 const { accessibleMenus, accessibleRoutes } = await generateAccess({ @@ -107,9 +128,10 @@ function setupAccessGuard(router: Router) { accessStore.setAccessMenus(accessibleMenus); accessStore.setAccessRoutes(accessibleRoutes); accessStore.setIsAccessChecked(true); + userStore.setUserRoles(userRoles); const redirectPath = (from.query.redirect ?? (to.path === preferences.app.defaultHomePath - ? userInfo.homePath || preferences.app.defaultHomePath + ? userInfo?.homePath || preferences.app.defaultHomePath : to.fullPath)) as string; return { diff --git a/apps/web-tdesign/src/router/index.ts b/apps/web-tdesign/src/router/index.ts index 484023034..5acec55ea 100644 --- a/apps/web-tdesign/src/router/index.ts +++ b/apps/web-tdesign/src/router/index.ts @@ -8,6 +8,7 @@ import { resetStaticRoutes } from '@vben/utils'; import { createRouterGuard } from './guard'; import { routes } from './routes'; +import { setupBaiduTongJi } from './tongji'; /** * @zh_CN 创建vue-router实例 @@ -33,5 +34,7 @@ const resetRoutes = () => resetStaticRoutes(router, routes); // 创建路由守卫 createRouterGuard(router); +// 设置百度统计 +setupBaiduTongJi(router); export { resetRoutes, router }; diff --git a/apps/web-tdesign/src/router/routes/core.ts b/apps/web-tdesign/src/router/routes/core.ts index 949b0b65a..22fd0017c 100644 --- a/apps/web-tdesign/src/router/routes/core.ts +++ b/apps/web-tdesign/src/router/routes/core.ts @@ -90,6 +90,23 @@ const coreRoutes: RouteRecordRaw[] = [ title: $t('page.auth.register'), }, }, + { + name: 'SocialLogin', + path: 'social-login', + component: () => + import('#/views/_core/authentication/social-login.vue'), + meta: { + title: $t('page.auth.login'), + }, + }, + { + name: 'SSOLogin', + path: 'sso-login', + component: () => import('#/views/_core/authentication/sso-login.vue'), + meta: { + title: $t('page.auth.login'), + }, + }, ], }, ]; diff --git a/apps/web-tdesign/src/router/routes/index.ts b/apps/web-tdesign/src/router/routes/index.ts index e6fb14402..738f9d3d9 100644 --- a/apps/web-tdesign/src/router/routes/index.ts +++ b/apps/web-tdesign/src/router/routes/index.ts @@ -34,4 +34,14 @@ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name); /** 有权限校验的路由列表,包含动态路由和静态路由 */ const accessRoutes = [...dynamicRoutes, ...staticRoutes]; -export { accessRoutes, coreRouteNames, routes }; + +// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/index.ts#L38-L45 +const componentKeys: string[] = Object.keys( + import.meta.glob('../../views/**/*.vue'), +) + .filter((item) => !item.includes('/modules/')) + .map((v) => { + const path = v.replace('../../views/', '/'); + return path.endsWith('.vue') ? path.slice(0, -4) : path; + }); +export { accessRoutes, componentKeys, coreRouteNames, routes }; diff --git a/apps/web-tdesign/src/router/routes/modules/demos.ts b/apps/web-tdesign/src/router/routes/modules/demos.ts deleted file mode 100644 index 9ce8ec992..000000000 --- a/apps/web-tdesign/src/router/routes/modules/demos.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { RouteRecordRaw } from 'vue-router'; - -import { $t } from '#/locales'; - -const routes: RouteRecordRaw[] = [ - { - meta: { - icon: 'ic:baseline-view-in-ar', - keepAlive: true, - order: 1000, - title: $t('demos.title'), - }, - name: 'Demos', - path: '/demos', - children: [ - { - meta: { - title: $t('demos.tdesign'), - }, - name: 'TDesignDemos', - path: '/demos/tdesign', - component: () => import('#/views/demos/tdesign/index.vue'), - }, - ], - }, -]; - -export default routes; diff --git a/apps/web-tdesign/src/router/routes/modules/vben.ts b/apps/web-tdesign/src/router/routes/modules/vben.ts deleted file mode 100644 index b1a3537c0..000000000 --- a/apps/web-tdesign/src/router/routes/modules/vben.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { RouteRecordRaw } from 'vue-router'; - -import { - VBEN_ANT_PREVIEW_URL, - VBEN_DOC_URL, - VBEN_ELE_PREVIEW_URL, - VBEN_GITHUB_URL, - VBEN_LOGO_URL, - VBEN_NAIVE_PREVIEW_URL, -} from '@vben/constants'; -import { SvgAntdvLogoIcon } from '@vben/icons'; - -import { IFrameView } from '#/layouts'; -import { $t } from '#/locales'; - -const routes: RouteRecordRaw[] = [ - { - meta: { - badgeType: 'dot', - icon: VBEN_LOGO_URL, - order: 9998, - title: $t('demos.vben.title'), - }, - name: 'VbenProject', - path: '/vben-admin', - children: [ - { - name: 'VbenDocument', - path: '/vben-admin/document', - component: IFrameView, - meta: { - icon: 'lucide:book-open-text', - link: VBEN_DOC_URL, - title: $t('demos.vben.document'), - }, - }, - { - name: 'VbenGithub', - path: '/vben-admin/github', - component: IFrameView, - meta: { - icon: 'mdi:github', - link: VBEN_GITHUB_URL, - title: 'Github', - }, - }, - { - name: 'VbenNaive', - path: '/vben-admin/naive', - component: IFrameView, - meta: { - badgeType: 'dot', - icon: 'logos:naiveui', - link: VBEN_NAIVE_PREVIEW_URL, - title: $t('demos.vben.naive-ui'), - }, - }, - { - name: 'VbenAntdv', - path: '/vben-admin/antdv', - component: IFrameView, - meta: { - badgeType: 'dot', - icon: SvgAntdvLogoIcon, - link: VBEN_ANT_PREVIEW_URL, - title: $t('demos.vben.antdv'), - }, - }, - { - name: 'VbenElementPlus', - path: '/vben-admin/ele', - component: IFrameView, - meta: { - badgeType: 'dot', - icon: 'logos:element', - link: VBEN_ELE_PREVIEW_URL, - title: $t('demos.vben.element-plus'), - }, - }, - ], - }, - { - name: 'VbenAbout', - path: '/vben-admin/about', - component: () => import('#/views/_core/about/index.vue'), - meta: { - icon: 'lucide:copyright', - title: $t('demos.vben.about'), - order: 9999, - }, - }, -]; - -export default routes; diff --git a/apps/web-tdesign/src/store/auth.ts b/apps/web-tdesign/src/store/auth.ts index b3b4b7494..5727a5a70 100644 --- a/apps/web-tdesign/src/store/auth.ts +++ b/apps/web-tdesign/src/store/auth.ts @@ -1,4 +1,6 @@ -import type { Recordable, UserInfo } from '@vben/types'; +import type { AuthPermissionInfo, Recordable, UserInfo } from '@vben/types'; + +import type { AuthApi } from '#/api'; import { ref } from 'vue'; import { useRouter } from 'vue-router'; @@ -10,7 +12,14 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { defineStore } from 'pinia'; import { notification } from '#/adapter/tdesign'; -import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; +import { + getAuthPermissionInfoApi, + loginApi, + logoutApi, + register, + smsLogin, + socialLogin, +} from '#/api'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { @@ -23,31 +32,53 @@ export const useAuthStore = defineStore('auth', () => { /** * 异步处理登录操作 * Asynchronously handle the login process + * @param type 登录类型 * @param params 登录表单数据 + * @param onSuccess 登录成功后的回调函数 */ async function authLogin( + type: 'mobile' | 'register' | 'social' | 'username', params: Recordable, onSuccess?: () => Promise | void, ) { // 异步处理用户登录操作并获取 accessToken let userInfo: null | UserInfo = null; try { + let loginResult: AuthApi.LoginResult; loginLoading.value = true; - const { accessToken } = await loginApi(params); + switch (type) { + case 'mobile': { + loginResult = await smsLogin(params as AuthApi.SmsLoginParams); + break; + } + case 'register': { + loginResult = await register(params as AuthApi.RegisterParams); + break; + } + case 'social': { + loginResult = await socialLogin(params as AuthApi.SocialLoginParams); + break; + } + default: { + loginResult = await loginApi(params); + } + } + const { accessToken, refreshToken } = loginResult; // 如果成功获取到 accessToken if (accessToken) { accessStore.setAccessToken(accessToken); - // 获取用户信息并存储到 accessStore 中 - const [fetchUserInfoResult, accessCodes] = await Promise.all([ - fetchUserInfo(), - getAccessCodesApi(), - ]); + accessStore.setRefreshToken(refreshToken); - userInfo = fetchUserInfoResult; + // 获取用户信息并存储到 userStore、accessStore 中 + // TODO @芋艿:清理掉 accessCodes 相关的逻辑 + // const [fetchUserInfoResult, accessCodes] = await Promise.all([ + // fetchUserInfo(), + // // getAccessCodesApi(), + // ]); + const fetchUserInfoResult = await fetchUserInfo(); - userStore.setUserInfo(userInfo); - accessStore.setAccessCodes(accessCodes); + userInfo = fetchUserInfoResult.user; if (accessStore.loginExpired) { accessStore.setLoginExpired(false); @@ -59,10 +90,10 @@ export const useAuthStore = defineStore('auth', () => { ); } - if (userInfo?.realName) { + if (userInfo?.nickname) { notification.success({ title: $t('authentication.loginSuccess'), - content: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, + content: `${$t('authentication.loginSuccessDesc')}:${userInfo?.nickname}`, duration: 3000, }); } @@ -78,7 +109,10 @@ export const useAuthStore = defineStore('auth', () => { async function logout(redirect: boolean = true) { try { - await logoutApi(); + const accessToken = accessStore.accessToken as string; + if (accessToken) { + await logoutApi(accessToken); + } } catch { // 不做任何处理 } @@ -97,10 +131,16 @@ export const useAuthStore = defineStore('auth', () => { } async function fetchUserInfo() { - let userInfo: null | UserInfo = null; - userInfo = await getUserInfoApi(); - userStore.setUserInfo(userInfo); - return userInfo; + // 加载 + let authPermissionInfo: AuthPermissionInfo | null = null; + authPermissionInfo = await getAuthPermissionInfoApi(); + // userStore + userStore.setUserInfo(authPermissionInfo.user); + userStore.setUserRoles(authPermissionInfo.roles); + // accessStore + accessStore.setAccessMenus(authPermissionInfo.menus); + accessStore.setAccessCodes(authPermissionInfo.permissions); + return authPermissionInfo; } function $reset() { diff --git a/apps/web-tdesign/src/views/_core/authentication/code-login.vue b/apps/web-tdesign/src/views/_core/authentication/code-login.vue index acfd1fd78..481e3363f 100644 --- a/apps/web-tdesign/src/views/_core/authentication/code-login.vue +++ b/apps/web-tdesign/src/views/_core/authentication/code-login.vue @@ -2,24 +2,100 @@ import type { VbenFormSchema } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { computed, ref } from 'vue'; +import type { AuthApi } from '#/api'; + +import { computed, onMounted, ref } from 'vue'; import { AuthenticationCodeLogin, z } from '@vben/common-ui'; +import { isTenantEnable } from '@vben/hooks'; import { $t } from '@vben/locales'; +import { useAccessStore } from '@vben/stores'; + +import { message } from '#/adapter/tdesign'; +import { sendSmsCode } from '#/api'; +import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth'; +import { useAuthStore } from '#/store'; defineOptions({ name: 'CodeLogin' }); +const authStore = useAuthStore(); +const accessStore = useAccessStore(); +const tenantEnable = isTenantEnable(); + const loading = ref(false); -const CODE_LENGTH = 6; +const CODE_LENGTH = 4; + +const loginRef = ref(); + +/** 获取租户列表,并默认选中 */ +const tenantList = ref([]); // 租户列表 +async function fetchTenantList() { + if (!tenantEnable) { + return; + } + try { + // 获取租户列表、域名对应租户 + const websiteTenantPromise = getTenantByWebsite(window.location.hostname); + tenantList.value = await getTenantSimpleList(); + + // 选中租户:域名 > store 中的租户 > 首个租户 + let tenantId: null | number = null; + const websiteTenant = await websiteTenantPromise; + if (websiteTenant?.id) { + tenantId = websiteTenant.id; + } + // 如果没有从域名获取到租户,尝试从 store 中获取 + if (!tenantId && accessStore.tenantId) { + tenantId = accessStore.tenantId; + } + // 如果还是没有租户,使用列表中的第一个 + if (!tenantId && tenantList.value?.[0]?.id) { + tenantId = tenantList.value[0].id; + } + + // 设置选中的租户编号 + accessStore.setTenantId(tenantId); + loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString()); + } catch (error) { + console.error('获取租户列表失败:', error); + } +} + +/** 组件挂载时获取租户信息 */ +onMounted(() => { + fetchTenantList(); +}); const formSchema = computed((): VbenFormSchema[] => { return [ + { + component: 'VbenSelect', + componentProps: { + options: tenantList.value.map((item) => ({ + label: item.name, + value: item.id.toString(), + })), + placeholder: $t('authentication.tenantTip'), + }, + fieldName: 'tenantId', + label: $t('authentication.tenant'), + rules: z.string().min(1, { message: $t('authentication.tenantTip') }), + dependencies: { + triggerFields: ['tenantId'], + if: tenantEnable, + trigger(values) { + if (values.tenantId) { + accessStore.setTenantId(Number(values.tenantId)); + } + }, + }, + }, { component: 'VbenInput', componentProps: { placeholder: $t('authentication.mobile'), }, - fieldName: 'phoneNumber', + fieldName: 'mobile', label: $t('authentication.mobile'), rules: z .string() @@ -40,6 +116,29 @@ const formSchema = computed((): VbenFormSchema[] => { return text; }, placeholder: $t('authentication.code'), + handleSendCode: async () => { + loading.value = true; + try { + const formApi = loginRef.value?.getFormApi(); + if (!formApi) { + throw new Error('表单未准备好'); + } + // 验证手机号 + await formApi.validateField('mobile'); + const isMobileValid = await formApi.isFieldValid('mobile'); + if (!isMobileValid) { + throw new Error('请输入有效的手机号码'); + } + + // 发送验证码 + const { mobile } = await formApi.getValues(); + const scene = 21; // 场景:短信验证码登录 + await sendSmsCode({ mobile, scene }); + message.success('验证码发送成功'); + } finally { + loading.value = false; + } + }, }, fieldName: 'code', label: $t('authentication.code'), @@ -55,13 +154,17 @@ const formSchema = computed((): VbenFormSchema[] => { * @param values 登录表单数据 */ async function handleLogin(values: Recordable) { - // eslint-disable-next-line no-console - console.log(values); + try { + await authStore.authLogin('mobile', values); + } catch (error) { + console.error('Error in handleLogin:', error); + } }