✨ feat: 表格组件泛型化
This commit is contained in:
parent
d6fc539639
commit
e2f6f1f324
@ -1,45 +1,53 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-5px">
|
||||
<el-card body-class="flex">
|
||||
<el-button type="danger">批量移除</el-button>
|
||||
<el-card class="header" body-class="flex items-center gap-12px">
|
||||
<club-avatar :avatar="club.avatar" :size="32" />
|
||||
<h3>{{ club.name }}#{{ club.id }}</h3>
|
||||
<el-button type="danger" :disabled="!selectedUsers.length" @click="removeSelected">
|
||||
批量移除
|
||||
</el-button>
|
||||
<el-button type="primary" @click="refresh">刷新</el-button>
|
||||
<el-button type="success" @click="showAddMemberDialog = true">添加成员</el-button>
|
||||
<search-input class="m-l-auto" placeholder="输入用户名" @submit="refresh" @reset="refresh" />
|
||||
</el-card>
|
||||
<el-card v-loading="refreshing" element-loading-text="正在加载">
|
||||
<el-card
|
||||
v-loading="refreshing"
|
||||
element-loading-text="正在加载"
|
||||
class="min-h-100px"
|
||||
body-class="flex items-center"
|
||||
>
|
||||
<paged-wrapper
|
||||
class="w-100%"
|
||||
pagination-class="w-min"
|
||||
:num="20"
|
||||
:handle-request="(params) => getUserByClubId(id, params)"
|
||||
:handle-request="(params) => getUserByClubId(club.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 }">
|
||||
<user-table :rows="data" selectable @selection-change="onUsersSelect">
|
||||
<template #action="{ row }">
|
||||
<div class="user-action-outer">
|
||||
<el-button
|
||||
type="danger"
|
||||
:loading="(row as UserInfoRender).removing"
|
||||
@click="remove(row)"
|
||||
>
|
||||
<el-button type="warning" :disabled="row.locking" @click="changeAuth(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" :loading="row.locking" @click="remove(row)">
|
||||
移除成员
|
||||
</el-button>
|
||||
</div>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</user-table>
|
||||
</paged-wrapper>
|
||||
</el-card>
|
||||
<club-add-member-dialog v-model="showAddMemberDialog" :club="club" @succeed="refresh" />
|
||||
<club-change-auth-dialog
|
||||
v-model="showChangeAuthDialog"
|
||||
:user="userEdit"
|
||||
:club="club"
|
||||
@succeed="refresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
@ -49,22 +57,33 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
.el-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
type UserInfoRender = UserInfo & {
|
||||
removing: boolean;
|
||||
locking: boolean;
|
||||
};
|
||||
const toUserInfoRender = (raw: UserInfo): UserInfoRender => ({ ...raw, removing: false });
|
||||
const toUserInfoRender = (raw: UserInfo): UserInfoRender => ({ ...raw, locking: false });
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import { getUserByClubId, removeUserFromClub } from '@/api';
|
||||
import { type Club } from '@/schemas/response';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import PagedWrapper from '@/components/PagedWrapper.vue';
|
||||
import ClubAvatar from '@/components/club/ClubAvatar.vue';
|
||||
import UserTable from '@/components/table/UserTable.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 }>();
|
||||
import { zip } from '@/utils/array';
|
||||
import ClubAddMemberDialog from './ClubAddMemberDialog.vue';
|
||||
import ClubChangeAuthDialog from './ClubChangeAuthDialog.vue';
|
||||
const { club } = defineProps<{ club: Club }>();
|
||||
const userStore = useUserStore();
|
||||
const refreshing = ref(true);
|
||||
async function refresh() {
|
||||
@ -79,7 +98,6 @@ 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[]) {
|
||||
@ -90,9 +108,9 @@ function remove(user: UserInfoRender) {
|
||||
type: 'warning',
|
||||
}).then(
|
||||
async () => {
|
||||
user.removing = true;
|
||||
user.locking = true;
|
||||
try {
|
||||
if (!(await removeUserFromClub(user.id, clubId))) {
|
||||
if (!(await removeUserFromClub(user.id, club.id))) {
|
||||
return;
|
||||
}
|
||||
ElMessage.success('移除成功');
|
||||
@ -101,10 +119,42 @@ function remove(user: UserInfoRender) {
|
||||
userStore.userInfo!.id === user.id && userStore.updateSelfUserInfo(),
|
||||
]);
|
||||
} finally {
|
||||
user.removing = false;
|
||||
user.locking = false;
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
function removeSelected() {
|
||||
ElMessageBox.confirm('确定移除选中的成员吗?', '警告', {
|
||||
type: 'warning',
|
||||
}).then(
|
||||
async () => {
|
||||
refreshing.value = true;
|
||||
const results = await Promise.all(
|
||||
selectedUsers.value.map((user) => removeUserFromClub(user.id, club.id, false)),
|
||||
);
|
||||
// NOTE ES2024
|
||||
const failed = zip(selectedUsers.value, results)
|
||||
.filter(([, res]) => !res)
|
||||
.map(([user]) => user)
|
||||
.toArray();
|
||||
for (const user of failed) {
|
||||
ElMessage.warning(`移除"${user.name}"失败`);
|
||||
}
|
||||
refresh();
|
||||
if (!failed.length) {
|
||||
ElMessage.success('移除成功');
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
const showAddMemberDialog = ref(false);
|
||||
const userEdit = ref<UserInfoRender>();
|
||||
const showChangeAuthDialog = ref(false);
|
||||
function changeAuth(user: UserInfoRender) {
|
||||
userEdit.value = user;
|
||||
showChangeAuthDialog.value = true;
|
||||
}
|
||||
</script>
|
||||
|
48
src/components/table/DataTable.vue
Normal file
48
src/components/table/DataTable.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<el-table :data="rows">
|
||||
<el-table-column
|
||||
v-if="selectable"
|
||||
type="selection"
|
||||
:selectable="typeof selectable === 'function' ? selectable : () => !!selectable"
|
||||
/>
|
||||
<slot></slot>
|
||||
<template v-for="{ key, show, ...props } of ensureArray(customCols)" :key="key">
|
||||
<el-table-column v-if="!show || show.value" v-slot="{ row }" :="props">
|
||||
<slot :name="key" :row="row" />
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
generic="T extends Record<string, unknown>, TCustomKey extends string = never"
|
||||
>
|
||||
import { ensureArray } from '@/utils/array';
|
||||
import type { MaybeArray, Override } from '@/utils/types';
|
||||
import type { ElTableColumn } from 'element-plus';
|
||||
import type { ComputedRef } from 'vue';
|
||||
import type { ComponentProps } from 'vue-component-type-helpers';
|
||||
export type CustomColumn<K> = Override<
|
||||
ComponentProps<typeof ElTableColumn>,
|
||||
{
|
||||
key: K;
|
||||
show?: ComputedRef<boolean>;
|
||||
},
|
||||
'prop'
|
||||
>;
|
||||
export type DataSlot<T> = (props: { row: T }) => unknown;
|
||||
export type DataSlotRecord<T, K extends keyof any = keyof T> = Record<K, DataSlot<T>>;
|
||||
const { rows, customCols = [] } = defineProps<{
|
||||
rows: T[];
|
||||
customCols?: MaybeArray<CustomColumn<TCustomKey>>;
|
||||
selectable?: boolean | ((row: T, index: number) => boolean);
|
||||
}>();
|
||||
type Slots = DataSlotRecord<T, TCustomKey> & {
|
||||
default: () => unknown;
|
||||
};
|
||||
defineSlots<Slots>();
|
||||
defineEmits<{
|
||||
selectionChange: [rows: T[]];
|
||||
}>();
|
||||
</script>
|
64
src/components/table/UserTable.vue
Normal file
64
src/components/table/UserTable.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<data-table
|
||||
:rows="rows"
|
||||
:custom-cols="customColsComputed"
|
||||
@selection-change="onSelect"
|
||||
:selectable="selectable"
|
||||
>
|
||||
<el-table-column prop="id" label="序号" />
|
||||
<el-table-column prop="name" label="用户名" />
|
||||
<!-- FIXME 插槽作用域还是any,疑似LSP的bug-->
|
||||
<template #auth="{ row }">
|
||||
{{ userAuthNameMapping[(row as T).auth.id] ?? row.auth.name }}
|
||||
</template>
|
||||
<template v-if="showClubAuth" #clubAuth="{ row }">
|
||||
{{ clubAuthNameMapping[(row as T).clubAuth?.id ?? 'NONE'] ?? row.clubAuth.name }}
|
||||
</template>
|
||||
<slot></slot>
|
||||
<template v-for="{ key } of ensureArray(customCols)" v-slot:[key]="props">
|
||||
<slot :name="key" :="props" />
|
||||
</template>
|
||||
<template #action="props">
|
||||
<slot name="action" :="props"></slot>
|
||||
</template>
|
||||
</data-table>
|
||||
</template>
|
||||
<script setup lang="ts" generic="T extends UserInfo, TCustomKey extends string = never">
|
||||
import type { UserInfo } from '@/schemas/response';
|
||||
import { ensureArray } from '@/utils/array';
|
||||
import type { MaybeArray } from '@/utils/types';
|
||||
import { computed } from 'vue';
|
||||
import type { ComponentProps } from 'vue-component-type-helpers';
|
||||
import DataTable, { type CustomColumn, type DataSlotRecord } from './DataTable.vue';
|
||||
import { clubAuthNameMapping, userAuthNameMapping } from '@/router/permissions';
|
||||
const {
|
||||
customCols = [],
|
||||
showUserAuth = true,
|
||||
showClubAuth = true,
|
||||
} = defineProps<{
|
||||
rows: T[];
|
||||
customCols?: MaybeArray<CustomColumn<TCustomKey>>;
|
||||
showUserAuth?: boolean;
|
||||
showClubAuth?: boolean;
|
||||
selectable?: ComponentProps<typeof DataTable<T>>['selectable'];
|
||||
}>();
|
||||
const customColsComputed = computed(
|
||||
(): CustomColumn<'auth' | 'clubAuth' | TCustomKey | 'action'>[] => [
|
||||
{ key: 'auth' as const, label: '用户权限组', show: computed(() => showUserAuth) },
|
||||
{ key: 'clubAuth' as const, label: '社团权限组', show: computed(() => showClubAuth) },
|
||||
...ensureArray(customCols),
|
||||
{ key: 'action' as const, label: '操作' },
|
||||
],
|
||||
);
|
||||
const slots = defineSlots<
|
||||
DataSlotRecord<T, TCustomKey | 'action'> & {
|
||||
default: () => unknown;
|
||||
}
|
||||
>();
|
||||
const emit = defineEmits<{
|
||||
selectionChange: [rows: T[]];
|
||||
}>();
|
||||
function onSelect(rows: T[]) {
|
||||
emit('selectionChange', rows);
|
||||
}
|
||||
</script>
|
@ -21,7 +21,8 @@
|
||||
<div class="gobang-list-page-main__no-rooms-title">没有房间</div>
|
||||
<el-button type="primary" :loading="loading" @click="refresh">刷新</el-button>
|
||||
</template>
|
||||
<el-table v-else :data="rooms">
|
||||
<data-table v-else :rows="rooms"></data-table>
|
||||
<el-table :data="rooms">
|
||||
<el-table-column prop="briefId" label="房间ID" />
|
||||
<el-table-column prop="briefId" :formatter="getPlayerCount" label="房间人数" />
|
||||
<el-table-column label="操作">
|
||||
|
@ -24,53 +24,43 @@
|
||||
v-slot="{ data }"
|
||||
ref="clubList"
|
||||
>
|
||||
<el-table :data="data" @selection-change="onSelect">
|
||||
<el-table-column type="selection" :selectable="({ deleting }: ClubRender) => !deleting" />
|
||||
<data-table
|
||||
:rows="data"
|
||||
:custom-cols="[
|
||||
{ key: 'avatar', label: '社团头像' },
|
||||
{ key: 'commit', label: '社团介绍' },
|
||||
{ key: 'action', label: '操作', minWidth: 100 },
|
||||
]"
|
||||
:selectable="({ deleting }) => !deleting"
|
||||
@selection-change="onSelect"
|
||||
>
|
||||
<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">
|
||||
<template #avatar="{ row }">
|
||||
<club-avatar :avatar="row.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)"
|
||||
>
|
||||
</template>
|
||||
<template #commit="{ row }">
|
||||
<el-button type="primary" plain :disabled="row.deleting" @click="onClubClick(row)">
|
||||
查看介绍
|
||||
</el-button>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" v-slot="{ row }" :min-width="100">
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<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
|
||||
:disabled="(row as ClubRender).deleting"
|
||||
:disabled="row.deleting"
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'ManageClubEdit',
|
||||
params: {
|
||||
id: (row as ClubRender).id,
|
||||
id: row.id,
|
||||
},
|
||||
})
|
||||
"
|
||||
@ -78,15 +68,24 @@
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:loading="(row as ClubRender).deleting"
|
||||
@click="onDeleteButtonClick(row)"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="row.deleting"
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'ManageClubMember',
|
||||
params: { id: row.id },
|
||||
})
|
||||
"
|
||||
>
|
||||
管理成员
|
||||
</el-button>
|
||||
<el-button type="danger" :loading="row.deleting" @click="onDeleteButtonClick(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</data-table>
|
||||
</paged-wrapper>
|
||||
</el-card>
|
||||
<club-detail-dialog v-model="showClubDialog" :club="showingClub" />
|
||||
@ -113,6 +112,7 @@ import ClubDetailDialog from '@/components/club/ClubDetailDialog.vue';
|
||||
import ManageBreadcrumb from '@/components/manage/ManageBreadcrumb.vue';
|
||||
import PagedWrapper from '@/components/PagedWrapper.vue';
|
||||
import SearchInput from '@/components/SearchInput.vue';
|
||||
import DataTable from '@/components/table/DataTable.vue';
|
||||
import { type Club } from '@/schemas/response';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
|
Loading…
x
Reference in New Issue
Block a user