1
0
mirror of https://github.com/FatttSnake/Pinnacle-OA.git synced 2026-04-05 15:01:23 +08:00

Added login, logout and getUserinfo (Include ui and server)

This commit is contained in:
2023-05-05 20:59:09 +08:00
parent a8dce8f8e0
commit 60b8460e03
32 changed files with 1022 additions and 151 deletions

View File

@@ -22,7 +22,7 @@ module.exports = {
rules: {
"no-cond-assign": "error",
"eqeqeq": "error",
"indent": ["error", 4],
"indent": ["error", 4, {"SwitchCase": 1}],
"prettier/prettier": [
"error",
{

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="473.5501403808594" height="514.205810546875"
viewBox="0 0 473.5501403808594 514.205810546875"><g style="mix-blend-mode:passthrough"><g style="mix-blend-mode:passthrough" transform="matrix(-0.09544334560632706,-0.9953708648681641,0.9953532218933105,-0.09562418609857559,-475.4320658580816,596.4368332676222)"><path d="M207.21426391601562,514.205810546875L357.9022639160156,845.999810546875L56.52586391601562,845.999810546875L207.21426391601562,514.205810546875Z" fill="#5495F1" fill-opacity="1"/></g><g style="mix-blend-mode:passthrough" transform="matrix(0.6958200931549072,-0.7182161211967468,0.7182161211967468,0.6958200931549072,-42.65635399221446,124.80157307758964)"><path d="M204.50979614257812,112.7599334716797L272.49279614257813,317.36393347167973L136.52679614257812,317.36393347167973L204.50979614257812,112.7599334716797Z" fill="#FFFFFF" fill-opacity="0.6000000238418579"/></g></g></svg>

After

Width:  |  Height:  |  Size: 964 B

View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="27.428573608398438" height="32.000003814697266"
viewBox="0 0 27.428573608398438 32.000003814697266"><g><path d="M22.8571,10.6667L25.9048,10.6667C26.7463,10.6667,27.4286,11.3489,27.4286,12.1905L27.4286,30.4762C27.4286,31.3178,26.7463,32,25.9048,32L1.52381,32C0.682233,32,0,31.3178,0,30.4762L0,12.1905C0,11.3489,0.682233,10.6667,1.52381,10.6667L4.57143,10.6667L4.57143,9.14286C4.57143,4.0934,8.66483,0,13.7143,0C18.7637,0,22.8571,4.0934,22.8571,9.14286L22.8571,10.6667ZM19.8095,10.6667L19.8095,9.14286C19.8095,5.77655,17.0806,3.04762,13.7143,3.04762C10.348,3.04762,7.61905,5.77655,7.61905,9.14286L7.61905,10.6667L19.8095,10.6667ZM12.1905,19.8095L12.1905,22.8571L15.2381,22.8571L15.2381,19.8095L12.1905,19.8095ZM6.09524,19.8095L6.09524,22.8571L9.14286,22.8571L9.14286,19.8095L6.09524,19.8095ZM18.2857,19.8095L18.2857,22.8571L21.3333,22.8571L21.3333,19.8095L18.2857,19.8095Z" fill-opacity="1"/></g></svg>

After

Width:  |  Height:  |  Size: 966 B

View File

@@ -1,4 +1,5 @@
const PRODUCTION_NAME = 'Pinnacle OA'
const TOKEN_NAME = 'JWT_TOKEN'
const COLOR_PRODUCTION = '#00D4FF'
const COLOR_BACKGROUND = '#D8D8D8'
const COLOR_FONT_MAIN = '#4D4D4D'
@@ -11,6 +12,7 @@ const SIZE_ICON_XL = '64px'
export {
PRODUCTION_NAME,
TOKEN_NAME,
COLOR_PRODUCTION,
COLOR_BACKGROUND,
COLOR_FONT_MAIN,

View File

@@ -5,18 +5,6 @@ import router from '@/router'
import '@/assets/css/base.css'
import '@/assets/css/common.css'
/*
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
from.path ? next({ path: from.path }) : next('/')
} else {
if (to.meta.title) {
document.title = PRODUCTION_NAME + ' - ' + to.meta.title
}
}
})
*/
const app = createApp(App)
app.use(router).mount('#app')

273
ui/src/pages/Login.vue Normal file
View File

@@ -0,0 +1,273 @@
<template>
<div class="background" @keyup.enter="login">
<div class="card-back">
<div class="production-name">
<span class="emphasize">{{ PRODUCTION_NAME() }}</span>
<br />
<span>自动化办公系统</span>
</div>
<img class="back-shape" :src="backShape" alt="back-shape" />
</div>
<div class="card-front">
<div class="login-title">登录</div>
<div class="input-box user-name-box">
<div class="center-box" style="padding: 10px">
<el-icon size="18">
<icon-pinnacle-user />
</el-icon>
</div>
<label for="user-name"></label
><input
type="text"
name="user-name"
id="user-name"
v-model="userName"
@keyup="userName = userName.replace(/\s+/g, '')"
placeholder="用户名"
/>
</div>
<div class="input-box password-box">
<div class="center-box" style="padding: 10px">
<el-icon size="18">
<icon-pinnacle-password />
</el-icon>
</div>
<label for="password"></label
><input
type="password"
name="password"
id="password"
v-model="password"
@keyup="password = password.replace(/\s+/g, '')"
placeholder="密码"
/>
</div>
<div class="captcha-set">
<div class="captcha-box">
<div class="input-box" style="height: 100%">
<label for="captcha"></label
><input
type="text"
name="captcha"
id="captcha"
v-model="captcha"
placeholder="验证码"
/>
</div>
</div>
<img :src="captchaSrc" alt="Captcha" @click="getNewCaptcha" />
</div>
<ElButton
size="large"
type="primary"
:disabled="loggingIn"
id="login-bt"
@click="login"
>
<template #default
><span style="font-size: clamp(2em, 1.5vw, 2.8em)"
>&ensp;&ensp;</span
></template
>
</ElButton>
</div>
</div>
</template>
<script lang="ts">
import { getCaptchaSrc, login, verifyCaptcha } from '@/utils/auth'
import backShape from '@/assets/svg/back-shape.svg'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
import { PRODUCTION_NAME } from '@/constants/Common.constants'
export default {
name: 'LoginPage',
data() {
return {
backShape,
captchaSrc: getCaptchaSrc(),
userName: '',
password: '',
captcha: '',
loggingIn: false
}
},
methods: {
PRODUCTION_NAME() {
return PRODUCTION_NAME
},
getNewCaptcha() {
this.captchaSrc = getCaptchaSrc()
},
async login() {
if (!this.userName) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>用户名</strong> 为空'
})
return
}
if (!this.password) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>密码</strong> 为空'
})
return
}
if (!this.captcha) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>验证码</strong> 为空'
})
return
}
if (!verifyCaptcha(this.captcha)) {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>验证码</strong> 错误'
})
return
}
if (await login(this.userName, this.password)) {
ElMessage.success({
dangerouslyUseHTMLString: true,
message: '<strong>登录成功</strong>'
})
this.loggingIn = true
const _this = this
setTimeout(function () {
_this.$router.push('/')
}, 1500)
} else {
ElMessage.error({
dangerouslyUseHTMLString: true,
message: '<strong>用户名</strong> 或 <strong>密码</strong> 错误'
})
}
}
}
}
</script>
<style scoped>
.background {
width: 100vw;
min-width: 900px;
height: 100vh;
min-height: 500px;
background: linear-gradient(to right, #5495f1, #82b5ff);
}
.card-back {
position: absolute;
left: 50%;
top: 50%;
width: 65vw;
min-width: 800px;
height: 25vw;
min-height: 350px;
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.6);
transform: translateX(-50%) translateY(-50%);
}
.production-name {
position: absolute;
left: 6%;
top: 8%;
font-size: clamp(2em, 2vw, 2.8em);
color: var(--font-main-color);
}
.production-name .emphasize {
font-size: 2em;
font-weight: bold;
color: var(--main-color);
}
.back-shape {
position: absolute;
height: 80%;
left: 30%;
top: 10%;
}
.card-front {
position: absolute;
left: 50%;
top: 50%;
width: 25vw;
min-width: 350px;
height: 35vw;
min-height: 500px;
border-radius: 15px;
background-color: white;
transform: translateY(-50%);
}
.login-title {
margin-top: 10%;
margin-left: 10%;
font-size: clamp(2.8em, 2.8vw, 4em);
letter-spacing: 0.25em;
font-weight: bold;
color: var(--main-color);
}
.input-box {
display: flex;
flex-direction: row;
background-color: #f0f0f0;
border-radius: 5px;
overflow: hidden;
}
.input-box input {
flex: 1;
font-size: 1.6em;
background-color: transparent;
padding-right: 10px;
color: var(--font-main-color);
}
.user-name-box,
.password-box {
margin: 10% auto -2% auto;
height: 10%;
width: 80%;
}
.captcha-set {
display: flex;
margin: 15% auto 12% auto;
height: 10%;
width: 80%;
}
.captcha-set img {
height: 100%;
cursor: pointer;
}
.captcha-box {
flex: 1;
padding-right: 10px;
height: 100%;
}
.captcha-box input {
padding: 0 10px;
height: 100%;
width: 100%;
}
#login-bt {
display: block;
margin: 0 auto;
width: 80%;
height: auto;
}
</style>

View File

@@ -106,7 +106,7 @@
</div>
<div class="user-info">
<div class="user-name">
<span>用户名</span>
<span>{{ username }}</span>
</div>
<div class="user-desc">
<span>用户介绍</span>
@@ -120,7 +120,7 @@
<el-button style="width: 100%">个人档案</el-button>
</div>
<div>
<el-button style="width: 100%">退出</el-button>
<el-button @click="logout" style="width: 100%">退出</el-button>
</div>
</div>
</template>
@@ -155,9 +155,17 @@ import {
SIZE_ICON_SM
} from '@/constants/Common.constants.js'
import _ from 'lodash'
import { getUsername, logout } from '@/utils/auth'
export default {
name: 'MainFrame',
data() {
return {
routes: _.filter(_.get(this.$router, 'options.routes[0].children'), 'meta.title'),
isCollapsed: false,
username: ''
}
},
methods: {
SIZE_ICON_LG() {
return SIZE_ICON_LG
@@ -176,16 +184,16 @@ export default {
},
COLOR_FONT_MAIN() {
return COLOR_FONT_MAIN
}
},
data() {
return {
routes: _.filter(_.get(this.$router, 'options.routes[0].children'), 'meta.title'),
isCollapsed: false
},
logout() {
logout()
this.$router.push({ name: 'Login' })
}
},
mounted() {
console.log(this.routes)
getUsername().then((res) => {
this.username = res.toString()
})
}
}
</script>

View File

@@ -1,4 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import { PRODUCTION_NAME } from '@/constants/Common.constants'
import { getLoginStatus } from '@/utils/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -23,8 +25,41 @@ const router = createRouter({
}
}
]
},
{
path: '/login',
component: async () => await import('@/pages/Login.vue'),
name: 'Login',
meta: {
title: '登录'
}
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
from.path !== '' ? next({ path: from.path }) : next('/')
} else {
if (to.meta.title !== '') {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
document.title = `${PRODUCTION_NAME} - ${to.meta.title}`
}
if (getLoginStatus()) {
if (to.name === 'Login') {
next('/')
} else {
next()
}
} else {
if (to.name === 'Login') {
next()
} else {
next('/login')
}
}
}
})
export default router

View File

@@ -0,0 +1,68 @@
import axios, { type AxiosError } from 'axios'
import { getToken, removeToken } from '@/utils/common'
import router from '@/router'
const service = axios.create({
baseURL: 'http://localhost:8621',
timeout: 10000,
withCredentials: false
})
service.interceptors.request.use(
(config) => {
const token = getToken()
if (token != null) {
config.headers.set('token', token)
}
return config
},
async (error) => {
return await Promise.reject(error)
}
)
service.interceptors.response.use(
(response) => {
return response
},
async (error) => {
if (error.response != null) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.log(`request error: ${error.response.code} - ${error.response.msg}`)
switch (error.response.code) {
case 30010:
removeToken()
await router.push({ name: 'Login' })
}
}
return await Promise.reject(error?.response?.data)
}
)
const request = {
async get<T = any>(url: string, data?: any): Promise<T> {
return await request.request('GET', url, { params: data })
},
async post<T = any>(url: string, data?: any): Promise<T> {
return await request.request('POST', url, { data })
},
async put<T = any>(url: string, data?: any): Promise<T> {
return await request.request('PUT', url, { data })
},
async delete<T = any>(url: string, data?: any): Promise<T> {
return await request.request('DELETE', url, { params: data })
},
async request<T = any>(method = 'GET', url: string, data?: any): Promise<T> {
return await new Promise((resolve, reject) => {
service({ method, url, ...data })
.then((res) => {
resolve(res as unknown as Promise<T>)
})
.catch((e: Error | AxiosError) => {
reject(e)
})
})
}
}
export default request

61
ui/src/utils/auth.ts Normal file
View File

@@ -0,0 +1,61 @@
import type { Captcha } from './common'
import {
getCaptcha,
getLocalStorage,
getToken,
removeLocalStorage,
setLocalStorage,
setToken
} from './common'
import { TOKEN_NAME } from '@/constants/Common.constants'
import _ from 'lodash'
import request from '@/services'
let captcha: Captcha
async function login(username: string, passwd: string): Promise<boolean> {
removeLocalStorage('username')
await request.post('/login', { username, passwd }).then((res: any) => {
const response = res.data
if (response.code === 20010) {
setToken(response.data.token)
}
})
return !_.isEmpty(getToken())
}
function logout(): void {
removeLocalStorage(TOKEN_NAME)
removeLocalStorage('username')
}
function getLoginStatus(): boolean {
return getLocalStorage(TOKEN_NAME) != null
}
async function getUsername(): Promise<string | null> {
if (!_.isEmpty(getLocalStorage('username'))) {
return getLocalStorage('username')
}
let username = ''
await request.get('/userInfo').then((res) => {
username = res.data.data.user.username
})
setLocalStorage('username', username)
return username
}
function getCaptchaSrc(): string {
captcha = getCaptcha(300, 150, 4)
return captcha.base64Src
}
function verifyCaptcha(value: string): boolean {
return captcha.value === value.replace(/\s*/g, '').toUpperCase()
}
export { login, logout, getLoginStatus, getUsername, getCaptchaSrc, verifyCaptcha }

View File

@@ -0,0 +1,153 @@
import { TOKEN_NAME } from '@/constants/Common.constants'
interface Captcha {
value: string
base64Src: string
}
function getQueryVariable(variable: string): string | null {
const query = window.location.search.substring(1)
const vars = query.split('&')
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=')
if (pair[0] === variable) {
return decodeURIComponent(pair[1].replace(/\+/g, ' '))
}
}
return null
}
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
}
function setLocalStorage(name: string, value: string): void {
localStorage.setItem(name, value)
}
function setToken(token: string): void {
setLocalStorage(TOKEN_NAME, token)
}
function getCookie(name: string): string | null {
const cookieArr = document.cookie.split(';')
for (let i = 0; i < cookieArr.length; i++) {
const cookiePair = cookieArr[i].split('=')
if (name === cookiePair[0].trim()) {
return decodeURIComponent(cookiePair[1])
}
}
return null
}
function getLocalStorage(name: string): string | null {
return localStorage.getItem(name)
}
function getToken(): string | null {
return getLocalStorage(TOKEN_NAME)
}
function removeCookie(name: string): void {
document.cookie = name + '=; max-age=0'
}
function removeLocalStorage(name: string): void {
localStorage.removeItem(name)
}
function removeToken(): void {
removeLocalStorage(TOKEN_NAME)
}
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)})`
}
function getCaptcha(width: number, high: number, num: number): Captcha {
const CHARTS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'.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
}
}
export type { Captcha }
export {
getQueryVariable,
getCookie,
getLocalStorage,
getToken,
setCookie,
setLocalStorage,
setToken,
removeCookie,
removeLocalStorage,
removeToken,
getCaptcha
}