feat: toolbar justify-between, selection, and card-fill list-scroll

- 搜索按钮置于表单最右(el-form--inline 设为 flex 容器)
- ty-crud 工具条 justify-between:左新增/#toolbar、右批量 #toolbar-right(selectedRows)
- ty-table 支持 selection 多选列 + selection-change
- 卡片铺满剩余空间、列表内滚动(shell 固定视口 + el-table height=100%)
- 用户管理页加批量删除示例

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 09:46:39 +08:00
parent 1ee3ed28a5
commit bc26c0aa93
5 changed files with 148 additions and 49 deletions

View File

@ -4,20 +4,28 @@
<ty-search-form :schema="searchSchema" :model-value="query" @search="handleSearch" @reset="handleReset" />
</el-card>
<el-card shadow="never">
<el-card class="ty-crud__table" shadow="never">
<div class="ty-crud__toolbar">
<div class="ty-crud__toolbar-left">
<el-button type="primary" @click="openCreate">新增{{ entityName }}</el-button>
<slot name="toolbar" />
</div>
<div class="ty-crud__toolbar-right">
<slot name="toolbar-right" :selected-rows="selectedRows" />
</div>
</div>
<div class="ty-crud__table-body">
<ty-table
:columns="columns"
:data="list"
:loading="loading"
:selection="selection"
:total="total"
v-model:page="page"
v-model:page-size="pageSize"
:actions-width="actionsWidth"
@selection-change="onSelectionChange"
>
<template #actions="{ row }">
<el-button
@ -44,6 +52,7 @@
</el-dropdown>
</template>
</ty-table>
</div>
</el-card>
<ty-form-dialog
@ -94,6 +103,8 @@ const props = withDefaults(
actions?: CrudAction[]
/** 操作列按钮超过该数量时折叠为「更多」。 */
collapseCount?: number
/** 是否启用多选列(配合 #toolbar-right 做批量操作)。 */
selection?: boolean
actionsWidth?: number | string
}>(),
{
@ -103,6 +114,7 @@ const props = withDefaults(
editReadonlyKeys: () => [],
actions: () => ['edit', 'delete'],
collapseCount: 2,
selection: false,
actionsWidth: 220,
},
)
@ -112,6 +124,7 @@ const list = ref<Record<string, any>[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
const selectedRows = ref<Record<string, any>[]>([])
const query = reactive<Record<string, any>>(buildModel(props.searchSchema))
@ -223,15 +236,42 @@ function handleCommand(action: CrudActionItem, row: Record<string, any>): void {
action.onClick(row)
}
function onSelectionChange(rows: Record<string, any>[]): void {
selectedRows.value = rows
}
defineExpose({ reload: fetchList })
onMounted(fetchList)
</script>
<style scoped lang="scss">
.ty-crud {
@apply flex h-full flex-col;
}
.ty-crud__search {
@apply mb-md;
@apply mb-md flex-shrink-0;
}
.ty-crud__table {
@apply flex min-h-0 flex-1 flex-col;
:deep(.el-card__body) {
@apply flex min-h-0 flex-1 flex-col;
}
}
.ty-crud__toolbar {
@apply mb-md;
@apply mb-md flex flex-shrink-0 items-center justify-between;
}
.ty-crud__toolbar-left,
.ty-crud__toolbar-right {
@apply flex items-center gap-2;
}
.ty-crud__table-body {
@apply min-h-0 flex-1;
}
</style>

View File

@ -1,6 +1,14 @@
<template>
<div class="ty-table">
<el-table v-loading="loading" :data="data" border>
<div class="ty-table__body">
<el-table
v-loading="loading"
:data="data"
border
height="100%"
@selection-change="onSelectionChange"
>
<el-table-column v-if="selection" type="selection" width="48" />
<el-table-column
v-for="col in columns"
:key="col.prop"
@ -29,6 +37,7 @@
</template>
</el-table-column>
</el-table>
</div>
<el-pagination
v-if="total > 0"
@ -57,6 +66,8 @@ const props = withDefaults(
/** 表格数据。 */
data: Record<string, any>[]
loading?: boolean
/** 是否启用多选列。 */
selection?: boolean
/** 总条数(分页用)。 */
total?: number
page?: number
@ -67,6 +78,7 @@ const props = withDefaults(
}>(),
{
loading: false,
selection: false,
total: 0,
page: 1,
pageSize: 10,
@ -78,6 +90,7 @@ const props = withDefaults(
const emit = defineEmits<{
(e: 'update:page', page: number): void
(e: 'update:pageSize', pageSize: number): void
(e: 'selection-change', rows: Record<string, any>[]): void
}>()
const currentPage = computed({
@ -96,6 +109,10 @@ function onSizeChange(size: number): void {
emit('update:pageSize', size)
}
function onSelectionChange(rows: Record<string, any>[]): void {
emit('selection-change', rows)
}
function formatDateTime(value: unknown): string {
if (value == null || value === '') return '-'
const d = new Date(Number(value))
@ -105,7 +122,15 @@ function formatDateTime(value: unknown): string {
</script>
<style scoped lang="scss">
.ty-table {
@apply flex h-full flex-col;
}
.ty-table__body {
@apply min-h-0 flex-1;
}
.ty-table__pagination {
@apply mt-md justify-end;
@apply mt-md flex-shrink-0 justify-end;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="app-shell" :style="cssVars">
<div class="app-shell" :class="{ 'is-not-fixed': !app.headerFixed }" :style="cssVars">
<ty-header
:title="title"
:logo="logo"
@ -245,7 +245,14 @@ watch(
.app-shell {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
&.is-not-fixed {
height: auto;
min-height: 100vh;
overflow: visible;
}
}
.app-body {
@ -260,11 +267,18 @@ watch(
flex: 1;
flex-direction: column;
min-width: 0;
min-height: 0;
}
.app-main {
flex: 1;
min-height: 0;
padding: 16px;
overflow-y: auto;
background: var(--wm-bg-color-page);
.is-not-fixed & {
overflow-y: visible;
}
}
</style>

View File

@ -1,6 +1,8 @@
<template>
<ty-crud
ref="crudRef"
entity-name="用户"
selection
:columns="columns"
:search-schema="searchSchema"
:form-schema="formSchema"
@ -12,7 +14,13 @@
id-key="id"
name-key="username"
:edit-readonly-keys="['username']"
/>
>
<template #toolbar-right="{ selectedRows }">
<el-button type="danger" :disabled="selectedRows.length === 0" @click="batchDelete(selectedRows)">
批量删除
</el-button>
</template>
</ty-crud>
<el-dialog v-model="resetPwd.visible" title="重置密码" width="420px" destroy-on-close>
<el-form label-width="80px">
@ -29,8 +37,8 @@
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { CrudAction, JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
const STATUS_MAP = {
@ -196,4 +204,16 @@ async function submitResetPwd(): Promise<void> {
ElMessage.success('密码已重置')
resetPwd.visible = false
}
const crudRef = ref()
async function batchDelete(rows: Record<string, any>[]): Promise<void> {
await ElMessageBox.confirm(`确认删除选中的 ${rows.length} 个用户?`, '提示', { type: 'warning' })
for (const r of rows) {
const i = users.findIndex((x) => x.id === r.id)
if (i >= 0) users.splice(i, 1)
}
ElMessage.success('删除成功')
crudRef.value?.reload()
}
</script>

File diff suppressed because one or more lines are too long