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:
parent
1ee3ed28a5
commit
bc26c0aa93
@ -4,20 +4,28 @@
|
|||||||
<ty-search-form :schema="searchSchema" :model-value="query" @search="handleSearch" @reset="handleReset" />
|
<ty-search-form :schema="searchSchema" :model-value="query" @search="handleSearch" @reset="handleReset" />
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never">
|
<el-card class="ty-crud__table" shadow="never">
|
||||||
<div class="ty-crud__toolbar">
|
<div class="ty-crud__toolbar">
|
||||||
|
<div class="ty-crud__toolbar-left">
|
||||||
<el-button type="primary" @click="openCreate">新增{{ entityName }}</el-button>
|
<el-button type="primary" @click="openCreate">新增{{ entityName }}</el-button>
|
||||||
<slot name="toolbar" />
|
<slot name="toolbar" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ty-crud__toolbar-right">
|
||||||
|
<slot name="toolbar-right" :selected-rows="selectedRows" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ty-crud__table-body">
|
||||||
<ty-table
|
<ty-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="list"
|
:data="list"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
:selection="selection"
|
||||||
:total="total"
|
:total="total"
|
||||||
v-model:page="page"
|
v-model:page="page"
|
||||||
v-model:page-size="pageSize"
|
v-model:page-size="pageSize"
|
||||||
:actions-width="actionsWidth"
|
:actions-width="actionsWidth"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
@ -44,6 +52,7 @@
|
|||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</template>
|
</template>
|
||||||
</ty-table>
|
</ty-table>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<ty-form-dialog
|
<ty-form-dialog
|
||||||
@ -94,6 +103,8 @@ const props = withDefaults(
|
|||||||
actions?: CrudAction[]
|
actions?: CrudAction[]
|
||||||
/** 操作列按钮超过该数量时折叠为「更多」。 */
|
/** 操作列按钮超过该数量时折叠为「更多」。 */
|
||||||
collapseCount?: number
|
collapseCount?: number
|
||||||
|
/** 是否启用多选列(配合 #toolbar-right 做批量操作)。 */
|
||||||
|
selection?: boolean
|
||||||
actionsWidth?: number | string
|
actionsWidth?: number | string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@ -103,6 +114,7 @@ const props = withDefaults(
|
|||||||
editReadonlyKeys: () => [],
|
editReadonlyKeys: () => [],
|
||||||
actions: () => ['edit', 'delete'],
|
actions: () => ['edit', 'delete'],
|
||||||
collapseCount: 2,
|
collapseCount: 2,
|
||||||
|
selection: false,
|
||||||
actionsWidth: 220,
|
actionsWidth: 220,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -112,6 +124,7 @@ const list = ref<Record<string, any>[]>([])
|
|||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
const selectedRows = ref<Record<string, any>[]>([])
|
||||||
|
|
||||||
const query = reactive<Record<string, any>>(buildModel(props.searchSchema))
|
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)
|
action.onClick(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectionChange(rows: Record<string, any>[]): void {
|
||||||
|
selectedRows.value = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ reload: fetchList })
|
||||||
|
|
||||||
onMounted(fetchList)
|
onMounted(fetchList)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.ty-crud {
|
||||||
|
@apply flex h-full flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
.ty-crud__search {
|
.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 {
|
.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>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ty-table">
|
<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
|
<el-table-column
|
||||||
v-for="col in columns"
|
v-for="col in columns"
|
||||||
:key="col.prop"
|
:key="col.prop"
|
||||||
@ -29,6 +37,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-if="total > 0"
|
v-if="total > 0"
|
||||||
@ -57,6 +66,8 @@ const props = withDefaults(
|
|||||||
/** 表格数据。 */
|
/** 表格数据。 */
|
||||||
data: Record<string, any>[]
|
data: Record<string, any>[]
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
/** 是否启用多选列。 */
|
||||||
|
selection?: boolean
|
||||||
/** 总条数(分页用)。 */
|
/** 总条数(分页用)。 */
|
||||||
total?: number
|
total?: number
|
||||||
page?: number
|
page?: number
|
||||||
@ -67,6 +78,7 @@ const props = withDefaults(
|
|||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
loading: false,
|
loading: false,
|
||||||
|
selection: false,
|
||||||
total: 0,
|
total: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -78,6 +90,7 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:page', page: number): void
|
(e: 'update:page', page: number): void
|
||||||
(e: 'update:pageSize', pageSize: number): void
|
(e: 'update:pageSize', pageSize: number): void
|
||||||
|
(e: 'selection-change', rows: Record<string, any>[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const currentPage = computed({
|
const currentPage = computed({
|
||||||
@ -96,6 +109,10 @@ function onSizeChange(size: number): void {
|
|||||||
emit('update:pageSize', size)
|
emit('update:pageSize', size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectionChange(rows: Record<string, any>[]): void {
|
||||||
|
emit('selection-change', rows)
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateTime(value: unknown): string {
|
function formatDateTime(value: unknown): string {
|
||||||
if (value == null || value === '') return '-'
|
if (value == null || value === '') return '-'
|
||||||
const d = new Date(Number(value))
|
const d = new Date(Number(value))
|
||||||
@ -105,7 +122,15 @@ function formatDateTime(value: unknown): string {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.ty-table {
|
||||||
|
@apply flex h-full flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ty-table__body {
|
||||||
|
@apply min-h-0 flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
.ty-table__pagination {
|
.ty-table__pagination {
|
||||||
@apply mt-md justify-end;
|
@apply mt-md flex-shrink-0 justify-end;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-shell" :style="cssVars">
|
<div class="app-shell" :class="{ 'is-not-fixed': !app.headerFixed }" :style="cssVars">
|
||||||
<ty-header
|
<ty-header
|
||||||
:title="title"
|
:title="title"
|
||||||
:logo="logo"
|
:logo="logo"
|
||||||
@ -245,7 +245,14 @@ watch(
|
|||||||
.app-shell {
|
.app-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.is-not-fixed {
|
||||||
|
height: auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-body {
|
.app-body {
|
||||||
@ -260,11 +267,18 @@ watch(
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
background: var(--wm-bg-color-page);
|
background: var(--wm-bg-color-page);
|
||||||
|
|
||||||
|
.is-not-fixed & {
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<ty-crud
|
<ty-crud
|
||||||
|
ref="crudRef"
|
||||||
entity-name="用户"
|
entity-name="用户"
|
||||||
|
selection
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:search-schema="searchSchema"
|
:search-schema="searchSchema"
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
@ -12,7 +14,13 @@
|
|||||||
id-key="id"
|
id-key="id"
|
||||||
name-key="username"
|
name-key="username"
|
||||||
:edit-readonly-keys="['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-dialog v-model="resetPwd.visible" title="重置密码" width="420px" destroy-on-close>
|
||||||
<el-form label-width="80px">
|
<el-form label-width="80px">
|
||||||
@ -29,8 +37,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import type { CrudAction, JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
|
import type { CrudAction, JsonSchema, PageQuery, PageResult, TableColumn } from '@/types/crud'
|
||||||
|
|
||||||
const STATUS_MAP = {
|
const STATUS_MAP = {
|
||||||
@ -196,4 +204,16 @@ async function submitResetPwd(): Promise<void> {
|
|||||||
ElMessage.success('密码已重置')
|
ElMessage.success('密码已重置')
|
||||||
resetPwd.visible = false
|
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>
|
</script>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user