🎈 perf: 更新主页
This commit is contained in:
parent
1000db2c11
commit
cd631ef10c
134
src/App.vue
134
src/App.vue
@ -1,65 +1,69 @@
|
||||
<template>
|
||||
<el-container class="app__container">
|
||||
<el-header class="app-header flex items-center">
|
||||
<el-icon v-show="mdLess" size="30" @click="showVerticalHeaderMenu = true">
|
||||
<icon-cs-menu />
|
||||
</el-icon>
|
||||
<router-link custom to="/" v-slot="{ navigate }">
|
||||
<el-icon size="50" @click="navigate" color="var(--el-color-primary)">
|
||||
<icon-cs-club />
|
||||
<el-config-provider :locale="zhCn">
|
||||
<el-container class="app__container">
|
||||
<el-header class="app-header flex items-center">
|
||||
<el-icon v-show="mdLess" size="30" @click="showVerticalHeaderMenu = true">
|
||||
<icon-cs-menu />
|
||||
</el-icon>
|
||||
<h3 v-show="showAppTitle" class="app__title" @click="navigate">社团展示系统</h3>
|
||||
</router-link>
|
||||
<header-menu v-show="!mdLess" mode="horizontal" />
|
||||
<el-dropdown v-if="!mdLess && userStore.logined">
|
||||
<header-user @login="onLoginButtonClick" @logout="logout" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :icon="UserFilled" @click="jumpToUserPage">个人主页</el-dropdown-item>
|
||||
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<header-user
|
||||
v-else
|
||||
@login="onLoginButtonClick"
|
||||
@logout="logout"
|
||||
@click="showVerticalHeaderMenu = true"
|
||||
/>
|
||||
</el-header>
|
||||
<el-main class="app-main" v-loading="!!pageStore.pageLoadingCount">
|
||||
<el-container>
|
||||
<router-view v-slot="{ Component: comp }">
|
||||
<transition appear mode="out-in" name="app-router-view">
|
||||
<component class="app-router-component" :is="comp" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-container>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<login-register-dialog
|
||||
v-model="showLoginRegisterDialog"
|
||||
:handle-login="login"
|
||||
:handle-register="register"
|
||||
/>
|
||||
<el-drawer
|
||||
class="app-vertical-drawer"
|
||||
v-model="showVerticalHeaderMenu"
|
||||
direction="ltr"
|
||||
size="250"
|
||||
:with-header="false"
|
||||
>
|
||||
<h3 v-show="!showAppTitle" class="app__title text-center">社团展示系统</h3>
|
||||
<template v-if="userStore.logined">
|
||||
<header-user @login="showLoginRegisterDialog = true" />
|
||||
<el-button :icon="UserFilled" @click="jumpToUserPage">个人主页</el-button>
|
||||
<el-button type="danger" :icon="CloseBold" @click="logout">退出登录</el-button>
|
||||
</template>
|
||||
<div v-else class="app-vertical-drawer__not-login-title flex justify-center items-center">
|
||||
尚未登录
|
||||
</div>
|
||||
<header-menu mode="vertical" @select="showVerticalHeaderMenu = false" />
|
||||
</el-drawer>
|
||||
<router-link custom to="/" v-slot="{ navigate }">
|
||||
<el-icon size="50" @click="navigate" color="var(--el-color-primary)">
|
||||
<icon-cs-club />
|
||||
</el-icon>
|
||||
<h3 v-show="showAppTitle" class="app__title" @click="navigate">社团展示系统</h3>
|
||||
</router-link>
|
||||
<header-menu v-show="!mdLess" mode="horizontal" />
|
||||
<el-dropdown v-if="!mdLess && userStore.logined">
|
||||
<header-user @login="onLoginButtonClick" @logout="logout" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :icon="UserFilled" @click="jumpToUserPage">
|
||||
个人主页
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<header-user
|
||||
v-else
|
||||
@login="onLoginButtonClick"
|
||||
@logout="logout"
|
||||
@click="showVerticalHeaderMenu = true"
|
||||
/>
|
||||
</el-header>
|
||||
<el-main class="app-main" v-loading="!!pageStore.pageLoadingCount">
|
||||
<el-container>
|
||||
<router-view v-slot="{ Component: comp }">
|
||||
<transition appear mode="out-in" name="app-router-view">
|
||||
<component class="app-router-component" :is="comp" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-container>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<login-register-dialog
|
||||
v-model="showLoginRegisterDialog"
|
||||
:handle-login="login"
|
||||
:handle-register="register"
|
||||
/>
|
||||
<el-drawer
|
||||
class="app-vertical-drawer"
|
||||
v-model="showVerticalHeaderMenu"
|
||||
direction="ltr"
|
||||
size="250"
|
||||
:with-header="false"
|
||||
>
|
||||
<h3 v-show="!showAppTitle" class="app__title text-center">社团展示系统</h3>
|
||||
<template v-if="userStore.logined">
|
||||
<header-user @login="showLoginRegisterDialog = true" />
|
||||
<el-button :icon="UserFilled" @click="jumpToUserPage">个人主页</el-button>
|
||||
<el-button type="danger" :icon="CloseBold" @click="logout">退出登录</el-button>
|
||||
</template>
|
||||
<div v-else class="app-vertical-drawer__not-login-title flex justify-center items-center">
|
||||
尚未登录
|
||||
</div>
|
||||
<header-menu mode="vertical" @select="showVerticalHeaderMenu = false" />
|
||||
</el-drawer>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.app-vertical-drawer {
|
||||
@ -127,6 +131,7 @@ import { usePageStore } from '@/stores/page.js';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { CloseBold, UserFilled } from '@element-plus/icons-vue';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { zhCn } from 'element-plus/es/locales';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
const userStore = useUserStore();
|
||||
@ -135,14 +140,19 @@ const { mdLess } = storeToRefs(useMediaStore());
|
||||
const showAppTitle = useMediaQuery('(width >= 390px)');
|
||||
const showLoginRegisterDialog = ref(false);
|
||||
const login: LoginRegisterDialogProps['handleLogin'] = async (params) => {
|
||||
const resp = await request('/api/user/login', loginRespSchema, { method: 'post', data: params });
|
||||
const resp = await request(loginRespSchema, {
|
||||
url: '/api/user/login',
|
||||
method: 'post',
|
||||
data: params,
|
||||
});
|
||||
if (!resp) {
|
||||
return false;
|
||||
}
|
||||
return userStore.updateSelfUserInfo();
|
||||
};
|
||||
const register: LoginRegisterDialogProps['handleRegister'] = async (params) => {
|
||||
const resp = await request('/api/user/create', registerRespSchema, {
|
||||
const resp = await request(registerRespSchema, {
|
||||
url: '/api/user/create',
|
||||
method: 'put',
|
||||
data: params,
|
||||
});
|
||||
|
@ -1,17 +1,24 @@
|
||||
import { REQUEST_BASE_URL } from '@/env';
|
||||
import { userInfoRespSchema, type AnyRespSchema, type SucceedRespOf } from '@/schemas/response';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import type { Override } from '@/utils/types';
|
||||
import axios, { type AxiosError, type AxiosRequestConfig, type Method } from 'axios';
|
||||
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { z } from 'zod';
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: REQUEST_BASE_URL,
|
||||
});
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
addAuth?: boolean;
|
||||
errorDescription?: string;
|
||||
}
|
||||
}
|
||||
// 自动添加token到请求中.
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const userStore = useUserStore();
|
||||
config.headers.setAuthorization(userStore.token, false);
|
||||
if (config.addAuth !== undefined && config.addAuth) {
|
||||
config.headers.setAuthorization(userStore.token, false);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
// 自动获取响应中的token.
|
||||
@ -25,16 +32,23 @@ axiosInstance.interceptors.response.use((response) => {
|
||||
return response;
|
||||
});
|
||||
export type RawResp = Record<string, unknown>;
|
||||
const errorMessage = (description: string, code: string | number | undefined, msg: string) =>
|
||||
`${description}:(${code})${msg}`;
|
||||
const errorMessage = (
|
||||
description: string = '请求错误',
|
||||
code: string | number | undefined,
|
||||
msg: string,
|
||||
) => `${description}:(${code})${msg}`;
|
||||
|
||||
interface RequestOptions extends Override<AxiosRequestConfig, { method?: Method }, 'url'> {
|
||||
errorDescription?: string;
|
||||
}
|
||||
export const requestRaw = <T = RawResp>(options: AxiosRequestConfig = {}) =>
|
||||
axiosInstance
|
||||
.request<T>(options)
|
||||
.then((r) => r.data)
|
||||
.catch((err: AxiosError): undefined => {
|
||||
ElMessage.error(errorMessage(options.errorDescription, err.code, err.message));
|
||||
});
|
||||
export function parseRawResp<T extends AnyRespSchema>(
|
||||
schema: T,
|
||||
raw: RawResp | undefined,
|
||||
errorDescription: string,
|
||||
errorDescription: string | undefined,
|
||||
): SucceedRespOf<z.infer<T>> | undefined {
|
||||
if (!raw) return;
|
||||
const resp = schema.parse(raw);
|
||||
@ -45,28 +59,17 @@ export function parseRawResp<T extends AnyRespSchema>(
|
||||
return resp as any;
|
||||
}
|
||||
export async function request<T extends AnyRespSchema>(
|
||||
url: string,
|
||||
schema: T,
|
||||
options: RequestOptions = {},
|
||||
options: AxiosRequestConfig = {},
|
||||
) {
|
||||
const { errorDescription = '请求错误' } = options;
|
||||
return parseRawResp(
|
||||
schema,
|
||||
await axiosInstance
|
||||
.request<RawResp>(Object.assign(options, { url }))
|
||||
.then((r) => r.data)
|
||||
.catch((err: AxiosError): undefined => {
|
||||
ElMessage.error(errorMessage(errorDescription, err.code, err.message));
|
||||
}),
|
||||
errorDescription,
|
||||
);
|
||||
return parseRawResp(schema, await requestRaw(options), options.errorDescription);
|
||||
}
|
||||
export async function getUserInfo(userID?: number) {
|
||||
let url = '/api/user/info';
|
||||
if (userID !== undefined) {
|
||||
url += `/${userID}`;
|
||||
}
|
||||
const resp = await request(url, userInfoRespSchema, { errorDescription: '获取用户信息失败' });
|
||||
const resp = await request(userInfoRespSchema, { url, errorDescription: '获取用户信息失败' });
|
||||
return resp;
|
||||
}
|
||||
export const getAvatarURL = (avatar: string | undefined) =>
|
||||
|
60
src/components/PagedWrapper.vue
Normal file
60
src/components/PagedWrapper.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'flex justify-center items-center': resp && !resp.data.total,
|
||||
}"
|
||||
>
|
||||
<template v-if="resp">
|
||||
<template v-if="resp.data.total">
|
||||
<div><slot :data="resp.data.data"></slot></div>
|
||||
<el-pagination
|
||||
class="m-t-10px"
|
||||
:class="paginationClass"
|
||||
:background="paginationBackground"
|
||||
:layout="paginationLayout"
|
||||
:total="total"
|
||||
:page-size="num"
|
||||
:current="page"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot name="error">
|
||||
<div class="font-5 font-bold">暂无数据</div>
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script generic="T extends AnyPagedRespSchema" setup lang="ts">
|
||||
import { request } from '@/api';
|
||||
import type { AnyPagedRespSchema, SucceedRespOf } from '@/schemas/response';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { ref } from 'vue';
|
||||
import type { z } from 'zod';
|
||||
type Succeed = SucceedRespOf<z.infer<T>>;
|
||||
type DataList = Succeed['data']['data'];
|
||||
const { schema, getRequestOptions, num } = defineProps<{
|
||||
schema: T;
|
||||
num: number;
|
||||
paginationClass?: string;
|
||||
paginationBackground?: boolean;
|
||||
paginationLayout?: string;
|
||||
getRequestOptions: (page: number, num: number) => AxiosRequestConfig;
|
||||
}>();
|
||||
defineSlots<{
|
||||
default: (props: { data: DataList }) => unknown;
|
||||
error: () => unknown;
|
||||
}>();
|
||||
const loading = ref(true);
|
||||
const resp = ref<Succeed>();
|
||||
const page = ref(1);
|
||||
const total = computed(() => resp.value?.data.total);
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
resp.value = await request(schema, getRequestOptions(page.value, num));
|
||||
loading.value = false;
|
||||
}
|
||||
defineExpose({ refresh });
|
||||
refresh();
|
||||
</script>
|
@ -90,7 +90,8 @@ async function updateVerifyImage() {
|
||||
const original = model.value.verifyImage;
|
||||
model.value.verifyImage = 'fetching';
|
||||
try {
|
||||
const verifyResponse = await request('/api/user/verify', verifyRespSchema, {
|
||||
const verifyResponse = await request(verifyRespSchema, {
|
||||
url: '/api/user/verify',
|
||||
errorDescription: '获取验证码失败',
|
||||
});
|
||||
if (!verifyResponse) return;
|
||||
|
@ -47,7 +47,7 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'Club',
|
||||
component: () => import('@/views/ClubPage.vue'),
|
||||
meta: {
|
||||
userPermissionId: UserPermissionId.CLUB_PAGE,
|
||||
clubPermissionId: ClubPermissionId.CLUB_PAGE,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -7,9 +7,18 @@ export enum UserPermissionId {
|
||||
* @deprecated
|
||||
*/
|
||||
USER_PAGE = 2,
|
||||
CLUB_PAGE = 7,
|
||||
/** 访问后台管理页面 */
|
||||
MANAGE_PAGE = 3,
|
||||
/** 管理用户权限 */
|
||||
MANAGE_USER = 4,
|
||||
}
|
||||
export enum ClubPermissionId {
|
||||
CLUB_PAGE = 1,
|
||||
CLUB_EDIT = 2,
|
||||
/** 管理社团信息权限 */
|
||||
MANAGE_CLUB = 1,
|
||||
/** 更改社团成员权限 */
|
||||
CHANGE_CLUB = 2,
|
||||
/** 查看社团主页权限 */
|
||||
CLUB_PAGE = 3,
|
||||
/** 社团发帖权限 */
|
||||
POST_CLUB = 4,
|
||||
}
|
||||
|
36
src/schemas/neusoft.ts
Normal file
36
src/schemas/neusoft.ts
Normal file
@ -0,0 +1,36 @@
|
||||
type WithExtra<T extends Record<string, unknown>> = T & Neusoft.ExtraFields;
|
||||
export namespace Neusoft {
|
||||
export type BaseResp<T extends Record<string, unknown>> = {
|
||||
code: number;
|
||||
msg: string;
|
||||
} & T;
|
||||
export type ExtraFields = {
|
||||
searchValue: string | null;
|
||||
createBy: string | null;
|
||||
createTime: string | null;
|
||||
updateBy: string | null;
|
||||
updateTime: string;
|
||||
remark: string | null;
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
export type PagedResp<T extends Record<string, unknown>> = BaseResp<{
|
||||
rows: T[];
|
||||
total: number;
|
||||
}>;
|
||||
export type DataResp<T extends Record<string, unknown>> = BaseResp<{
|
||||
data: T;
|
||||
}>;
|
||||
}
|
||||
export namespace Neusoft {
|
||||
export type Gallery = WithExtra<{
|
||||
id: number;
|
||||
hotNewsId: number;
|
||||
imageUrl: string;
|
||||
imageName: string;
|
||||
}>;
|
||||
export type GalleryResp = PagedResp<Gallery>;
|
||||
}
|
||||
export namespace Neusoft {
|
||||
export const SMART_PARTY_BASE_URL = 'http://124.93.196.45:10091/Neusoft/party';
|
||||
export const isSucceed = (resp: BaseResp<{}>) => resp.code >= 200 && resp.code < 300;
|
||||
}
|
@ -67,13 +67,16 @@ export const verifyRespSchema = createRespSchema(
|
||||
);
|
||||
export const uploadAvatarRespSchema = createRespSchema(z.string());
|
||||
export const clubSchema = idAndNameSchema.extend({
|
||||
avatar: z.optional(z.string()),
|
||||
commit: z.string(),
|
||||
});
|
||||
2;
|
||||
export type Club = z.infer<typeof clubSchema>;
|
||||
export const createPagedRespSchema = <T extends z.ZodTypeAny>(data: T) =>
|
||||
createRespSchema(
|
||||
z.object({
|
||||
data,
|
||||
data: data.array(),
|
||||
total: z.number(),
|
||||
}),
|
||||
);
|
||||
export type AnyPagedRespSchema = ReturnType<typeof createPagedRespSchema>;
|
||||
export const clubListRespSchema = createPagedRespSchema(clubSchema);
|
||||
|
@ -3,11 +3,11 @@ import { defineStore } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export const useMediaStore = defineStore('media', () => {
|
||||
const smLess = useMediaQuery('(width < 576px)');
|
||||
const smLess = useMediaQuery('(width < 640px)');
|
||||
const smOnly = computed(() => !smLess.value && mdLess.value);
|
||||
const mdLess = useMediaQuery('(width < 768px)');
|
||||
const mdOnly = computed(() => !mdLess.value && lgLess.value);
|
||||
const lgLess = useMediaQuery('(width < 992px)');
|
||||
const lgLess = useMediaQuery('(width < 1024px)');
|
||||
const lgOnly = computed(() => !lgLess.value && xlLess.value);
|
||||
const xlLess = useMediaQuery('(width < 1280px)');
|
||||
const xl = computed(() => !xlLess.value);
|
||||
|
@ -6,6 +6,7 @@ import { ensureArray } from '@/utils/array';
|
||||
import type { MaybePromise } from '@/utils/types';
|
||||
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
@ -40,19 +41,17 @@ export const useUserStore = defineStore('user', () => {
|
||||
!clubPermissionId || ensureArray(clubPermissionId).every((id) => clubPermissions.value.has(id));
|
||||
const initializing = ref(false);
|
||||
const logined = computed(() => (userInfo.value && userInfo.value.id !== -1) ?? false);
|
||||
// watch(
|
||||
// userInfo,
|
||||
// (info, old) => {
|
||||
// router.push({
|
||||
// path:
|
||||
// info && (info.id !== -1 || !old || old.id === -1)
|
||||
// ? router.currentRoute.value.fullPath
|
||||
// : '/',
|
||||
// force: true,
|
||||
// });
|
||||
// },
|
||||
// { flush: 'sync' },
|
||||
// );
|
||||
watch(
|
||||
userInfo,
|
||||
(info, old) => {
|
||||
if (isEqual(info, old)) return;
|
||||
router.push({
|
||||
path: info && info.id !== -1 ? router.currentRoute.value.fullPath : '/',
|
||||
force: true,
|
||||
});
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
const isSelf = (id: string | number) => userInfo.value?.id === Number(id);
|
||||
async function updateSelfUserInfo(): Promise<boolean> {
|
||||
initializing.value = true;
|
||||
|
@ -103,8 +103,13 @@ export function useGameSocket<TRequest extends SimplePart<string>, TResp extends
|
||||
}
|
||||
},
|
||||
onDisconnected(_, ev) {
|
||||
// 正常断开
|
||||
if (ev.code === 1000) return;
|
||||
if (
|
||||
// 正常断开
|
||||
ev.code === 1000 ||
|
||||
// 连接未完成就断开
|
||||
ev.code === 1006
|
||||
)
|
||||
return;
|
||||
ElMessage.error(`连接已断开:${ev.reason}`);
|
||||
router.push('/');
|
||||
},
|
||||
|
@ -83,14 +83,14 @@ export enum RoomState {
|
||||
}
|
||||
/** 房间UUID */
|
||||
export type RoomId = string;
|
||||
export interface Room {
|
||||
export type Room = {
|
||||
id: RoomId;
|
||||
full: boolean;
|
||||
state: RoomState;
|
||||
isWhiteAcceptRestart: boolean;
|
||||
isBlackAcceptRestart: boolean;
|
||||
canWhiteDown: boolean;
|
||||
}
|
||||
};
|
||||
export type RoomDetail = {
|
||||
roomId: RoomId;
|
||||
state: RoomState;
|
||||
@ -178,9 +178,7 @@ import {
|
||||
type UseGameSocketOptions,
|
||||
} from '@/utils/game-socket';
|
||||
import { onMounted, ref } from 'vue';
|
||||
interface RoomRender extends Room {
|
||||
briefId: string;
|
||||
}
|
||||
type RoomRender = Room & { briefId: string };
|
||||
function toRoomRender(room: Room): RoomRender {
|
||||
return Object.assign(room, {
|
||||
briefId: room.id.slice(0, 8),
|
||||
|
@ -1,36 +1,46 @@
|
||||
<template>
|
||||
<el-main class="page-main keep-padding flex justify-center bg-white">
|
||||
<div class="md-w-700px lg-w-950px xl-w-1200px flex flex-col gap-10px">
|
||||
<el-main
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载"
|
||||
class="page-main keep-padding flex justify-center bg-white"
|
||||
>
|
||||
<div class="max-sm-w-350px sm-w-550px md-w-700px lg-w-950px xl-w-1200px flex flex-col gap-10px">
|
||||
<el-carousel class="rounded-10px" height="300px">
|
||||
<el-carousel-item class="slider-item">
|
||||
<img class="slider-item__img" src="/bg2.jpg" />
|
||||
</el-carousel-item>
|
||||
<el-carousel-item class="slider-item">
|
||||
<img class="slider-item__img" src="/bg2.jpg" />
|
||||
</el-carousel-item>
|
||||
<el-carousel-item class="slider-item">
|
||||
<img class="slider-item__img" src="/bg2.jpg" />
|
||||
</el-carousel-item>
|
||||
<el-carousel-item class="slider-item">
|
||||
<img class="slider-item__img" src="/bg2.jpg" />
|
||||
</el-carousel-item>
|
||||
<el-carousel-item class="slider-item">
|
||||
<img class="slider-item__img" src="/bg2.jpg" />
|
||||
</el-carousel-item>
|
||||
<el-carousel-item class="slider-item">
|
||||
<img class="slider-item__img" src="/bg2.jpg" />
|
||||
<el-carousel-item v-for="g of gallery" class="slider-item">
|
||||
<img class="slider-item__img" :src="Neusoft.SMART_PARTY_BASE_URL + g.imageUrl" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div class="grid grid-cols-2 gap-10px">
|
||||
<div class="grid-col-span-2">
|
||||
<div class="title">社团列表</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">社团资讯</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">社团活动</div>
|
||||
</div>
|
||||
<paged-wrapper
|
||||
class="grid-col-span-2"
|
||||
pagination-background
|
||||
:schema="clubListRespSchema"
|
||||
:num="10"
|
||||
pagination-class="w-min"
|
||||
pagination-layout="prev, pager, next"
|
||||
:get-request-options="getClubListRequestOptions"
|
||||
v-slot="{ data }"
|
||||
>
|
||||
<div v-show="data">
|
||||
<div class="title m-b-10px">社团列表</div>
|
||||
<div
|
||||
class="grid gap-10px justify-center max-sm-grid-cols-3 sm-grid-cols-5 md-grid-cols-7 lg-grid-cols-9"
|
||||
>
|
||||
<div v-for="club of data" class="flex flex-col items-center">
|
||||
<el-avatar :size="90" :src="getAvatarURL(club.avatar)">
|
||||
<div class="text-black">暂无图片</div>
|
||||
</el-avatar>
|
||||
<div class="text-5 font-bold">{{ club.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</paged-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
@ -48,7 +58,28 @@
|
||||
}
|
||||
</style>
|
||||
<script lang="ts" setup>
|
||||
import { getAvatarURL, requestRaw } from '@/api';
|
||||
import PagedWrapper from '@/components/PagedWrapper.vue';
|
||||
import { Neusoft } from '@/schemas/neusoft';
|
||||
import { clubListRespSchema } from '@/schemas/response';
|
||||
import { useUserStore } from '@/stores/user.js';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import type { ComponentProps } from 'vue-component-type-helpers';
|
||||
const userStore = useUserStore();
|
||||
const loading = ref(false);
|
||||
const gallery = ref<Neusoft.Gallery[]>();
|
||||
const getClubListRequestOptions: ComponentProps<typeof PagedWrapper>['getRequestOptions'] = (
|
||||
page,
|
||||
num,
|
||||
) => ({
|
||||
url: '/api/club/',
|
||||
params: { page, num },
|
||||
});
|
||||
requestRaw<Neusoft.GalleryResp>({
|
||||
url: `${Neusoft.SMART_PARTY_BASE_URL}/system/rotation/list`,
|
||||
addAuth: false,
|
||||
}).then((resp) => {
|
||||
if (!resp || !Neusoft.isSucceed(resp)) return;
|
||||
gallery.value = resp.rows;
|
||||
});
|
||||
</script>
|
||||
|
@ -105,13 +105,15 @@ const upload: UploadRequestHandler = async ({ file }) => {
|
||||
try {
|
||||
const formdata = new FormData();
|
||||
formdata.append('file', file);
|
||||
const uploadResp = await request('/api/avatar/upload', uploadAvatarRespSchema, {
|
||||
const uploadResp = await request(uploadAvatarRespSchema, {
|
||||
url: '/api/avatar/upload',
|
||||
method: 'post',
|
||||
data: formdata,
|
||||
errorDescription: '上传头像失败',
|
||||
});
|
||||
if (!uploadResp) return;
|
||||
const changeResp = await request('/api/user/avatar', ordinarySchema, {
|
||||
const changeResp = await request(ordinarySchema, {
|
||||
url: '/api/user/avatar',
|
||||
method: 'put',
|
||||
params: { code: uploadResp.data },
|
||||
errorDescription: '上传头像失败',
|
||||
@ -146,7 +148,8 @@ async function submit() {
|
||||
allSucceed = true;
|
||||
try {
|
||||
if (username !== userStore.userInfo!.name) {
|
||||
const resp = await request('/api/user/rename', ordinarySchema, {
|
||||
const resp = await request(ordinarySchema, {
|
||||
url: '/api/user/rename',
|
||||
method: 'put',
|
||||
data: {
|
||||
id: userStore.userInfo!.id,
|
||||
|
@ -8,7 +8,7 @@
|
||||
<template v-if="!smLess">
|
||||
<div
|
||||
v-if="!loading"
|
||||
class="max-md-w-500px md-w-700px lg-w-950px flex flex-col"
|
||||
class="max-md-w-600px md-w-700px lg-w-950px flex flex-col"
|
||||
:class="[{ 'gap-10px': userInfo }, !userInfo ? 'justify-center items-center' : '']"
|
||||
>
|
||||
<template v-if="userInfo">
|
||||
|
Loading…
x
Reference in New Issue
Block a user