🐞 fix: 与后端接口同步
This commit is contained in:
parent
4f3dc11101
commit
790384fb8e
3
.env
3
.env
@ -1,3 +1,4 @@
|
||||
VITE_REQUEST_BASE_URL=https://wzpmc.cn:18080
|
||||
# VITE_WEBSOCKET_BASE_URL=ws://172.16.114.84:58080
|
||||
# VITE_REQUEST_BASE_URL=http://172.16.114.84:58082
|
||||
VITE_WEBSOCKET_BASE_URL=wss://wzpmc.cn:18080
|
||||
# VITE_WEBSOCKET_BASE_URL=ws://172.16.114.84:58082
|
||||
|
@ -2,7 +2,7 @@
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.svg" sizes="512x512" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>社团展示系统</title>
|
||||
</head>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
27
public/favicon.svg
Normal file
27
public/favicon.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_1" data-name="图层_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2048 2048">
|
||||
<!-- Generator: Adobe Illustrator 29.0.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 192) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #409eff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0"
|
||||
d="M1040.5,1950.8c-483.9,0-877.7-393.8-877.7-877.7S556.6,195.4,1040.5,195.4s877.7,393.8,877.7,877.7c.4,483.9-393.4,877.7-877.7,877.7h0ZM1040.5,323.1c-413.4,0-749.6,336.6-749.6,749.6s336.6,749.6,749.6,749.6,749.6-336.6,749.6-749.6c.4-413-336.1-749.6-749.6-749.6h0Z" />
|
||||
<path class="st0"
|
||||
d="M921.4,557.2c0,37.5,30.3,67.9,67.8,68,37.5,0,67.9-30.3,68-67.8h0c0-37.6-30.4-68-67.9-68s-67.9,30.4-67.9,67.9h0Z" />
|
||||
<path class="st0"
|
||||
d="M989.3,689.1c-73,0-132-59.4-132-132s58.9-132.4,132-132.4,132,59.4,132,132-59.4,132.4-132,132.4ZM989.3,552.9c-2.1,0-3.8,1.7-3.8,3.8s1.7,3.8,3.8,3.8,3.8-1.7,3.8-3.8-1.7-3.8-3.8-3.8Z" />
|
||||
<path class="st0"
|
||||
d="M658.3,966.8c0,79,64.1,143.1,143.1,143.1s143.1-64.1,143.1-143.1-64.1-143.1-143.1-143.1-143.1,64.1-143.1,143.1Z" />
|
||||
<path class="st0"
|
||||
d="M801.3,1173.9c-114.5,0-207.1-93.1-207.1-207.1s93.1-207.1,207.1-207.1,207.1,93.1,207.1,207.1-93.1,207.1-207.1,207.1ZM801.3,887.7c-43.6,0-79,35.4-79,79s35.4,79,79,79,79-35.4,79-79-35.4-79-79-79Z" />
|
||||
<path class="st0"
|
||||
d="M1186.6,880.1c0,35.1,28.4,63.7,63.6,63.7,35.1,0,63.7-28.4,63.7-63.6h0c0-35.3-28.4-63.8-63.6-63.8-35.1,0-63.7,28.4-63.7,63.6h0Z" />
|
||||
<path class="st0"
|
||||
d="M1250.2,1008.2c-70.5,0-127.7-57.2-127.7-127.7s57.2-127.7,127.7-127.7,127.7,57.2,127.7,127.7-56.8,127.7-127.7,127.7Z" />
|
||||
<path class="st0"
|
||||
d="M453.3,1806.4c-187.5,0-327.6-60.6-401-175.1-99.9-155.9-55.1-385.2,123.4-628.7l103.4,76c-143.5,195.6-187.9,376.3-119.2,483.9,59.8,93.1,204.6,132.8,398.5,109.3,495.4-88,942.2-375.8,1226.6-790.6,96.5-161.9,119.2-306.7,61.1-397.6-67.5-105.5-245.2-141.8-475.8-97.4l-24.3-126c288.3-56,510.4.4,608.2,154.2,86.7,135.4,65.3,325-60.2,533.9l-2.1,3.4c-152.5,222.5-344.7,409.6-571,555.7-226.4,146.1-476.2,243.9-741.4,290.9l-3.4.4c-42.7,5.1-83.7,7.7-122.6,7.7Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -1 +1,32 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733997112543" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1743" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M76.8 76.8h870.4v870.4H76.8z" fill="#FFDD50" p-id="1744"></path><path d="M559.97952 312.51968a150.86592 150.73792 0 1 0 301.73184 0 150.86592 150.73792 0 1 0-301.73184 0Z" fill="" p-id="1745"></path><path d="M161.3312 710.8352a150.86592 150.73792 0 1 0 301.73184 0 150.86592 150.73792 0 1 0-301.73184 0Z" fill="" p-id="1746"></path><path d="M942.05952 330.4448H81.94048c-9.9072 0-17.94048-8.02304-17.94048-17.92512s8.03328-17.92512 17.94048-17.92512h860.11904c9.9072 0 17.94048 8.02304 17.94048 17.92512s-8.03328 17.92512-17.94048 17.92512z" fill="" p-id="1747"></path><path d="M312.19712 960a17.93536 17.93536 0 0 1-17.94048-17.92512V81.92512a17.93024 17.93024 0 0 1 17.94048-17.92512 17.93024 17.93024 0 0 1 17.94048 17.92512v860.14976a17.93536 17.93536 0 0 1-17.94048 17.92512z" fill="" p-id="1748"></path><path d="M942.05952 728.76032H81.94048c-9.9072 0-17.94048-8.02816-17.94048-17.92512s8.03328-17.92512 17.94048-17.92512h860.11904c9.9072 0 17.94048 8.02816 17.94048 17.92512s-8.03328 17.92512-17.94048 17.92512z" fill="" p-id="1749"></path><path d="M710.84544 960a17.93536 17.93536 0 0 1-17.94048-17.92512V81.92512c0-9.90208 8.03328-17.92512 17.94048-17.92512s17.94048 8.02304 17.94048 17.92512v860.14976a17.94048 17.94048 0 0 1-17.94048 17.92512z" fill="" p-id="1750"></path><path d="M179.80416 312.51968a132.39296 132.28032 0 1 0 264.78592 0 132.39296 132.28032 0 1 0-264.78592 0Z" fill="#FFFFFF" p-id="1751"></path><path d="M312.19712 197.632c63.40096 0 114.98496 51.53792 114.98496 114.88768s-51.584 114.8928-114.98496 114.8928-114.99008-51.54304-114.99008-114.8928S248.79104 197.632 312.19712 197.632m0-35.85024c-83.31776 0-150.86592 67.48672-150.86592 150.73792 0 83.2512 67.54304 150.73792 150.86592 150.73792 83.31776 0 150.86592-67.48672 150.86592-150.73792-0.00512-83.2512-67.54816-150.73792-150.86592-150.73792z" fill="" p-id="1752"></path><path d="M578.67264 710.8352a132.1728 132.06528 0 1 0 264.3456 0 132.1728 132.06528 0 1 0-264.3456 0Z" fill="#FFFFFF" p-id="1753"></path><path d="M710.84544 595.94752c63.40096 0 114.98496 51.53792 114.98496 114.88768s-51.584 114.88768-114.98496 114.88768c-63.40608 0-114.98496-51.53792-114.98496-114.88768s51.57888-114.88768 114.98496-114.88768m0-35.85024c-83.31776 0-150.86592 67.48672-150.86592 150.73792 0 83.2512 67.54304 150.73792 150.86592 150.73792s150.86592-67.48672 150.86592-150.73792c-0.00512-83.2512-67.54816-150.73792-150.86592-150.73792z" fill="" p-id="1754"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733997112543"
|
||||
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1743"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M76.8 76.8h870.4v870.4H76.8z" fill="#FFDD50" p-id="1744"></path>
|
||||
<path d="M559.97952 312.51968a150.86592 150.73792 0 1 0 301.73184 0 150.86592 150.73792 0 1 0-301.73184 0Z" fill=""
|
||||
p-id="1745"></path>
|
||||
<path d="M161.3312 710.8352a150.86592 150.73792 0 1 0 301.73184 0 150.86592 150.73792 0 1 0-301.73184 0Z" fill=""
|
||||
p-id="1746"></path>
|
||||
<path
|
||||
d="M942.05952 330.4448H81.94048c-9.9072 0-17.94048-8.02304-17.94048-17.92512s8.03328-17.92512 17.94048-17.92512h860.11904c9.9072 0 17.94048 8.02304 17.94048 17.92512s-8.03328 17.92512-17.94048 17.92512z"
|
||||
fill="" p-id="1747"></path>
|
||||
<path
|
||||
d="M312.19712 960a17.93536 17.93536 0 0 1-17.94048-17.92512V81.92512a17.93024 17.93024 0 0 1 17.94048-17.92512 17.93024 17.93024 0 0 1 17.94048 17.92512v860.14976a17.93536 17.93536 0 0 1-17.94048 17.92512z"
|
||||
fill="" p-id="1748"></path>
|
||||
<path
|
||||
d="M942.05952 728.76032H81.94048c-9.9072 0-17.94048-8.02816-17.94048-17.92512s8.03328-17.92512 17.94048-17.92512h860.11904c9.9072 0 17.94048 8.02816 17.94048 17.92512s-8.03328 17.92512-17.94048 17.92512z"
|
||||
fill="" p-id="1749"></path>
|
||||
<path
|
||||
d="M710.84544 960a17.93536 17.93536 0 0 1-17.94048-17.92512V81.92512c0-9.90208 8.03328-17.92512 17.94048-17.92512s17.94048 8.02304 17.94048 17.92512v860.14976a17.94048 17.94048 0 0 1-17.94048 17.92512z"
|
||||
fill="" p-id="1750"></path>
|
||||
<path d="M179.80416 312.51968a132.39296 132.28032 0 1 0 264.78592 0 132.39296 132.28032 0 1 0-264.78592 0Z"
|
||||
fill="#FFFFFF" p-id="1751"></path>
|
||||
<path
|
||||
d="M312.19712 197.632c63.40096 0 114.98496 51.53792 114.98496 114.88768s-51.584 114.8928-114.98496 114.8928-114.99008-51.54304-114.99008-114.8928S248.79104 197.632 312.19712 197.632m0-35.85024c-83.31776 0-150.86592 67.48672-150.86592 150.73792 0 83.2512 67.54304 150.73792 150.86592 150.73792 83.31776 0 150.86592-67.48672 150.86592-150.73792-0.00512-83.2512-67.54816-150.73792-150.86592-150.73792z"
|
||||
fill="" p-id="1752"></path>
|
||||
<path d="M578.67264 710.8352a132.1728 132.06528 0 1 0 264.3456 0 132.1728 132.06528 0 1 0-264.3456 0Z" fill="#FFFFFF"
|
||||
p-id="1753"></path>
|
||||
<path
|
||||
d="M710.84544 595.94752c63.40096 0 114.98496 51.53792 114.98496 114.88768s-51.584 114.88768-114.98496 114.88768c-63.40608 0-114.98496-51.53792-114.98496-114.88768s51.57888-114.88768 114.98496-114.88768m0-35.85024c-83.31776 0-150.86592 67.48672-150.86592 150.73792 0 83.2512 67.54304 150.73792 150.86592 150.73792s150.86592-67.48672 150.86592-150.73792c-0.00512-83.2512-67.54816-150.73792-150.86592-150.73792z"
|
||||
fill="" p-id="1754"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
11
src/App.vue
11
src/App.vue
@ -100,7 +100,9 @@
|
||||
display: flex;
|
||||
}
|
||||
.app-router-component {
|
||||
--el-main-padding: 0;
|
||||
&:not(.keep-padding) {
|
||||
--el-main-padding: 0;
|
||||
}
|
||||
}
|
||||
.app-router-view-enter-active,
|
||||
.app-router-view-leave-active {
|
||||
@ -113,7 +115,7 @@
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { axiosInstance, request, type RawResp } from '@/api';
|
||||
import { request } from '@/api';
|
||||
import HeaderMenu from '@/components/app/HeaderMenu.vue';
|
||||
import HeaderUser from '@/components/app/HeaderUser.vue';
|
||||
import type { LoginRegisterDialogProps } from '@/components/app/LoginRegisterDialog.vue';
|
||||
@ -126,7 +128,7 @@ import { useUserStore } from '@/stores/user';
|
||||
import { CloseBold, UserFilled } from '@element-plus/icons-vue';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
const userStore = useUserStore();
|
||||
const pageStore = usePageStore();
|
||||
const { mdLess } = storeToRefs(useMediaStore());
|
||||
@ -168,4 +170,7 @@ function jumpToUserPage() {
|
||||
},
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
userStore.updateSelfUserInfo();
|
||||
});
|
||||
</script>
|
||||
|
@ -1,18 +1,10 @@
|
||||
import { REQUEST_BASE_URL } from '@/env';
|
||||
import {
|
||||
userInfoRespSchema,
|
||||
userInfoRespSchemaNullable,
|
||||
type AnyRespSchema,
|
||||
type SucceedRespOf,
|
||||
type SucceedUserInfoResp,
|
||||
type SucceedUserInfoRespNullable,
|
||||
} from '@/schemas/response';
|
||||
import { userInfoRespSchema, type AnyRespSchema, type SucceedRespOf } from '@/schemas/response';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import type { Nullable, Override } from '@/utils/types';
|
||||
import type { Override } from '@/utils/types';
|
||||
import axios, { type AxiosError, type AxiosRequestConfig, type Method } from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { z } from 'zod';
|
||||
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: REQUEST_BASE_URL,
|
||||
});
|
||||
@ -25,7 +17,7 @@ axiosInstance.interceptors.request.use((config) => {
|
||||
// 自动获取响应中的token.
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
const userStore = useUserStore();
|
||||
const authorization = response.headers['set-authorization'] as string | undefined;
|
||||
const authorization = response.headers['add-authorization'] as string | undefined;
|
||||
if (authorization) {
|
||||
console.log(123);
|
||||
userStore.token = authorization;
|
||||
@ -47,7 +39,7 @@ export function parseRawResp<T extends AnyRespSchema>(
|
||||
if (!raw) return;
|
||||
const resp = schema.parse(raw);
|
||||
if (resp.type === 'error') {
|
||||
ElMessage.error(errorMessage(errorDescription, resp.code, resp.msg));
|
||||
ElMessage.error(errorMessage(errorDescription, resp.status, resp.msg));
|
||||
return;
|
||||
}
|
||||
return resp as any;
|
||||
@ -69,18 +61,13 @@ export async function request<T extends AnyRespSchema>(
|
||||
errorDescription,
|
||||
);
|
||||
}
|
||||
export async function getUserInfo(): Promise<SucceedUserInfoResp>;
|
||||
export async function getUserInfo(userID: number): Promise<SucceedUserInfoRespNullable>;
|
||||
export async function getUserInfo(userID?: number) {
|
||||
let url = '/api/user/info';
|
||||
if (userID !== undefined) {
|
||||
url += `/${userID}`;
|
||||
}
|
||||
const resp = await request(
|
||||
url,
|
||||
userID !== undefined ? userInfoRespSchemaNullable : userInfoRespSchema,
|
||||
);
|
||||
const resp = await request(url, userInfoRespSchema, { errorDescription: '获取用户信息失败' });
|
||||
return resp;
|
||||
}
|
||||
export const getAvatarURL = (avatar: Nullable<string>) =>
|
||||
(avatar && `${import.meta.env.VITE_REQUEST_BASE_URL}/api/user/avatar/${avatar}`) ?? undefined;
|
||||
export const getAvatarURL = (avatar: string | undefined) =>
|
||||
avatar && `${REQUEST_BASE_URL}/api/user/avatar/${avatar}`;
|
||||
|
@ -167,7 +167,7 @@ const show = defineModel<boolean>({ default: false });
|
||||
const loginRegisterDialogActiveName = ref('login');
|
||||
const loginFormRef = ref<FormInstance>();
|
||||
const loginFormData = reactive({
|
||||
username: 'wubaopu2',
|
||||
username: 'wubaopu1',
|
||||
password: '123456',
|
||||
verifyImage: 'none' as VerifyImagePath,
|
||||
verifyCode: '',
|
||||
|
38
src/components/user/UserMenu.vue
Normal file
38
src/components/user/UserMenu.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<el-menu
|
||||
:class="mobile ? 'user-menu--h' : 'user-menu--v'"
|
||||
:mode="mobile ? 'horizontal' : 'vertical'"
|
||||
router
|
||||
:default-active="$route.fullPath"
|
||||
>
|
||||
<h4 v-if="!mobile" class="mb-5px text-center">个人中心</h4>
|
||||
<el-divider v-if="!mobile" />
|
||||
<el-menu-item :index="`/user/${$route.params.id}/post`">发帖记录</el-menu-item>
|
||||
<template v-if="userStore.isSelf(userInfo.id)">
|
||||
<el-divider v-if="!mobile" />
|
||||
<el-menu-item :index="`/user/${$route.params.id}/edit`">编辑信息</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.user-menu--v {
|
||||
--el-menu-item-height: 40px;
|
||||
border: none;
|
||||
.el-menu-item {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
.user-menu--h {
|
||||
--el-menu-horizontal-height: 40px;
|
||||
--el-menu-item-height: 40px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import type { UserInfo } from '@/schemas/response';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
withDefaults(defineProps<{ userInfo: UserInfo; mobile?: boolean }>(), {
|
||||
mobile: false,
|
||||
});
|
||||
const userStore = useUserStore();
|
||||
</script>
|
@ -1,4 +1,6 @@
|
||||
// export const DEV = import.meta.env.DEV;
|
||||
export const DEV = false;
|
||||
export const DEV = import.meta.env.DEV;
|
||||
export const REQUEST_BASE_URL = import.meta.env.VITE_REQUEST_BASE_URL;
|
||||
export const WEBSOCKET_BASE_URL = import.meta.env.VITE_WEBSOCKET_BASE_URL;
|
||||
interface A {
|
||||
a(this: HTMLElement & A): this;
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ function createRespSchema<T extends z.ZodTypeAny>(data: T) {
|
||||
return z.union([
|
||||
z
|
||||
.object({
|
||||
code: z.number().min(200).max(299),
|
||||
status: z.number().min(200).max(299),
|
||||
msg: z.string(),
|
||||
time: z.coerce.date(),
|
||||
timestamp: z.coerce.date(),
|
||||
})
|
||||
.extend({ data: data })
|
||||
.transform((raw) =>
|
||||
@ -16,9 +16,9 @@ function createRespSchema<T extends z.ZodTypeAny>(data: T) {
|
||||
),
|
||||
z
|
||||
.object({
|
||||
code: z.number(),
|
||||
status: z.number(),
|
||||
msg: z.string(),
|
||||
time: z.coerce.date(),
|
||||
timestamp: z.coerce.date(),
|
||||
})
|
||||
.transform((raw) =>
|
||||
Object.assign(raw, {
|
||||
@ -32,8 +32,6 @@ export type SucceedRespOf<T> = Extract<T, { type: 'success' }>;
|
||||
export type ErrorRespOf<T> = Exclude<SucceedRespOf<T>, T>;
|
||||
export type AnyRespSchema = ReturnType<typeof createRespSchema>;
|
||||
export const ordinarySchema = createRespSchema(z.literal(true));
|
||||
export const loginRespSchema = ordinarySchema;
|
||||
export const registerRespSchema = ordinarySchema;
|
||||
export const idAndNameSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
@ -43,20 +41,20 @@ const authSchema = idAndNameSchema.extend({
|
||||
permissions: idAndNameSchema.array(),
|
||||
});
|
||||
const userInfoDataSchema = idAndNameSchema.extend({
|
||||
avatar: z.nullable(z.string()),
|
||||
avatar: z.optional(z.string()),
|
||||
auth: authSchema,
|
||||
club: z.nullable(
|
||||
club: z.optional(
|
||||
idAndNameSchema.extend({
|
||||
commit: z.string(),
|
||||
auth: authSchema,
|
||||
}),
|
||||
),
|
||||
chubAuth: z.optional(authSchema),
|
||||
});
|
||||
export const userInfoRespSchema = createRespSchema(userInfoDataSchema);
|
||||
export const userInfoRespSchemaNullable = createRespSchema(userInfoDataSchema.nullable());
|
||||
export type SucceedUserInfoResp = SucceedRespOf<z.infer<typeof userInfoRespSchema>>;
|
||||
export type SucceedUserInfoRespNullable = SucceedRespOf<z.infer<typeof userInfoRespSchemaNullable>>;
|
||||
export type UserInfo = SucceedUserInfoResp['data'];
|
||||
export const loginRespSchema = userInfoRespSchema;
|
||||
export const registerRespSchema = userInfoRespSchema;
|
||||
export const verifyRespSchema = createRespSchema(
|
||||
z
|
||||
.object({
|
||||
|
@ -4,6 +4,7 @@ import type { UserInfo } from '@/schemas/response';
|
||||
import { type idAndNameSchema } from '@/schemas/response';
|
||||
import type { MaybePromise } from '@/utils/types';
|
||||
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { z } from 'zod';
|
||||
@ -59,6 +60,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
if (!res.every((r) => r === undefined || r)) return false;
|
||||
token.value = null;
|
||||
await updateSelfUserInfo();
|
||||
ElMessage.info('退出成功');
|
||||
return true;
|
||||
}
|
||||
return {
|
||||
|
@ -37,6 +37,7 @@ export interface UseGameSocketOptions<
|
||||
// 只有拥有依赖关系的请求才可以有finally
|
||||
[P in keyof TRelations]?: (error: boolean) => void;
|
||||
};
|
||||
disconnected?(): void;
|
||||
}
|
||||
export function useGameSocket<TRequest extends SimplePart<string>, TResp extends BaseResp>() {
|
||||
return <TRelations extends Relations<TRequest['name'], TResp['name']>>(
|
||||
@ -102,7 +103,6 @@ export function useGameSocket<TRequest extends SimplePart<string>, TResp extends
|
||||
}
|
||||
},
|
||||
onDisconnected(_, ev) {
|
||||
console.log('disconnected');
|
||||
// 正常断开
|
||||
if (ev.code === 1000) return;
|
||||
ElMessage.error(`连接已断开:${ev.reason}`);
|
||||
|
@ -247,13 +247,9 @@ const userStore = useUserStore();
|
||||
const { smLess, mdLess } = storeToRefs(useMediaStore());
|
||||
let canExit = false;
|
||||
let confirming = false;
|
||||
useLogoutInterceptor(() => {
|
||||
if (confirming) {
|
||||
return;
|
||||
}
|
||||
if (canExit || matchState.value !== MatchState.GAMING) return true;
|
||||
function confirm(description: string) {
|
||||
confirming = true;
|
||||
ElMessageBox.confirm('你正在游戏中, 是否退出登录?', '警告', {
|
||||
return ElMessageBox.confirm(description, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@ -261,15 +257,21 @@ useLogoutInterceptor(() => {
|
||||
.then((action: Action) => {
|
||||
if (action !== 'confirm') return;
|
||||
canExit = true;
|
||||
confirming = false;
|
||||
return userStore.logout();
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
confirming = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
useLogoutInterceptor(async () => {
|
||||
if (confirming) {
|
||||
return;
|
||||
}
|
||||
if (canExit || matchState.value !== MatchState.GAMING) return true;
|
||||
await confirm('你正在游戏中, 是否退出登录?');
|
||||
return canExit;
|
||||
});
|
||||
onBeforeRouteLeave((to, _, next) => {
|
||||
onBeforeRouteLeave(async (_, __, next) => {
|
||||
if (confirming) {
|
||||
next(false);
|
||||
return;
|
||||
@ -279,21 +281,8 @@ onBeforeRouteLeave((to, _, next) => {
|
||||
return;
|
||||
}
|
||||
confirming = true;
|
||||
ElMessageBox.confirm('你正在游戏中, 是否退出房间?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then((action: Action) => {
|
||||
if (action !== 'confirm') return;
|
||||
canExit = true;
|
||||
confirming = false;
|
||||
router.push(to);
|
||||
})
|
||||
.catch(() => {
|
||||
confirming = false;
|
||||
});
|
||||
next(false);
|
||||
await confirm('你正在游戏中, 是否退出房间?');
|
||||
next(canExit);
|
||||
});
|
||||
const roomId = useRoute().params.id as RoomId | undefined;
|
||||
const canvas = ref<HTMLCanvasElement>();
|
||||
@ -421,9 +410,7 @@ const otherUser = computed(() => {
|
||||
});
|
||||
watch(
|
||||
() => otherUser.value?.id,
|
||||
() => {
|
||||
updateMatchState();
|
||||
},
|
||||
() => updateMatchState(),
|
||||
);
|
||||
const white = ref(false);
|
||||
const grid = computed<Grid | undefined>(() =>
|
||||
|
@ -1,12 +1,19 @@
|
||||
<template>
|
||||
<div class="page-root">
|
||||
<div class="page" v-if="userStore.userInfo">
|
||||
<div>用户名:{{ userStore.userInfo.name }}</div>
|
||||
<div>id:{{ userStore.userInfo.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-main class="keep-padding flex flex-col items-center">
|
||||
<el-carousel class="w-1000px" height="300px">
|
||||
<el-carousel-item class="slider-item">
|
||||
<img class="slider-item__img" src="/bg2.jpg" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</el-main>
|
||||
</template>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.slider-item {
|
||||
}
|
||||
.slider-item__img {
|
||||
@apply w-full h-full select-none rounded-10px;
|
||||
}
|
||||
</style>
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
|
||||
|
@ -2,19 +2,26 @@
|
||||
<div>
|
||||
<h4 class="p">编辑资料</h4>
|
||||
<el-divider />
|
||||
<div class="p outer">
|
||||
<div class="p outer flex">
|
||||
<el-avatar class="self-center" :size="100" :src="getAvatarURL(userStore.userInfo!.avatar)" />
|
||||
<el-upload class="self-center" :show-file-list="false" :http-request="upload">
|
||||
<template #trigger>
|
||||
<el-link type="primary" :icon="Upload" :underline="false" :limit="1">上传头像</el-link>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div class="self-center flex gap-2">
|
||||
<el-upload :show-file-list="false" :http-request="upload">
|
||||
<template #trigger>
|
||||
<el-link type="primary" :icon="Upload" :disabled="uploading" :underline="false">
|
||||
上传头像
|
||||
</el-link>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-link type="danger" :icon="Close" :disabled="uploading" :underline="false">
|
||||
清除头像
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="p outer">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
class="flex flex-col items-stretch"
|
||||
class="flex flex-col items-stretch md-p-l-100px md-p-r-100px"
|
||||
hide-required-asterisk
|
||||
@submit.prevent
|
||||
:model="data"
|
||||
@ -39,16 +46,13 @@
|
||||
.outer {
|
||||
@apply flex flex-col gap-10px;
|
||||
}
|
||||
.el-form {
|
||||
padding: 0 100px;
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
import { getAvatarURL, request } from '@/api';
|
||||
import { usernameLength } from '@/components/app/LoginRegisterDialog.vue';
|
||||
import { ordinarySchema, uploadAvatarRespSchema } from '@/schemas/response';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { Upload } from '@element-plus/icons-vue';
|
||||
import { Close, Upload } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElMessage,
|
||||
type FormInstance,
|
||||
@ -70,6 +74,7 @@ export default defineComponent({
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
const userStore = useUserStore();
|
||||
const uploading = ref(false);
|
||||
const upload: UploadRequestHandler = async ({ file }) => {
|
||||
if (!file.type.startsWith('image')) {
|
||||
ElMessage.error('只能上传图片');
|
||||
@ -79,22 +84,27 @@ const upload: UploadRequestHandler = async ({ file }) => {
|
||||
ElMessage.error('文件大小不能超过1MB');
|
||||
return;
|
||||
}
|
||||
const formdata = new FormData();
|
||||
formdata.append('file', file);
|
||||
const uploadResp = await request('/api/user/avatar', uploadAvatarRespSchema, {
|
||||
method: 'post',
|
||||
data: formdata,
|
||||
errorDescription: '上传头像失败',
|
||||
});
|
||||
if (!uploadResp) return;
|
||||
const changeResp = await request('/api/user/avatar', ordinarySchema, {
|
||||
method: 'put',
|
||||
params: { code: uploadResp.data },
|
||||
errorDescription: '上传头像失败',
|
||||
});
|
||||
if (!changeResp) return;
|
||||
await userStore.updateSelfUserInfo();
|
||||
ElMessage.success('头像上传成功');
|
||||
uploading.value = true;
|
||||
try {
|
||||
const formdata = new FormData();
|
||||
formdata.append('file', file);
|
||||
const uploadResp = await request('/api/user/avatar', uploadAvatarRespSchema, {
|
||||
method: 'post',
|
||||
data: formdata,
|
||||
errorDescription: '上传头像失败',
|
||||
});
|
||||
if (!uploadResp) return;
|
||||
const changeResp = await request('/api/user/avatar', ordinarySchema, {
|
||||
method: 'put',
|
||||
params: { code: uploadResp.data },
|
||||
errorDescription: '上传头像失败',
|
||||
});
|
||||
if (!changeResp) return;
|
||||
await userStore.updateSelfUserInfo();
|
||||
ElMessage.success('头像上传成功');
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
const formRef = ref<FormInstance>();
|
||||
const data = reactive({
|
||||
|
@ -2,65 +2,68 @@
|
||||
<el-main
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载"
|
||||
class="page-main !flex justify-center"
|
||||
class="page-main !flex"
|
||||
:class="[!smLess ? 'justify-center' : 'page-main--mobile flex-col']"
|
||||
>
|
||||
<div
|
||||
v-if="!loading"
|
||||
class="max-lg-w-700px lg-w-950px flex flex-col"
|
||||
:class="[{ 'gap-10px': userInfo }, !userInfo ? 'justify-center items-center' : '']"
|
||||
>
|
||||
<template v-if="userInfo">
|
||||
<el-card body-class="h-150px flex gap-20px relative">
|
||||
<el-avatar :size="80" :src="getAvatarURL(userInfo.avatar)"></el-avatar>
|
||||
<div class="flex flex-col gap-5px">
|
||||
<h4>{{ userInfo.name }}</h4>
|
||||
<div class="text-3 color-gray">ID: {{ userInfo.id }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon color="var(--el-color-primary)"><icon-cs-user-auth /></el-icon>
|
||||
<div>权限组:{{ userInfo.auth.name }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 flex justify-center items-center">
|
||||
<el-icon size="2rem" :color="userInfo.club ? 'var(--el-color-primary)' : 'black'">
|
||||
<icon-cs-club />
|
||||
</el-icon>
|
||||
<template v-if="!smLess">
|
||||
<div
|
||||
v-if="!loading"
|
||||
class="max-md-w-500px md-w-700px lg-w-950px flex flex-col"
|
||||
:class="[{ 'gap-10px': userInfo }, !userInfo ? 'justify-center items-center' : '']"
|
||||
>
|
||||
<template v-if="userInfo">
|
||||
<el-card body-class="h-150px flex gap-20px relative">
|
||||
<el-avatar :size="80" :src="getAvatarURL(userInfo.avatar)"></el-avatar>
|
||||
<div class="flex flex-col gap-5px">
|
||||
<h4>{{ userInfo.name }}</h4>
|
||||
<div class="text-3 color-gray">ID: {{ userInfo.id }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon color="var(--el-color-primary)"><icon-cs-user-auth /></el-icon>
|
||||
<div>权限组:{{ userInfo.auth.name }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="userInfo.club?.name">所属社团:{{ userInfo.club.name }}</template>
|
||||
<template v-else>暂未加入社团</template>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 flex justify-center items-center">
|
||||
<el-icon size="2rem" :color="userInfo.club ? 'var(--el-color-primary)' : 'black'">
|
||||
<icon-cs-club />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="userInfo.club?.name">所属社团:{{ userInfo.club.name }}</template>
|
||||
<template v-else>暂未加入社团</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="userStore.isSelf(userInfo.id)"
|
||||
class="absolute bottom-20px right-20px"
|
||||
@click="$router.push({ name: 'UserEdit' })"
|
||||
>
|
||||
编辑资料
|
||||
</el-button>
|
||||
</el-card>
|
||||
<div class="flex gap-10px">
|
||||
<el-card class="aside w-200px self-start">
|
||||
<user-menu :user-info="userInfo" />
|
||||
</el-card>
|
||||
<el-card class="main flex-1">
|
||||
<router-view />
|
||||
</el-card>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="userStore.isSelf(userInfo.id)"
|
||||
class="absolute bottom-20px right-20px"
|
||||
@click="$router.push({ name: 'UserEdit' })"
|
||||
>
|
||||
编辑资料
|
||||
</el-button>
|
||||
</el-card>
|
||||
<div class="flex gap-10px">
|
||||
<el-card class="aside w-200px self-start">
|
||||
<el-menu class="menu" router :default-active="$route.fullPath">
|
||||
<h4 class="mb-5px text-center">个人中心</h4>
|
||||
<el-divider />
|
||||
<el-menu-item :index="`/user/${$route.params.id}/post`">发帖记录</el-menu-item>
|
||||
<template v-if="userStore.isSelf(userInfo.id)">
|
||||
<el-divider />
|
||||
<el-menu-item :index="`/user/${$route.params.id}/edit`">编辑信息</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-card>
|
||||
<el-card class="main flex-1">
|
||||
<router-view />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="font-bold text-30px">无法获取用户信息</div>
|
||||
<div class="flex">
|
||||
<el-button type="primary" @click="$router.back()">返回上页</el-button>
|
||||
<el-button type="primary" @click="loadUserInfo">刷新</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="font-bold text-30px">无法获取用户信息</div>
|
||||
<div class="flex">
|
||||
<el-button type="primary" @click="$router.back()">返回上页</el-button>
|
||||
<el-button type="primary" @click="loadUserInfo">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="m-t-150px flex-1 flex flex-col bg-white rounded-t-20px overflow-hidden" v-else>
|
||||
<template v-if="userInfo">
|
||||
<user-menu :user-info="userInfo" mobile />
|
||||
<div class="flex-1 bg-white">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -69,6 +72,9 @@
|
||||
<style lang="scss" scoped>
|
||||
.page-main {
|
||||
--el-main-padding: 30px 0;
|
||||
&--mobile {
|
||||
background-image: url('/bg2.jpg');
|
||||
}
|
||||
}
|
||||
.el-card {
|
||||
transition: none;
|
||||
@ -82,13 +88,6 @@
|
||||
:deep(.el-divider) {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.menu {
|
||||
--el-menu-item-height: 40px;
|
||||
border: none;
|
||||
.el-menu-item {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
--el-card-padding: 10px 0;
|
||||
--main-h-padding: 10px;
|
||||
@ -96,6 +95,7 @@
|
||||
</style>
|
||||
<script lang="ts" setup>
|
||||
import { getAvatarURL, getUserInfo } from '@/api';
|
||||
import UserMenu from '@/components/user/UserMenu.vue';
|
||||
import { type UserInfo } from '@/schemas/response';
|
||||
import { useMediaStore } from '@/stores/media';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
@ -103,7 +103,7 @@ import { ElMessage } from 'element-plus';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const { lgLess, lgOnly } = storeToRefs(useMediaStore());
|
||||
const { smLess } = storeToRefs(useMediaStore());
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const loading = ref(true);
|
||||
|
Loading…
x
Reference in New Issue
Block a user