Browse Source

增加登录功能

main
dark 7 months ago
parent
commit
7be3de5101
  1. 6
      src/App.tsx
  2. BIN
      src/assets/login.png
  3. 4
      src/layout/RootLayout.tsx
  4. 11
      src/locales/lang/en-US.ts
  5. 11
      src/locales/lang/zh-CN.ts
  6. 123
      src/pages/login/index.css
  7. 90
      src/pages/login/index.tsx
  8. 2
      src/service/base.ts
  9. 7
      src/service/system.ts
  10. 20
      src/store/system.ts
  11. 24
      src/store/user.ts
  12. 1
      src/types/login.d.ts
  13. 1
      src/types/menus.d.ts
  14. 14
      src/utils/index.ts

6
src/App.tsx

@ -7,23 +7,25 @@ import { Provider, useAtom, useAtomValue } from 'jotai'
import './App.css'
import { useEffect } from 'react'
import { RootProvider } from './routes.tsx'
import PageLoading from '@/components/page-loading'
function App() {
const [ appData, ] = useAtom(appAtom)
const { data, isError, isPending } = useAtomValue(menuDataAtom)
const { data, isError, error, isPending } = useAtomValue(menuDataAtom)
useEffect(() => {
initI18n()
}, [])
if (isError) {
console.error(error)
return <div>Error</div>
}
if (isPending) {
return <div>Loading...</div>
return <PageLoading/>
}
return (

BIN
src/assets/login.png

4
src/layout/RootLayout.tsx

@ -122,10 +122,10 @@ export default () => {
)}
menuItemRender={(item, dom) => {
return <div onClick={() => {
setPathname(item.path || '/welcome')
setPathname(item.path || '/dashboard')
}}
>
<Link to={item.path} target={item.type === 3 ? '_blank' : '_self'}>
<Link to={item.path} target={item.type === 'url' ? '_blank' : '_self'}>
{dom}
</Link>
</div>

11
src/locales/lang/en-US.ts

@ -24,6 +24,17 @@ export default {
logout: 'logout',
}
},
login:{
title: 'Account Password Login',
username: 'Username',
usernameMsg: 'Please enter your username',
password: 'Password',
passwordMsg: 'Please enter your password',
code: 'Verification Code',
codeMsg: 'Please enter the verification code',
submit: 'Login',
success: 'Login success'
},
home: {
welcome: 'Welcome to'
},

11
src/locales/lang/zh-CN.ts

@ -23,6 +23,17 @@ export default {
logout: '退出登录',
}
},
login:{
title: '账户密码登录',
username: '用户名',
usernameMsg: '请输入用户名',
password: '密码',
passwordMsg: '请输入密码',
code: '验证码',
codeMsg: '请输入验证码',
submit: '登录',
success: '登录成功'
},
home: {
welcome: '欢迎使用'
},

123
src/pages/login/index.css

@ -0,0 +1,123 @@
body {
margin: 0;
padding: 0;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
overflow: hidden;
.login-container {
display: flex;
align-items: center;
height: 100vh;
background-image: url("@/assets/login.png");
background-repeat: no-repeat;
background-size: cover;
.language {
position: absolute;
top: 10px;
right: 10px;
/*color: #fff;*/
font-size: 14px;
cursor: pointer;
}
.loginBlock {
width: 100%;
height: 100%;
padding: 40px 0;
display: flex;
align-items: center;
justify-content: center;
}
.innerBlock {
width: 356px;
margin: 0 auto;
}
.logo {
height: 30px;
}
.infoLine {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
}
.infoLeft {
color: #666;
font-size: 14px;
}
.desc {
margin: 24px 0;
color: #999;
font-size: 16px;
cursor: pointer;
}
.active {
color: #333;
font-weight: bold;
font-size: 24px;
}
.line {
display: inline-block;
width: 1px;
height: 12px;
background: #999;
}
.innerBeforeInput {
margin-left: 10px;
color: #999;
}
.innerBeforeInput .line {
margin-left: 10px;
}
.innerAfterInput {
margin-right: 10px;
color: #999;
}
.innerAfterInput .line {
margin-right: 10px;
vertical-align: middle;
}
.sendCode {
max-width: 65px;
margin-right: 10px;
}
.otherLogin {
color: #666;
font-size: 14px;
}
.icon {
margin-left: 10px;
}
.link {
color: #5584ff;
font-size: 14px;
text-align: left;
}
.submitBtn {
width: 100%;
}
}
}

90
src/pages/login/index.tsx

@ -1,14 +1,96 @@
import SelectLang from '@/components/select-lang'
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, useSearch, useNavigate } from '@tanstack/react-router'
import { Button, Form, Input, message, Space } from 'antd'
import { useAtom } from 'jotai'
import './index.css'
import { useTranslation } from '@/i18n.ts'
import { loginAtom, loginFormAtom } from '@/store/user.ts'
import { memo, useEffect } from 'react'
const Login = () => {
const Login = memo(() => {
const navigator = useNavigate()
// @ts-ignore 从url中获取redirect参数
const search = useSearch({ form: '/login' })
const { t } = useTranslation()
const [ values, setValues ] = useAtom(loginFormAtom)
const [ { isPending, isSuccess, mutate } ] = useAtom(loginAtom)
const [ form ] = Form.useForm()
const handleSubmit = () => {
form.validateFields().then(() => {
mutate(values)
})
}
useEffect(() => {
if (isSuccess) {
message.success(t('login.success'))
navigator({
to: search?.redirect ?? '/'
})
}
}, [ isSuccess ])
return (
<div>
<div className={'login-container'}>
<div className={'language'}>
<SelectLang/>
</div>
<div className={'loginBlock'}>
<div className={'innerBlock'}>
<div className={'desc'}>
<span className={'active'}>
{t('login.title')}
</span>
</div>
<Form form={form}
initialValues={values}
onValuesChange={(_, allValues) => {
setValues(allValues)
}}
size="large">
<Form.Item name={'username'}
rules={[ { required: true, message: t('login.usernameMsg') } ]}>
<Input maxLength={20} placeholder={t('login.username')}/>
</Form.Item>
<Form.Item name={'password'}
rules={[ { required: true, message: t('login.passwordMsg') } ]}>
<Input.Password placeholder={t('login.password')}/>
</Form.Item>
<Form.Item noStyle>
<Space direction="horizontal">
<Form.Item name={'code'}
rules={[ { required: true, message: t('login.codeMsg') } ]}>
<Input placeholder={t('login.code')}/>
{/*<img src="https://img.alicdn.com/tfs/TB1KtN6mKH2gK0jSZJnXXaT1FXa-1014-200.png" alt="验证码" />*/}
</Form.Item>
</Space>
</Form.Item>
<Form.Item style={{ marginBottom: 10 }}>
<Button
htmlType={'submit'}
type="primary"
onClick={handleSubmit}
className={'submitBtn'}
loading={isPending}
disabled={isPending}
>
{t('login.submit')}
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
)
}
})
export const Route = createFileRoute('/login')({
component: Login

2
src/service/base.ts

@ -9,7 +9,7 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
return {
list: (params?: TParams & TPage) => {
return request.get<TResult[]>(api, { ...options, params }).then(data=>data.data)
return request.post<TResult[]>(api, { ...options, params }).then(data=>data.data)
},
add: (data: TParams) => {
return request.post<TResult>(`${api}/add`, data, options)

7
src/service/system.ts

@ -12,8 +12,15 @@ const systemServ = {
},
login: (data: LoginRequest) => {
return request.post<LoginResponse>('/sys/login', data)
},
user:{
menus:()=>{
return request.get<IMenu[]>('/sys/user/menus')
}
}
}
export default systemServ

20
src/store/system.ts

@ -1,11 +1,11 @@
import { IAppData, MenuItem } from '@/types'
import { formatMenuData } from '@/utils'
import { isAuthenticated } from '@/utils/auth.ts'
import { atom, createStore } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
import { atomWithStorage } from 'jotai/utils'
import systemServ from '../service/system.ts'
import { changeLanguage as setLang } from 'i18next'
import { AxiosResponse } from 'axios'
/**
* app全局状态
@ -18,6 +18,10 @@ export const appAtom = atomWithStorage<Partial<IAppData>>('app', {
language: 'zh-CN',
})
appStore.sub(appAtom, () => {
})
export const getAppData = () => {
return appStore.get(appAtom)
@ -50,15 +54,15 @@ export const setToken = (token: string) => {
updateAppData({ token })
}
export const menuDataAtom = atomWithQuery(() => ({
queryKey: [ 'menus' ],
export const menuDataAtom = atomWithQuery((get) => ({
enabled: !!get(appAtom).token,
queryKey: [ 'user_menus', get(appAtom).token ],
queryFn: async () => {
if (!isAuthenticated()) {
return []
}
return await systemServ.menus.list()
return await systemServ.user.menus()
},
select: data => formatMenuData(data as any ?? []),
select: (data: AxiosResponse) => {
return formatMenuData(data.data.rows as any ?? [])
}
}))
export const selectedMenuIdAtom = atom<number>(0)

24
src/store/user.ts

@ -1,8 +1,30 @@
import { atom } from 'jotai/index'
import { IAuth } from '../types'
import { IAuth } from '@/types'
import { LoginRequest } from '@/types/login'
import { atomWithMutation } from 'jotai-tanstack-query'
import systemServ from '@/service/system.ts'
import { isDev } from '@/utils'
export const authAtom = atom<IAuth>({
isLogin: false,
authKey: []
})
const devLogin = {
username: 'SupperAdmin',
password: 'kk123456',
code: '123456'
}
export const loginFormAtom = atom<LoginRequest>({
...(isDev ? devLogin : {})
} as LoginRequest)
export const loginAtom = atomWithMutation<any, LoginRequest>(() => ({
mutationKey: [ 'login' ],
mutationFn: async (params) => {
return await systemServ.login(params)
},
onSuccess: (data) => {
console.log('login success', data)
}
}))

1
src/types/login.d.ts

@ -1,4 +1,3 @@
export interface LoginLogRequest {
key: string,
start: string,

1
src/types/menus.d.ts

@ -8,6 +8,7 @@ export interface MenuButton {
export interface IMenu {
id: number,
key: string,
parent_id: number,
sort: number,
code: string,

14
src/utils/index.ts

@ -2,6 +2,8 @@ import { IMenu } from '@/types/menus'
import { MenuItem } from '@/types'
import { getIcon } from '@/components/icon'
//vite环境变量, 判断是否是开发环境
export const isDev = import.meta.env.MODE === 'development'
// 格式化菜单数据, 把children转换成routes
export const formatMenuData = (data: IMenu[]) => {
@ -11,12 +13,18 @@ export const formatMenuData = (data: IMenu[]) => {
item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' })
}
if (!item.children || !item.children.length) {
result.push(item)
result.push({
...item,
key: item.name,
name: item.title
})
} else {
const { children, ...other } = item
const { children, name, ...other } = item
result.push({
...other,
children,
key: name,
name: other.title,
children: formatMenuData(children),
routes: formatMenuData(children),
})

Loading…
Cancel
Save