Complete main UI #37

Merged
FatttSnake merged 192 commits from FatttSnake into dev 2024-02-23 16:31:17 +08:00
7 changed files with 540 additions and 6 deletions
Showing only changes of commit 85aba29ced - Show all commits

View File

@@ -0,0 +1,161 @@
@use "@/assets/css/mixins" as mixins;
@use "@/assets/css/constants" as constants;
[data-component=sign] {
background-color: #D2D0DD;
user-select: none;
a:hover {
color: constants.$production-color;
}
.sign-box {
position: relative;
background-color: constants.$origin-color;
width: 900px;
height: 600px;
overflow: hidden;
border-radius: 12px;
.left, .right {
opacity: 1;
animation: 1s ease;
@include mixins.unique-keyframes {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
> * {
width: 100%;
height: 100%;
}
&.hidden {
opacity: 0;
animation: 1s ease;
@include mixins.unique-keyframes {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
> * {
.title {
margin-bottom: 20px;
transform: translateY(-10px);
.primary {
font-size: 2.4em;
font-weight: bolder;
color: constants.$production-color;
}
.secondary {
font-size: 1.2em;
}
}
.form {
width: 300px;
.addition {
justify-content: space-between;
margin-bottom: 14px;
> * {
flex: 0 0 auto;
}
}
.footer {
text-align: center;
a {
color: constants.$production-color;
}
}
}
}
.sign-up {
}
.sign-in {
}
}
.cover {
position: absolute;
height: 100%;
width: 50%;
background-color: #F3F4F8;
animation: 0.8s ease;
@include mixins.unique-keyframes {
0% {
left: 50%;
}
100% {
left: 0;
}
}
.ball-box {
position: relative;
overflow: hidden;
background-color: #F1F2F7;
.ball {
position: absolute;
width: 128px;
height: 128px;
background-color: constants.$production-color;
border-radius: 50%;
bottom: 0;
left: 50%;
transform: translateX(-50%) translateY(50%);
}
}
.mask {
transform: rotateZ(180deg);
filter: blur(12px);
.ball {
width: 140px;
height: 140px;
}
}
}
&.switch {
.cover {
left: 50%;
animation: 0.8s ease;
@include mixins.unique-keyframes {
0% {
left: 0;
}
100% {
left: 50%;
}
}
}
}
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M768 426.666667V341.333333A256 256 0 0 0 256 341.333333v85.333334a85.333333 85.333333 0 0 0-85.333333 85.333333v341.333333a85.333333 85.333333 0 0 0 85.333333 85.333334h512a85.333333 85.333333 0 0 0 85.333333-85.333334v-341.333333a85.333333 85.333333 0 0 0-85.333333-85.333333zM341.333333 341.333333a170.666667 170.666667 0 0 1 341.333334 0v85.333334H341.333333z m298.666667 320a21.333333 21.333333 0 0 1 21.333333-21.333333h42.666667a21.333333 21.333333 0 0 1 21.333333 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333333 21.333333h-42.666667a21.333333 21.333333 0 0 1-21.333333-21.333333z m-170.666667 0a21.333333 21.333333 0 0 1 21.333334-21.333333h42.666666a21.333333 21.333333 0 0 1 21.333334 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333334 21.333333h-42.666666a21.333333 21.333333 0 0 1-21.333334-21.333333z m-170.666666 42.666667v-42.666667a21.333333 21.333333 0 0 1 21.333333-21.333333h42.666667a21.333333 21.333333 0 0 1 21.333333 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333333 21.333333h-42.666667a21.333333 21.333333 0 0 1-21.333333-21.333333z" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import '@/assets/css/pages/login.scss' import '@/assets/css/pages/login-.scss'
import { import {
PERMISSION_LOGIN_SUCCESS, PERMISSION_LOGIN_SUCCESS,
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR, PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,

347
src/pages/Sign.tsx Normal file
View File

@@ -0,0 +1,347 @@
import React, { useState } from 'react'
import '@/assets/css/pages/sign.scss'
import FitFullscreen from '@/components/common/FitFullscreen'
import FitCenter from '@/components/common/FitCenter'
import FlexBox from '@/components/common/FlexBox'
import { useNavigate } from 'react-router'
import Icon from '@ant-design/icons'
import { getUserInfo, login, setToken } from '@/util/auth.tsx'
import {
PERMISSION_LOGIN_SUCCESS,
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR,
PERMISSION_USER_DISABLE,
PERMISSION_USERNAME_NOT_FOUND
} from '@/constants/common.constants.ts'
import { AppContext } from '@/App.tsx'
import { utcToLocalTime } from '@/util/datetime.tsx'
import { useUpdatedEffect } from '@/util/hooks.tsx'
const SignUp: React.FC = () => {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
return (
<div className={'sign-up'}>
<FitCenter>
<FlexBox>
<div>Welcome join us</div>
<AntdButton
onClick={() =>
navigate(
`/login${
searchParams.toString() ? `?${searchParams.toString()}` : ''
}`
)
}
>
</AntdButton>
<AntdForm>
<AntdForm.Item></AntdForm.Item>
</AntdForm>
</FlexBox>
</FitCenter>
</div>
)
}
const Forget: React.FC = () => {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const [isLoading, setIsLoading] = useState(false)
const [isFinish, setIsFinish] = useState(false)
const handleOnFinish = () => {
setIsFinish(true)
}
const handleOnRetry = () => {
setIsFinish(false)
}
return (
<div className={'forget'}>
<FitCenter>
<FlexBox>
<div className={'title'}>
<div className={'primary'}></div>
<div className={'secondary'}>Retrieve password</div>
</div>
<AntdForm autoComplete={'on'} onFinish={handleOnFinish} className={'form'}>
{!isFinish ? (
<>
<AntdForm.Item
name={'account'}
rules={[{ required: true, message: '请输入邮箱' }]}
>
<AntdInput
prefix={<Icon component={IconFatwebEmail} />}
placeholder={'邮箱'}
disabled={isLoading}
/>
</AntdForm.Item>
<AntdForm.Item>
<AntdButton
style={{ width: '100%' }}
type={'primary'}
htmlType={'submit'}
disabled={isLoading}
loading={isLoading}
>
&ensp;&ensp;&ensp;&ensp;
</AntdButton>
</AntdForm.Item>
</>
) : (
<div style={{ marginBottom: 10 }}>
<a onClick={handleOnRetry}></a>
</div>
)}
<div className={'footer'}>
<a
onClick={() =>
navigate(
`/login${
searchParams.toString()
? `?${searchParams.toString()}`
: ''
}`
)
}
>
</a>
</div>
</AntdForm>
</FlexBox>
</FitCenter>
</div>
)
}
const SignIn: React.FC = () => {
const { refreshRouter } = useContext(AppContext)
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const [isSigningIn, setIsSigningIn] = useState(false)
const handleOnFinish = (loginForm: LoginForm) => {
if (isSigningIn) {
return
}
setIsSigningIn(true)
void login(loginForm.account, loginForm.password)
.then((res) => {
const response = res.data
const { code, data } = response
switch (code) {
case PERMISSION_LOGIN_SUCCESS:
setToken(data?.token ?? '')
void message.success('登录成功')
setTimeout(() => {
void getUserInfo().then((user) => {
refreshRouter()
if (searchParams.has('redirect')) {
navigate(searchParams.get('redirect') ?? '/')
} else {
navigate('/')
}
notification.success({
message: '欢迎回来',
description: (
<>
<span>
<strong>{user.userInfo.nickname}</strong>
</span>
<br />
<span>
{user.lastLoginTime
? `${utcToLocalTime(user.lastLoginTime)}${
user.lastLoginIp
}`
: '无'}
</span>
</>
),
placement: 'topRight'
})
})
}, 1500)
break
case PERMISSION_USERNAME_NOT_FOUND:
case PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR:
void message.error(
<>
<strong></strong><strong></strong>
</>
)
setIsSigningIn(false)
break
case PERMISSION_USER_DISABLE:
void message.error(
<>
<strong></strong>
</>
)
setIsSigningIn(false)
break
default:
void message.error(
<>
<strong></strong>
</>
)
setIsSigningIn(false)
}
})
.catch(() => {
setIsSigningIn(false)
})
}
return (
<div className={'sign-in'}>
<FitCenter>
<FlexBox>
<div className={'title'}>
<div className={'primary'}></div>
<div className={'secondary'}>Welcome back</div>
</div>
<AntdForm autoComplete={'on'} onFinish={handleOnFinish} className={'form'}>
<AntdForm.Item
name={'account'}
rules={[{ required: true, message: '账号为空' }]}
>
<AntdInput
prefix={<Icon component={IconFatwebUser} />}
placeholder={'邮箱/用户名'}
disabled={isSigningIn}
/>
</AntdForm.Item>
<AntdForm.Item
name={'password'}
rules={[{ required: true, message: '密码为空' }]}
>
<AntdInput.Password
prefix={<Icon component={IconFatwebPassword} />}
placeholder={'密码'}
disabled={isSigningIn}
/>
</AntdForm.Item>
<FlexBox direction={'horizontal'} className={'addition'}>
<AntdCheckbox disabled={isSigningIn}></AntdCheckbox>
<a
onClick={() => {
navigate(
`/forget${
searchParams.toString()
? `?${searchParams.toString()}`
: ''
}`
)
}}
>
</a>
</FlexBox>
<AntdForm.Item>
<AntdButton
style={{ width: '100%' }}
type={'primary'}
htmlType={'submit'}
disabled={isSigningIn}
loading={isSigningIn}
>
&ensp;&ensp;&ensp;&ensp;
</AntdButton>
</AntdForm.Item>
<div className={'footer'}>
<a
onClick={() =>
navigate(
`/register${
searchParams.toString()
? `?${searchParams.toString()}`
: ''
}`
)
}
>
</a>
</div>
</AntdForm>
</FlexBox>
</FitCenter>
</div>
)
}
const Sign: React.FC = () => {
const lastPage = useRef('none')
const currentPage = useRef('none')
const match = useMatches().reduce((_, second) => second)
const [isSwitch, setIsSwitch] = useState(false)
const leftPage = ['register', 'forget']
useUpdatedEffect(() => {
lastPage.current = currentPage.current
currentPage.current = match.id
setIsSwitch(leftPage.includes(currentPage.current))
}, [match.id])
const leftComponent = () => {
switch (leftPage.includes(currentPage.current) ? currentPage.current : lastPage.current) {
case 'forget':
return <Forget />
default:
return <SignUp />
}
}
const rightComponent = () => {
switch (leftPage.includes(currentPage.current) ? lastPage.current : currentPage.current) {
default:
return <SignIn />
}
}
return (
<>
<FitFullscreen data-component={'sign'}>
<FitCenter>
<FlexBox
direction={'horizontal'}
className={`sign-box${isSwitch ? ' switch' : ''}`}
>
<div className={`left${!isSwitch ? ' hidden' : ''}`}>{leftComponent()}</div>
<div className={`right${isSwitch ? ' hidden' : ''}`}>
{rightComponent()}
</div>
<FlexBox className={'cover'}>
<div className={'ball-box'}>
<div className={'ball'} />
</div>
<div className={'ball-box'}>
<div className={'mask'}>
<div className={'ball'} />
</div>
</div>
</FlexBox>
</FlexBox>
</FitCenter>
</FitFullscreen>
</>
)
}
export default Sign

View File

@@ -16,15 +16,20 @@ const ToolsFramework: React.FC = () => {
<Sidebar <Sidebar
title={'个人中心'} title={'个人中心'}
bottomFixed={ bottomFixed={
hasPathPermission('/system') ? ( <SidebarItemList>
<SidebarItemList> {hasPathPermission('/system') ? (
<SidebarItem <SidebarItem
path={'/system'} path={'/system'}
icon={IconFatwebSetting} icon={IconFatwebSetting}
text={'系统配置'} text={'系统配置'}
/> />
</SidebarItemList> ) : undefined}
) : undefined <SidebarItem
path={'/tools'}
icon={IconFatwebBack}
text={'回到氮工具'}
/>
</SidebarItemList>
} }
> >
<SidebarItemList> <SidebarItemList>

View File

@@ -6,17 +6,37 @@ import user from '@/router/user'
import tools from '@/router/tools' import tools from '@/router/tools'
import { getAuthRoute, mapJsonToRoute, setTitle } from '@/util/route' import { getAuthRoute, mapJsonToRoute, setTitle } from '@/util/route'
const lazySignPage = React.lazy(() => import('@/pages/Sign'))
const root: RouteJsonObject[] = [ const root: RouteJsonObject[] = [
{ {
path: '/', path: '/',
absolutePath: '/', absolutePath: '/',
component: React.lazy(() => import('@/AuthRoute')), component: React.lazy(() => import('@/AuthRoute')),
children: [ children: [
{
path: 'register',
absolutePath: '/register',
id: 'register',
component: lazySignPage
},
{
path: 'confirm',
absolutePath: '/confirm',
id: 'confirm',
component: lazySignPage
},
{
path: 'forget',
absolutePath: '/forget',
id: 'forget',
component: lazySignPage
},
{ {
path: 'login', path: 'login',
absolutePath: '/login', absolutePath: '/login',
id: 'login', id: 'login',
component: React.lazy(() => import('@/pages/Login')) component: lazySignPage
}, },
{ {
path: 'loading', path: 'loading',