🐞 fix: 与后端接口同步

This commit is contained in:
Litrix 2024-12-24 20:42:51 +08:00
parent 4f3dc11101
commit 790384fb8e
17 changed files with 261 additions and 166 deletions

3
.env
View File

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

View File

@ -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
View 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

View File

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

View File

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

View File

@ -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}`;

View File

@ -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: '',

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

View File

@ -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;
}

View File

@ -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({

View File

@ -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 {

View File

@ -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}`);

View File

@ -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>(() =>

View File

@ -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';

View File

@ -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({

View File

@ -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);