feat: 添加社团成员管理功能

This commit is contained in:
Litrix2 2025-01-12 21:19:50 +08:00
parent bf663be047
commit 63a3222c32
17 changed files with 330 additions and 141 deletions

1
.gitignore vendored
View File

@ -33,3 +33,4 @@ coverage
*.lockb
components.d.ts
.VSCodeCounter

View File

@ -14,7 +14,7 @@ declare module 'axios' {
interface AxiosRequestConfig {
addAuth?: boolean;
errorDescription?: string;
validationErrorCB?: ValidationErrorCallback;
onValidationError?: ValidationErrorCallback;
}
}
// 自动添加token到请求中.
@ -73,7 +73,7 @@ export async function request<T extends AnyRespSchema>(
schema,
await requestRaw(options),
options.errorDescription,
options.validationErrorCB,
options.onValidationError,
);
}
export async function patchRequest<T extends Record<string, ComparablePrimitive>>(

View File

@ -1,19 +1,26 @@
import {
clubListRespSchema,
clubRespSchema,
ordinarySchema,
uploadAvatarRespSchema,
userInfoListRespSchema,
userInfoRespSchema,
} from '@/schemas/response';
import { type ValidationErrorCallback, request } from '.';
import { REQUEST_BASE_URL } from '@/env';
import { ElMessage, type UploadRawFile } from 'element-plus';
export type PagedParams = {
/** 页码从1开始 */
page: number;
/** 每页数据量 */
num: number;
};
export async function getUserInfo({
userID,
validationErrorCB,
onValidationError,
}: {
userID?: number;
validationErrorCB?: ValidationErrorCallback;
onValidationError?: ValidationErrorCallback;
}) {
let url = '/api/user/info';
if (userID !== undefined) {
@ -22,7 +29,7 @@ export async function getUserInfo({
const resp = await request(userInfoRespSchema, {
url,
errorDescription: '获取用户信息失败',
validationErrorCB,
onValidationError,
});
return resp;
}
@ -57,6 +64,11 @@ export const getClubById = (id: number) =>
url: `/api/club/${id}`,
errorDescription: '获取社团信息失败',
}).then((r) => r?.data);
export const getClubList = (params: PagedParams) =>
request(clubListRespSchema, {
url: '/api/club/',
params: params,
});
export const changeClubName = (id: number, name: string) =>
request(ordinarySchema, {
url: '/api/club/name',
@ -81,4 +93,19 @@ export const changeClubAvatar = (id: number, code: string) =>
},
errorDescription: '修改社团头像失败',
});
export const getUserByClubId = (clubId: number, params: PagedParams) =>
request(userInfoListRespSchema, {
url: '/api/club/user/list',
params: Object.assign(params, { clubId }),
errorDescription: '获取社团成员列表失败',
});
export const removeUserFromClub = (userId: number, clubId: number) =>
request(ordinarySchema, {
url: '/api/club/user/remove',
method: 'delete',
params: {
userId,
clubId,
},
});
// #endregion

View File

@ -5,6 +5,7 @@
}
:root {
--header-height: 50px;
--search-input-gap: 12px;
}
body {
background-color: rgb(244, 246, 249);

View File

@ -1,12 +1,12 @@
<template>
<div
:class="{
'flex justify-center items-center': resp && !resp.data.total,
'flex justify-center items-center': !hasData,
}"
>
<template v-if="resp">
<template v-if="resp.data.total">
<div><slot :data="converter?.(resp.data.data) ?? (resp.data.data as R)"></slot></div>
<template v-if="hasData">
<div><slot :data="converted"></slot></div>
<el-pagination
v-if="!hidePager"
class="m-t-10px"
@ -27,51 +27,52 @@
</template>
</div>
</template>
<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';
<script
generic="T extends SucceedRespOf<z.infer<AnyPagedRespSchema>>, R = T['data']['data']"
setup
lang="ts"
>
import { type PagedParams } from '@/api';
import type { AnyPagedRespSchema, SucceedRespOf } from '@/schemas/response';
import { computed } from 'vue';
import type { AxiosRequestConfig } from 'axios';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import type { z } from 'zod';
const {
schema,
requestOptions,
handleRequest,
num,
hidePager = false,
converter,
} = defineProps<{
schema: T;
num: number;
paginationClass?: string;
paginationBackground?: boolean;
paginationLayout?: string;
hidePager?: boolean;
requestOptions: (params: { page: number; num: number }) => AxiosRequestConfig;
converter?: (raw: DataList<T>) => R;
handleRequest: (params: PagedParams) => Promise<T | undefined>;
converter?: (raw: T['data']['data']) => R;
}>();
defineSlots<{
default: (props: { data: R }) => unknown;
empty: () => unknown;
}>();
const emit = defineEmits<{
change: [data: DataList<T>];
loadComplete: [];
change: [data: R];
loadComplete: [succeed: boolean];
}>();
const loading = ref(true);
const resp = ref<Succeed<T>>();
const resp = ref<T>();
const hasData = computed(() => !!resp.value?.data.data.length);
const converted = computed(() => {
const data = resp.value?.data.data;
if (!data) return [] as R;
return converter?.(data) ?? (data as R);
});
const page = ref(1);
const total = computed(() => resp.value?.data.total);
async function refresh() {
loading.value = true;
resp.value = await request(schema, requestOptions({ page: page.value, num }));
if (resp.value) {
emit('change', resp.value.data.data);
}
emit('loadComplete');
resp.value = await handleRequest({ page: page.value, num });
emit('change', converted.value);
emit('loadComplete', !!resp.value);
loading.value = false;
return !!resp.value;
}

View File

@ -0,0 +1,26 @@
<template>
<div class="search-input flex gap-$search-input-gap">
<el-input v-model="model" class="w-50" :placeholder="placeholder" />
<el-button type="primary" plain @click="$emit('submit')">查询</el-button>
<el-button type="warning" plain @click="reset">重置</el-button>
</div>
</template>
<style lang="scss" scoped>
.el-button {
margin: 0;
}
</style>
<script setup lang="ts">
defineProps<{
placeholder?: string;
}>();
defineEmits<{
submit: [];
reset: [];
}>();
const model = defineModel({ default: '' });
function reset() {
model.value = '';
}
defineExpose({ reset });
</script>

View File

@ -76,10 +76,9 @@ const upload: UploadRequestHandler = async ({ file }) => {
try {
const uploadResp = await uploadAvatar(file);
if (!uploadResp) return;
const changeResp = await changeClubAvatar(id, uploadResp.data);
if (!changeResp) return;
await refresh();
if (!(await changeClubAvatar(id, uploadResp.data))) return;
ElMessage.success('社团头像修改成功');
await refresh();
} finally {
uploading.value = false;
}

View File

@ -3,9 +3,8 @@
pagination-background
pagination-class="w-min"
:hide-pager="brief"
:schema="clubListRespSchema"
:num="10"
:request-options="getClubListRequestOptions"
:handle-request="getClubList"
>
<template #default="{ data }">
<div class="flex gap-10px">
@ -23,20 +22,17 @@
</paged-wrapper>
</template>
<script setup lang="ts">
import { getClubList } from '@/api';
import PagedWrapper from '@/components/PagedWrapper.vue';
import { clubListRespSchema, type Club } from '@/schemas/response';
import { type Club } from '@/schemas/response';
import { useUserStore } from '@/stores/user';
import type { ComponentProps } from 'vue-component-type-helpers';
import ClubListItem from './ClubListItem.vue';
const userStore = useUserStore();
const { click = false, brief = false } = defineProps<{
brief?: boolean;
click?: boolean;
}>();
const emit = defineEmits<{
defineEmits<{
click: [club: Club];
}>();
const getClubListRequestOptions: ComponentProps<typeof PagedWrapper>['requestOptions'] = (
params,
) => ({ url: '/api/club/', params });
</script>

View File

@ -0,0 +1,110 @@
<template>
<div class="flex flex-col gap-5px">
<el-card body-class="flex">
<el-button type="danger">批量移除</el-button>
<el-button type="primary" @click="refresh">刷新</el-button>
<search-input class="m-l-auto" placeholder="输入用户名" @submit="refresh" @reset="refresh" />
</el-card>
<el-card v-loading="refreshing" element-loading-text="正在加载">
<paged-wrapper
pagination-class="w-min"
:num="20"
:handle-request="(params) => getUserByClubId(id, params)"
:converter="(raw) => raw.map(toUserInfoRender)"
@change="onUsersChange"
@load-complete="refreshing = false"
v-slot="{ data }"
ref="memberList"
>
<el-table :data="data" @selection-change="onUsersSelect">
<el-table-column
type="selection"
:selectable="({ removing }: UserInfoRender) => !removing"
/>
<el-table-column prop="id" label="序号" />
<el-table-column prop="name" label="用户名" />
<el-table-column label="权限组" v-slot="{ row }">
{{ (row as UserInfoRender).clubAuth?.name ?? '无' }}
</el-table-column>
<el-table-column label="操作" :min-width="100" v-slot="{ row }">
<div class="user-action-outer">
<el-button
type="danger"
:loading="(row as UserInfoRender).removing"
@click="remove(row)"
>
移除成员
</el-button>
</div>
</el-table-column>
</el-table>
</paged-wrapper>
</el-card>
</div>
</template>
<style lang="scss" scoped>
.user-action-outer {
@apply flex flex-wrap gap-10px;
.el-button {
margin: 0;
}
}
</style>
<script lang="ts">
type UserInfoRender = UserInfo & {
removing: boolean;
};
const toUserInfoRender = (raw: UserInfo): UserInfoRender => ({ ...raw, removing: false });
</script>
<script setup lang="ts">
import { getUserByClubId, removeUserFromClub } from '@/api';
import { ref, useTemplateRef } from 'vue';
import PagedWrapper from '@/components/PagedWrapper.vue';
import type { UserInfo } from '@/schemas/response';
import { useUserStore } from '@/stores/user';
import { ElMessage, ElMessageBox } from 'element-plus';
import SearchInput from '../SearchInput.vue';
const { id: clubId } = defineProps<{ id: number }>();
const userStore = useUserStore();
const refreshing = ref(true);
async function refresh() {
refreshing.value = true;
try {
await memberListRef.value!.refresh();
} finally {
refreshing.value = false;
}
}
const memberListRef = useTemplateRef('memberList');
const users = ref<UserInfoRender[]>([]);
const selectedUsers = ref<UserInfoRender[]>([]);
function onUsersChange(v: UserInfoRender[]) {
console.log(v);
users.value = v;
}
function onUsersSelect(v: UserInfoRender[]) {
selectedUsers.value = v;
}
function remove(user: UserInfoRender) {
ElMessageBox.confirm('确定移除该成员吗?', '警告', {
type: 'warning',
}).then(
async () => {
user.removing = true;
try {
if (!(await removeUserFromClub(user.id, clubId))) {
return;
}
ElMessage.success('移除成功');
await Promise.all([
refresh(),
userStore.userInfo!.id === user.id && userStore.updateSelfUserInfo(),
]);
} finally {
user.removing = false;
}
},
() => {},
);
}
</script>

View File

@ -189,6 +189,25 @@
animation: tile-common-appear 0.2s ease both;
}
</style>
<script lang="ts">
class FutureMap<T = void> extends Map<number, Future<T>> {
add(tileId: number) {
const future = Promise.withResolvers<T>();
this.set(tileId, future);
return future.promise;
}
resolve(tileId: number, value: T) {
this.get(tileId)?.resolve(value);
this.delete(tileId);
}
reject(tileId: number, reason: unknown) {
this.get(tileId)?.reject(reason);
this.delete(tileId);
}
}
</script>
<script lang="ts" setup>
import { useGame2048Store, type GameState, type Tile } from '@/stores/2048';
import { chainIterables } from '@/utils';
@ -199,10 +218,8 @@ import { useEventListener } from '@vueuse/core';
import { ElNotification } from 'element-plus';
import { add, pick, sample, shuffle } from 'lodash-es';
import { storeToRefs } from 'pinia';
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { computed, onMounted, onUnmounted, reactive, ref, useTemplateRef, watch } from 'vue';
const game2048Store = useGame2048Store();
type SingleTileLine = (Tile | undefined)[];
type OverlappedTileLine = Tile[][];
/**
@ -234,24 +251,6 @@ type TileTransitionFuture = Future;
*/
type MergedGrid = (LineMergeResult | undefined)[];
class FutureMap<T = void> extends Map<number, Future<T>> {
add(tileId: number) {
const future = Promise.withResolvers<T>();
this.set(tileId, future);
return future.promise;
}
resolve(tileId: number, value: T) {
this.get(tileId)?.resolve(value);
this.delete(tileId);
}
reject(tileId: number, reason: unknown) {
this.get(tileId)?.reject(reason);
this.delete(tileId);
}
}
const tileWidth = 105;
const tileWidthStyle = computed(() => `${tileWidth}px`);
const borderWidth = 15;
@ -338,24 +337,19 @@ function storeTileGrid() {
/**
* 获取块的位置.
*/
function getTilePosition(y: number, x: number) {
return {
left: `${borderWidth + x * (tileWidth + borderWidth)}px`,
top: `${borderWidth + y * (tileWidth + borderWidth)}px`,
};
}
const getTilePosition = (y: number, x: number) => ({
left: `${borderWidth + x * (tileWidth + borderWidth)}px`,
top: `${borderWidth + y * (tileWidth + borderWidth)}px`,
});
/**
* 获取数块除位置以外的样式.
*/
function getNumberTileStyle(tile: Tile) {
const { number } = tile;
return {
fontSize: `${number < 128 ? 55 : number < 1024 ? 45 : 35}px`,
color: number < 8 ? 'rgb(119, 110, 101)' : 'white',
backgroundColor: tileBackgroundColorMapping[number] ?? 'rgb(56, 56, 56)',
};
}
const getNumberTileStyle = ({ number }: Tile) => ({
fontSize: `${number < 128 ? 55 : number < 1024 ? 45 : 35}px`,
color: number < 8 ? 'rgb(119, 110, 101)' : 'white',
backgroundColor: tileBackgroundColorMapping[number] ?? 'rgb(56, 56, 56)',
});
/**
* 添加随机数块, 空间不够则跳过.
@ -398,9 +392,7 @@ function addRandomTiles(randomCount: number, randomNumbers: number[] = [2, 4]) {
* 获取数块Div的对应Id.
* @param element 数块Div.
*/
function getElementTileId(element: HTMLDivElement) {
return parseInt(element.dataset['tileId']!);
}
const getElementTileId = (element: HTMLDivElement) => parseInt(element.dataset['tileId']!);
/**
* 合并算法.
@ -501,9 +493,9 @@ function mergeLine(line: SingleTileLine): [lineMergeREsult: LineMergeResult, cha
* @return 是否有活动空间
*/
async function mergeTiles(direction: Directions) {
function getMergedGrid(
const getMergedGrid = (
d: Directions = direction,
): [MergedGrid, changed: boolean, maxNumber: number] {
): [MergedGrid, changed: boolean, maxNumber: number] => {
let changed = false;
let maxNumber = 0;
const mergedGrid: MergedGrid = Array(firstIndices.length).fill(undefined);
@ -524,9 +516,11 @@ async function mergeTiles(direction: Directions) {
mergedGrid[first] = lineMergeResult;
}
return [mergedGrid, changed, maxNumber];
}
};
function forEachMergedLine(callback: (lineMergeResult: LineMergeResult, first: number) => void) {
const forEachMergedLine = (
callback: (lineMergeResult: LineMergeResult, first: number) => void,
) => {
for (const first of firstIndices()) {
const mergeResult = mergedGrid[first];
if (mergeResult === undefined) {
@ -534,9 +528,9 @@ async function mergeTiles(direction: Directions) {
}
callback(mergeResult, first);
}
}
};
function forEachMergedTiles(
const forEachMergedTiles = (
callback: (
data: LineMergeResult & {
tiles: Tile[];
@ -545,7 +539,7 @@ async function mergeTiles(direction: Directions) {
first: number,
second: number,
) => void,
) {
) => {
forEachMergedLine((lineMergeResult, first) => {
for (const [lineIndex, second] of secondIndices().entries()) {
const tiles = get2DArrayItem(tileGrid, first, second, isColFirst());
@ -560,10 +554,10 @@ async function mergeTiles(direction: Directions) {
);
}
});
}
};
function firstIndices(d: Directions = direction) {
return Array.from(
const firstIndices = (d: Directions = direction) =>
Array.from(
(function* () {
switch (d) {
case Directions.UP:
@ -581,10 +575,9 @@ async function mergeTiles(direction: Directions) {
}
})(),
);
}
function secondIndices(d: Directions = direction) {
return Array.from(
const secondIndices = (d: Directions = direction) =>
Array.from(
(function* () {
switch (d) {
case Directions.UP:
@ -611,11 +604,8 @@ async function mergeTiles(direction: Directions) {
}
})(),
);
}
function isColFirst(d: Directions = direction) {
return d == Directions.LEFT || d == Directions.RIGHT;
}
const isColFirst = (d: Directions = direction) => d == Directions.LEFT || d == Directions.RIGHT;
const [mergedGrid, changed, maxNumber] = getMergedGrid(direction);
if (maxNumber >= game2048Store.successNumber) {
@ -721,13 +711,13 @@ async function move(key: string) {
}
}
const gameContainer = ref<HTMLDivElement>();
interface Pos {
const gameContainerRef = useTemplateRef('gameContainer');
type Pos = {
x: number;
y: number;
}
};
let startPos: Pos = { x: -1, y: -1 };
useEventListener(gameContainer, 'touchstart', (e) => {
useEventListener(gameContainerRef, 'touchstart', (e) => {
const firstTouch = e.changedTouches[0];
startPos = { x: firstTouch.clientX, y: firstTouch.clientY };
});
@ -747,7 +737,7 @@ const triggerMove = (deltaX: number, deltaY: number) => {
}
}
};
useEventListener(gameContainer, 'touchend', (e) => {
useEventListener(gameContainerRef, 'touchend', (e) => {
const firstTouch = e.changedTouches[0];
const [x, y] = [firstTouch.clientX - startPos.x, firstTouch.clientY - startPos.y];
triggerMove(x, y);

View File

@ -2,7 +2,12 @@ import { tempErrorRouteName, usePageStore } from '@/stores/page';
import { useUserStore } from '@/stores/user';
import type { MaybeArray } from '@/utils/types';
import { PageErrorType, type PageErrorReason } from '@/views/ErrorPage.vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import {
createRouter,
createWebHistory,
type RouteLocationNormalizedGeneric,
type RouteRecordRaw,
} from 'vue-router';
import { ClubPermissionId, UserPermissionId } from './permissions';
import { ElMessage } from 'element-plus';
export type RoutePermissionRelation = 'AND' | 'OR';
@ -22,6 +27,13 @@ declare module 'vue-router' {
breadcrumb?: string;
}
}
function validateParamId(to: RouteLocationNormalizedGeneric, from: RouteLocationNormalizedGeneric) {
const id = Number(to.params.id);
if (isNaN(id) || id < 1) {
ElMessage.error('参数错误');
return from;
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
@ -132,18 +144,22 @@ const routes: RouteRecordRaw[] = [
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;
}
},
beforeEnter: validateParamId,
props: (to) => ({ id: Number(to.params.id) }),
meta: {
breadcrumb: '编辑社团',
},
},
{
path: ':id/member',
name: 'ManageClubMember',
component: () => import('@/views/manage/ManageClubMemberPage.vue'),
beforeEnter: validateParamId,
props: (to) => ({ id: Number(to.params.id) }),
meta: {
breadcrumb: '社团成员',
},
},
],
meta: {
userPermission: {
@ -177,6 +193,9 @@ const router = createRouter({
});
router.beforeEach(async (to) => {
if (to.name === tempErrorRouteName) {
return true;
}
const userStore = useUserStore();
const pageStore = usePageStore();
pageStore.setNewRouteId(to);

View File

@ -54,15 +54,15 @@ export const clubSchema = idAndNameSchema.extend({
export type Club = z.infer<typeof clubSchema>;
export const clubRespSchema = createRespSchema(clubSchema);
export const clubListRespSchema = createPagedRespSchema(clubSchema);
const userInfoDataSchema = idAndNameSchema.extend({
const userInfoSchema = idAndNameSchema.extend({
avatar: z.optional(z.string()),
auth: authSchema,
club: z.optional(clubSchema),
clubAuth: z.optional(authSchema),
});
export const userInfoRespSchema = createRespSchema(userInfoDataSchema);
export type SucceedUserInfoResp = SucceedRespOf<z.infer<typeof userInfoRespSchema>>;
export type UserInfo = SucceedUserInfoResp['data'];
export const userInfoRespSchema = createRespSchema(userInfoSchema);
export type UserInfo = SucceedRespOf<z.infer<typeof userInfoRespSchema>>['data'];
export const userInfoListRespSchema = createPagedRespSchema(userInfoSchema);
export const loginRespSchema = userInfoRespSchema;
export const registerRespSchema = userInfoRespSchema;
export const verifyRespSchema = createRespSchema(

View File

@ -88,7 +88,7 @@ export const useUserStore = defineStore('user', () => {
initializing.value = true;
try {
const resp = await getUserInfo({
validationErrorCB(resp) {
onValidationError(resp) {
console.log('error');
// 验证失败则清除token
if (resp.status === 401) {

View File

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

View File

@ -5,22 +5,21 @@
<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
<search-input
v-model="searchClubName"
class="w-50 m-l-auto m-r-12px"
class="m-l-auto"
ref="searchInput"
placeholder="请输入社团名称"
@submit="refresh"
@reset="refresh"
/>
<el-button type="primary" plain>查询</el-button>
<el-button type="warning" plain>重置</el-button>
</el-card>
<el-card v-loading="refreshing" element-loading-text="正在加载" class="min-h-200px">
<el-card v-loading="refreshing" element-loading-text="正在加载" class="min-h-100px">
<paged-wrapper
pagination-class="w-min"
:schema="clubListRespSchema"
:request-options="getClubListRO"
:handle-request="getClubList"
:num="20"
:converter="(raw) => raw.map(toClubRender)"
@change="onClubListChange"
@load-complete="refreshing = false"
v-slot="{ data }"
ref="clubList"
@ -48,9 +47,21 @@
查看介绍
</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-table-column label="操作" v-slot="{ row }" :min-width="100">
<div class="club-action-outer">
<el-button
type="primary"
plain
:disabled="(row as ClubRender).deleting"
@click="
$router.push({
name: 'ManageClubMember',
params: { id: (row as ClubRender).id },
})
"
>
管理成员
</el-button>
<el-button
type="warning"
plain
@ -83,6 +94,7 @@
</template>
<style lang="scss" scoped>
.club-action-outer {
@apply flex flex-wrap gap-10px;
.el-button {
margin: 0;
}
@ -92,41 +104,35 @@
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,
});
const toClubRender = (raw: Club): ClubRender => ({ ...raw, deleting: false });
</script>
<script setup lang="ts">
import { getClubList } from '@/api';
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 SearchInput from '@/components/SearchInput.vue';
import { type Club } from '@/schemas/response';
import { ElMessage, ElMessageBox } 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 searchClubName = ref('');
const searchInputRef = useTemplateRef('searchInput');
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 onSelect(clubs: ClubRender[]) {
selectedClubs.value = clubs;
}
function onDeleteButtonClick(club: ClubRender) {
ElMessageBox.confirm('是否删除该社团,此操作无法恢复!', '警告', {