feat: 完成登录注册功能

This commit is contained in:
Litrix2 2024-04-14 14:40:34 +08:00
parent ad41a017fe
commit 233af2b1e3
18 changed files with 428 additions and 446 deletions

View File

@ -1,4 +1,4 @@
# ITZX-Clubs-Home-Web
A clubs home web for Hangzhou Electron & Information Vocational School
building by vue3 and typescript
A clubs home web for Hangzhou Electron & Information Vocational School
building by vue3 and typescript

5
components.d.ts vendored
View File

@ -10,8 +10,6 @@ declare module 'vue' {
BackgroundComp: typeof import('./src/components/BackgroundComp.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
@ -22,12 +20,9 @@ declare module 'vue' {
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElLink: typeof import('element-plus/es')['ElLink']
ElRow: typeof import('element-plus/es')['ElRow']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
Game2048: typeof import('./src/components/Game2048.vue')['default']
HomeItem: typeof import('./src/components/HomeItem.vue')['default']
IconCsLock: typeof import('~icons/cs/lock')['default']
IconEpAvatar: typeof import('~icons/ep/avatar')['default']
IconEpUserFilled: typeof import('~icons/ep/user-filled')['default']

View File

@ -1,66 +1,56 @@
<template>
<el-container>
<el-header height="50px">
<div class="logo-container">社团展示系统</div>
<div class="title-container">社团展示系统</div>
<div class="content">
<el-dropdown>
<el-icon :size="30"><icon-ep-avatar></icon-ep-avatar></el-icon>
<div v-if="userStore.userInfo !== null" class="username">
{{ userStore.userInfo.name }}
</div>
<el-dropdown v-if="userStore.userInfo !== null">
<el-avatar :icon="userStore.userInfo.avatar || Avatar" :size="40"></el-avatar>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="true">你尚未登录</el-dropdown-item>
<el-dropdown-item @click="showLoginDialog = true" :icon="Avatar">
登录/注册
</el-dropdown-item>
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-avatar
v-else
style="color: black; user-select: none; cursor: pointer"
:size="40"
@click="showLoginRegisterDialog = true"
>
登录
</el-avatar>
</div>
</el-header>
</el-container>
<el-dialog class="login-dialog" width="400" v-model="showLoginDialog" :align-center="true">
<el-dialog
class="login-dialog"
width="400"
v-model="showLoginRegisterDialog"
:align-center="true"
>
<el-tabs v-model="loginRegisterDialogActiveName">
<el-tab-pane label="登录" name="login">
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules">
<el-form-item prop="username">
<el-input placeholder="请输入用户名" v-model="loginFormData.username">
<template #prepend>
<el-icon><icon-ep-user-filled /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="请输入密码" v-model="loginFormData.password">
<template #prepend>
<el-icon><icon-cs-lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button type="primary" style="width: 100%" @click="submitForm">登录</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="注册" name="register">
<el-form ref="registerFormRef" :model="registerFormData" :rules="registerFormRules">
<el-form-item prop="username">
<el-input placeholder="请输入注册用户名" v-model="registerFormData.username">
<template #prepend>
<el-icon><icon-ep-user-filled /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="请输入密码" v-model="registerFormData.password">
<template #prepend>
<el-icon><icon-cs-lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="loginFormData.username"
placeholder="请输入用户名"
:disabled="logining"
>
<template #prepend>
<el-icon><icon-ep-user-filled /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginFormData.password"
type="password"
placeholder="请确认密码"
v-model="registerFormData.confirmPassword"
placeholder="请输入密码"
:disabled="logining"
>
<template #prepend>
<el-icon><icon-cs-lock /></el-icon>
@ -68,7 +58,63 @@
</el-input>
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button type="primary" style="width: 100%" @click="submitForm">登录</el-button>
<el-button
type="primary"
style="width: 100%"
@click="submitLoginForm"
:loading="logining"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="注册" name="register">
<el-form ref="registerFormRef" :model="registerFormData" :rules="registerFormRules">
<el-form-item prop="username">
<el-input
v-model="registerFormData.username"
placeholder="请输入注册用户名"
:disabled="registering"
>
<template #prepend>
<el-icon><icon-ep-user-filled /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerFormData.password"
type="password"
placeholder="请输入密码"
:disabled="registering"
>
<template #prepend>
<el-icon><icon-cs-lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerFormData.confirmPassword"
type="password"
placeholder="请确认密码"
:disabled="registering"
>
<template #prepend>
<el-icon><icon-cs-lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button
type="primary"
style="width: 100%"
@click="submitRegisterForm"
:loading="registering"
>
注册
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
@ -83,27 +129,58 @@
align-items: center;
border-bottom: 1px solid var(--el-border-color);
}
.content {
display: flex;
align-items: center;
}
.title-container {
font-size: 1.2em;
}
.username {
margin-right: 10px;
}
</style>
<script setup lang="ts">
import { Avatar } from '@element-plus/icons-vue';
import { type FormInstance, type FormRules } from 'element-plus';
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import axiosInstance from '@/api';
import { loginResponseSchema, registerResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
import { errorMessage, timeout } from '@/utils';
import { Avatar, CloseBold } from '@element-plus/icons-vue';
import { AxiosError } from 'axios';
import { type FormInstance, type FormRules, ElMessage } from 'element-plus';
import { partial, pick } from 'lodash-es';
import { onMounted, reactive, ref, watch } from 'vue';
import { RouterView, useRoute, useRouter } from 'vue-router';
import { z } from 'zod';
const router = useRouter();
const showLoginDialog = ref(false);
const route = useRoute();
const userStore = useUserStore();
const showLoginRegisterDialog = ref(false);
watch(showLoginRegisterDialog, (value) => {
if (value) {
logining.value = false;
loginFormRef.value?.resetFields();
registering.value = false;
registerFormRef.value?.resetFields();
}
});
const loginRegisterDialogActiveName = ref('login');
const loginFormRef = ref<FormInstance>();
const loginFormData = reactive({
username: '',
password: ''
username: 'wubaopu2',
password: '123456'
});
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 6, message: '用户名长度不能小于3位', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
]
});
const logining = ref(false);
const registerFormRef = ref<FormInstance>();
const registerFormData = reactive({
username: '',
@ -111,16 +188,18 @@ const registerFormData = reactive({
confirmPassword: ''
});
const registerFormRules = reactive<FormRules<typeof registerFormData>>({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
...loginFormRules,
confirmPassword: [
{
required: true,
message: '请输入密码',
trigger: 'blur',
validator(...[, value, callback]) {
if (value !== registerFormData.password) {
callback(new Error('密码不一致'));
validator: (_, value, callback) => {
console.log(value);
if (value === '') {
callback('请输入密码');
} else if (value !== registerFormData.password) {
callback('密码不一致');
} else if (value.length < 6) {
callback('密码长度不能小于6位');
} else {
callback();
}
@ -128,11 +207,101 @@ const registerFormRules = reactive<FormRules<typeof registerFormData>>({
}
]
});
async function submitForm() {
const registering = ref(false);
const loginErrorMessage = partial(errorMessage, '登录');
interface LoginParams {
username: string;
password: string;
}
async function login(params: LoginParams = pick(loginFormData, 'username', 'password')) {
try {
await loginFormRef.value?.validate();
} catch {
console.log('验证失败');
const loginResp = loginResponseSchema.parse(
(await axiosInstance.post('/api/user/login', params)).data
);
if (loginResp.type === 'error') {
ElMessage.error(loginErrorMessage(loginResp.code, loginResp.msg));
return false;
}
return true;
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(loginErrorMessage(e.code, e.message));
} else if (e instanceof z.ZodError) {
ElMessage.error('登录失败: 响应数据错误');
}
return false;
}
}
async function submitLoginForm() {
let succeed = false;
logining.value = true;
try {
try {
await loginFormRef.value?.validate();
} catch (e) {
return;
}
succeed = await login();
} finally {
if (succeed) {
// await timeout(1000);
window.location.reload();
} else {
logining.value = false;
}
}
}
const registerErrorMessage = partial(errorMessage, '注册');
async function register() {
try {
const registerResp = registerResponseSchema.parse(
(
await axiosInstance.put('/api/user/create', {
username: registerFormData.username,
password: registerFormData.password,
auth: 1
})
).data
);
if (registerResp.type === 'error') {
ElMessage.error({
message: registerErrorMessage(registerResp.code, registerResp.msg)
});
return false;
}
console.log(registerResp);
return true;
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(registerErrorMessage(e.code, e.message));
} else if (e instanceof z.ZodError) {
ElMessage.error('注册失败: 响应数据错误');
}
console.log('error', e);
return false;
}
}
async function submitRegisterForm() {
let succeed = false;
registering.value = true;
try {
try {
await registerFormRef.value?.validate();
} catch {
return;
}
succeed = await register();
} finally {
if (succeed) {
window.location.reload();
} else {
registering.value = false;
}
}
}
async function logout() {
userStore.$reset();
await router.push('/');
window.location.reload();
}
</script>

View File

@ -3,14 +3,16 @@ import { useUserStore } from '@/stores';
const axiosInstance = axios.create({
baseURL: 'http://wzpmc.cn:18080/'
});
// 自动添加token到请求中.
axiosInstance.interceptors.request.use((config) => {
const userStore = useUserStore();
config.headers.setAuthorization(userStore.token, false);
return config;
});
// 自动获取响应中的token.
axiosInstance.interceptors.response.use((response) => {
const userStore = useUserStore();
const authorization = response.headers['Set-Authorization'] as string | undefined;
const authorization = response.headers['set-authorization'] as string | undefined;
if (authorization !== undefined) {
userStore.token = authorization;
}

View File

@ -143,6 +143,7 @@ import { IdDispenser, get2DArrayItem } from '@/utils';
import { sample, shuffle, pick } from 'lodash-es';
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { useEventListener } from '@vueuse/core';
import { ElNotification } from 'element-plus';
const game2048Store = useGame2048Store();
/**
* 方向键代码.

2
src/keys/index.ts Normal file
View File

@ -0,0 +1,2 @@
import { type InjectionKey } from 'vue';
export {};

View File

@ -1,21 +1,77 @@
import axiosInstance from '@/api';
import MainPage from '@/views/MainPage.vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
type Route = RouteRecordRaw;
const routes: Route[] = [];
import { userInfoResponseSchema, type Route } from '@/schemas';
import { useUserStore } from '@/stores';
import { errorMessage } from '@/utils';
import { ElMessage } from 'element-plus';
import { type Component } from 'vue';
import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home'
},
{
path: '/404',
component: () => import('@/views/NotFoundPage.vue')
}
];
const router = createRouter({
history: createWebHistory(),
history: createWebHashHistory(),
routes: routes
});
let status = false;
router.beforeEach(async (to) => {
console.log(2);
if (!status) {
const value = await axiosInstance.get('/api/user/info');
router.addRoute({ name: 'test', path: '/', component: MainPage });
status = true;
return to.fullPath;
const componentMap: Record<string, () => Promise<Component>> = {
'MainPage.vue': () => import('@/views/MainPage.vue')
};
function _getAvailableRoutes(routeResponses: Route[], root: boolean, pathList: string[]) {
const res: RouteRecordRaw[] = [];
const { routeMap } = useUserStore();
for (const routeResponse of routeResponses) {
const path = routeResponse.name.toLowerCase();
pathList.push(path);
const route = {
path: root ? `/${path}` : path,
component: componentMap[routeResponse.component],
children: _getAvailableRoutes(routeResponse.children, false, pathList.concat(path))
};
routeMap.set(`/${pathList.join('/')}`, root ? route : routeMap.get(`/${pathList[0]}`)!);
res.push(route);
}
return res;
}
function getAvailableRoutes(routeResponse: Route[]) {
_getAvailableRoutes(routeResponse, true, []);
}
router.beforeEach(async (to) => {
const userStore = useUserStore();
console.log(userStore.userInfo);
// await timeout(1000);
if (!userStore.isInitialized) {
const userInfoResponse = userInfoResponseSchema.parse(
(await axiosInstance.get('/api/user/info')).data
);
console.log(userInfoResponse);
if (userInfoResponse.type === 'error') {
ElMessage.error(
errorMessage('获取用户信息失败', userInfoResponse.code, userInfoResponse.msg)
);
return false;
}
if (userStore.token !== null) {
userStore.updateUserInfo(userInfoResponse);
}
getAvailableRoutes(userInfoResponse.data.auth.permissions[0].routers);
userStore.isInitialized = true;
}
if (userStore.addedRouteSet.has(to.fullPath) || to.fullPath === '/404') {
return true;
}
const route = userStore.routeMap.get(to.fullPath);
if (route !== undefined) {
router.addRoute(route);
userStore.addedRouteSet.add(to.fullPath);
return to;
} else {
return '/404';
}
return true;
});
export default router;

52
src/schemas/index.ts Normal file
View File

@ -0,0 +1,52 @@
import { z } from 'zod';
function createResponseSchema<T extends z.ZodTypeAny>(data: T) {
return z.union([
z
.object({
code: z.literal(200),
msg: z.string(),
time: z.coerce.date()
})
.extend({ data: data })
.transform((raw) =>
Object.assign(raw, {
type: 'success' as const
})
),
z
.object({
code: z.number(),
msg: z.string(),
time: z.coerce.date()
})
.transform((raw) =>
Object.assign(raw, {
type: 'error' as const
})
)
]);
}
export type SucceedResponse<T> = T extends { type: 'success' } ? T : never;
export type ErrorResponse<T> = Exclude<SucceedResponse<T>, T>;
const ordinaryResponse = createResponseSchema(z.literal(true));
export const loginResponseSchema = ordinaryResponse;
export const registerResponseSchema = ordinaryResponse;
const idAndNameSchema = z.object({
id: z.number(),
name: z.string()
});
export interface Route extends z.infer<typeof idAndNameSchema> {
component: string;
children: Route[];
}
const routeResponsesSchema: z.ZodSchema<Route[]> = z.lazy(() =>
idAndNameSchema.extend({ component: z.string(), children: routeResponsesSchema }).array()
);
export const userInfoResponseSchema = createResponseSchema(
idAndNameSchema.extend({
avatar: z.string(),
auth: idAndNameSchema.extend({
permissions: idAndNameSchema.extend({ routers: routeResponsesSchema }).array()
})
})
);

View File

@ -1,4 +1,4 @@
import { create2DArray, useRefresher } from '@/utils';
import { create2DArray, useRefresh } from '@/utils';
import { useLocalStorage } from '@vueuse/core';
import { defineStore } from 'pinia';
import { watch } from 'vue';
@ -9,7 +9,7 @@ export interface Tile {
fromOthers: boolean;
}
export const useGame2048Store = defineStore('2048', () => {
const { key: gameKey, refresh: refreshGame } = useRefresher();
const { key: gameKey, refresh: refreshGame } = useRefresh();
function create() {
return create2DArray(height.value, width.value, () => []);
}

View File

@ -1,3 +1,3 @@
export * from './background';
export * from './2048';
export * from './user'
export * from './user';

View File

@ -1,6 +1,31 @@
import { userInfoResponseSchema, type SucceedResponse } from '@/schemas';
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
import { pick } from 'lodash-es';
import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core';
import { reactive, ref } from 'vue';
import { type RouteRecordRaw } from 'vue-router';
import { z } from 'zod';
type SucceedUserInfoResponse = SucceedResponse<z.infer<typeof userInfoResponseSchema>>;
type UserInfo = Pick<SucceedUserInfoResponse['data'], 'id' | 'name' | 'avatar'>;
export const useUserStore = defineStore('user', () => {
const token = useLocalStorage<string | null>('token', null);
return { token };
const token = useLocalStorage<string>('token', null, {
serializer: StorageSerializers.string
});
const userInfo = useLocalStorage<UserInfo>('user-info', null, {
serializer: StorageSerializers.object
});
const routeMap = reactive(new Map<string, RouteRecordRaw>());
const addedRouteSet = reactive(new Set<string>());
const isInitialized = ref(false);
function updateUserInfo(userInfoResponse: SucceedUserInfoResponse) {
userInfo.value = pick(userInfoResponse.data, 'id', 'name', 'avatar');
}
function $reset() {
token.value = null;
userInfo.value = null;
routeMap.clear();
addedRouteSet.clear();
isInitialized.value = false;
}
return { token, userInfo, routeMap, addedRouteSet, isInitialized, updateUserInfo, $reset };
});

13
src/utils/2d-array.ts Normal file
View File

@ -0,0 +1,13 @@
export function create2DArray<T>(height: number, width: number, cell: T | (() => T)): T[][] {
return Array.from({ length: height }, () =>
Array.from({ length: width }, () => (cell instanceof Function ? cell() : cell))
);
}
export function get2DArrayItem<T>(
grid: T[][],
first: number,
second: number,
colFirst: boolean
): T {
return colFirst ? grid[first][second] : grid[second][first];
}

7
src/utils/api.ts Normal file
View File

@ -0,0 +1,7 @@
export function errorMessage(
operation: string,
code: string | number | undefined,
message: string
) {
return `${operation}失败: ${code !== undefined ? `(${code})${message}` : message}`;
}

View File

@ -1,5 +1,7 @@
import { ref } from 'vue';
import { ref, watch } from 'vue';
export function timeout(delay: number = 0) {
return new Promise<void>((resolve) => setTimeout(resolve, delay));
}
export class IdDispenser {
#maxId: number;
#usedIdSet: Set<number>;
@ -35,25 +37,11 @@ export class IdDispenser {
this.#usedIdSet.clear();
}
}
export function timeout(delay: number = 0) {
return new Promise<void>((resolve) => setTimeout(resolve, delay));
}
export function create2DArray<T>(height: number, width: number, cell: T | (() => T)): T[][] {
return Array.from({ length: height }, () =>
Array.from({ length: width }, () => (cell instanceof Function ? cell() : cell))
);
}
export function get2DArrayItem<T>(
grid: T[][],
first: number,
second: number,
colFirst: boolean
): T {
return colFirst ? grid[first][second] : grid[second][first];
}
export function useRefresher() {
export function useRefresh() {
const dispenser = new IdDispenser();
const key = ref(dispenser.getId());
watch(key, (value) => console.log(value));
return {
key,
refresh() {
@ -61,3 +49,6 @@ export function useRefresher() {
}
};
}
export * from './2d-array';
export * from './api';

View File

@ -1,299 +0,0 @@
<template>
<background-comp :comp-id="backgroundStore.newCompId()" />
<div class="login-container login-container">
<transition name="login">
<el-card v-if="show" class="login-box" :body-style="{ height: '100%', padding: 0 }">
<el-row style="height: 100%">
<el-col style="height: 100%" :xs="0" :sm="12">
<div class="banner-column" :style="{ backgroundImage: bannerUrl }">
<div v-if="sayingContent.saying" class="saying-box">
<div class="saying-content">
{{ sayingContent.saying }}
</div>
<div v-if="sayingAuthor" class="saying-author">{{ sayingAuthor }}</div>
</div>
</div>
</el-col>
<el-col style="height: 100%" :xs="24" :sm="12">
<div class="login-column">
<div class="title">社团展示系统</div>
<el-form label-width="auto" ref="formRef" :model="formData" :rules="formRules">
<el-form-item prop="username">
<el-input placeholder="请输入用户名" v-model="formData.username">
<template #prepend>
<el-icon><icon-ep-user-filled /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="请输入密码" v-model="formData.password">
<template #prepend>
<el-icon><icon-cs-lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button type="primary" style="width: 100%" @click="submitForm">登录</el-button>
</el-form-item>
</el-form>
<div class="register">
<div style="margin-right: 0.5rem">还没有账号?</div>
<el-link type="primary">注册</el-link>
</div>
</div>
</el-col>
</el-row>
</el-card>
</transition>
</div>
<teleport to="body">
<transition name="saying-bottom" appear>
<div v-if="show && sayingContent.saying" class="saying-bottom">
<div class="saying-content">
{{ sayingContent.sliced ? sayingContent.saying.slice(0, -1) : sayingContent.saying }}
</div>
<div class="saying-author">{{ sayingAuthor }}</div>
</div>
</transition>
</teleport>
</template>
<style scoped lang="scss">
@media screen and (max-width: 768px) {
.login-box {
width: 300px;
height: 300px;
}
.saying-bottom {
transform: none;
}
}
@media screen and (min-width: 768px) {
.login-box {
width: 600px;
height: 350px;
}
.saying-bottom {
transform: translateY(25px);
}
}
.login-container {
width: 100vw;
height: 100vh;
}
.login-container,
.login-column,
.register {
display: flex;
justify-content: center;
align-items: center;
}
.login-box {
margin: 0 30px;
border: none;
box-shadow: var(--el-box-shadow-dark);
transition:
width 0.3s ease,
height 0.3s ease,
opacity 0.8s ease,
transform 0.8s ease;
}
.login-enter-from {
opacity: 0;
transform: scale(0.9);
}
.login-enter-to {
opacity: 1;
transform: none;
}
.banner-column {
height: 100%;
padding: 10px;
display: flex;
flex-direction: column;
text-align: right;
justify-content: flex-end;
text-indent: 2em;
background-position: center;
background-size: cover;
}
.saying-content,
.saying-author {
color: white;
}
.saying-box .saying-content {
font-size: 1.6rem;
font-weight: bold;
overflow: hidden;
}
.saying-box .saying-author {
font-size: 1.2rem;
}
.saying-bottom {
position: fixed;
bottom: 0px;
width: 100%;
display: flex;
height: 25px;
line-height: 25px;
background-color: rgba(black, 0.5);
transition:
transform 0.3s ease,
opacity 0.8s ease;
}
.saying-bottom-enter-from {
opacity: 0;
}
.saying-bottom-enter-to {
opacity: 1;
}
.saying-bottom .saying-content {
margin-right: auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.saying-bottom .saying-author {
white-space: nowrap;
}
.login-column {
height: 100%;
flex-direction: column;
padding: 10px;
}
.title {
margin-bottom: 20px;
font-size: 20px;
font-weight: bold;
}
.register {
vertical-align: center;
}
</style>
<script setup lang="ts">
import { useBackgroundStore, type BackgroundOptions } from '@/stores';
import { timeout } from '@/utils';
import md5 from 'crypto-js/md5';
import type { FormInstance, FormRules } from 'element-plus';
import { random } from 'lodash-es';
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as z from 'zod';
const backgroundStore = useBackgroundStore();
const show = ref(false);
const bannerUrl = ref<string>();
const sayingContent = ref({ saying: '', sliced: false });
const sayingAuthor = ref('');
const formRef = ref<FormInstance>();
const formData = reactive({
username: '',
password: ''
});
const crypted = computed(() => ({
username: md5(formData.username).toString(),
password: md5(formData.password).toString()
}));
const formRules = reactive<FormRules<typeof formData>>({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
});
const sayingSchema = z.object({
code: z.literal(200),
data: z.object({
id: z.number(),
tag: z.string(),
name: z.string(),
origin: z.string(),
content: z.string(),
created_at: z.coerce.date(),
updated_at: z.coerce.date()
})
});
const maxSayingLength = 40;
let unMounted = false;
async function setBackground(
resolveTiming: BackgroundOptions['resolveTiming'] = 'transitionStart'
) {
const backgrounds = await Promise.all([
import('@/assets/bg1.jpg'),
import('@/assets/bg2.jpg'),
import('@/assets/bg3.jpg'),
import('@/assets/bg4.png'),
import('@/assets/bg5.jpg')
]);
try {
await backgroundStore.addURL(backgrounds[random(backgrounds.length - 1)].default, {
resolveTiming: resolveTiming
});
} catch {
/* empty */
}
}
async function setBanner() {
await Promise.race([
(async () => {
{
const resp = await fetch((await import('@/assets/banner.jpg')).default);
const blobUrl = URL.createObjectURL(await resp.blob());
bannerUrl.value = `url("${blobUrl}")`;
}
try {
const resp = await fetch('https://api.xygeng.cn/one');
if (!resp.ok) {
throw new Error(resp.statusText);
}
const data = sayingSchema.parse(await resp.json());
sayingContent.value =
data.data.content.length <= maxSayingLength
? { saying: data.data.content, sliced: false }
: { saying: data.data.content.slice(0, maxSayingLength) + '...', sliced: true };
sayingAuthor.value = data.data.origin;
} catch (e) {
console.error('无法获取名言:', e);
}
})(),
timeout(1000)
]);
}
function submitForm() {
if (formRef.value === undefined) {
return;
}
formRef.value.validate((isValid) => {
if (!isValid) {
return;
}
});
}
onMounted(async () => {
await setBanner();
await setBackground();
show.value = true;
while (!unMounted) {
await timeout(10000);
await setBackground();
}
});
onUnmounted(() => {
unMounted = true;
backgroundStore.$reset();
});
</script>

View File

@ -1,4 +1,4 @@
<template>test</template>
<template>test{{ Math.random() }}</template>
<script setup lang="ts">
import { Avatar } from '@element-plus/icons-vue';

View File

@ -1,33 +0,0 @@
<template>
<div class="page-root">
<div class="number-404">404</div>
<div class="not-found">Not Found: {{ $route.path }}</div>
<button class="button" @click="$router.back()">返回</button>
</div>
</template>
<style scoped lang="scss">
.page-root {
width: 100vw;
height: 100vh;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.number-404 {
font-weight: bold;
letter-spacing: 10px;
font-size: 100px;
}
.button {
margin-top: 10px;
}
</style>
<script setup lang="tsx">
const a = (
<>
<h1></h1>
</>
);
</script>

View File

@ -0,0 +1 @@
<template>404</template>