mirror of
https://github.com/SOR2171/Springboot4-Vue3_Demo.git
synced 2026-10-24 03:51:11 +08:00
git init
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {useDark, useToggle} from "@vueuse/core";
|
||||
|
||||
useDark({
|
||||
selector: 'html',
|
||||
attribute: 'class',
|
||||
valueDark: 'dark',
|
||||
valueLight: 'light',
|
||||
})
|
||||
|
||||
useDark({
|
||||
onChanged(dark) {
|
||||
useToggle(dark)
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from "element-plus";
|
||||
import type {
|
||||
ApiResponse,
|
||||
LoginResponse,
|
||||
FailureCallback,
|
||||
SuccessCallback
|
||||
} from "../interfaces/ApiTypes.ts";
|
||||
import { deleteAccessToken, storeAccessToken, takeAccessToken } from "../utils/JwtUtils.ts";
|
||||
import router from "../router";
|
||||
|
||||
// --- Constants & Defaults ---
|
||||
|
||||
export const chatStreamURL = `/api/chat/stream`;
|
||||
|
||||
const defaultFailure: FailureCallback = (message, code, url) => {
|
||||
console.warn(`Request to ${url} failed ${code}: ${message}`);
|
||||
ElMessage.warning('Something went wrong: ' + message);
|
||||
}
|
||||
|
||||
const defaultError: ErrorCallback = (err) => {
|
||||
console.error(err);
|
||||
ElMessage.error('Something went wrong, please contact the administrator.');
|
||||
}
|
||||
|
||||
const accessHeader = (): Record<string, string> => {
|
||||
const token = takeAccessToken();
|
||||
return token ? {
|
||||
'Authorization': 'Bearer ' + token
|
||||
} : {};
|
||||
};
|
||||
|
||||
// --- Internal Request Helpers ---
|
||||
|
||||
function internalPost<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
header: Record<string, string>,
|
||||
success: SuccessCallback<T>,
|
||||
failure: FailureCallback = defaultFailure,
|
||||
error: ErrorCallback = defaultError
|
||||
): void {
|
||||
axios.post<ApiResponse<T>>(url, data, { headers: header })
|
||||
.then(({ data }) => {
|
||||
if (data.code === 200) {
|
||||
success(data.data);
|
||||
} else {
|
||||
failure(data.message, data.code, url);
|
||||
}
|
||||
})
|
||||
.catch(err => error(err));
|
||||
}
|
||||
|
||||
function internalGet<T>(
|
||||
url: string,
|
||||
header: Record<string, string>,
|
||||
success: SuccessCallback<T>,
|
||||
failure: FailureCallback = defaultFailure,
|
||||
error: ErrorCallback = defaultError
|
||||
): void {
|
||||
axios.get<ApiResponse<T>>(url, { headers: header })
|
||||
.then(({ data }) => {
|
||||
if (data.code === 200) {
|
||||
success(data.data);
|
||||
} else {
|
||||
failure(data.message, data.code, url);
|
||||
}
|
||||
})
|
||||
.catch(err => error(err));
|
||||
}
|
||||
|
||||
// --- Exported Functions ---
|
||||
|
||||
/**
|
||||
* Generic GET request
|
||||
* @param url API Endpoint
|
||||
* @param success Callback on success (code 200)
|
||||
* @param failure Callback on business failure (code != 200)
|
||||
* @param error Callback on network/system error
|
||||
*/
|
||||
export function get<T = any>(
|
||||
url: string,
|
||||
success: SuccessCallback<T>,
|
||||
failure: FailureCallback = defaultFailure,
|
||||
error: ErrorCallback = defaultError
|
||||
): void {
|
||||
internalGet<T>(url, accessHeader(), success, failure, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic POST request
|
||||
* @param url API Endpoint
|
||||
* @param data Request body
|
||||
* @param success Callback on success (code 200)
|
||||
* @param failure Callback on business failure (code != 200)
|
||||
* @param error Callback on network/system error
|
||||
*/
|
||||
export function post<T = any>(
|
||||
url: string,
|
||||
data: any,
|
||||
success: SuccessCallback<T>,
|
||||
failure: FailureCallback = defaultFailure,
|
||||
error: ErrorCallback = defaultError
|
||||
): void {
|
||||
internalPost<T>(url, data, accessHeader(), success, failure, error);
|
||||
}
|
||||
|
||||
export function login(
|
||||
username: string,
|
||||
password: string,
|
||||
remember: boolean,
|
||||
success: SuccessCallback<LoginResponse>
|
||||
): void {
|
||||
internalPost<LoginResponse>(
|
||||
'/api/auth/login',
|
||||
{
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
{
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
(data) => {
|
||||
storeAccessToken(data.token, remember, data.expire);
|
||||
ElMessage.success(`Login successfully! Welcome back, ${data.username}`);
|
||||
success(data);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function logout(success: () => void): void {
|
||||
deleteAccessToken();
|
||||
ElMessage.success('Logout successfully!');
|
||||
router.push('/');
|
||||
success();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface JwtAuth {
|
||||
token: string;
|
||||
expire: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
expire: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export type SuccessCallback<T = any> = (data: T) => void;
|
||||
export type FailureCallback = (message: string, code: number, url: string) => void;
|
||||
@@ -0,0 +1,14 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from "./router/index.ts";
|
||||
// import axios from "axios";
|
||||
import 'element-plus/dist/index.css';
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
|
||||
// axios.defaults.baseURL = 'http://localhost:8080'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title?: string;
|
||||
}
|
||||
}
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'welcome',
|
||||
component: () => import('../views/WelcomeView.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'welcome-login',
|
||||
component: () => import('../views/welcome/LoginPage.vue'),
|
||||
meta: { title: 'Welcome - login' },
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
name: 'welcome-register',
|
||||
component: () => import('../views/welcome/RegisterPage.vue'),
|
||||
meta: { title: 'Welcome - register' },
|
||||
},
|
||||
{
|
||||
path: 'reset',
|
||||
name: 'welcome-reset',
|
||||
component: () => import('../views/welcome/ResetPage.vue'),
|
||||
meta: { title: 'Welcome - reset' },
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: () => import('../views/HomeView.vue'),
|
||||
meta: { title: 'home' },
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const nearestTitle = to.meta.title;
|
||||
|
||||
if (nearestTitle) {
|
||||
document.title = nearestTitle;
|
||||
} else {
|
||||
document.title = 'frontend - page';
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.scss' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { JwtAuth } from "../interfaces/ApiTypes.ts";
|
||||
import { ElMessage } from "element-plus";
|
||||
import router from "../router";
|
||||
|
||||
const authItemName = 'access_token';
|
||||
|
||||
function takeAccessToken(): string | null {
|
||||
const str = localStorage.getItem(authItemName) || sessionStorage.getItem(authItemName);
|
||||
if (!str) return null;
|
||||
|
||||
try {
|
||||
const authObject: JwtAuth = JSON.parse(str);
|
||||
if (authObject.expire < Date.now()) {
|
||||
deleteAccessToken();
|
||||
ElMessage.warning('Your session has expired, please log in again.');
|
||||
return null;
|
||||
}
|
||||
return authObject.token;
|
||||
} catch (e) {
|
||||
deleteAccessToken();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function storeAccessToken(token: string, remember: boolean, expire: number): void {
|
||||
const authObject: JwtAuth = {
|
||||
token: token,
|
||||
expire: expire
|
||||
};
|
||||
const authString = JSON.stringify(authObject);
|
||||
if (remember) {
|
||||
localStorage.setItem(authItemName, authString);
|
||||
} else {
|
||||
sessionStorage.setItem(authItemName, authString);
|
||||
localStorage.removeItem(authItemName);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAccessToken(): void {
|
||||
localStorage.removeItem(authItemName);
|
||||
sessionStorage.removeItem(authItemName);
|
||||
}
|
||||
|
||||
function loginPageTokenInit(): void {
|
||||
const storedToken = takeAccessToken();
|
||||
if (storedToken) {
|
||||
router.push('/home')
|
||||
}
|
||||
}
|
||||
|
||||
function homePageTokenInit(): void {
|
||||
const storedToken = takeAccessToken();
|
||||
if (!storedToken) {
|
||||
router.push('/').then(() => {
|
||||
ElMessage.warning('Have not logged in yet, please log in.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
takeAccessToken,
|
||||
storeAccessToken,
|
||||
deleteAccessToken,
|
||||
loginPageTokenInit,
|
||||
homePageTokenInit
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { logout } from "../api";
|
||||
import { homePageTokenInit } from "../utils/JwtUtils";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
onMounted(() => {
|
||||
homePageTokenInit();
|
||||
});
|
||||
|
||||
const handleLogout = () => {
|
||||
logout(() => {
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center-container">
|
||||
<el-button @click="handleLogout" type="primary">logout</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted } from "vue";
|
||||
import { loginPageTokenInit } from "../utils/JwtUtils.ts";
|
||||
|
||||
onMounted(() => {
|
||||
loginPageTokenInit()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 100vw;height: 100vh;overflow: hidden;display: flex">
|
||||
<div id="left-card">
|
||||
<el-image style="width: 100%;height: 100%" fit="cover"
|
||||
src="src/assets/welcome-image.png"/>
|
||||
</div>
|
||||
<div id="welcome-title">
|
||||
<div>welcome here.</div>
|
||||
</div>
|
||||
<div id="right-card">
|
||||
<router-view v-slot="{Component}">
|
||||
<transition name="el-fade-in-linear" mode="out-in">
|
||||
<component :is="Component"/>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#left-card {
|
||||
flex: 1;
|
||||
background-color: black
|
||||
}
|
||||
|
||||
@media (max-width: 512px) {
|
||||
#left-card {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#right-card {
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 1;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
@media (min-width: 513px) {
|
||||
#right-card {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
#welcome-title {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 120px;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-shadow: 0 0 10px black;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import {reactive, ref} from "vue";
|
||||
import {Lock, User} from "@element-plus/icons-vue";
|
||||
import {login} from "../../api";
|
||||
import router from "../../router/index.ts";
|
||||
|
||||
const formRef = ref()
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false
|
||||
})
|
||||
|
||||
const formRule = {
|
||||
username: [
|
||||
{ required: true, message: 'please input username or email', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: 'please input password', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
function userLogin() {
|
||||
formRef.value.validate((valid:boolean) => {
|
||||
if (valid) {
|
||||
login(form.username, form.password, form.remember, () => router.push('/home'))
|
||||
} else {
|
||||
console.log('error submit!')
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="text-align: center;margin: 32px">
|
||||
<div style="margin-top: 169px">
|
||||
<div style="font-size: 28px;font-weight: bold">Login</div>
|
||||
<div style="font-size: 14px;color: slategray">Please enter your username (or your email) and your password</div>
|
||||
</div>
|
||||
|
||||
<div style="margin: 32px">
|
||||
<el-form model="form" :rules="formRule" ref="formRef">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.username" maxlength="63" type="text" placeholder="username or email">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<User/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-input v-model="form.password" maxlength="20" type="password" placeholder="password">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12" style="text-align: left">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="form.remember" label="remember me"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: right">
|
||||
<el-link @click="router.push('/reset')">
|
||||
forget password?
|
||||
</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin-top: 24px">
|
||||
<el-button @click="userLogin" style="width: 280px" type="primary">
|
||||
Sign in
|
||||
</el-button>
|
||||
</div>
|
||||
<el-divider>
|
||||
<span style="font-size: 10px;color: slategray">have no account?</span>
|
||||
</el-divider>
|
||||
<div style="margin-top: 16px">
|
||||
<el-button @click="router.push('/register')" style="width: 280px" type="default">
|
||||
Sign up
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import {reactive, ref} from "vue";
|
||||
import {Bell, Lock, Message, User} from "@element-plus/icons-vue";
|
||||
import router from "../../router/index.js";
|
||||
import {get, post} from "../../api";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const coldTime = ref(0);
|
||||
const formRef = ref()
|
||||
const isCounting = ref(false)
|
||||
let timer: number | null = null;
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
email: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const startColdTime = (): void => {
|
||||
isCounting.value = true;
|
||||
coldTime.value = 60;
|
||||
|
||||
timer = setInterval(() => {
|
||||
coldTime.value--;
|
||||
if (coldTime.value <= 0) {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
timer = null;
|
||||
isCounting.value = false;
|
||||
coldTime.value = 0;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const validateUsername = (_rule: any, value: string, callback: (err?: Error) => void): void => {
|
||||
if (value === "") {
|
||||
callback(new Error("Please input username"));
|
||||
} else if (!/^[a-zA-Z0-9\u4e00-\u9fa5]+$/.test(value)) {
|
||||
callback(new Error("Only letters, numbers, or Chinese characters"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const validatePasswordRepeat = (_rule: any, value: string, callback: (err?: Error) => void): void => {
|
||||
if (value === "") {
|
||||
callback(new Error("Please repeat password"));
|
||||
} else if (value !== form.password) {
|
||||
callback(new Error("Passwords do not match"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const rule = {
|
||||
username: [
|
||||
{validator: validateUsername, trigger: ['blur', 'change']},
|
||||
{min: 4, max: 16, message: 'Length should be 4 to 16', trigger: 'blur'}
|
||||
],
|
||||
password: [
|
||||
{required: true, message: 'Please input password', trigger: 'blur'},
|
||||
{min: 6, max: 20, message: 'Length should be 6 to 20', trigger: ['blur', 'change']}
|
||||
],
|
||||
passwordRepeat: [
|
||||
{validator: validatePasswordRepeat, trigger: ['blur', 'change']},
|
||||
],
|
||||
email: [
|
||||
{required: true, message: 'Please input email', trigger: 'blur'},
|
||||
{type: 'email', message: 'Please input a valid email', trigger: ['blur', 'change']}
|
||||
],
|
||||
code: [
|
||||
{required: true, message: 'Please input verification code', trigger: 'blur'}
|
||||
]
|
||||
}
|
||||
|
||||
function askCode(): void {
|
||||
get(
|
||||
`/api/auth/ask-code?email=${form.email}&type=register`,
|
||||
(response: unknown) => {
|
||||
coldTime.value = 60;
|
||||
startColdTime();
|
||||
ElMessage.success(`Verification code sent to ${form.email}`);
|
||||
console.log(response);
|
||||
},
|
||||
(err: unknown) => {
|
||||
coldTime.value = 0;
|
||||
ElMessage.error("Email address is invalid or already registered");
|
||||
console.log(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function register(): void {
|
||||
formRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
post(
|
||||
"/api/auth/register",
|
||||
{...form},
|
||||
() => {
|
||||
ElMessage.success("Registration successfully!");
|
||||
router.push("/");
|
||||
},
|
||||
(err: unknown) => {
|
||||
ElMessage.error("Please check your information and try again.");
|
||||
console.log(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="text-align: center;margin: 32px">
|
||||
<div style="margin-top: 169px; margin-bottom: 50px">
|
||||
<div style="font-size: 28px;font-weight: bold">Register</div>
|
||||
<div style="font-size: 14px;color: slategray">welcome to join us!</div>
|
||||
</div>
|
||||
<div style="margin: 32px;">
|
||||
<el-form :model="form" :rules="rule" ref="formRef">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username" maxlength="16" type="text" placeholder="Username">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<User/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" maxlength="20" type="password" placeholder="Password">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="passwordRepeat">
|
||||
<el-input v-model="form.passwordRepeat" maxlength="20" type="password" placeholder="Repeat password">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="form.email" type="text" placeholder="Email">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Message/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="code">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="11">
|
||||
<el-input v-model="form.code" maxlength="6" type="text" placeholder="Verify code">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Bell/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="10">
|
||||
<el-button @click="askCode" :disabled="coldTime" type="success" plain style="width: 100%">
|
||||
{{ coldTime > 0 ? `Wait for ${coldTime}s` : 'Get Code' }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="margin-top: 24px">
|
||||
<el-button @click="register" style="width: 280px" type="primary">Register</el-button>
|
||||
</div>
|
||||
<el-divider>
|
||||
<span style="font-size: 10px;color: slategray">already have a account?</span>
|
||||
</el-divider>
|
||||
<div style="margin-top: 16px">
|
||||
<el-button @click="router.push('/')" style="width: 280px" type="default">
|
||||
Sign in
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<script setup lang="ts">
|
||||
import {reactive, ref} from "vue";
|
||||
import {Bell, Lock, Message} from "@element-plus/icons-vue";
|
||||
import {get, post} from "../../api";
|
||||
import {ElMessage} from "element-plus";
|
||||
import router from "../../router/index.js";
|
||||
|
||||
const activeStep = ref(0);
|
||||
const coldTime = ref(0);
|
||||
const formRef = ref()
|
||||
const isCounting = ref(false)
|
||||
let timer: number | null = null;
|
||||
|
||||
const form = reactive({
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
email: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const startColdTime = () => {
|
||||
isCounting.value = true
|
||||
coldTime.value = 60
|
||||
|
||||
timer = setInterval(() => {
|
||||
coldTime.value--
|
||||
if (coldTime.value <= 0) {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
timer = null
|
||||
isCounting.value = false
|
||||
coldTime.value = 0
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const validatePasswordRepeat = (_rule: any, value: string, callback: (err?: Error) => void): void => {
|
||||
if (value === "") {
|
||||
callback(new Error("Please repeat password"));
|
||||
} else if (value !== form.password) {
|
||||
callback(new Error("Passwords do not match"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const rule = {
|
||||
password: [
|
||||
{required: true, message: 'Please input password', trigger: 'blur'},
|
||||
{min: 6, max: 20, message: 'Length should be 6 to 20', trigger: ['blur', 'change']}
|
||||
],
|
||||
passwordRepeat: [
|
||||
{validator: validatePasswordRepeat, trigger: ['blur', 'change']},
|
||||
],
|
||||
email: [
|
||||
{required: true, message: 'Please input email', trigger: 'blur'},
|
||||
{type: 'email', message: 'Please input a valid email', trigger: ['blur', 'change']}
|
||||
],
|
||||
code: [
|
||||
{required: true, message: 'Please input verification code', trigger: 'blur'}
|
||||
]
|
||||
}
|
||||
|
||||
function askCode() {
|
||||
get(
|
||||
`/api/auth/ask-code?email=${form.email}&type=reset`,
|
||||
(response) => {
|
||||
coldTime.value = 60;
|
||||
startColdTime();
|
||||
ElMessage.success(`Verification code sent to ${form.email}`);
|
||||
console.log(response);
|
||||
},
|
||||
(err) => {
|
||||
coldTime.value = 0;
|
||||
ElMessage.error('Email address is invalid or already registered');
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
formRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
post(
|
||||
'/api/auth/reset',
|
||||
{...form},
|
||||
() => {
|
||||
ElMessage.success('Reset successfully!')
|
||||
router.push('/')
|
||||
},
|
||||
(err) => {
|
||||
ElMessage.error('Please check your information and try again.')
|
||||
console.log(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="text-align: center;margin: 32px">
|
||||
<div class="title">
|
||||
Reset Password
|
||||
</div>
|
||||
<div style="margin: 32px">
|
||||
<el-steps :active="activeStep" finish-status="success" align-center>
|
||||
<el-step title="Verify Email"/>
|
||||
<el-step title="Reset Password"/>
|
||||
</el-steps>
|
||||
</div>
|
||||
|
||||
<div style="margin: 32px" v-if="activeStep === 0">
|
||||
<div class="description">
|
||||
Please enter your email address.
|
||||
</div>
|
||||
<el-form :model="form" :rules="rule" ref="formRef">
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="form.email" type="text" placeholder="Email">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Message/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="code">
|
||||
<el-row justify="space-between">
|
||||
<el-col :span="11">
|
||||
<el-input v-model="form.code" maxlength="6" type="text" placeholder="Verify code">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Bell/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="10">
|
||||
<el-button @click="askCode" :disabled="coldTime" type="success" plain style="width: 100%">
|
||||
{{ coldTime > 0 ? `Wait for ${coldTime}s` : 'Get Code' }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="margin: 32px" v-if="activeStep === 1">
|
||||
<div class="description">
|
||||
Please enter new password.
|
||||
</div>
|
||||
<el-form :model="form" :rules="rule" ref="formRef">
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" maxlength="20" type="password" placeholder="Password">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="passwordRepeat">
|
||||
<el-input v-model="form.passwordRepeat" maxlength="20" type="password" placeholder="Repeat password">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 24px" v-if="activeStep === 0">
|
||||
<el-button @click="activeStep++" :disabled="!(form.code.toString().length === 6)" style="width: 280px"
|
||||
type="primary">
|
||||
Go Next Step
|
||||
</el-button>
|
||||
</div>
|
||||
<div style="margin-top: 24px" v-if="activeStep === 1">
|
||||
<el-button @click="resetPassword" style="width: 280px" type="primary">
|
||||
Reset
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider>
|
||||
<span style="font-size: 10px;color: slategray">Wanna go back?</span>
|
||||
</el-divider>
|
||||
|
||||
<div style="margin-top: 24px" v-if="activeStep === 0">
|
||||
<el-button @click="router.push('/')" style="width: 280px" type="default">
|
||||
Go Back
|
||||
</el-button>
|
||||
</div>
|
||||
<div style="margin-top: 24px" v-if="activeStep === 1">
|
||||
<el-button @click="activeStep--" style="width: 280px" type="default">
|
||||
Go Last Step
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<
|
||||
<style scoped>
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-top: 169px
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
color: slategray;
|
||||
margin-bottom: 32px
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user