This commit is contained in:
2023-09-03 16:05:52 +08:00
commit c4211ddf7c
28 changed files with 1740 additions and 0 deletions

42
src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

12
src/App.tsx Normal file
View File

@@ -0,0 +1,12 @@
import React from 'react'
import router from '@/router'
const App: React.FC = () => {
return (
<>
<RouterProvider router={router} />
</>
)
}
export default App

20
src/AuthRoute.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { getLoginStatus } from '@/utils/auth.ts'
const AuthRoute = () => {
const match = useMatches()[1]
const handle = match.handle as RouteHandle
const outlet = useOutlet()
const isLogin = getLoginStatus()
return useMemo(() => {
if (handle?.auth && !isLogin) {
return <Navigate to="/login" />
}
if (isLogin && match.pathname === '/login') {
return <Navigate to="/" />
}
return outlet
}, [handle?.auth, isLogin, match.pathname, outlet])
}
export default AuthRoute

63
src/assets/css/base.css Normal file
View File

@@ -0,0 +1,63 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
em,
i {
font-style: normal
}
li {
list-style: none
}
img {
border: 0;
vertical-align: middle
}
button {
cursor: pointer
}
a {
color: #666;
text-decoration: none
}
button,
input {
font-family: Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
border: 0;
outline: none;
}
body {
-webkit-font-smoothing: antialiased;
background-color: #fff;
font: 12px/1.5 Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
color: #666
}
.hide,
.none {
display: none
}
.clearfix:after {
visibility: hidden;
clear: both;
display: block;
content: ".";
height: 0
}
.clearfix {
*zoom: 1
}

106
src/assets/css/common.css Normal file
View File

@@ -0,0 +1,106 @@
:root {
--main-color: #00D4FF;
--background-color: #F5F5F5;
--font-main-color: #4D4D4D;
--font-secondary-color: #9E9E9E;
}
.body {
color: var(--font-main-color);
user-select: none;
min-width: 1800px;
min-height: 400px;
}
.fill {
height: 100%;
width: 100%;
}
.fill-with {
width: 100%;
}
.fill-height {
height: 100%;
}
.background-white {
background-color: white;
}
.center-box {
display: flex;
justify-content: center;
align-items: center;
}
.vertical-center-box {
display: flex;
align-items: center;
}
.horizontal-center-box {
display: flex;
justify-content: center;
}
.icon-size-xs {
width: 16px;
height: 16px;
}
.icon-size-xs > use {
width: 16px;
height: 16px;
}
.icon-size-sm {
width: 20px;
height: 20px;
}
.icon-size-sm > use {
width: 20px;
height: 20px;
}
.icon-size-md {
width: 24px;
height: 24px;
}
.icon-size-md > use {
width: 24px;
height: 24px;
}
.icon-size-lg {
width: 32px;
height: 32px;
}
.icon-size-lg > use {
width: 32px;
height: 32px;
}
.icon-size-xl {
width: 64px;
height: 64px;
}
.icon-size-xl > use {
width: 64px;
height: 64px;
}
.icon-size-menu {
width: 23px;
height: 23px;
}
.icon-size-menu > use {
width: 23px;
height: 23px;
}

63
src/assets/css/login.css Normal file
View File

@@ -0,0 +1,63 @@
.login-background {
display: flex;
height: 100vh;
width: 100vw;
justify-content: center;
align-items: center;
background-color: #B3E5FC;
}
.login-box {
display: flex;
width: 800px;
height: 450px;
background-color: #448AFF;
}
.login-box-left {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex: 2;
}
.login-box-left-text {
font-size: 3rem;
color: white;
font-weight: bold;
}
.login-box-left-text>div:last-child {
font-size: 1.8rem;
font-weight: normal;
}
.login-box-right {
position: relative;
height: 100%;
flex: 3;
background-color: white;
}
.login-from-text {
position: absolute;
top: 60px;
left: 40px;
font-weight: bold;
font-size: 2rem;
}
.login-from {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 30px;
width: 100%;
height: 100%;
}
.login-from-item {
width: 80%;
}

View File

@@ -0,0 +1,35 @@
.top-bar {
display: flex;
justify-content: right;
background-color: #317ece;
padding: 10px 20px;
}
.top-bar > button:hover {
color: #F5F5F5;
border-color: #F5F5F5;
}
.search-row {
display: flex;
gap: 20px;
margin: 10px 10px;
}
.search-row > * {
display: flex;
gap: 5px;
align-items: center;
flex: 1;
font-size: 1rem;
}
.search-row > *:not(.operation-buttons) > *:last-child {
flex: 1;
}
.operation-buttons {
display: flex;
justify-content: right;
gap: 10px;
}

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g style="mix-blend-mode:passthrough"><g style="mix-blend-mode:passthrough"><path d="M11.2633,0.229798C11.6966,-0.0765992,12.3034,-0.0765992,12.7367,0.229798C12.7367,0.229798,23.5367,7.86616,23.5367,7.86616C23.829,8.07284,24,8.39063,24,8.72727C24,8.72727,24,20.7273,24,20.7273C24,21.5953,23.6207,22.4277,22.9456,23.0414C22.2704,23.6552,21.3548,24,20.4,24C20.4,24,3.6,24,3.6,24C2.64522,24,1.72955,23.6552,1.05442,23.0414C0.379284,22.4277,0,21.5953,0,20.7273C0,20.7273,0,8.72727,0,8.72727C0,8.39063,0.170968,8.07284,0.463271,7.86616C0.463271,7.86616,11.2633,0.229798,11.2633,0.229798C11.2633,0.229798,11.2633,0.229798,11.2633,0.229798ZM9.6,21.8182C9.6,21.8182,14.4,21.8182,14.4,21.8182C14.4,21.8182,14.4,13.0909,14.4,13.0909C14.4,13.0909,9.6,13.0909,9.6,13.0909C9.6,13.0909,9.6,21.8182,9.6,21.8182C9.6,21.8182,9.6,21.8182,9.6,21.8182ZM16.8,21.8182C16.8,21.8182,16.8,12,16.8,12C16.8,11.3975,16.2628,10.9091,15.6,10.9091C15.6,10.9091,8.4,10.9091,8.4,10.9091C7.73726,10.9091,7.2,11.3975,7.2,12C7.2,12,7.2,21.8182,7.2,21.8182C7.2,21.8182,3.6,21.8182,3.6,21.8182C3.28174,21.8182,2.97652,21.7032,2.75147,21.4987C2.52643,21.2941,2.4,21.0166,2.4,20.7273C2.4,20.7273,2.4,9.26081,2.4,9.26081C2.4,9.26081,12,2.47294,12,2.47294C12,2.47294,21.6,9.26081,21.6,9.26081C21.6,9.26081,21.6,20.7273,21.6,20.7273C21.6,21.0166,21.4735,21.2941,21.2485,21.4987C21.0235,21.7032,20.7182,21.8182,20.4,21.8182C20.4,21.8182,16.8,21.8182,16.8,21.8182C16.8,21.8182,16.8,21.8182,16.8,21.8182Z" fill-rule="evenodd" fill-opacity="1"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,85 @@
const PRODUCTION_NAME = 'Pinnacle OA'
const TOKEN_NAME = 'JWT_TOKEN'
const COLOR_PRODUCTION = '#00D4FF'
const COLOR_BACKGROUND = '#F5F5F5'
const COLOR_TOP = 'rgba(234,46,13,0.85)'
const COLOR_FONT_MAIN = '#4D4D4D'
const COLOR_FONT_SECONDARY = '#9E9E9E'
const SIZE_ICON_XS = '16px'
const SIZE_ICON_SM = '20px'
const SIZE_ICON_MD = '24px'
const SIZE_ICON_LG = '32px'
const SIZE_ICON_XL = '64px'
// Response Code
const SYSTEM_OK = 20000
const LOGIN_SUCCESS = 20010
const LOGIN_USERNAME_PASSWORD_ERROR = 20011
const OLD_PASSWORD_NOT_MATCH = 20012
const LOGOUT_SUCCESS = 20015
const LOGOUT_FAILED = 20016
const TOKEN_IS_ILLEGAL = 20017
const TOKEN_HAS_EXPIRED = 20018
const TOKEN_RENEW_SUCCESS = 20019
const DATABASE_SELECT_OK = 20021
const DATABASE_SAVE_OK = 20022
const DATABASE_UPDATE_OK = 20023
const DATABASE_DELETE_OK = 20024
const DATABASE_SELECT_ERROR = 20031
const DATABASE_SAVE_ERROR = 20032
const DATABASE_UPDATE_ERROR = 20033
const DATABASE_DELETE_ERROR = 20034
const DATABASE_TIMEOUT_ERROR = 20035
const DATABASE_CONNECT_ERROR = 20036
const DATABASE_DATA_TO_LONG = 20037
const DATABASE_DATA_VALIDATION_FAILED = 20038
const DATABASE_EXECUTE_ERROR = 20039
const UNAUTHORIZED = 30010
const ACCESS_DENIED = 30030
const USER_DISABLE = 30031
const SYSTEM_ERROR = 50001
const SYSTEM_TIMEOUT = 50002
export {
PRODUCTION_NAME,
TOKEN_NAME,
COLOR_PRODUCTION,
COLOR_BACKGROUND,
COLOR_FONT_MAIN,
COLOR_FONT_SECONDARY,
COLOR_TOP,
SIZE_ICON_XS,
SIZE_ICON_SM,
SIZE_ICON_MD,
SIZE_ICON_LG,
SIZE_ICON_XL,
SYSTEM_OK,
LOGIN_SUCCESS,
LOGIN_USERNAME_PASSWORD_ERROR,
OLD_PASSWORD_NOT_MATCH,
LOGOUT_SUCCESS,
LOGOUT_FAILED,
TOKEN_IS_ILLEGAL,
TOKEN_HAS_EXPIRED,
TOKEN_RENEW_SUCCESS,
DATABASE_SELECT_OK,
DATABASE_SAVE_OK,
DATABASE_UPDATE_OK,
DATABASE_DELETE_OK,
DATABASE_SELECT_ERROR,
DATABASE_SAVE_ERROR,
DATABASE_UPDATE_ERROR,
DATABASE_DELETE_ERROR,
DATABASE_TIMEOUT_ERROR,
DATABASE_CONNECT_ERROR,
DATABASE_DATA_TO_LONG,
DATABASE_DATA_VALIDATION_FAILED,
DATABASE_EXECUTE_ERROR,
UNAUTHORIZED,
ACCESS_DENIED,
USER_DISABLE,
SYSTEM_ERROR,
SYSTEM_TIMEOUT
}

69
src/index.css Normal file
View File

@@ -0,0 +1,69 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

14
src/main.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import '@/assets/css/base.css'
import '@/assets/css/common.css'
import zh_CN from 'antd/locale/zh_CN'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AntdConfigProvider locale={zh_CN}>
<App />
</AntdConfigProvider>
</React.StrictMode>
)

11
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react'
const Home: React.FC = () => {
return (
<>
<h1>FatWeb</h1>
</>
)
}
export default Home

106
src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,106 @@
import React from 'react'
import { login } from '@/utils/auth.ts'
import { LOGIN_SUCCESS, LOGIN_USERNAME_PASSWORD_ERROR } from '@/constants/Common.constants.ts'
import { setToken } from '@/utils/common.ts'
import '@/assets/css/login.css'
const Login: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage()
const navigate = useNavigate()
const [isLoggingIn, setIsLoggingIn] = useState(false)
const onFinish = (values: LoginForm) => {
setIsLoggingIn(true)
void login(values.username, values.password).then((value) => {
const res = value.data
const { code, data } = res
switch (code) {
case LOGIN_SUCCESS:
setToken(data?.token ?? '')
void messageApi.success('登录成功')
setTimeout(() => {
navigate('/')
}, 1500)
break
case LOGIN_USERNAME_PASSWORD_ERROR:
void messageApi.error(
<>
<strong></strong><strong></strong>
</>
)
setIsLoggingIn(false)
break
default:
void messageApi.error(
<>
<strong></strong>
</>
)
setIsLoggingIn(false)
}
})
}
return (
<>
{contextHolder}
<div className={'login-background'}>
<div className={'login-box'}>
<div className={'login-box-left'}>
<div className={'login-box-left-text'}>
<div></div>
<div>Welcome back</div>
</div>
</div>
<div className={'login-box-right'}>
<div className={'login-from-text'}>
<span>&ensp;</span>
</div>
<AntdForm
name="login-form"
autoComplete="on"
onFinish={onFinish}
className={'login-from'}
>
<AntdForm.Item
className={'login-from-item'}
name={'username'}
rules={[{ required: true, message: '用户名为空' }]}
>
<AntdInput
prefix={<UserOutlined />}
placeholder={'用户名'}
disabled={isLoggingIn}
/>
</AntdForm.Item>
<AntdForm.Item
className={'login-from-item'}
name={'password'}
rules={[{ required: true, message: '密码为空' }]}
>
<AntdInput.Password
prefix={<LockOutlined />}
placeholder={'密码'}
disabled={isLoggingIn}
/>
</AntdForm.Item>
<AntdForm.Item className={'login-from-item'}>
<AntdButton
style={{ width: '100%' }}
type={'primary'}
htmlType={'submit'}
disabled={isLoggingIn}
loading={isLoggingIn}
>
&ensp;&ensp;&ensp;&ensp;
</AntdButton>
</AntdForm.Item>
</AntdForm>
</div>
</div>
</div>
</>
)
}
export default Login

37
src/router/index.tsx Normal file
View File

@@ -0,0 +1,37 @@
import React from 'react'
const routes: RouteObject[] = [
{
path: '/',
Component: React.lazy(() => import('@/AuthRoute')),
children: [
{
path: '/login',
id: 'login',
Component: React.lazy(() => import('@/pages/Login'))
},
{
path: '',
id: 'manager',
Component: React.lazy(() => import('@/pages/Home')),
children: [
{
id: 'manager-sub',
path: 'sub',
Component: React.lazy(() => import('@/pages/Home'))
}
],
handle: {
auth: false
}
},
{
path: '*',
element: <Navigate to="/" replace />
}
]
}
]
const router = createBrowserRouter(routes)
export default router

155
src/services/index.tsx Normal file
View File

@@ -0,0 +1,155 @@
import axios, { type AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import jwtDecode, { JwtPayload } from 'jwt-decode'
import { clearLocalStorage, getToken, setToken } from '@/utils/common'
import {
ACCESS_DENIED,
DATABASE_DATA_TO_LONG,
DATABASE_DATA_VALIDATION_FAILED,
DATABASE_EXECUTE_ERROR,
TOKEN_HAS_EXPIRED,
TOKEN_IS_ILLEGAL,
TOKEN_RENEW_SUCCESS,
UNAUTHORIZED
} from '@/constants/Common.constants'
import { message } from 'antd'
const service: AxiosInstance = axios.create({
baseURL: 'http://localhost:8181',
timeout: 10000,
withCredentials: false
})
service.defaults.paramsSerializer = (params: Record<string, string>) => {
return Object.keys(params)
.filter((it) => {
return Object.prototype.hasOwnProperty.call(params, it)
})
.reduce((pre, curr) => {
return params[curr] !== null
? (pre !== '' ? pre + '&' : '') + curr + '=' + encodeURIComponent(params[curr])
: pre
}, '')
}
service.interceptors.request.use(
async (config) => {
let token = getToken()
if (token !== null) {
const jwt = jwtDecode<JwtPayload>(token)
if (!jwt.exp) {
return config
}
if (
jwt.exp * 1000 - new Date().getTime() < 1200000 &&
jwt.exp * 1000 - new Date().getTime() > 0
) {
await axios
.get('http://localhost:8181/token', {
headers: { token }
})
.then((value: AxiosResponse<_Response<Token>>) => {
const response = value.data
if (response.code === TOKEN_RENEW_SUCCESS) {
setToken(response.data?.token ?? '')
}
})
}
token = getToken()
config.headers.set('token', token)
}
return config
},
async (error) => {
return await Promise.reject(error)
}
)
service.interceptors.response.use(
(response: AxiosResponse<_Response<never>>) => {
switch (response.data.code) {
case UNAUTHORIZED:
case TOKEN_IS_ILLEGAL:
case TOKEN_HAS_EXPIRED:
clearLocalStorage()
void message.error(
<>
<strong></strong>
</>
)
setTimeout(function () {
location.reload()
}, 1500)
throw response?.data
case ACCESS_DENIED:
void message.error(
<>
<strong></strong>
</>
)
throw response?.data
case DATABASE_DATA_TO_LONG:
void message.error(
<>
<strong></strong>
</>
)
throw response?.data
case DATABASE_DATA_VALIDATION_FAILED:
void message.error(
<>
<strong></strong>
</>
)
throw response?.data
case DATABASE_EXECUTE_ERROR:
void message.error(
<>
<strong></strong>
</>
)
throw response?.data
}
return response
},
async (error: AxiosError) => {
void message.error(
<>
<strong></strong>
</>
)
return await Promise.reject(error?.response?.data)
}
)
const request = {
async get<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('GET', url, { params: data })
},
async post<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('POST', url, { data })
},
async put<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('PUT', url, { data })
},
async delete<T>(url: string, data?: object): Promise<AxiosResponse<_Response<T>>> {
return await request.request('DELETE', url, { params: data })
},
async request<T>(
method = 'GET',
url: string,
data?: AxiosRequestConfig
): Promise<AxiosResponse<_Response<T>>> {
return await new Promise((resolve, reject) => {
service({ method, url, ...data })
.then((res) => {
resolve(res as unknown as Promise<AxiosResponse<_Response<T>>>)
})
.catch((e: Error | AxiosError) => {
reject(e)
})
})
}
}
export default request

65
src/utils/auth.ts Normal file
View File

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

136
src/utils/common.ts Normal file
View File

@@ -0,0 +1,136 @@
import { TOKEN_NAME } from '@/constants/Common.constants'
export function getQueryVariable(variable: string): string | null {
const query = window.location.search.substring(1)
const vars = query.split('&')
for (const value of vars) {
const pair = value.split('=')
if (pair[0] === variable) {
return decodeURIComponent(pair[1].replace(/\+/g, ' '))
}
}
return null
}
export function setCookie(
name: string,
value: string,
daysToLive: number | null,
path: string | null
): void {
let cookie = name + '=' + encodeURIComponent(value)
if (typeof daysToLive === 'number') {
cookie = `${cookie}; max-age=${daysToLive * 24 * 60 * 60}`
}
if (typeof path === 'string') {
cookie += '; path=' + path
}
document.cookie = cookie
}
export function setLocalStorage(name: string, value: string): void {
localStorage.setItem(name, value)
}
export function setToken(token: string): void {
setLocalStorage(TOKEN_NAME, token)
}
export function getCookie(name: string): string | null {
const cookieArr = document.cookie.split(';')
for (const cookie of cookieArr) {
const cookiePair = cookie.split('=')
if (cookiePair[0].trim() === name) {
return decodeURIComponent(cookiePair[1])
}
}
return null
}
export function getLocalStorage(name: string): string | null {
return localStorage.getItem(name)
}
export function getToken(): string | null {
return getLocalStorage(TOKEN_NAME)
}
export function removeCookie(name: string): void {
document.cookie = name + '=; max-age=0'
}
export function removeLocalStorage(name: string): void {
localStorage.removeItem(name)
}
export function removeToken(): void {
removeLocalStorage(TOKEN_NAME)
}
export function clearLocalStorage(): void {
localStorage.clear()
}
export function getCaptcha(width: number, high: number, num: number): Captcha {
const CHARTS = '23456789ABCDEFGHJKLMNPRSTUVWXYZabcdefghijklmnpqrstuvwxyz'.split('')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
ctx.rect(0, 0, width, high)
ctx.clip()
ctx.fillStyle = randomColor(200, 250)
ctx.fillRect(0, 0, width, high)
for (let i = 0.05 * width * high; i > 0; i--) {
ctx.fillStyle = randomColor(0, 256)
ctx.fillRect(randomInt(0, width), randomInt(0, high), 1, 1)
}
ctx.font = `${high - 4}px Consolas`
ctx.fillStyle = randomColor(160, 200)
let value = ''
for (let i = 0; i < num; i++) {
const x = ((width - 10) / num) * i + 5
const y = high - 12
const r = Math.PI * randomFloat(-0.12, 0.12)
const ch = CHARTS[randomInt(0, CHARTS.length)]
value += ch
ctx.translate(x, y)
ctx.rotate(r)
ctx.fillText(ch, 0, 0)
ctx.rotate(-r)
ctx.translate(-x, -y)
}
const base64Src = canvas.toDataURL('image/jpg')
return {
value,
base64Src
}
}
function randomInt(start: number, end: number): number {
if (start > end) {
const t = start
start = end
end = t
}
start = Math.ceil(start)
end = Math.floor(end)
return start + Math.floor(Math.random() * (end - start))
}
function randomFloat(start: number, end: number): number {
return start + Math.random() * (end - start)
}
function randomColor(start: number, end: number): string {
return `rgb(${randomInt(start, end)},${randomInt(start, end)},${randomInt(start, end)})`
}

31
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,31 @@
/// <reference types="vite/client" />
type Captcha = {
value: string
base64Src: string
}
type RouteHandle = {
auth: boolean
}
type _Response<T> = {
code: number
msg: string
data: T | null
}
type Token = {
token: string
}
type User = {
id: number
username: string
enable: number
}
type LoginForm = {
username: string
password: string
}