mirror of
https://github.com/SOR2171/Springboot4-Vue3_Demo.git
synced 2026-10-23 19:41:12 +08:00
git init
This commit is contained in:
@@ -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
|
||||
@@ -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).
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
Vendored
+51
@@ -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']
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Generated
+3437
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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()],
|
||||
}),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user