feat: 新增编辑社团信息

This commit is contained in:
Litrix2 2025-01-12 09:58:10 +08:00
parent d0f6c76984
commit bf663be047
19 changed files with 517 additions and 152 deletions

View File

@ -1,14 +1,11 @@
import { REQUEST_BASE_URL } from '@/env';
import {
userInfoRespSchema,
type AnyRespSchema,
type ErrorResp,
type SucceedRespOf,
} from '@/schemas/response';
import { type AnyRespSchema, type ErrorResp, type SucceedRespOf } from '@/schemas/response';
import { useUserStore } from '@/stores/user';
import type { ComparablePrimitive } from '@/utils/types';
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
import { ElMessage } from 'element-plus';
import type { z } from 'zod';
export * from './request';
export const axiosInstance = axios.create({
baseURL: REQUEST_BASE_URL,
});
@ -79,23 +76,36 @@ export async function request<T extends AnyRespSchema>(
options.validationErrorCB,
);
}
export async function getUserInfo({
userID,
validationErrorCB,
}: {
userID?: number;
validationErrorCB?: ValidationErrorCallback;
}) {
let url = '/api/user/info';
if (userID !== undefined) {
url += `/${userID}`;
export async function patchRequest<T extends Record<string, ComparablePrimitive>>(
obj: T,
old: NoInfer<T>,
mapping: {
[P in keyof T]?: (
v: T[P],
) => Promise<SucceedRespOf<z.infer<AnyRespSchema>> | boolean | undefined>;
},
) {
let changed = false,
succeed = true;
let k: keyof T;
const promises: Promise<unknown>[] = [];
for (k in obj) {
const v = obj[k];
if (v !== old[k] && mapping[k]) {
changed = true;
const p = mapping[k]!(v);
if (p)
promises.push(
p.then((resp) => {
if (!resp) succeed = false;
}),
);
}
}
const resp = await request(userInfoRespSchema, {
url,
errorDescription: '获取用户信息失败',
validationErrorCB,
});
return resp;
await Promise.all(promises);
const res = !changed ? 'unchanged' : succeed ? 'succeed' : 'error';
if (res === 'unchanged') {
ElMessage.info('没有要修改的信息');
}
return res;
}
export const getAvatarURL = (avatar: string | undefined) =>
avatar && `${REQUEST_BASE_URL}/api/avatar/${avatar}`;

84
src/api/request.ts Normal file
View File

@ -0,0 +1,84 @@
import {
clubRespSchema,
ordinarySchema,
uploadAvatarRespSchema,
userInfoRespSchema,
} from '@/schemas/response';
import { type ValidationErrorCallback, request } from '.';
import { REQUEST_BASE_URL } from '@/env';
import { ElMessage, type UploadRawFile } from 'element-plus';
export async function getUserInfo({
userID,
validationErrorCB,
}: {
userID?: number;
validationErrorCB?: ValidationErrorCallback;
}) {
let url = '/api/user/info';
if (userID !== undefined) {
url += `/${userID}`;
}
const resp = await request(userInfoRespSchema, {
url,
errorDescription: '获取用户信息失败',
validationErrorCB,
});
return resp;
}
// #region Avatar
export const getAvatarURL = (avatar: string | undefined) =>
avatar && `${REQUEST_BASE_URL}/api/avatar/${avatar}`;
export function validateAvatarFile(file: UploadRawFile) {
if (!file.type.startsWith('image')) {
ElMessage.error('只能上传图片');
return false;
}
if (file.size > 1024 * 1024) {
ElMessage.error('文件大小不能超过1MB');
return false;
}
return true;
}
export function uploadAvatar(file: File) {
const formData = new FormData();
formData.append('file', file);
return request(uploadAvatarRespSchema, {
url: '/api/avatar/upload',
method: 'post',
data: formData,
errorDescription: '上传头像失败',
});
}
// #endregion
// #region 社团
export const getClubById = (id: number) =>
request(clubRespSchema, {
url: `/api/club/${id}`,
errorDescription: '获取社团信息失败',
}).then((r) => r?.data);
export const changeClubName = (id: number, name: string) =>
request(ordinarySchema, {
url: '/api/club/name',
method: 'put',
data: { clubId: id, name },
errorDescription: '修改社团名称失败',
});
export const changeClubCommit = (id: number, commit: string) =>
request(ordinarySchema, {
url: '/api/club/commit',
method: 'put',
data: { clubId: id, commit },
errorDescription: '修改社团简介失败',
});
export const changeClubAvatar = (id: number, code: string) =>
request(ordinarySchema, {
url: '/api/club/avatar',
method: 'put',
params: {
code,
clubId: id,
},
errorDescription: '修改社团头像失败',
});
// #endregion

View File

@ -6,7 +6,7 @@
>
<template v-if="resp">
<template v-if="resp.data.total">
<div><slot :data="resp.data.data"></slot></div>
<div><slot :data="converter?.(resp.data.data) ?? (resp.data.data as R)"></slot></div>
<el-pagination
v-if="!hidePager"
class="m-t-10px"
@ -27,18 +27,20 @@
</template>
</div>
</template>
<script generic="T extends AnyPagedRespSchema" setup lang="ts">
<script lang="ts">
type Succeed<T extends z.ZodTypeAny> = SucceedRespOf<z.infer<T>>;
type DataList<T extends z.ZodTypeAny> = Succeed<T>['data']['data'];
</script>
<script generic="T extends AnyPagedRespSchema, R extends DataList<T> = DataList<T>" setup lang="ts">
import { request } from '@/api';
import type { AnyPagedRespSchema, SucceedRespOf } from '@/schemas/response';
import { computed } from 'vue';
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,
requestOptions: getRequestOptions,
requestOptions,
num,
hidePager = false,
} = defineProps<{
@ -48,20 +50,30 @@ const {
paginationBackground?: boolean;
paginationLayout?: string;
hidePager?: boolean;
requestOptions: (page: number, num: number) => AxiosRequestConfig;
requestOptions: (params: { page: number; num: number }) => AxiosRequestConfig;
converter?: (raw: DataList<T>) => R;
}>();
defineSlots<{
default: (props: { data: DataList }) => unknown;
default: (props: { data: R }) => unknown;
empty: () => unknown;
}>();
const emit = defineEmits<{
change: [data: DataList<T>];
loadComplete: [];
}>();
const loading = ref(true);
const resp = ref<Succeed>();
const resp = ref<Succeed<T>>();
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));
resp.value = await request(schema, requestOptions({ page: page.value, num }));
if (resp.value) {
emit('change', resp.value.data.data);
}
emit('loadComplete');
loading.value = false;
return !!resp.value;
}
defineExpose({ refresh });
refresh();

View File

@ -151,8 +151,8 @@ export interface LoginRegisterDialogProps {
handleLogin: (params: LoginParams) => Promise<boolean>;
handleRegister: (params: RegisterParams) => Promise<boolean>;
}
export const usernameLength = 3;
export const passwordLength = usernameLength;
export const minUsernameLength = 3;
export const minPasswordLength = minUsernameLength;
</script>
<script lang="ts" setup>
import VerifyInput, { type VerifyImagePath } from '@/components/app/VerifyInput.vue';
@ -180,11 +180,11 @@ watch(show, (show) => {
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [
{ required: true, message: '请输入用户名' },
{ min: usernameLength, message: `用户名长度不能小于${usernameLength}` },
{ min: minUsernameLength, message: `用户名长度不能小于${minUsernameLength}` },
],
password: [
{ required: true, message: '请输入密码' },
{ min: passwordLength, message: `密码长度不能小于${passwordLength}` },
{ min: minPasswordLength, message: `密码长度不能小于${minPasswordLength}` },
],
verifyCode: [
{ required: !DEV, message: '请输入验证码' },
@ -238,8 +238,8 @@ const registerFormRules = reactive<FormRules<typeof registerFormData>>({
callback('请输入密码');
} else if (value !== registerFormData.password) {
callback('密码不一致');
} else if (value.length < passwordLength) {
callback(`密码长度不能小于${passwordLength}`);
} else if (value.length < minPasswordLength) {
callback(`密码长度不能小于${minPasswordLength}`);
} else {
callback();
}

View File

@ -0,0 +1,12 @@
<template>
<el-avatar shape="square" class="select-none" :size="size" :src="getAvatarURL(avatar)">
<slot><div class="text-black">暂无图片</div></slot>
</el-avatar>
</template>
<script setup lang="ts">
import { getAvatarURL } from '@/api';
defineProps<{
avatar: string | undefined;
size: number;
}>();
</script>

View File

@ -0,0 +1,35 @@
<template>
<el-dialog v-model="show" :class="$style['el-dialog']" align-center>
<template #header><b>社团信息</b></template>
<div class="flex flex-col" v-if="club">
<club-list-item :club="club" mode="h" />
<div>简介{{ club.commit }}</div>
<template v-if="userStore.logined && userInfo">
<el-button
v-if="userInfo.club && userInfo.club.id === club.id"
class="self-end"
type="primary"
>
进入主页
</el-button>
<el-button v-else class="self-end" type="success">加入社团</el-button>
</template>
</div>
</el-dialog>
</template>
<style lang="scss" module>
.el-dialog {
position: relative;
--el-text-color-regular: black;
}
</style>
<script setup lang="ts">
import ClubListItem from '@/components/club/ClubListItem.vue';
import type { Club } from '@/schemas/response';
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const { club } = defineProps<{ club: Club | undefined }>();
const show = defineModel<boolean>({ required: true });
</script>

View File

@ -0,0 +1,111 @@
<template>
<el-card v-loading="!club" element-loading-text="正在加载" class="min-h-200px">
<template #header>
<h4>编辑社团资料</h4>
</template>
<template v-if="club">
<div class="flex justify-center">
<div class="grid gap-15px">
<el-form :model="data" :rules="rules" ref="form">
<el-form-item prop="name" label="社团名称">
<el-input v-model="data.name" />
</el-form-item>
<el-form-item prop="commit" label="社团简介">
<el-input type="textarea" v-model="data.commit" />
</el-form-item>
</el-form>
<div class="flex flex-col gap-15px items-center">
<club-avatar :avatar="club.avatar" :size="100" />
<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>
</div>
<div class="col-span-2 text-center">
<el-button type="primary" @click="submit" :loading="submitting">修改信息</el-button>
</div>
</div>
</div>
</template>
</el-card>
</template>
<style lang="scss" scoped>
.el-divider {
margin: 15px 0;
}
</style>
<script lang="ts">
export const minClubNameLength = 2;
</script>
<script setup lang="ts">
import {
changeClubAvatar,
changeClubCommit,
changeClubName,
getClubById,
patchRequest,
uploadAvatar,
validateAvatarFile,
} from '@/api';
import type { Club } from '@/schemas/response';
import { Upload } from '@element-plus/icons-vue';
import { ElMessage, type FormRules, type UploadRequestHandler } from 'element-plus';
import { pick } from 'lodash-es';
import { reactive, ref, useTemplateRef } from 'vue';
import ClubAvatar from './ClubAvatar.vue';
const { id } = defineProps<{ id: number }>();
const club = ref<Club>();
const data = ref({
name: '',
commit: '',
});
const rules = reactive<FormRules<typeof data>>({
name: [
{ required: true, message: '请输入社团名' },
{ min: minClubNameLength, message: `社团名长度不能小于${minClubNameLength}` },
],
});
const formRef = useTemplateRef('form');
const uploading = ref(false);
const upload: UploadRequestHandler = async ({ file }) => {
if (!validateAvatarFile(file)) return;
uploading.value = true;
try {
const uploadResp = await uploadAvatar(file);
if (!uploadResp) return;
const changeResp = await changeClubAvatar(id, uploadResp.data);
if (!changeResp) return;
await refresh();
ElMessage.success('社团头像修改成功');
} finally {
uploading.value = false;
}
};
const submitting = ref(false);
async function submit() {
try {
await formRef.value!.validate();
} catch {
return;
}
submitting.value = true;
const res = await patchRequest(data.value, club.value!, {
name: (name) => changeClubName(id, name),
commit: (commit) => changeClubCommit(id, commit),
});
if (res === 'succeed') {
await refresh();
ElMessage.success('修改成功');
}
submitting.value = false;
}
async function refresh() {
club.value = await getClubById(id);
if (!club.value) return;
data.value = pick(club.value, ['name', 'commit']);
}
refresh();
</script>

View File

@ -37,10 +37,6 @@ const emit = defineEmits<{
click: [club: Club];
}>();
const getClubListRequestOptions: ComponentProps<typeof PagedWrapper>['requestOptions'] = (
page,
num,
) => ({
url: '/api/club/',
params: { page, num },
});
params,
) => ({ url: '/api/club/', params });
</script>

View File

@ -9,10 +9,8 @@
:key="club.id"
@click="click && $emit('click', club)"
>
<el-avatar shape="square" class="select-none" :size="100" :src="getAvatarURL(club.avatar)">
<div class="text-black">暂无图片</div>
</el-avatar>
<div class="flex flex-col" :class="{ 'items-center': mode === 'v' }">
<club-avatar :size="100" :avatar="club.avatar" />
<div class="flex flex-col" :class="[{ 'items-center': mode === 'v' }, textOuterClass]">
<div class="text-black font-bold" :class="mode === 'v' ? 'text-5' : 'text-7'">
{{ club.name }}
</div>
@ -31,14 +29,15 @@
</div>
</template>
<script setup lang="ts">
import { getAvatarURL } from '@/api';
import type { Club } from '@/schemas/response';
import { useUserStore } from '@/stores/user';
import ClubAvatar from './ClubAvatar.vue';
const userStore = useUserStore();
const { click = false, mode = 'v' } = defineProps<{
club: Club;
click?: boolean;
mode?: 'h' | 'v';
textOuterClass?: string;
}>();
defineEmits<{ click: [club: Club] }>();
</script>

View File

View File

@ -4,6 +4,7 @@ import type { MaybeArray } from '@/utils/types';
import { PageErrorType, type PageErrorReason } from '@/views/ErrorPage.vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import { ClubPermissionId, UserPermissionId } from './permissions';
import { ElMessage } from 'element-plus';
export type RoutePermissionRelation = 'AND' | 'OR';
export type RoutePermission<T extends number> =
| MaybeArray<T>
@ -121,8 +122,29 @@ const routes: RouteRecordRaw[] = [
},
{
path: 'club',
name: 'ManageClub',
component: () => import('@/views/manage/ManageClubPage.vue'),
children: [
{
path: '',
name: 'ManageClub',
component: () => import('@/views/manage/ManageClubPage.vue'),
},
{
path: ':id/edit',
name: 'ManageClubEdit',
component: () => import('@/views/manage/ManageClubEditPage.vue'),
beforeEnter(to, from) {
const id = Number(to.params.id);
if (isNaN(id) || id < 1) {
ElMessage.error('社团ID无效');
return from;
}
},
props: (to) => ({ id: Number(to.params.id) }),
meta: {
breadcrumb: '编辑社团',
},
},
],
meta: {
userPermission: {
id: UserPermissionId.MANAGE_PAGE,

View File

@ -26,6 +26,9 @@ const createRespSchema = <T extends z.ZodTypeAny>(data: T) =>
}),
),
]);
export type AnyRespSchema = ReturnType<typeof createRespSchema>;
export type ErrorResp = Extract<z.infer<AnyRespSchema>, { type: 'error' }>;
export type SucceedRespOf<T extends z.infer<AnyRespSchema>> = Extract<T, { type: 'success' }>;
export const createPagedRespSchema = <T extends z.ZodTypeAny>(data: T) =>
createRespSchema(
z.object({
@ -33,9 +36,7 @@ export const createPagedRespSchema = <T extends z.ZodTypeAny>(data: T) =>
total: z.number(),
}),
);
export type AnyRespSchema = ReturnType<typeof createRespSchema>;
export type ErrorResp = Extract<z.infer<AnyRespSchema>, { type: 'error' }>;
export type SucceedRespOf<T> = Extract<T, { type: 'success' }>;
export type SucceedPagedDataOf<T extends z.infer<AnyPagedRespSchema>> = SucceedRespOf<T>['data'];
export type AnyPagedRespSchema = ReturnType<typeof createPagedRespSchema>;
export const ordinarySchema = createRespSchema(z.literal(true));
export const idAndNameSchema = z.object({
@ -51,6 +52,7 @@ export const clubSchema = idAndNameSchema.extend({
commit: z.string(),
});
export type Club = z.infer<typeof clubSchema>;
export const clubRespSchema = createRespSchema(clubSchema);
export const clubListRespSchema = createPagedRespSchema(clubSchema);
const userInfoDataSchema = idAndNameSchema.extend({
avatar: z.optional(z.string()),

View File

@ -99,7 +99,12 @@ export const useUserStore = defineStore('user', () => {
}
},
});
if (!resp) return false;
if (!resp) {
if (!token.value) {
userInfo.value = null;
}
return false;
}
userInfo.value = resp.data;
return true;
} finally {

View File

@ -1,3 +1,5 @@
import type { Primitive } from 'zod';
export type MaybePromise<T> = T | Promise<T>;
export type MaybeArray<T, R extends boolean = false> = T | (R extends false ? T[] : readonly T[]);
export type Future<T = void> = PromiseWithResolvers<T>;
@ -9,3 +11,4 @@ export type Override<T extends object, R extends Partial<T> = {}, O extends keyo
R;
export type Nullable<T = never> = T | null | undefined;
export type StrictOmit<T, K extends keyof T> = Omit<T, K>;
export type ComparablePrimitive = Exclude<Primitive, symbol>;

View File

@ -8,45 +8,19 @@
<club-list click @click="onClubClick" />
</el-card>
</div>
<el-dialog :class="$style['el-dialog']" v-model="showClubDialog" align-center>
<template #title><b>社团信息</b></template>
<div class="flex flex-col" v-if="showingClub">
<club-list-item :club="showingClub" mode="h" />
<div>简介{{ showingClub.commit }}</div>
<template v-if="userStore.logined && userInfo">
<el-button
v-if="userInfo.club && userInfo.club.id === showingClub.id"
class="self-end"
type="primary"
>
进入主页
</el-button>
<el-button v-else class="self-end" type="success">加入社团</el-button>
</template>
</div>
</el-dialog>
<club-detail-dialog v-model="showClubDialog" :club="showingClub" />
</el-main>
</template>
<style lang="scss" module>
.el-dialog {
position: relative;
--el-text-color-regular: black;
}
</style>
<style lang="scss" scoped>
.el-card {
--el-card-padding: 15px;
}
</style>
<script lang="ts" setup>
import ClubDetailDialog from '@/components/club/ClubDetailDialog.vue';
import ClubList from '@/components/club/ClubList.vue';
import ClubListItem from '@/components/club/ClubListItem.vue';
import type { Club } from '@/schemas/response';
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const showingClub = ref<Club>();
const showClubDialog = ref(false);
function onClubClick(club: Club) {

View File

@ -0,0 +1,11 @@
<template>
<el-main class="flex flex-col gap-5px">
<manage-breadcrumb />
<club-edit-card :id="id" />
</el-main>
</template>
<script setup lang="ts">
defineProps<{ id: number }>();
import ClubEditCard from '@/components/club/ClubEditCard.vue';
import ManageBreadcrumb from '@/components/manage/ManageBreadcrumb.vue';
</script>

View File

@ -2,34 +2,151 @@
<el-main class="flex flex-col gap-5px">
<manage-breadcrumb />
<el-card body-class="flex">
<el-input v-model="searchClubName" class="w-50 m-r-12px" placeholder="请输入社团名称" />
<el-button type="success">新建社团</el-button>
<el-button type="danger" :disabled="!selectedClubs.length">批量删除</el-button>
<el-button type="primary" @click="refresh">刷新</el-button>
<el-input
v-model="searchClubName"
class="w-50 m-l-auto m-r-12px"
placeholder="请输入社团名称"
/>
<el-button type="primary" plain>查询</el-button>
<el-button type="warning" plain>重置</el-button>
</el-card>
<el-card>
<el-card v-loading="refreshing" element-loading-text="正在加载" class="min-h-200px">
<paged-wrapper
pagination-class="w-min"
:schema="clubListRespSchema"
:request-options="options"
:request-options="getClubListRO"
:num="20"
:converter="(raw) => raw.map(toClubRender)"
@change="onClubListChange"
@load-complete="refreshing = false"
v-slot="{ data }"
ref="clubList"
>
<el-table :data="data"></el-table>
<el-table :data="data" @selection-change="onSelect">
<el-table-column type="selection" :selectable="({ deleting }: ClubRender) => !deleting" />
<el-table-column prop="id" label="序号" />
<el-table-column prop="name" label="社团名称" />
<el-table-column label="社团封面" v-slot="{ row }">
<club-avatar :avatar="(row as ClubRender).avatar" :size="50">
<div class="text-black text-2.5 lh-4">
暂无
<br />
图片
</div>
</club-avatar>
</el-table-column>
<el-table-column label="社团介绍" v-slot="{ row }">
<el-button
type="primary"
plain
:disabled="(row as ClubRender).deleting"
@click="onClubClick(row)"
>
查看介绍
</el-button>
</el-table-column>
<el-table-column label="操作" v-slot="{ row }" :width="300" align="right">
<div class="club-action-outer flex flex-wrap gap-10px justify-end">
<el-button type="primary" plain>管理成员</el-button>
<el-button
type="warning"
plain
:disabled="(row as ClubRender).deleting"
@click="
$router.push({
name: 'ManageClubEdit',
params: {
id: (row as ClubRender).id,
},
})
"
>
编辑
</el-button>
<el-button
type="danger"
:loading="(row as ClubRender).deleting"
@click="onDeleteButtonClick(row)"
>
删除
</el-button>
</div>
</el-table-column>
</el-table>
</paged-wrapper>
</el-card>
<club-detail-dialog v-model="showClubDialog" :club="showingClub" />
</el-main>
</template>
<script setup lang="ts">
import ManageBreadcrumb from '@/components/manage/ManageBreadcrumb.vue';
import PagedWrapper from '@/components/PagedWrapper.vue';
import { clubListRespSchema, type SucceedRespOf } from '@/schemas/response';
import { ref } from 'vue';
import type { ComponentProps } from 'vue-component-type-helpers';
import type { z } from 'zod';
const searchClubName = ref('');
const loading = ref(true);
const clubList = ref<SucceedRespOf<z.infer<typeof clubListRespSchema>>['data']['data']>();
const options: ComponentProps<typeof PagedWrapper>['requestOptions'] = (page, num) => ({
<style lang="scss" scoped>
.club-action-outer {
.el-button {
margin: 0;
}
}
</style>
<script lang="ts">
type ClubRender = Club & {
deleting: boolean;
};
const toClubRender = (club: Club): ClubRender => ({ ...club, deleting: false });
export const getClubListRO: ComponentProps<typeof PagedWrapper>['requestOptions'] = (params) => ({
url: '/api/club/',
params: { page, num },
params,
});
</script>
<script setup lang="ts">
import ClubAvatar from '@/components/club/ClubAvatar.vue';
import ClubDetailDialog from '@/components/club/ClubDetailDialog.vue';
import ManageBreadcrumb from '@/components/manage/ManageBreadcrumb.vue';
import PagedWrapper from '@/components/PagedWrapper.vue';
import { clubListRespSchema, type Club } from '@/schemas/response';
import { ElMessageBox, ElMessage } from 'element-plus';
import { ref, useTemplateRef } from 'vue';
import type { ComponentProps } from 'vue-component-type-helpers';
const searchClubName = ref('');
const showingClub = ref<ClubRender>();
const showClubDialog = ref(false);
const clubListRef = useTemplateRef('clubList');
const refreshing = ref(true);
const clubs = ref<ClubRender[]>([]);
const selectedClubs = ref<ClubRender[]>([]);
function refresh() {
refreshing.value = true;
clubListRef.value!.refresh();
}
function onClubListChange(data: ClubRender[]) {
clubs.value = data;
}
function onClubClick(club: ClubRender) {
showingClub.value = club;
showClubDialog.value = true;
}
function onSelect(v: ClubRender[]) {
selectedClubs.value = v;
}
function onDeleteButtonClick(club: ClubRender) {
ElMessageBox.confirm('是否删除该社团,此操作无法恢复!', '警告', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消',
}).then(
async () => {
// TODO
club.deleting = true;
try {
if (!(await clubListRef.value!.refresh())) {
return;
}
ElMessage.success('删除成功');
} finally {
club.deleting = false;
}
},
() => {},
);
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<el-main class="flex flex-col gap-10px">
<el-main class="flex flex-col gap-5px">
<manage-breadcrumb />
<el-card body-class="flex">
你好

View File

@ -1,11 +1,11 @@
<template>
<div>
<div v-if="userStore.userInfo">
<template v-if="!smLess">
<h4 class="p">编辑资料</h4>
<el-divider />
</template>
<div class="p outer flex">
<el-avatar class="self-center" :size="100" :src="getAvatarURL(userStore.userInfo!.avatar)" />
<el-avatar class="self-center" :size="100" :src="getAvatarURL(userStore.userInfo.avatar)" />
<div class="self-center flex gap-2">
<el-upload :show-file-list="false" :http-request="upload">
<template #trigger>
@ -26,8 +26,8 @@
:model="data"
:rules="rules"
>
<el-form-item prop="username" label="用户名">
<el-input v-model="data.username" />
<el-form-item prop="name" label="用户名">
<el-input v-model="data.name" />
</el-form-item>
<el-form-item
:class="{
@ -59,9 +59,9 @@
}
</style>
<script lang="ts">
import { getAvatarURL, request } from '@/api';
import { usernameLength } from '@/components/app/LoginRegisterDialog.vue';
import { ordinarySchema, uploadAvatarRespSchema } from '@/schemas/response';
import { getAvatarURL, patchRequest, request, uploadAvatar, validateAvatarFile } from '@/api';
import { minUsernameLength } from '@/components/app/LoginRegisterDialog.vue';
import { ordinarySchema } from '@/schemas/response';
import { useMediaStore } from '@/stores/media';
import { useUserStore } from '@/stores/user';
import { Upload } from '@element-plus/icons-vue';
@ -90,24 +90,10 @@ const userStore = useUserStore();
const uploading = ref(false);
const { smLess } = storeToRefs(useMediaStore());
const upload: UploadRequestHandler = async ({ file }) => {
if (!file.type.startsWith('image')) {
ElMessage.error('只能上传图片');
return;
}
if (file.size > 1024 * 1024) {
ElMessage.error('文件大小不能超过1MB');
return;
}
if (!validateAvatarFile(file)) return;
uploading.value = true;
try {
const formdata = new FormData();
formdata.append('file', file);
const uploadResp = await request(uploadAvatarRespSchema, {
url: '/api/avatar/upload',
method: 'post',
data: formdata,
errorDescription: '上传头像失败',
});
const uploadResp = await uploadAvatar(file);
if (!uploadResp) return;
const changeResp = await request(ordinarySchema, {
url: '/api/user/avatar',
@ -124,52 +110,38 @@ const upload: UploadRequestHandler = async ({ file }) => {
};
const formRef = ref<FormInstance>();
const data = reactive({
username: userStore.userInfo!.name,
name: userStore.userInfo!.name,
});
const rules = reactive<FormRules<typeof data>>({
username: [
name: [
{ required: true, message: '请输入用户名' },
{ min: usernameLength, message: `用户名长度不能小于${usernameLength}` },
{ min: minUsernameLength, message: `用户名长度不能小于${minUsernameLength}` },
],
});
const submitting = ref(false);
async function submit() {
try {
await formRef.value?.validate();
await formRef.value!.validate();
} catch {
return;
}
submitting.value = true;
const { username } = data;
let changed = false,
allSucceed = true;
try {
if (username !== userStore.userInfo!.name) {
const resp = await request(ordinarySchema, {
const res = await patchRequest(data, userStore.userInfo!, {
name: (name) =>
request(ordinarySchema, {
url: '/api/user/rename',
method: 'put',
data: {
id: userStore.userInfo!.id,
newName: username,
newName: name,
},
errorDescription: '修改用户名失败',
});
if (resp) {
changed = true;
} else {
allSucceed = false;
}
}
} finally {
if (changed) {
await userStore.updateSelfUserInfo();
if (allSucceed) {
ElMessage.success('修改成功');
}
} else if (allSucceed) {
ElMessage.info('没有要修改的信息');
}
submitting.value = false;
}),
});
if (res === 'succeed') {
await userStore.updateSelfUserInfo();
ElMessage.success('修改成功');
}
submitting.value = false;
}
</script>