feat: 登录功能+用户状态管理

master
赵亚鹏 1 month ago
parent f0f4467bea
commit 014cd5e49a

@ -1,34 +1,24 @@
import request from '@/axios'
import type { UserType } from './types'
import type { UserLoginRequest, LoginResponse } from './types'
interface RoleParams {
roleName: string
}
export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
return request.post({ url: '/mock/user/login', data })
/**
*
* @param data
* @returns Promise token
*/
export const loginApi = (data: UserLoginRequest): Promise<IResponse<LoginResponse>> => {
return request.post({
url: '/api/login',
data
})
}
/**
* 退
* @returns Promise
*/
export const loginOutApi = (): Promise<IResponse> => {
return request.get({ url: '/mock/user/loginOut' })
}
export const getUserListApi = ({ params }: AxiosConfig) => {
return request.get<{
code: string
data: {
list: UserType[]
total: number
}
}>({ url: '/mock/user/list', params })
}
export const getAdminRoleApi = (
params: RoleParams
): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
return request.get({ url: '/mock/role/list', params })
}
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
return request.get({ url: '/mock/role/list2', params })
return request.post({
url: '/api/loginOut'
})
}

@ -1,11 +1,34 @@
export interface UserLoginType {
username: string
password: string
export interface IResponse<T = any> {
code: number
msg: string
data: T
}
export interface UserType {
/**
*
*/
export interface UserLoginRequest {
username: string
password: string
role: string
roleId: string
}
/**
*
*/
export interface UserInfo {
id: number // 用户ID
username: string // 登录账号
realName: string // 真实姓名
roleId: number // 权限等级 (1=管理员, 2=班主任, 3=老师)
deptId?: number // 所属班级ID (班主任才有值)
status: number // 账号状态 (0=禁用, 1=正常)
// ... 其他你需要的字段
}
/**
*
*/
export interface LoginResponse {
token: string // JWT Token
userInfo: UserInfo // 用户详细信息
}

@ -6,6 +6,8 @@ const request = (option: AxiosConfig) => {
const { url, method, params, data, headers, responseType } = option
const userStore = useUserStoreWithOut()
const token = userStore.getToken() // 先获取 token函数调用拿到字符串值
return service.request({
url: url,
method,
@ -14,8 +16,9 @@ const request = (option: AxiosConfig) => {
responseType: responseType,
headers: {
'Content-Type': CONTENT_TYPE,
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken ?? '',
...headers
// 正确设置 Authorization 头:键名固定为 'Authorization',值为 Bearer + tokentoken 存在时)
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers // 合并传入的自定义 headers覆盖默认值
}
})
}

@ -52,7 +52,7 @@ const toPage = (path: string) => {
class="w-[calc(var(--logo-height)-25px)] rounded-[50%]"
/>
<span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">{{
userStore.getUserInfo?.username
userStore.getUserInfo()?.username
}}</span>
</div>
<template #dropdown>

@ -19,7 +19,7 @@ router.beforeEach(async (to, from, next) => {
const appStore = useAppStoreWithOut()
const userStore = useUserStoreWithOut()
if (userStore.getUserInfo) {
if (userStore.getUserInfo()) {
if (to.path === '/login') {
next({ path: '/' })
} else {

@ -93,7 +93,7 @@ export const useTagsViewStore = defineStore('tagsView', {
const userStore = useUserStoreWithOut()
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = userStore.getUserInfo
this.visitedViews = userStore.getUserInfo()
? this.visitedViews.filter((tag) => tag?.meta?.affix)
: []
},

@ -1,102 +1,243 @@
import { defineStore } from 'pinia'
import { store } from '../index'
import { UserLoginType, UserType } from '@/api/login/types'
import { ElMessageBox } from 'element-plus'
import { loginApi, loginOutApi } from '@/api/login'
import type { UserInfo, LoginResponse, IResponse, UserLoginRequest } from '@/api/login/types'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { loginOutApi } from '@/api/login'
import { useTagsViewStore } from './tagsView'
import router from '@/router'
import { ref } from 'vue'
interface UserState {
userInfo?: UserType
tokenKey: string
token: string
roleRouters?: string[] | AppCustomRouteRecordRaw[]
rememberMe: boolean
loginInfo?: UserLoginType
}
/**
* Pinia Store
* 'user' StoreID
* setup StoreGettersActions
*/
export const useUserStore = defineStore('user', () => {
// 状态定义 (State) - 响应式变量
export const useUserStore = defineStore('user', {
state: (): UserState => {
return {
userInfo: undefined,
tokenKey: 'Authorization',
token: '',
roleRouters: undefined,
// 记住我
rememberMe: true,
loginInfo: undefined
}
},
getters: {
getTokenKey(): string {
return this.tokenKey
},
getToken(): string {
return this.token
},
getUserInfo(): UserType | undefined {
return this.userInfo
},
getRoleRouters(): string[] | AppCustomRouteRecordRaw[] | undefined {
return this.roleRouters
},
getRememberMe(): boolean {
return this.rememberMe
},
getLoginInfo(): UserLoginType | undefined {
return this.loginInfo
/**
* (Token)
* @description API
* HTTPToken
* @initialValue localStoragelocalStorage
*/
const token = ref(localStorage.getItem('token') || '')
/**
*
*/
const userInfo = ref<UserInfo | null>(
(() => {
try {
return JSON.parse(localStorage.getItem('userInfo') || 'null')
} catch {
return null
}
})() // 这里加上括号,立即执行这个工厂函数
)
/**
* ID
* @description ID访
*/
const roleId = ref(Number(localStorage.getItem('roleId')) || 0)
/**
*
* @description
* @initialValue undefined
*/
const loginInfo = ref<UserLoginRequest | undefined>()
/**
*
* @description loginInfo
* @initialValue localStorage'false''true'===
*/
const rememberMe = ref(localStorage.getItem('rememberMe') === 'true')
// 计算属性 (Getters) - 状态访问器
/**
* Token
*/
const getToken = () => token.value
/**
*
*/
const getUserInfo = () => userInfo.value
/**
* ID
*/
const getRoleId = () => roleId.value
/**
*
*/
const getLoginInfo = () => loginInfo.value
/**
*
*/
const getRememberMe = () => rememberMe.value
// 方法 (Actions) - 业务逻辑处理
/**
*
* @param {UserLoginRequest} formData -
*/
const login = async (formData: UserLoginRequest) => {
try {
// 发起登录API请求并显式指定响应数据的类型
const res = (await loginApi(formData)) as IResponse<LoginResponse>
// 检查后端返回的状态码200通常代表成功
if (res.code === 200 && res.data) {
const { token: newToken, userInfo: newUserInfo } = res.data
setToken(newToken)
setUserInfo(newUserInfo)
setRoleId(newUserInfo.roleId)
// 将成功获取的数据返回给调用方(通常是登录页面的组件)
return res.data
}
ElMessage.error(res.msg || '登录失败') // 显示错误信息,优先使用后端返回的消息
return Promise.reject(res) // 将错误信息reject出去让调用方可以捕获并处理
} catch (error) {
console.error('登录过程中发生异常:', error)
ElMessage.error('网络错误或后端接口异常')
return Promise.reject(error)
}
},
actions: {
setTokenKey(tokenKey: string) {
this.tokenKey = tokenKey
},
setToken(token: string) {
this.token = token
},
setUserInfo(userInfo?: UserType) {
this.userInfo = userInfo
},
setRoleRouters(roleRouters: string[] | AppCustomRouteRecordRaw[]) {
this.roleRouters = roleRouters
},
logoutConfirm() {
const { t } = useI18n()
ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
}
const setToken = (val: string) => {
token.value = val
localStorage.setItem('token', val)
}
/**
*
*/
const setUserInfo = (val: UserInfo | null) => {
userInfo.value = val
localStorage.setItem('userInfo', JSON.stringify(val))
}
/**
* ID
*/
const setRoleId = (val: number) => {
roleId.value = val
localStorage.setItem('roleId', val.toString())
}
/**
*
*/
const setLoginInfo = (val: UserLoginRequest | undefined) => {
loginInfo.value = val
localStorage.setItem('loginInfo', JSON.stringify(val))
}
/**
*
*/
const setRememberMe = (val: boolean) => {
rememberMe.value = val
localStorage.setItem('rememberMe', val.toString())
}
// 核心业务方法 - 登出、状态重置等
/**
*
*/
const resetStore = () => {
// 清除所有标签页
const tagsViewStore = useTagsViewStore()
tagsViewStore.delAllViews()
// 调用所有setter方法将状态重置为初始值
setToken('')
setUserInfo(null)
setRoleId(0)
setLoginInfo(undefined)
setRememberMe(false)
// 使用router.replace强制跳转到登录页replace不会向history栈添加新记录
router.replace('/login')
}
/**
*
* @description Token
*/
const logout = () => {
resetStore()
}
const logoutConfirm = () => {
const { t } = useI18n()
ElMessageBox.confirm(
t('common.loginOutMessage'), // 从国际化文件中获取确认消息
t('common.reminder'), // 从国际化文件中获取对话框标题
{
confirmButtonText: t('common.ok'), // 确认按钮文本
cancelButtonText: t('common.cancel'), // 取消按钮文本
type: 'warning' // 对话框类型,用于显示不同的图标
}
)
.then(async () => {
// 用户点击“确认”后执行的逻辑
await loginOutApi().catch(() => {})
// 执行核心的状态重置和页面跳转
resetStore()
// 显示登出成功的消息
ElMessage.success(t('common.logoutSuccess'))
})
.then(async () => {
const res = await loginOutApi().catch(() => {})
if (res) {
this.reset()
}
})
.catch(() => {})
},
reset() {
const tagsViewStore = useTagsViewStore()
tagsViewStore.delAllViews()
this.setToken('')
this.setUserInfo(undefined)
this.setRoleRouters([])
router.replace('/login')
},
logout() {
this.reset()
},
setRememberMe(rememberMe: boolean) {
this.rememberMe = rememberMe
},
setLoginInfo(loginInfo: UserLoginType | undefined) {
this.loginInfo = loginInfo
}
},
persist: true
.catch(() => {
// 用户点击“取消”后执行的逻辑,此处不做任何操作
})
}
// 暴露状态和方法
return {
// 响应式状态
token,
userInfo,
roleId,
loginInfo,
rememberMe,
// Getters
getToken,
getUserInfo,
getRoleId,
getLoginInfo,
getRememberMe,
// Actions
login,
setToken,
setUserInfo,
setRoleId,
setLoginInfo,
setRememberMe,
resetStore,
logout,
logoutConfirm
}
})
/**
* 使userStore
*/
export const useUserStoreWithOut = () => {
return useUserStore(store)
return useUserStore()
}

@ -2,312 +2,198 @@
import { reactive, ref, watch, onMounted, unref } from 'vue'
import { Form, FormSchema } from '@/components/Form'
import { useI18n } from '@/hooks/web/useI18n'
import { ElCheckbox, ElLink } from 'element-plus'
import { ElCheckbox, ElMessage } from 'element-plus'
import { useForm } from '@/hooks/web/useForm'
import { loginApi, getTestRoleApi, getAdminRoleApi } from '@/api/login'
import { loginApi } from '@/api/login' //
import { useAppStore } from '@/store/modules/app'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { UserType } from '@/api/login/types'
import { useValidator } from '@/hooks/web/useValidator'
import { Icon } from '@/components/Icon'
import { useRouter } from 'vue-router' // hook
import type { RouteRecordRaw } from 'vue-router'
import { LoginResponse, IResponse, UserLoginRequest } from '@/api/login/types'
import { useValidator } from '@/hooks/web/useValidator' // hook
import { useUserStore } from '@/store/modules/user'
import { BaseButton } from '@/components/Button'
//
const { required } = useValidator()
//
const emit = defineEmits(['to-register'])
const appStore = useAppStore()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const { currentRoute, addRoute, push } = useRouter()
const { t } = useI18n()
//
const appStore = useAppStore() // app
const userStore = useUserStore() //
const permissionStore = usePermissionStore() //
const { currentRoute, addRoute, push } = useRouter() //
//
const rules = {
username: [required()],
password: [required()]
username: [required()], //
password: [required()] //
}
//
const schema = reactive<FormSchema[]>([
{
field: 'title',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return <h2 class="text-2xl font-bold text-center w-[100%]">{t('login.login')}</h2>
}
}
}
},
{
field: 'username',
field: 'username', //
label: t('login.username'),
// value: 'admin',
component: 'Input',
colProps: {
span: 24
},
componentProps: {
placeholder: 'admin or test'
}
component: 'Input', //
colProps: { span: 24 }, //
componentProps: { placeholder: t('login.enterUsername') } //
},
{
field: 'password',
label: t('login.password'),
// value: 'admin',
component: 'InputPassword',
colProps: {
span: 24
},
component: 'InputPassword', //
colProps: { span: 24 }, //
componentProps: {
style: {
width: '100%'
},
placeholder: 'admin or test',
// enter
onKeydown: (_e: any) => {
if (_e.key === 'Enter') {
_e.stopPropagation() //
signIn()
}
}
placeholder: t('login.enterPassword'),
onKeydown: (e: KeyboardEvent) => e.key === 'Enter' && signIn() //
}
},
{
field: 'tool',
colProps: {
span: 24
},
field: 'remember',
colProps: { span: 24 },
formItemProps: {
slots: {
default: () => {
return (
<>
<div class="flex justify-between items-center w-[100%]">
<ElCheckbox v-model={remember.value} label={t('login.remember')} size="small" />
<ElLink type="primary" underline={false}>
{t('login.forgetPassword')}
</ElLink>
</div>
</>
)
}
//
default: () => (
<ElCheckbox v-model={remember.value} label={t('login.rememberMe')} size="small" />
)
}
}
},
{
field: 'login',
colProps: {
span: 24
},
colProps: { span: 24 },
formItemProps: {
slots: {
default: () => {
return (
<>
<div class="w-[100%]">
<BaseButton
loading={loading.value}
type="primary"
class="w-[100%]"
onClick={signIn}
>
{t('login.login')}
</BaseButton>
</div>
<div class="w-[100%] mt-15px">
<BaseButton class="w-[100%]" onClick={toRegister}>
{t('login.register')}
</BaseButton>
</div>
</>
)
}
}
}
},
{
field: 'other',
component: 'Divider',
label: t('login.otherLogin'),
componentProps: {
contentPosition: 'center'
}
},
{
field: 'otherIcon',
colProps: {
span: 24
},
formItemProps: {
slots: {
default: () => {
return (
<>
<div class="flex justify-between w-[100%]">
<Icon
icon="vi-ant-design:github-filled"
size={iconSize}
class="cursor-pointer ant-icon"
color={iconColor}
hoverColor={hoverColor}
/>
<Icon
icon="vi-ant-design:wechat-filled"
size={iconSize}
class="cursor-pointer ant-icon"
color={iconColor}
hoverColor={hoverColor}
/>
<Icon
icon="vi-ant-design:alipay-circle-filled"
size={iconSize}
color={iconColor}
hoverColor={hoverColor}
class="cursor-pointer ant-icon"
/>
<Icon
icon="vi-ant-design:weibo-circle-filled"
size={iconSize}
color={iconColor}
hoverColor={hoverColor}
class="cursor-pointer ant-icon"
/>
</div>
</>
)
}
//
default: () => (
<BaseButton loading={loading.value} type="primary" class="w-full" onClick={signIn}>
{t('login.login')}
</BaseButton>
)
}
}
}
])
const iconSize = 30
//
const remember = ref(userStore.getRememberMe) //
const loading = ref(false) //
const redirect = ref<string>('') //
const { formRegister, formMethods } = useForm() //
const { getFormData, getElFormExpose, setValues } = formMethods //
const remember = ref(userStore.getRememberMe)
const initLoginInfo = () => {
const loginInfo = userStore.getLoginInfo
//
onMounted(() => {
//
const loginInfo = userStore.getLoginInfo as unknown as UserLoginRequest | undefined
if (loginInfo) {
const { username, password } = loginInfo
setValues({ username, password })
//
setValues({ username: loginInfo.username, password: loginInfo.password })
}
}
onMounted(() => {
initLoginInfo()
//
permissionStore.setIsAddRouters(false)
})
const { formRegister, formMethods } = useForm()
const { getFormData, getElFormExpose, setValues } = formMethods
const loading = ref(false)
const iconColor = '#999'
const hoverColor = 'var(--el-color-primary)'
const redirect = ref<string>('')
//
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
() => currentRoute.value, //
(route) => {
//
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
{ immediate: true } //
)
//
//
const signIn = async () => {
//
const formRef = await getElFormExpose()
await formRef?.validate(async (isValid) => {
if (isValid) {
loading.value = true
const formData = await getFormData<UserType>()
try {
const res = await loginApi(formData)
//
const isValid = await formRef?.validate()
if (!isValid) return //
loading.value = true //
try {
//
const formData = await getFormData<UserLoginRequest>()
//
const res = (await loginApi(formData)) as IResponse<LoginResponse>
//
if (res.code === 200 && res.data) {
const { token, userInfo } = res.data
//
userStore.setToken(token) // token
userStore.setUserInfo(userInfo) //
userStore.setRoleId(userInfo.roleId) // ID
userStore.setRememberMe(remember.value()) //
//
if (unref(remember)) {
userStore.setLoginInfo(formData) //
} else {
userStore.setLoginInfo(undefined) //
}
if (res) {
//
if (unref(remember)) {
userStore.setLoginInfo({
username: formData.username,
password: formData.password
})
} else {
userStore.setLoginInfo(undefined)
}
userStore.setRememberMe(unref(remember))
userStore.setUserInfo(res.data)
// 使
if (appStore.getDynamicRouter) {
getRole()
} else {
await permissionStore.generateRoutes('static').catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
}
} finally {
loading.value = false
//
if (appStore.getDynamicRouter) {
await generateStaticRoutes() //
} else {
//
await permissionStore.generateRoutes('static').catch(() => {})
//
permissionStore.getAddRouters.forEach((route) => addRoute(route as RouteRecordRaw))
//
permissionStore.setIsAddRouters(true)
//
push({ path: redirect.value || permissionStore.getAddRouters[0]?.path || '/dashboard' })
}
//
ElMessage.success(t('login.loginSuccess'))
} else {
//
ElMessage.error(res.msg || t('login.loginFail'))
}
})
}
//
const getRole = async () => {
const formData = await getFormData<UserType>()
const params = {
roleName: formData.username
}
const res =
appStore.getDynamicRouter && appStore.getServerDynamicRouter
? await getAdminRoleApi(params)
: await getTestRoleApi(params)
if (res) {
const routers = res.data || []
userStore.setRoleRouters(routers)
appStore.getDynamicRouter && appStore.getServerDynamicRouter
? await permissionStore.generateRoutes('server', routers).catch(() => {})
: await permissionStore.generateRoutes('frontEnd', routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
} catch (error) {
//
console.error('登录失败:', error)
ElMessage.error(t('login.networkError')) //
} finally {
loading.value = false //
}
}
//
const toRegister = () => {
emit('to-register')
//
const generateStaticRoutes = async () => {
//
await permissionStore.generateRoutes('static').catch(() => {})
//
permissionStore.getAddRouters.forEach((route) => addRoute(route as RouteRecordRaw))
//
permissionStore.setIsAddRouters(true)
//
push({ path: redirect.value || permissionStore.getAddRouters[0]?.path || '/dashboard' })
}
// //
// const toRegister = () => emit('to-register') //
</script>
<template>
<!-- 表单组件 -->
// eslint-disable-next-line prettier/prettier
<Form
:schema="schema"
:rules="rules"
label-position="top"
hide-required-asterisk
size="large"
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
@register="formRegister"
/>
</template>

Loading…
Cancel
Save