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
+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>