551 lines
14 KiB
Vue
Raw Normal View History

<template>
<el-container class="app-root" direction="vertical">
<el-header class="app-header" height="50px">
<div class="app-header-inner">
<router-link v-slot="{ navigate }" :to="{ path: '/', force: true }" custom>
<div class="header-title" @click="navigate()">社团展示系统</div>
</router-link>
<transition name="header-content-container">
2024-12-12 12:41:24 +08:00
<template v-if="!userStore.initializing">
<nav class="header-content-container">
<el-menu
2024-12-10 20:04:14 +08:00
:default-active="$route.fullPath"
:ellipsis="false"
:router="true"
mode="horizontal"
>
<div style="flex: 1"></div>
<el-menu-item index="/2048">
2024-12-12 12:41:24 +08:00
<img alt="2048" class="header-menu-image" src="/2048.png" />
</el-menu-item>
</el-menu>
2024-12-09 20:43:08 +08:00
<template v-if="userStore.userInfo && userStore.userInfo.id !== -1">
<div class="username">
{{ userStore.userInfo.name }}
</div>
<el-dropdown ref="dropdownRef">
<el-avatar :icon="userStore.userInfo.avatar || Avatar" :size="40"></el-avatar>
<template #dropdown>
<el-dropdown-menu>
2024-12-09 20:43:08 +08:00
<!-- <router-link v-slot="{ navigate }" custom to="/user">
<el-dropdown-item :icon="UserFilled" @click="navigate()">
个人主页
</el-dropdown-item>
2024-12-09 20:43:08 +08:00
</router-link> -->
<el-dropdown-item :icon="CloseBold" @click="logout">
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<el-avatar
v-else
:size="40"
style="color: black; user-select: none; cursor: pointer"
@click="showLoginRegisterDialog = true"
>
登录
</el-avatar>
</nav>
</template>
<div v-else class="header-content-container">
<el-icon class="loading-icon is-loading" color="dimgrey" size="25">
<icon-cs-loading />
</el-icon>
</div>
</transition>
</div>
2024-04-09 17:57:51 +08:00
</el-header>
2024-12-09 20:43:08 +08:00
<el-main v-loading="!!pageStore.pageLoadingCount">
<router-view v-slot="{ Component: comp }">
<transition appear mode="out-in" name="page-root">
<component :is="comp" />
</transition>
</router-view>
</el-main>
2024-04-09 17:57:51 +08:00
</el-container>
2024-04-14 14:40:34 +08:00
<el-dialog
v-model="showLoginRegisterDialog"
:align-center="true"
class="login-dialog"
width="400"
2024-04-14 14:40:34 +08:00
>
2024-04-10 17:51:13 +08:00
<el-tabs v-model="loginRegisterDialogActiveName">
<el-tab-pane label="登录" name="login">
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" @submit.prevent>
2024-04-10 17:51:13 +08:00
<el-form-item prop="username">
2024-04-14 14:40:34 +08:00
<el-input
v-model="loginFormData.username"
:disabled="logining"
placeholder="请输入用户名"
2024-04-14 14:40:34 +08:00
>
2024-04-10 17:51:13 +08:00
<template #prepend>
<el-icon>
<icon-ep-user-filled />
</el-icon>
2024-04-10 17:51:13 +08:00
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
2024-04-14 14:40:34 +08:00
<el-input
v-model="loginFormData.password"
:disabled="logining"
placeholder="请输入密码"
type="password"
2024-04-14 14:40:34 +08:00
>
2024-04-10 17:51:13 +08:00
<template #prepend>
<el-icon>
<icon-cs-lock />
</el-icon>
2024-04-10 17:51:13 +08:00
</template>
</el-input>
</el-form-item>
2024-04-19 20:52:42 +08:00
<el-form-item prop="verifyCode">
<verify-input v-model="loginFormData" :disabled="logining" />
</el-form-item>
2024-04-18 18:02:43 +08:00
<el-form-item style="margin-bottom: 0">
<el-button
:loading="logining"
native-type="submit"
2024-04-18 18:02:43 +08:00
style="width: 100%"
type="primary"
2024-04-19 20:52:42 +08:00
@click="submitLoginForm"
2024-04-18 18:02:43 +08:00
>
登录
</el-button>
</el-form-item>
2024-04-10 17:51:13 +08:00
</el-form>
</el-tab-pane>
<el-tab-pane label="注册" name="register">
<el-form
ref="registerFormRef"
:model="registerFormData"
:rules="registerFormRules"
@submit.prevent
>
2024-04-10 17:51:13 +08:00
<el-form-item prop="username">
2024-04-14 14:40:34 +08:00
<el-input
v-model="registerFormData.username"
:disabled="registering"
placeholder="请输入注册用户名"
2024-04-14 14:40:34 +08:00
>
2024-04-10 17:51:13 +08:00
<template #prepend>
<el-icon>
<icon-cs-user />
</el-icon>
2024-04-10 17:51:13 +08:00
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
2024-04-14 14:40:34 +08:00
<el-input
v-model="registerFormData.password"
:disabled="registering"
placeholder="请输入密码"
type="password"
2024-04-14 14:40:34 +08:00
>
2024-04-10 17:51:13 +08:00
<template #prepend>
<el-icon>
<icon-cs-lock />
</el-icon>
2024-04-10 17:51:13 +08:00
</template>
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
2024-04-14 14:40:34 +08:00
v-model="registerFormData.confirmPassword"
:disabled="registering"
placeholder="请确认密码"
type="password"
2024-04-10 17:51:13 +08:00
>
<template #prepend>
<el-icon>
<icon-cs-lock />
</el-icon>
2024-04-10 17:51:13 +08:00
</template>
</el-input>
</el-form-item>
2024-04-19 20:52:42 +08:00
<el-form-item prop="verifyCode">
<verify-input v-model="registerFormData" :disabled="registering" />
</el-form-item>
2024-04-10 17:51:13 +08:00
<el-form-item style="margin-bottom: 0">
2024-04-14 14:40:34 +08:00
<el-button
:loading="registering"
native-type="submit"
2024-04-14 14:40:34 +08:00
style="width: 100%"
type="primary"
2024-04-14 14:40:34 +08:00
@click="submitRegisterForm"
>
注册
</el-button>
2024-04-10 17:51:13 +08:00
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template>
<style lang="scss" scoped>
.app-root {
min-height: 100vh;
}
.app-header {
--el-header-padding: 0;
--el-menu-horizontal-height: 50px;
--el-menu-base-level-padding: 10px;
position: sticky;
top: 0;
2024-04-09 17:57:51 +08:00
display: flex;
justify-content: center;
2024-04-09 17:57:51 +08:00
border-bottom: 1px solid var(--el-border-color);
box-shadow: 0 1px 5px rgba(black, 0.1);
background-color: white;
z-index: 100;
overflow: hidden;
2024-04-09 17:57:51 +08:00
}
.app-header-inner {
position: relative;
width: var(--page-content-width);
height: 100%;
display: flex;
justify-content: space-between;
align-items: stretch;
margin: 0 15px;
}
.header-content-container {
height: 50px;
flex: 1;
2024-04-14 14:40:34 +08:00
display: flex;
justify-content: flex-end;
2024-04-14 14:40:34 +08:00
align-items: center;
transition-property: opacity;
transition-duration: 0.3s;
&:has(.loading-icon) {
position: absolute;
top: 0;
right: 0;
}
2024-04-14 14:40:34 +08:00
}
2024-04-18 17:57:35 +08:00
.header-content-container .el-menu {
flex: 1;
2024-06-05 18:02:15 +08:00
margin-right: 10px;
}
.header-content-container .header-menu-image {
width: 40px;
display: flex;
align-items: center;
}
.header-content-container-enter-from,
.header-content-container-leave-to {
opacity: 0;
}
.el-main {
display: flex;
justify-content: center;
position: relative;
padding: 0;
&:has(.page-root-enter-active, .page-root-leave-active) {
overflow-x: hidden;
}
}
.page-root {
transition-property: opacity, transform;
transition-duration: 0.3s;
&:not(.page-max-width) {
width: var(--page-content-width);
margin: 0 15px;
}
&.page-max-width {
width: 100vw;
}
}
.page-root-enter-from {
opacity: 0;
&:not(.page-max-width) {
transform: translateX(-30px);
}
}
.page-root-leave-to {
opacity: 0;
&:not(.page-max-width) {
transform: translateX(30px);
}
}
.page-loading-mask {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
transition-property: opacity;
background-color: rgb(black, 0.3);
z-index: 99;
}
.page-loading-icon-container {
width: 100px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
background-color: rgb(black, 0.5);
2024-06-05 18:02:15 +08:00
}
.nav-img-items {
display: flex;
justify-content: center;
align-items: center;
}
.nav-img-items img {
width: 40px;
height: 40px;
}
.game-2048 {
margin-right: 20px;
}
.header-title {
font-size: 20px;
line-height: 50px;
user-select: none;
cursor: pointer;
2024-04-09 17:57:51 +08:00
}
2024-04-14 14:40:34 +08:00
.username {
margin-right: 10px;
}
2024-04-09 17:57:51 +08:00
</style>
<script lang="ts" setup>
2024-12-09 20:43:08 +08:00
import axiosInstance, { type RawResp } from '@/api';
2024-04-18 18:02:43 +08:00
import { type VerifyImagePath } from '@/components/VerifyInput.vue';
import { loginResponseSchema, registerResponseSchema } from '@/schemas';
2024-12-12 12:41:24 +08:00
import { useUserStore } from '@/stores/user';
import { usePageStore } from '@/stores/page';
2024-12-10 20:04:14 +08:00
import { errorMessage } from '@/utils';
2024-12-10 11:38:40 +08:00
import { Avatar, CloseBold } from '@element-plus/icons-vue';
2024-04-14 14:40:34 +08:00
import { AxiosError } from 'axios';
2024-04-18 17:57:35 +08:00
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
2024-04-19 20:52:42 +08:00
import { partial } from 'lodash-es';
import { reactive, ref, watch } from 'vue';
2024-12-10 20:04:14 +08:00
import { RouterView, useRoute } from 'vue-router';
import router from './router';
2024-12-12 12:41:24 +08:00
import { onMounted } from 'vue';
2024-04-14 14:40:34 +08:00
const userStore = useUserStore();
const pageStore = usePageStore();
2024-12-10 20:04:14 +08:00
const showLoginRegisterDialog = ref(false);
watch(
2024-12-12 12:41:24 +08:00
() => userStore.initializing,
(value) => {
2024-12-10 20:04:14 +08:00
console.log(1);
if (!value && userStore.token === null) {
showLoginRegisterDialog.value = true;
}
2024-12-10 20:04:14 +08:00
},
{ immediate: true },
);
2024-04-14 14:40:34 +08:00
watch(showLoginRegisterDialog, (value) => {
if (value) {
logining.value = false;
2024-04-19 20:52:42 +08:00
loginFormData.verifyImage = 'none';
2024-04-14 14:40:34 +08:00
loginFormRef.value?.resetFields();
registering.value = false;
2024-04-19 20:52:42 +08:00
registerFormData.verifyImage = 'none';
2024-04-14 14:40:34 +08:00
registerFormRef.value?.resetFields();
}
});
2024-04-10 17:51:13 +08:00
const loginRegisterDialogActiveName = ref('login');
const loginFormRef = ref<FormInstance>();
2024-12-10 11:38:40 +08:00
let loginFormData = reactive({
2024-04-14 14:40:34 +08:00
username: 'wubaopu2',
2024-04-18 17:57:35 +08:00
password: '123456',
verifyImage: 'none' as VerifyImagePath,
2024-12-10 20:04:14 +08:00
verifyCode: '',
2024-04-10 17:51:13 +08:00
});
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
2024-04-14 14:40:34 +08:00
username: [
2024-04-19 20:52:42 +08:00
{ required: true, message: '请输入用户名' },
2024-12-12 12:41:24 +08:00
{ min: 3, message: '用户名长度不能小于3位' },
2024-04-14 14:40:34 +08:00
],
password: [
2024-04-19 20:52:42 +08:00
{ required: true, message: '请输入密码' },
2024-12-10 20:04:14 +08:00
{ min: 6, message: '密码长度不能小于6位' },
2024-04-19 20:52:42 +08:00
],
verifyCode: [
{ required: true, message: '请输入验证码' },
2024-12-10 20:04:14 +08:00
{ pattern: /[0-9A-Za-z]{4}/, message: '验证码不符合格式' },
],
2024-04-10 17:51:13 +08:00
});
2024-04-14 14:40:34 +08:00
const logining = ref(false);
2024-04-10 17:51:13 +08:00
const registerFormRef = ref<FormInstance>();
2024-12-10 11:38:40 +08:00
let registerFormData = reactive({
2024-04-10 17:51:13 +08:00
username: '',
password: '',
2024-04-18 17:57:35 +08:00
confirmPassword: '',
verifyImage: 'none' as VerifyImagePath,
2024-12-10 20:04:14 +08:00
verifyCode: '',
2024-04-10 17:51:13 +08:00
});
const registerFormRules = reactive<FormRules<typeof registerFormData>>({
2024-04-14 14:40:34 +08:00
...loginFormRules,
2024-04-10 17:58:10 +08:00
confirmPassword: [
{
validator(_, value, callback) {
2024-04-14 14:40:34 +08:00
if (value === '') {
callback('请输入密码');
} else if (value !== registerFormData.password) {
callback('密码不一致');
} else if (value.length < 6) {
callback('密码长度不能小于6位');
2024-04-10 17:58:10 +08:00
} else {
callback();
}
2024-12-10 20:04:14 +08:00
},
},
],
2024-04-10 17:51:13 +08:00
});
2024-04-14 14:40:34 +08:00
const registering = ref(false);
const loginErrorMessage = partial(errorMessage, '登录');
2024-04-14 14:40:34 +08:00
interface LoginParams {
username: string;
password: string;
2024-04-18 18:02:43 +08:00
key: string;
code: string;
2024-04-14 14:40:34 +08:00
}
2024-12-10 20:04:14 +08:00
watch(
() => userStore.userInfo,
() => {
router.push({ path: router.currentRoute.value.fullPath, force: true });
},
);
2024-04-18 17:57:35 +08:00
2024-12-09 20:43:08 +08:00
async function login(params: LoginParams): Promise<boolean> {
const loginRespRaw = await axiosInstance
.post<RawResp>('/api/user/login', params)
.then((r) => r.data)
.catch((e: AxiosError) => {
2024-04-14 14:40:34 +08:00
ElMessage.error(loginErrorMessage(e.code, e.message));
2024-12-09 20:43:08 +08:00
});
if (!loginRespRaw) return false;
const loginResp = loginResponseSchema.parse(loginRespRaw);
if (loginResp.type === 'error') {
ElMessage.error(loginErrorMessage(loginResp.code, loginResp.msg));
2024-04-14 14:40:34 +08:00
return false;
}
2024-12-10 20:04:14 +08:00
return userStore.updateSelfUserInfo(true);
2024-04-14 14:40:34 +08:00
}
2024-04-14 14:40:34 +08:00
async function submitLoginForm() {
logining.value = true;
try {
try {
await loginFormRef.value?.validate();
} catch (e) {
return;
}
2024-12-10 11:38:40 +08:00
if (typeof loginFormData.verifyImage !== 'object') {
2024-04-19 20:52:42 +08:00
ElMessage.error('请获取验证码');
return;
}
const {
username,
password,
verifyImage: { key },
2024-12-10 20:04:14 +08:00
verifyCode: code,
2024-04-19 20:52:42 +08:00
} = loginFormData;
2024-12-10 20:04:14 +08:00
if (
!(await login({
username,
password,
key,
code,
}))
)
return;
showLoginRegisterDialog.value = false;
2024-04-14 14:40:34 +08:00
} finally {
2024-12-10 20:04:14 +08:00
loginFormData.verifyImage = 'none';
loginFormRef.value?.resetFields('verifyCode');
logining.value = false;
2024-04-14 14:40:34 +08:00
}
}
2024-04-14 14:40:34 +08:00
const registerErrorMessage = partial(errorMessage, '注册');
2024-04-19 20:52:42 +08:00
interface RegisterParams extends LoginParams {
auth: number;
}
2024-12-10 20:04:14 +08:00
async function register(params: RegisterParams): Promise<boolean> {
const raw = await axiosInstance
.put<RawResp>('/api/user/create', params)
.then((r) => r.data)
.catch((err: AxiosError) => {
ElMessage.error(registerErrorMessage(err.code, err.message));
});
if (!raw) return false;
const resp = registerResponseSchema.parse(raw);
if (resp.type === 'error') {
ElMessage.error({
message: registerErrorMessage(resp.code, resp.msg),
});
2024-04-14 14:40:34 +08:00
return false;
}
2024-12-10 20:04:14 +08:00
return userStore.updateSelfUserInfo(true);
2024-04-14 14:40:34 +08:00
}
2024-04-14 14:40:34 +08:00
async function submitRegisterForm() {
registering.value = true;
2024-04-10 17:51:13 +08:00
try {
2024-04-14 14:40:34 +08:00
try {
await registerFormRef.value?.validate();
} catch {
return;
}
2024-12-10 11:38:40 +08:00
if (typeof registerFormData.verifyImage !== 'object') {
2024-04-19 20:52:42 +08:00
ElMessage.error('请输入验证码');
return;
}
const {
username,
password,
verifyImage: { key },
2024-12-10 20:04:14 +08:00
verifyCode: code,
2024-04-19 20:52:42 +08:00
} = registerFormData;
2024-12-10 20:04:14 +08:00
if (!(await register({ username, password, key, code, auth: 1 }))) return;
showLoginRegisterDialog.value = false;
2024-04-14 14:40:34 +08:00
} finally {
2024-12-10 20:04:14 +08:00
registerFormData.verifyImage = 'none';
registerFormRef.value?.resetFields('verifyCode');
registering.value = false;
2024-04-10 17:51:13 +08:00
}
}
2024-04-14 14:40:34 +08:00
async function logout() {
2024-12-10 20:04:14 +08:00
userStore.token = null;
2024-12-09 20:43:08 +08:00
await userStore.updateSelfUserInfo(true);
2024-04-14 14:40:34 +08:00
}
2024-12-12 12:41:24 +08:00
onMounted(() => {
console.log(userStore.token);
});
</script>