feat: 表格组件泛型化

This commit is contained in:
Litrix 2025-01-14 11:28:07 +08:00
parent d6fc539639
commit e2f6f1f324
5 changed files with 227 additions and 64 deletions

View File

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

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

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

View File

@ -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="操作">

View File

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