Add login to ToolsFramework #35

Merged
FatttSnake merged 3 commits from FatttSnake into dev 2023-10-14 23:29:43 +08:00
10 changed files with 165 additions and 46 deletions

View File

@@ -1,4 +1,4 @@
export function kebabCase(key: string): string { export const kebabCase = (key: string) => {
const result: string = key.replace(/([A-Z])/g, ' $1').trim() const result: string = key.replace(/([A-Z])/g, ' $1').trim()
return result.split(' ').join('-').toLowerCase() return result.split(' ').join('-').toLowerCase()
} }
@@ -395,7 +395,7 @@ const isAntd = (compName: string): boolean => {
return antdNames.has(compName) return antdNames.has(compName)
} }
export function AntDesignResolver(options: AntDesignResolverOptions = {}): ComponentResolver { export const AntDesignResolver = (options: AntDesignResolverOptions = {}): ComponentResolver => {
return { return {
type: 'component', type: 'component',
resolve: (name: string) => { resolve: (name: string) => {

View File

@@ -1,5 +1,6 @@
import { getLoginStatus } from '@/utils/auth.ts' import { getLoginStatus } from '@/utils/auth.ts'
import { PRODUCTION_NAME } from '@/constants/Common.constants.ts' import { PRODUCTION_NAME } from '@/constants/Common.constants.ts'
import { getRedirectUrl } from '@/utils/common.ts'
const AuthRoute = () => { const AuthRoute = () => {
const matches = useMatches() const matches = useMatches()
@@ -16,9 +17,7 @@ const AuthRoute = () => {
if (matches.some(({ handle }) => (handle as RouteHandle)?.auth) && !isLogin) { if (matches.some(({ handle }) => (handle as RouteHandle)?.auth) && !isLogin) {
return ( return (
<Navigate <Navigate
to={`/login?redirect=${encodeURIComponent( to={getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`)}
`${lastMatch.pathname}${location.search}`
)}`}
/> />
) )
} }

View File

@@ -1,6 +1,9 @@
$origin-color: white; $origin-color: white;
$main-color: #4E47BB; $main-color: #4E47BB;
$secondary-color: #BAB8E5; $secondary-color: #BAB8E5;
$error-color: #ff4d4f;
$error-secondary-color: #ff7875;
$blue-color: #1677ff;
$active-color: #EBECFFD; $active-color: #EBECFFD;
$background-color: #F5F5F5; $background-color: #F5F5F5;
$font-main-color: #4D4D4D; $font-main-color: #4D4D4D;

View File

@@ -13,7 +13,6 @@ body {
user-select: none; user-select: none;
transition: all .3s; transition: all .3s;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
.title { .title {
display: flex; display: flex;
@@ -21,6 +20,7 @@ body {
font-weight: bold; font-weight: bold;
padding: 10px 14px; padding: 10px 14px;
color: constants.$main-color; color: constants.$main-color;
overflow: hidden;
.icon-box { .icon-box {
display: flex; display: flex;
@@ -153,12 +153,10 @@ body {
animation: 0.3s ease; animation: 0.3s ease;
@include mixins.unique-keyframes { @include mixins.unique-keyframes {
0% { 0% {
display: block;
transform: translateX(-10px); transform: translateX(-10px);
opacity: 0; opacity: 0;
} }
100% { 100% {
display: block;
transform: translateX(0); transform: translateX(0);
opacity: 1; opacity: 1;
} }
@@ -188,14 +186,14 @@ body {
padding: 8px 14px; padding: 8px 14px;
color: constants.$main-color; color: constants.$main-color;
.icon-box { .icon-user {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-left: 4px; margin-left: 4px;
padding: 10px; padding: 10px;
width: 32px; width: 36px;
height: 32px; height: 36px;
font-size: constants.$SIZE_ICON_XS; font-size: constants.$SIZE_ICON_XS;
border: 2px constants.$font-secondary-color solid; border: 2px constants.$font-secondary-color solid;
color: constants.$font-secondary-color; color: constants.$font-secondary-color;
@@ -205,10 +203,27 @@ body {
.text { .text {
flex: 1; flex: 1;
padding-left: 8px; padding-left: 10px;
font-size: 1.4em; font-size: 1.4em;
color: constants.$font-main-color; color: constants.$font-main-color;
user-select: text; user-select: text;
a{
color: constants.$main-color;
text-decoration: underline;
}
}
.icon-exit {
font-size: constants.$SIZE_ICON_XS;
color: constants.$error-color;
padding: 6px 10px;
cursor: pointer;
&:hover {
border-radius: 8px;
background-color: constants.$background-color;
}
} }
} }
@@ -234,9 +249,50 @@ body {
} }
.footer { .footer {
position: relative;
.text { .text {
display: none; display: none;
} }
.submenu-exit {
display: none;
position: absolute;
padding-left: 6px;
left: 100%;
.content {
padding: 8px;
border-radius: 8px;
background-color: constants.$origin-color;
.icon-exit {
padding: 4px 8px;
&:hover {
border-radius: 8px;
background-color: constants.$background-color;
}
}
}
&.hide {
display: none!important;
}
}
&:hover .submenu-exit {
display: block;
animation: 0.3s ease;
@include mixins.unique-keyframes {
0% {
transform: translateX(-10px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
}
} }
} }
} }

1
src/assets/svg/exit.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M835.669333 554.666667h-473.173333A42.453333 42.453333 0 0 1 320 512a42.666667 42.666667 0 0 1 42.474667-42.666667h473.173333l-161.813333-161.834666a42.666667 42.666667 0 0 1 60.330666-60.330667l234.666667 234.666667a42.666667 42.666667 0 0 1 0 60.330666l-234.666667 234.666667a42.666667 42.666667 0 0 1-60.330666-60.330667L835.669333 554.666667zM554.666667 42.666667a42.666667 42.666667 0 1 1 0 85.333333H149.525333C137.578667 128 128 137.578667 128 149.482667v725.034666C128 886.4 137.6 896 149.525333 896H554.666667a42.666667 42.666667 0 1 1 0 85.333333H149.525333A106.816 106.816 0 0 1 42.666667 874.517333V149.482667A106.773333 106.773333 0 0 1 149.525333 42.666667H554.666667z" /></svg>

After

Width:  |  Height:  |  Size: 779 B

View File

@@ -4,9 +4,15 @@ import '@/assets/css/pages/tools-framework.scss'
import Icon from '@ant-design/icons' import Icon from '@ant-design/icons'
import { toolsJsonObjects } from '@/router/tools.tsx' import { toolsJsonObjects } from '@/router/tools.tsx'
import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar.tsx' import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar.tsx'
import { getLocalStorage, setLocalStorage } from '@/utils/common.ts' import { getLocalStorage, getRedirectUrl, setLocalStorage } from '@/utils/common.ts'
import { getLoginStatus, logout } from '@/utils/auth.ts'
import { NavLink, Outlet } from 'react-router-dom'
const ToolsFramework: React.FC = () => { const ToolsFramework: React.FC = () => {
const matches = useMatches()
const lastMatch = matches.reduce((_, second) => second)
const location = useLocation()
const navigate = useNavigate()
const hideScrollbarRef = useRef<HideScrollbarElement>(null) const hideScrollbarRef = useRef<HideScrollbarElement>(null)
const [submenuTop, setSubmenuTop] = useState(0) const [submenuTop, setSubmenuTop] = useState(0)
const [submenuLeft, setSubmenuLeft] = useState(0) const [submenuLeft, setSubmenuLeft] = useState(0)
@@ -37,6 +43,21 @@ const ToolsFramework: React.FC = () => {
} }
} }
const handleClickAvatar = () => {
if (getLoginStatus()) {
navigate(`${lastMatch.pathname}${location.search}`)
} else {
navigate(getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`))
}
}
const handleLogout = () => {
logout()
setTimeout(() => {
window.location.reload()
}, 1500)
}
return ( return (
<> <>
<FitFullScreen className={'flex-horizontal'}> <FitFullScreen className={'flex-horizontal'}>
@@ -183,13 +204,42 @@ const ToolsFramework: React.FC = () => {
</div> </div>
<div className={'separate'} style={{ marginTop: 0, marginBottom: 0 }} /> <div className={'separate'} style={{ marginTop: 0, marginBottom: 0 }} />
<div className={'footer'}> <div className={'footer'}>
<span className={'icon-box'}> <span className={'icon-user'} onClick={handleClickAvatar}>
<Icon component={IconFatwebUser} /> <Icon component={IconFatwebUser} />
</span> </span>
<span className={'text'}></span> <span hidden={getLoginStatus()} className={'text'}>
<NavLink
to={getRedirectUrl(
'/login',
`${lastMatch.pathname}${location.search}`
)}
>
</NavLink>
</span>
<span hidden={!getLoginStatus()} className={'text'}>
</span>
<div
hidden={!getLoginStatus()}
className={`submenu-exit${!getLoginStatus() ? ' hide' : ''}`}
>
<div className={'content'}>
<span
hidden={!getLoginStatus()}
className={'icon-exit'}
onClick={handleLogout}
>
<Icon component={IconFatwebExit} />
</span>
</div> </div>
</div> </div>
<div className={'right-panel'}></div> </div>
</div>
<div className={'right-panel'}>
<Outlet />
</div>
</FitFullScreen> </FitFullScreen>
</> </>
) )

View File

@@ -309,4 +309,9 @@ const tools: RouteObject[] = toolsJsonObjects.map((value) => ({
})) }))
})) }))
tools.push({
path: '*',
element: <Navigate to="/tools" replace />
})
export default tools export default tools

View File

@@ -42,7 +42,7 @@ service.interceptors.request.use(
) { ) {
await axios await axios
.get(import.meta.env.VITE_API_TOKEN_URL, { .get(import.meta.env.VITE_API_TOKEN_URL, {
headers: { token } headers: { Authorization: `Bearer ${token}` }
}) })
.then((value: AxiosResponse<_Response<Token>>) => { .then((value: AxiosResponse<_Response<Token>>) => {
const response = value.data const response = value.data
@@ -53,7 +53,7 @@ service.interceptors.request.use(
} }
token = getToken() token = getToken()
config.headers.set('token', token) config.headers.set('Authorization', `Bearer ${token}`)
} }
return config return config
}, },
@@ -74,7 +74,7 @@ service.interceptors.response.use(
<strong></strong> <strong></strong>
</> </>
) )
setTimeout(function () { setTimeout(() => {
location.reload() location.reload()
}, 1500) }, 1500)
throw response?.data throw response?.data

View File

@@ -1,27 +1,28 @@
import { clearLocalStorage, getCaptcha, getLocalStorage, setLocalStorage } from './common' import { getCaptcha, getLocalStorage, removeLocalStorage, setLocalStorage } from './common'
import { SYSTEM_OK, TOKEN_NAME } from '@/constants/Common.constants' import { SYSTEM_OK, TOKEN_NAME } from '@/constants/Common.constants'
import request from '@/services' import request from '@/services'
let captcha: Captcha let captcha: Captcha
export async function login(username: string, password: string) { export const login = async (username: string, password: string) => {
return await request.post<Token>('/login', { return await request.post<Token>('/login', {
username, username,
password password
}) })
} }
export function logout(): void { export const logout = () => {
void request.get('/logout').finally(() => { void request.post('/logout').finally(() => {
clearLocalStorage() removeLocalStorage('userInfo')
removeLocalStorage(TOKEN_NAME)
}) })
} }
export function getLoginStatus(): boolean { export const getLoginStatus = () => {
return getLocalStorage(TOKEN_NAME) !== null return getLocalStorage(TOKEN_NAME) !== null
} }
export async function getUser(): Promise<User> { export const getUser = async (): Promise<User> => {
if (getLocalStorage('userInfo') !== null) { if (getLocalStorage('userInfo') !== null) {
return new Promise((resolve) => { return new Promise((resolve) => {
resolve(JSON.parse(getLocalStorage('userInfo') as string) as User) resolve(JSON.parse(getLocalStorage('userInfo') as string) as User)
@@ -30,7 +31,7 @@ export async function getUser(): Promise<User> {
return requestUser() return requestUser()
} }
export async function requestUser(): Promise<User> { export const requestUser = async () => {
let user: User | null let user: User | null
await request.get<User>('/user/info').then((value) => { await request.get<User>('/user/info').then((value) => {
@@ -49,17 +50,17 @@ export async function requestUser(): Promise<User> {
}) })
} }
export async function getUsername(): Promise<string> { export const getUsername = async () => {
const user = await getUser() const user = await getUser()
return user.username return user.username
} }
export function getCaptchaSrc(): string { export const getCaptchaSrc = () => {
captcha = getCaptcha(300, 150, 4) captcha = getCaptcha(300, 150, 4)
return captcha.base64Src return captcha.base64Src
} }
export function verifyCaptcha(value: string): boolean { export const verifyCaptcha = (value: string) => {
return captcha.value.toLowerCase() === value.replace(/\s*/g, '').toLowerCase() return captcha.value.toLowerCase() === value.replace(/\s*/g, '').toLowerCase()
} }

View File

@@ -1,6 +1,6 @@
import { TOKEN_NAME } from '@/constants/Common.constants' import { TOKEN_NAME } from '@/constants/Common.constants'
export function getQueryVariable(variable: string): string | null { export const getQueryVariable = (variable: string) => {
const query = window.location.search.substring(1) const query = window.location.search.substring(1)
const vars = query.split('&') const vars = query.split('&')
for (const value of vars) { for (const value of vars) {
@@ -12,12 +12,12 @@ export function getQueryVariable(variable: string): string | null {
return null return null
} }
export function setCookie( export const setCookie = (
name: string, name: string,
value: string, value: string,
daysToLive: number | null, daysToLive: number | null,
path: string | null path: string | null
): void { ) => {
let cookie = `${name}=${encodeURIComponent(value)}` let cookie = `${name}=${encodeURIComponent(value)}`
if (typeof daysToLive === 'number') { if (typeof daysToLive === 'number') {
@@ -31,15 +31,15 @@ export function setCookie(
document.cookie = cookie document.cookie = cookie
} }
export function setLocalStorage(name: string, value: string): void { export const setLocalStorage = (name: string, value: string) => {
localStorage.setItem(name, value) localStorage.setItem(name, value)
} }
export function setToken(token: string): void { export const setToken = (token: string) => {
setLocalStorage(TOKEN_NAME, token) setLocalStorage(TOKEN_NAME, token)
} }
export function getCookie(name: string): string | null { export const getCookie = (name: string) => {
const cookieArr = document.cookie.split(';') const cookieArr = document.cookie.split(';')
for (const cookie of cookieArr) { for (const cookie of cookieArr) {
@@ -52,31 +52,31 @@ export function getCookie(name: string): string | null {
return null return null
} }
export function getLocalStorage(name: string): string | null { export const getLocalStorage = (name: string) => {
return localStorage.getItem(name) return localStorage.getItem(name)
} }
export function getToken(): string | null { export const getToken = () => {
return getLocalStorage(TOKEN_NAME) return getLocalStorage(TOKEN_NAME)
} }
export function removeCookie(name: string): void { export const removeCookie = (name: string) => {
document.cookie = `${name}=; max-age=0` document.cookie = `${name}=; max-age=0`
} }
export function removeLocalStorage(name: string): void { export const removeLocalStorage = (name: string) => {
localStorage.removeItem(name) localStorage.removeItem(name)
} }
export function removeToken(): void { export const removeToken = () => {
removeLocalStorage(TOKEN_NAME) removeLocalStorage(TOKEN_NAME)
} }
export function clearLocalStorage(): void { export const clearLocalStorage = () => {
localStorage.clear() localStorage.clear()
} }
export function getCaptcha(width: number, high: number, num: number): Captcha { export const getCaptcha = (width: number, high: number, num: number) => {
const CHARTS = '23456789ABCDEFGHJKLMNPRSTUVWXYZabcdefghijklmnpqrstuvwxyz'.split('') const CHARTS = '23456789ABCDEFGHJKLMNPRSTUVWXYZabcdefghijklmnpqrstuvwxyz'.split('')
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
@@ -116,7 +116,7 @@ export function getCaptcha(width: number, high: number, num: number): Captcha {
} }
} }
function randomInt(start: number, end: number): number { const randomInt = (start: number, end: number) => {
if (start > end) { if (start > end) {
const t = start const t = start
start = end start = end
@@ -127,10 +127,14 @@ function randomInt(start: number, end: number): number {
return start + Math.floor(Math.random() * (end - start)) return start + Math.floor(Math.random() * (end - start))
} }
function randomFloat(start: number, end: number): number { const randomFloat = (start: number, end: number) => {
return start + Math.random() * (end - start) return start + Math.random() * (end - start)
} }
function randomColor(start: number, end: number): string { const randomColor = (start: number, end: number) => {
return `rgb(${randomInt(start, end)},${randomInt(start, end)},${randomInt(start, end)})` return `rgb(${randomInt(start, end)},${randomInt(start, end)},${randomInt(start, end)})`
} }
export const getRedirectUrl = (path: string, redirectUrl: string): string => {
return `${path}?redirect=${encodeURIComponent(redirectUrl)}`
}