用户反馈: dark 主题下大量页面卡片/表头是白底, 文字看不清, 视觉割裂 根因 (W5.x v2 token 迁移遗漏): - v1 token (--rr-page-bg / --rr-bg-card / --rr-table-header-bg / --rr-row-hover / --rr-border-color / --rr-text-secondary) 在 themes.scss 完全没定义 - v2 用 --bg-soft / --surface / --surface-2 / --border / --ink-2 / --ink-3, 但大量组件没改 fallback, 永远走 hex fallback (#F1F5F9 / #FFFFFF) - EP el-table 内部用 getCssVar(color, white) = --el-color-white, 未在 tokens.scss 重定向, dark 下仍是 #FFFFFF - CSS 变量级联问题: :root 块 var(--surface) 解析时, dark 块的 --surface 重写还没生效 修法 (3 类共 11 文件): 1. EP token 补齐 (themes.scss dark 块 + tokens.scss :root 块): - --el-color-white / --el-color-black: #1E293B / #F8FAFC - --el-fill-color-blank / --el-fill-color-light: surface / bg-soft - --el-table-tr-bg-color / --el-table-bg-color: surface - --el-table-expanded-cell-bg-color: surface - --el-table-header-bg-color: surface-2 (表头略亮区分) - --el-table-row-hover-bg-color: bg-soft 2. v1 token 清理 (4 组件): - AppLayout.vue: 13 处 --rr-page-bg → --bg-soft - DataTable.vue: --rr-table-header-bg/--rr-text-secondary/--rr-row-hover/--rr-bg-card/--rr-border-color → v2 token - SearchBar.vue / ListTabs.vue: 同上 3. 业务页硬编码色 (5 业务页): - OpenAppListView.vue: 4 状态 badge + 表格头 #F8FAFC → var(--*-soft) - ApplyApiKeyDialog.vue: header #F8FAFC → var(--surface-2) - InquiryListView.vue / LogOperationView.vue / LogErrorView.vue: 4 类 badge → var(--amber/brand/green/red-soft) Playwright 1920 视口 dark 主题实测 (5 个典型页): - Dashboard: 侧栏 rgb(30,41,59), 主区 rgb(15,23,42), 表格 rgb(30,41,59) ✅ - 用户列表 / 应用列表 / 订单列表 / 询价列表: 全部统一深色 ✅ - 之前: elTableBg rgb(255,255,255) 纯白 → 现在: rgb(30,41,59) 暗 surface - 文字、badge、按钮、分页器、搜索条全部 dark 适配 不动调色板 (按用户要求 严重问题优先): --border/--ink-2 等保持原值
308 lines
9.0 KiB
Vue
308 lines
9.0 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, nextTick, ref, watch, onMounted } 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: 10, // W17.3.5 默认每页 10 条 (按用户原话)
|
|
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) }
|
|
|
|
// W17.8.2: 表格自动撑满 (列总宽 < 容器宽时, 给弹性列加 minWidth 填满差额)
|
|
// W17.8.2 改: 差额加到"中间弹性列" (第一个有 minWidth 且非 fixed 的列), 不是最后一列
|
|
// 原因: 最后一列通常是"操作列" (fixed: right, width 固定), 加差额会撑大操作列破坏布局
|
|
// 解决大视口下列少导致右侧大面积空白
|
|
const tableRef = ref<HTMLElement | null>(null)
|
|
const lastColMinWidth = ref<number>(0)
|
|
const flexColIdx = computed(() => {
|
|
// 优先找第一个有 minWidth 且非 fixed 的数据列 (不是 selection/status)
|
|
const idx = props.columns.findIndex(c => c.minWidth && !c.fixed && c.type !== 'selection' && c.type !== 'status')
|
|
return idx >= 0 ? idx : props.columns.length - 1
|
|
})
|
|
|
|
async function autoFillTable() {
|
|
await nextTick()
|
|
// 双重 nextTick 等待列渲染
|
|
await nextTick()
|
|
const el = tableRef.value
|
|
if (!el) return
|
|
const table = el.querySelector('.el-table') as HTMLElement | null
|
|
if (!table) return
|
|
const containerW = el.clientWidth
|
|
const tableW = table.scrollWidth
|
|
if (tableW >= containerW - 4) {
|
|
lastColMinWidth.value = 0
|
|
return
|
|
}
|
|
// 差额 + 一些 buffer (避免 border 抖动)
|
|
const diff = containerW - tableW + 4
|
|
lastColMinWidth.value = diff
|
|
}
|
|
|
|
onMounted(() => {
|
|
autoFillTable()
|
|
window.addEventListener('resize', autoFillTable)
|
|
})
|
|
|
|
watch(() => props.data, () => autoFillTable(), { flush: 'post' })
|
|
watch(() => props.columns, () => autoFillTable(), { flush: 'post' })
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="tableRef" 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: '48px' }"
|
|
:header-cell-style="{
|
|
background: 'var(--surface-2)',
|
|
color: 'var(--ink-3)',
|
|
fontWeight: '600',
|
|
fontSize: '12px',
|
|
padding: '14px 16px',
|
|
borderBottom: '1px solid var(--border)',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.04em',
|
|
whiteSpace: 'nowrap',
|
|
textAlign: 'center',
|
|
lineHeight: '20px'
|
|
}"
|
|
:cell-style="{
|
|
padding: '10px 16px',
|
|
borderBottom: '1px solid var(--border)',
|
|
color: 'var(--ink-2)',
|
|
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="idx === flexColIdx && lastColMinWidth > 0 ? Math.max(Number(col.minWidth) || 0, lastColMinWidth) : 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"
|
|
:pager-count="5"
|
|
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(--surface);
|
|
border: 1px solid var(--border);
|
|
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(--bg-soft) !important;
|
|
}
|
|
// 表格本体圆角配合容器
|
|
:deep(.el-table) {
|
|
border-radius: 10px 10px 0 0;
|
|
overflow: hidden;
|
|
}
|
|
:deep(.el-table__inner-wrapper) {
|
|
border-radius: 10px 10px 0 0;
|
|
}
|
|
// 表头单元格强制不换行 (兜底, header-cell-style 的 whiteSpace 在某些列宽挤压下失效)
|
|
:deep(.el-table__header-wrapper .cell),
|
|
:deep(.el-table__header th .cell) {
|
|
white-space: nowrap !important;
|
|
overflow: visible !important;
|
|
}
|
|
// 表头内容容器允许溢出不被截断
|
|
:deep(.el-table th .cell) {
|
|
word-break: keep-all;
|
|
}
|
|
// 去掉 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: 12px 20px;
|
|
font-size: 13px; /* W17.3.6 跟内容行一致 (13px) */
|
|
color: var(--ink-3);
|
|
background: var(--surface);
|
|
border-top: 1px solid var(--border);
|
|
&__info { color: var(--ink-3); }
|
|
:deep(.el-pagination) {
|
|
--el-pagination-bg-color: transparent;
|
|
--el-pagination-font-size: 13px; /* W17.3.6 跟内容行 cell 字号一致 (13px) */
|
|
font-size: 13px;
|
|
}
|
|
:deep(.el-pager li) {
|
|
border-radius: 6px;
|
|
margin: 0 2px;
|
|
}
|
|
:deep(.el-pagination .btn-prev),
|
|
:deep(.el-pagination .btn-next) {
|
|
border-radius: 6px;
|
|
}
|
|
}
|
|
</style>
|