Browse Source

chore: 登陆、权限接口适配

main
刘凯 1 year ago
parent
commit
abcec2371e
  1. 4
      .env.development
  2. 51
      src/api/base/user/index.ts
  3. 24
      src/api/base/user/types.ts
  4. 2
      src/layouts/default/header/components/lock/LockModal.vue
  5. 6
      src/layouts/default/header/components/user-dropdown/index.vue
  6. 13
      src/router/guard/permissionGuard.ts
  7. 25
      src/store/modules/lock.ts
  8. 57
      src/store/modules/user.ts
  9. 1
      src/types/axios.d.ts
  10. 10
      src/utils/http/axios/index.ts
  11. 2
      src/views/base/lock/LockPage.vue
  12. 36
      src/views/base/login/LoginForm.vue
  13. 4
      src/views/base/login/SessionTimeoutLogin.vue
  14. 2
      src/views/dashboard/workbench/components/WorkbenchHeader.vue

4
.env.development

@ -7,13 +7,13 @@ VITE_PUBLIC_PATH = /
# 本地开发代理,可以解决跨域及多地址代理
# 如果接口地址匹配到,则会转发到http://localhost:3000,防止本地出现跨域问题
# 可以有多个,注意多个不能换行,否则代理将会失效
VITE_PROXY = []
VITE_PROXY = [["/api","https://cop-demo.szsciit.com/api"]]
# 是否删除Console.log
VITE_DROP_CONSOLE = false
# 接口地址,如果没有跨域问题,直接在这里配置即可
VITE_GLOB_API_URL = http://192.168.1.100:48081/admin-api
VITE_GLOB_API_URL =
# 文件上传接口 可选
VITE_GLOB_UPLOAD_URL = /upload

51
src/api/base/user/index.ts

@ -4,13 +4,23 @@ import type { ErrorMessageMode } from '@/types/axios'
export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') {
return defHttp.post<LoginResult>({
url: '/auth/login',
params,
url: '/api/deframe-auth/oauth/token',
params: {
username: params.username,
password: params.password,
grant_type: 'captcha',
scope: 'all',
type: 'account',
},
headers: {
'tenant-id': '000000',
'Tenant-Id': params.tenantId,
'Captcha-Key': params.captchaKey,
'Captcha-Code': params.captchaCode,
},
}, {
errorMessageMode: mode,
joinParamsToUrl: true,
isTransformResponse: false,
})
}
@ -22,8 +32,43 @@ export function getUserInfo() {
})
}
export interface __MenuItem {
id: string
parentId: string
code: string
name: string
alias: string
path: string
source: string
sort: number
category: number
action: number
children?: __MenuItem[]
}
export function getUserRouters() {
return defHttp.get<__MenuItem[]>({
url: '/api/deframe-system/menu/routes',
})
}
export function getUserButtons() {
return defHttp.get({
url: '/api/deframe-system/menu/buttons',
})
}
export function doLogout() {
return defHttp.post({
url: '/auth/logout',
})
}
export function getLoginCaptcha() {
return defHttp.get<{ image: string, key: string }>({
url: '/api/deframe-auth/oauth/captcha',
}, {
isTransformResponse: false,
withoutAuth: true,
})
}

24
src/api/base/user/types.ts

@ -3,13 +3,15 @@ import type { MenuItem } from '@/api/system/menu/types'
export interface LoginParams {
username: string
password: string
tenantId: string
captchaKey: string
captchaCode: string
}
export interface LoginResult {
export interface LoginResult extends User {
userId: string
accessToken: string
refreshToken: string
expiresTime: number
access_token: string
refresh_token: string
}
export interface UserInfo {
@ -19,14 +21,12 @@ export interface UserInfo {
}
export interface User {
id: string
tenantId: string
user_id: string
tenant_id: string
account: string
realName: string
real_name: string
nick_name: string
avatar?: string
email?: string
mobile?: string
remark?: string
deptId: string
roleId: string
role_id: string
role_name: string
}

2
src/layouts/default/header/components/lock/LockModal.vue

@ -13,7 +13,7 @@ const { t } = useI18n()
const userStore = useUserStore()
const lockStore = useLockStore()
const getRealName = computed(() => userStore.getUserInfo?.user.realName)
const getRealName = computed(() => userStore.getUserInfo?.user.real_name)
const [register, { closeModal }] = useModalInner()
const [registerForm, { validateFields, resetFields }] = useForm({

6
src/layouts/default/header/components/user-dropdown/index.vue

@ -32,8 +32,8 @@ const { getShowDoc, getUseLockPage } = useHeaderSetting()
const userStore = useUserStore()
const getUserInfo = computed(() => {
const { realName = '', avatar } = userStore.getUserInfo.user || {}
return { realName, avatar: avatar || headerImg }
const { real_name = '', avatar } = userStore.getUserInfo.user || {}
return { real_name, avatar: avatar || headerImg }
})
const [register, { openModal }] = useModal()
@ -76,7 +76,7 @@ function handleMenuClick(e: MenuInfo) {
</Avatar>
<span :class="`${prefixCls}__info hidden md:block`">
<span :class="`${prefixCls}__name`" class="truncate">
{{ getUserInfo.realName }}
{{ getUserInfo.real_name }}
</span>
</span>
</span>

13
src/router/guard/permissionGuard.ts

@ -6,7 +6,9 @@ import { PageEnum } from '@/enums/pageEnum'
import { useUserStoreWithOut } from '@/store/modules/user'
import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic'
import { useSystemEnumStoreWithOut } from '@/store/modules/systemEnum'
import { getAuthCache } from '@/utils/auth'
import { USER_INFO_KEY } from '@/enums/cacheEnum'
import type { UserInfo } from '@/api/base/user/types'
// import { RootRoute } from '@/router/routes'
@ -19,7 +21,6 @@ const whitePathList: PageEnum[] = [LOGIN_PATH]
export function createPermissionGuard(router: Router) {
const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStoreWithOut()
const systemEnumStore = useSystemEnumStoreWithOut()
router.beforeEach(async (to, from, next) => {
// if (
@ -33,13 +34,14 @@ export function createPermissionGuard(router: Router) {
// }
const token = userStore.getAccessToken
const userInfo = getAuthCache(USER_INFO_KEY) as UserInfo
// Whitelist can be directly entered
if (whitePathList.includes(to.path as PageEnum)) {
if (to.path === LOGIN_PATH && token) {
const isSessionTimeout = userStore.getSessionTimeout
try {
await userStore.afterLoginAction()
await userStore.afterLoginAction(false, userInfo?.user)
if (!isSessionTimeout) {
next((to.query?.redirect as string) || '/')
return
@ -80,13 +82,10 @@ export function createPermissionGuard(router: Router) {
return
}
if (!systemEnumStore.initialized)
systemEnumStore.initialize()
// get userinfo while last fetch time is empty
if (userStore.getLastUpdateTime === 0) {
try {
await userStore.getUserInfoAction()
await userStore.getUserInfoAction(userInfo?.user)
}
catch (err) {
next()

25
src/store/modules/lock.ts

@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { useUserStore } from './user'
// import { useUserStore } from './user'
import type { LockInfo } from '@/types/store'
import { LOCK_INFO_KEY } from '@/enums/cacheEnum'
@ -29,7 +30,7 @@ export const useLockStore = defineStore('app-lock', {
},
// Unlock
async unLock(password?: string) {
const userStore = useUserStore()
// const userStore = useUserStore()
if (this.lockInfo?.pwd === password) {
this.resetLockInfo()
return true
@ -37,17 +38,17 @@ export const useLockStore = defineStore('app-lock', {
const tryLogin = async () => {
// TODO 滑块验证码
try {
const username = userStore.getUserInfo?.user.realName
const res = await userStore.login({
username,
password: password!,
goHome: false,
mode: 'none',
})
if (res)
this.resetLockInfo()
// const username = userStore.getUserInfo?.user.real_name
// const res = await userStore.login({
// username,
// password: password!,
// goHome: false,
// mode: 'none',
// })
// if (res)
// this.resetLockInfo()
return res
// return res
}
catch (error) {
return false

57
src/store/modules/user.ts

@ -13,8 +13,10 @@ import { usePermissionStore } from '@/store/modules/permission'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { getAuthCache, setAuthCache, setTenantId } from '@/utils/auth'
import { doLogout, getUserInfo, loginApi } from '@/api/base/user'
import type { LoginParams, UserInfo } from '@/api/base/user/types'
import type { __MenuItem } from '@/api/base/user'
import { doLogout, getUserButtons, getUserRouters, loginApi } from '@/api/base/user'
import type { LoginParams, User, UserInfo } from '@/api/base/user/types'
import type { MenuItem } from '@/api/system/menu/types'
interface UserState {
userInfo: Nullable<UserInfo>
@ -98,22 +100,22 @@ export const useUserStore = defineStore('app-user', {
try {
const { goHome = true, mode, ...loginParams } = params
const data = await loginApi(loginParams, mode)
const { accessToken, refreshToken } = data
const { access_token, refresh_token, ...user } = data
// save token
this.setAccessToken(accessToken)
this.setRefreshToken(refreshToken)
return this.afterLoginAction(goHome)
this.setAccessToken(access_token)
this.setRefreshToken(refresh_token)
return this.afterLoginAction(goHome, user)
}
catch (error) {
return Promise.reject(error)
}
},
async afterLoginAction(goHome?: boolean): Promise<UserInfo | null> {
async afterLoginAction(goHome?: boolean, user?: User): Promise<UserInfo | null> {
if (!this.getAccessToken)
return null
// get user info
const userInfo = await this.getUserInfoAction()
const userInfo = await this.getUserInfoAction(user!)
const sessionTimeout = this.sessionTimeout
if (sessionTimeout) {
@ -136,12 +138,45 @@ export const useUserStore = defineStore('app-user', {
}
return userInfo
},
async getUserInfoAction(): Promise<UserInfo | null> {
async getUserInfoAction(user: User): Promise<UserInfo | null> {
if (!this.getAccessToken)
return null
const userInfo = await getUserInfo()
setTenantId(userInfo.user.tenantId)
function normalizeMenus(__menus: __MenuItem[]): MenuItem[] {
const menus: MenuItem[] = []
for (let i = 0; i < __menus.length; i++) {
const __menu = __menus[i]
menus.push({
id: __menu.id,
parentId: __menu.parentId,
name: __menu.name,
component: `${__menu.path}.vue`,
// componentName?: string
path: __menu.path,
icon: __menu.source,
sort: __menu.sort,
children: __menu.children ? normalizeMenus(__menu.children) : [],
code: __menu.code,
type: 1,
visible: 1,
keepAlive: 0,
})
}
return menus
}
let userInfo: UserInfo | null = null
try {
const [menus, buttons] = await Promise.all([
getUserRouters(),
getUserButtons(),
])
userInfo = { menus: normalizeMenus(menus), buttons, user }
}
catch {
return null
}
setTenantId(userInfo.user.tenant_id)
usePermissionStore().changePermissionCode(userInfo.buttons)
this.setUserInfo(userInfo)
return userInfo

1
src/types/axios.d.ts vendored

@ -28,6 +28,7 @@ export interface RequestOptions {
withToken?: boolean
// 请求重试机制
retryRequest?: RetryRequest
withoutAuth?: boolean
}
export interface RetryRequest {

10
src/utils/http/axios/index.ts

@ -4,6 +4,7 @@
import type { AxiosInstance, AxiosResponse } from 'axios'
import { clone } from 'lodash-es'
import axios from 'axios'
import CryptoJS from 'crypto-js'
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform'
import { VAxios } from './Axios'
import { checkStatus } from './checkStatus'
@ -27,6 +28,8 @@ const { createMessage, createErrorModal, createSuccessModal } = useMessage()
// 请求白名单,无须token的接口
const whiteList: string[] = ['/login', '/refresh-token']
const auth = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse('cop_boss:cop_boss_secret'))
/**
* @description: 便
*/
@ -195,15 +198,18 @@ const transform: AxiosTransform = {
const token = getAccessToken()
if (token && !isToken) {
// jwt token
(config as Recordable).headers.Authorization = options.authenticationScheme
(config as Recordable).headers['DEFrame-Auth'] = options.authenticationScheme
? `${options.authenticationScheme} ${token}`
: token
: `bearer ${token}`
}
// 设置租户
const tenantId = getTenantId()
if (tenantId)
config.headers['tenant-id'] = tenantId
if (!(config as any).requestOptions.withoutAuth)
config.headers.Authorization = `Basic ${auth}`
return config
},

2
src/views/base/lock/LockPage.vue

@ -85,7 +85,7 @@ function handleShowForm(show = false) {
<div :class="`${prefixCls}-entry__header enter-x`">
<img :src="userinfo.user.avatar || headerImg" :class="`${prefixCls}-entry__header-img`">
<p :class="`${prefixCls}-entry__header-name`">
{{ userinfo.user.realName }}
{{ userinfo.user.real_name }}
</p>
</div>
<InputPassword v-model:value="password" :placeholder="t('sys.lock.placeholder')" class="enter-x" />

36
src/views/base/login/LoginForm.vue

@ -1,12 +1,17 @@
<script lang="ts" setup>
import { computed, reactive, ref, unref } from 'vue'
import { Col, Form, Input, Row } from 'ant-design-vue'
import { useAsyncState } from '@vueuse/core'
import CryptoJS from 'crypto-js'
import { LoadingOutlined } from '@ant-design/icons-vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormRules, useFormValid, useLoginState } from './useLogin'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useUserStore } from '@/store/modules/user'
import { useDesign } from '@/hooks/web/useDesign'
import { getLoginCaptcha } from '@/api/base/user'
import type { LoginParams } from '@/api/base/user/types'
const FormItem = Form.Item
const InputPassword = Input.Password
@ -22,9 +27,12 @@ const { getFormRules } = useFormRules()
const formRef = ref()
const loading = ref(false)
const formData = reactive({
const formData = reactive<LoginParams>({
tenantId: '345618',
username: 'admin',
password: '123456',
password: '&demo8&!',
captchaKey: '',
captchaCode: '',
})
const { validForm } = useFormValid(formRef)
@ -37,19 +45,21 @@ async function handleLogin() {
try {
loading.value = true
const userInfo = await userStore.login({
password: data.password,
username: data.username,
mode: 'none', //
...formData,
captchaKey: captcha.value!.key,
password: CryptoJS.MD5(formData.password).toString(),
mode: 'none',
})
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.user.realName}`,
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.user.real_name}`,
duration: 3,
})
}
}
catch (error) {
refreshCaptcha()
createErrorModal({
title: t('sys.api.errorTip'),
content: (error as unknown as Error).message || t('sys.api.networkExceptionMsg'),
@ -60,6 +70,8 @@ async function handleLogin() {
loading.value = false
}
}
const { state: captcha, execute: refreshCaptcha, isLoading: isLoadingCaptcha } = useAsyncState(getLoginCaptcha, undefined)
</script>
<template>
@ -68,6 +80,9 @@ async function handleLogin() {
v-show="getShow" ref="formRef" class="enter-x p-4" :model="formData" :rules="getFormRules"
@keypress.enter="handleLogin"
>
<FormItem name="tenantId" class="enter-x">
<Input v-model:value="formData.tenantId" size="large" placeholder="租户编码" class="fix-auto-fill" />
</FormItem>
<FormItem name="username" class="enter-x">
<Input
v-model:value="formData.username" size="large" :placeholder="t('sys.login.userName')"
@ -83,6 +98,15 @@ async function handleLogin() {
class="fix-auto-fill"
/>
</FormItem>
<FormItem>
<div flex="~ justify-between items-center gap-12px">
<Input v-model:value="formData.captchaCode" size="large" placeholder="验证码" class="fix-auto-fill w-0 flex-1 min-w-auto!" />
<div w="100px" text="center">
<LoadingOutlined v-if="isLoadingCaptcha" />
<img v-else w-full :src="captcha?.image" @click="refreshCaptcha()">
</div>
</div>
</FormItem>
<Row class="enter-x">
<Col :span="12">

4
src/views/base/login/SessionTimeoutLogin.vue

@ -10,11 +10,11 @@ const userId = ref<Nullable<number | string>>(0)
onMounted(() => {
// UserId
userId.value = userStore.getUserInfo?.user.id
userId.value = userStore.getUserInfo?.user.user_id
})
onBeforeUnmount(() => {
if (userId.value && userId.value !== userStore.getUserInfo.user.id)
if (userId.value && userId.value !== userStore.getUserInfo.user.user_id)
document.location.reload()
else if (permissionStore.getLastBuildMenuTime === 0)

2
src/views/dashboard/workbench/components/WorkbenchHeader.vue

@ -13,7 +13,7 @@ const userinfo = computed(() => userStore.getUserInfo)
<Avatar :src="userinfo.user.avatar || headerImg" :size="72" class="!mx-auto !block" />
<div class="mt-2 flex flex-col justify-center md:ml-6 md:mt-0">
<h1 class="text-md md:text-lg">
早安, {{ userinfo.user.realName }}, 开始您一天的工作吧
早安, {{ userinfo.user.real_name }}, 开始您一天的工作吧
</h1>
<span class="text-secondary"> 今日晴20 - 32 </span>
</div>

Loading…
Cancel
Save