admin_new/src/components/DataTable/DataTable.vue

256 lines
6.9 KiB
Vue

<script setup lang="ts">
/**
* F006 通用 DataTable 组件 (W17 设计规范 v2 整改)
*
* 按 ui-preview-v2.html 模板:
* - 表头 text-transform: uppercase + 左对齐
* - 单元格紧凑 padding 12px 16px + 左对齐
* - 颜色全用 var(--xxx) 主题化
* - 表格外无卡片包裹
*
* 用法:
* <DataTable
* :columns="[
* { prop: 'id', label: '单号', width: 180 },
* { label: '操作', width: 100, slot: 'action' }
* ]"
* :data="list" :loading="loading" :total="total" :page="page" :limit="limit"
* @page-change="onPageChange"
* >
* <template #action="{ row }">
* <el-button text type="primary" size="small">查看</el-button>
* </template>
* </DataTable>
*/
import { computed } from 'vue'
interface Column {
prop?: string
label: string
width?: number | string
minWidth?: number | string
type?: 'status' | 'text' | 'selection'
statusMap?: Record<string, { label: string; type: string }>
slot?: string
formatter?: (row: any) => string
align?: 'left' | 'center' | 'right'
fixed?: 'left' | 'right' | true
}
interface Props {
columns: Column[]
data: any[]
loading?: boolean
total?: number
page?: number
limit?: number
showPagination?: boolean
pageSizes?: number[]
rowKey?: string
stripe?: boolean
borderless?: boolean
emptyText?: string
errorText?: string
error?: string | null
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
total: 0,
page: 1,
limit: 20,
showPagination: true,
pageSizes: () => [10, 20, 50, 100],
rowKey: 'id',
stripe: false,
borderless: false,
emptyText: '暂无数据',
errorText: '',
error: null
})
const emit = defineEmits<{
(e: 'row-click', row: any): void
(e: 'page-change', page: number): void
(e: 'limit-change', limit: number): void
(e: 'selection-change', rows: any[]): void
}>()
const hasStatusCol = computed(() => props.columns.some(c => c.type === 'status'))
const showEmpty = computed(() => !props.loading && props.data.length === 0)
const showPager = computed(() => props.showPagination && props.total > 0 && props.data.length > 0)
function getStatusMeta(col: Column, value: string) {
return col.statusMap?.[value] || { label: value || '--', type: 'info' }
}
function onSelectionChange(rows: any[]) { emit('selection-change', rows) }
function onRowClick(row: any) { emit('row-click', row) }
function onPageChange(p: number) { emit('page-change', p) }
function onLimitChange(l: number) { emit('limit-change', l) }
</script>
<template>
<div class="rr-data-table" :class="{ borderless }">
<el-table
:data="data"
:loading="loading"
:stripe="stripe"
:border="!borderless"
:row-key="rowKey"
style="width: 100%"
@row-click="onRowClick"
@selection-change="onSelectionChange"
row-class-name="rr-clickable-row"
:header-row-style="{ height: '40px' }"
:header-cell-style="{
background: 'var(--rr-table-header-bg, #F8FAFC)',
color: 'var(--rr-text-secondary, #64748B)',
fontWeight: '600',
fontSize: '12px',
padding: '11px 16px',
borderBottom: '1px solid var(--rr-border-color, #E2E8F0)',
textTransform: 'uppercase',
letterSpacing: '0.04em',
whiteSpace: 'nowrap',
textAlign: 'left',
height: '40px'
}"
:cell-style="{
padding: '12px 16px',
borderBottom: '1px solid var(--rr-border-color, #E2E8F0)',
color: 'var(--rr-text-secondary, #334155)',
fontSize: '13px',
textAlign: 'left',
verticalAlign: 'middle'
}"
>
<template v-for="(col, idx) in columns" :key="idx">
<el-table-column
v-if="col.type === 'selection'"
type="selection"
:width="col.width || 50"
:align="col.align || 'left'"
:fixed="col.fixed"
/>
<el-table-column
v-else
:prop="col.type === 'status' || col.slot ? undefined : col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:align="col.align || 'left'"
:fixed="col.fixed"
:show-overflow-tooltip="true"
>
<template v-if="col.slot" #default="{ row }">
<slot :name="col.slot" :row="row" />
</template>
<template v-else-if="col.type === 'status'" #default="{ row }">
<el-tag
:type="(getStatusMeta(col, row[col.prop!]).type) as any"
size="small"
effect="light"
>
{{ getStatusMeta(col, row[col.prop!]).label }}
</el-tag>
</template>
<template v-else-if="col.formatter" #default="{ row }">
{{ col.formatter?.(row) }}
</template>
</el-table-column>
</template>
<template #empty>
<div class="rr-empty">
<span v-if="error" class="rr-empty__error">{{ error || errorText }}</span>
<span v-else>{{ emptyText }}</span>
</div>
</template>
</el-table>
<div v-if="showPager" class="rr-pager">
<div class="rr-pager__info">
共 {{ total }} 条
</div>
<el-pagination
:current-page="page"
:page-size="limit"
:total="total"
:page-sizes="pageSizes"
layout="sizes, prev, pager, next, jumper"
background
@current-change="onPageChange"
@size-change="onLimitChange"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.rr-data-table {
width: 100%;
background: var(--rr-bg-card, #FFFFFF);
border: 1px solid var(--rr-border-color, #E2E8F0);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
// 行 hover 高亮
:deep(.el-table__row) {
transition: background 0.15s;
}
:deep(.el-table__row:hover > td.el-table__cell) {
background-color: var(--rr-row-hover, #F8FAFC) !important;
}
// 表格本体圆角配合容器
:deep(.el-table) {
border-radius: 10px 10px 0 0;
overflow: hidden;
}
:deep(.el-table__inner-wrapper) {
border-radius: 10px 10px 0 0;
}
// 去掉 el-table 默认的 border
:deep(.el-table--border) {
border: none;
}
:deep(.el-table::before), :deep(.el-table::after) {
display: none;
}
// borderless 模式:整体无边框
&.borderless {
border: none;
box-shadow: none;
}
}
.rr-empty {
text-align: center;
padding: 32px 0;
color: var(--ink-3, #94A3B8);
font-size: 13px;
&__error {
color: var(--red, #EF4444);
}
}
.rr-pager {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
font-size: 12.5px;
color: var(--ink-3, #94A3B8);
background: var(--rr-bg-card, #FFFFFF);
border-top: 1px solid var(--rr-border-color, #F1F5F9);
&__info { color: var(--ink-3, #94A3B8); }
:deep(.el-pagination) {
--el-pagination-bg-color: transparent;
}
:deep(.el-pager li) {
border-radius: 6px;
margin: 0 2px;
}
:deep(.el-pagination .btn-prev),
:deep(.el-pagination .btn-next) {
border-radius: 6px;
}
}
</style>