This commit is contained in:
SOR
2026-04-04 08:32:57 +08:00
commit b3e1fd9e18
76 changed files with 6650 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
src/assets
+5
View File
@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
+10
View File
@@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}
+51
View File
@@ -0,0 +1,51 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ChatPage: typeof import('./src/components/home/ChatPage.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElFooter: typeof import('element-plus/es')['ElFooter']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
HeaderBar: typeof import('./src/components/home/HeaderBar.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="index" level="application" />
</component>
</module>
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vue.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css"/>
<title>frontend</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+3437
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vueuse/core": "^14.0.0",
"axios": "^1.13.2",
"dompurify": "^3.3.0",
"element-plus": "^2.11.7",
"markdown-it": "^14.1.0",
"marked": "^17.0.1",
"unplugin-auto-import": "^20.3.0",
"uuid": "^13.0.0",
"vue": "^3.5.24",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"sass-embedded": "^1.93.3",
"typescript": "~5.9.3",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.2.4",
"vue-tsc": "^3.1.4"
}
}
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07"
height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198">
<path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"/>
<path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"/>
<path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

+27
View File
@@ -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>
+136
View File
@@ -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();
}
+19
View File
@@ -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;
+14
View File
@@ -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')
+60
View File
@@ -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
+15
View File
@@ -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;
}
+66
View File
@@ -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
}
+31
View File
@@ -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>
+64
View File
@@ -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>
+97
View File
@@ -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>
+206
View File
@@ -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>
+220
View File
@@ -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>
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
base: "./",
server: {
host: '0.0.0.0',
port: 5173,
allowedHosts: true
},
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})