Browse Source

WIP: PR2

dev
hechang27-sprt 3 months ago
parent
commit
e08a80633c
  1. 519
      packages/base/data/list-table-v2/list-table-v2.vue
  2. 16
      packages/base/data/list-table-v2/useVirtualRows.ts
  3. 2
      packages/base/index.ts

519
packages/base/data/list-table-v2/list-table-v2.vue

@ -0,0 +1,519 @@
<template>
<div class="list-table-v2">
<!-- Debug info -->
<div v-if="debug" class="debug-info">
<div>Container Width: {{ containerWidth }}</div>
<div>Total Height: {{ virtualTotalHeight }}</div>
<div>Visible Rows: {{ visibleRows.length }}</div>
<div>Total Rows: {{ pageData.length }}</div>
<div>
Column configs:
{{
computedConfigs.map((c) => ({
key: c.key,
flexBasis: Math.round(c.flexBasis),
flexGrow: c.flexGrow.toFixed(1),
}))
}}
</div>
</div>
<!-- Main table container with ResizeObserver for width tracking -->
<div ref="tableContainerRef" class="table-container" :style="tableContainerStyle">
<!-- Header row (fixed) -->
<div class="table-header" :style="{ height: `${resolvedHeaderHeight}px` }">
<div v-for="col in visibleColumns" :key="col.key" class="header-cell" :style="getColumnStyle(col)">
<slot :name="`header-${col.key}`">
<span class="header-cell-text">{{ getHeaderText(col) }}</span>
</slot>
</div>
</div>
<!-- Scrollable body -->
<div ref="scrollBodyRef" class="table-body" :style="{ height: `${viewportHeight}px` }" @scroll="handleScroll">
<!-- Total height spacer -->
<div class="table-spacer" :style="{ height: `${virtualTotalHeight}px` }">
<!-- Visible rows -->
<div
v-for="row in visibleRows"
:key="row.index"
class="table-row"
:style="{
transform: `translateY(${row.offsetY}px)`,
height: `${row.height}px`,
}"
@click="handleRowClick(row.index)"
>
<div
v-for="col in visibleColumns"
:key="col.key"
class="table-cell"
:style="getColumnStyle(col)"
@click="handleCellClick(row.index, col)"
>
<slot :name="col.key" :row="getRow(row.index)">
<span class="cell-text">{{ getRow(row.index) ? formatCellValue(getRow(row.index)!, col) : '--' }}</span>
</slot>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div v-if="page" class="my-pagination">
<el-pagination
:small="state.size.size == 'small'"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="example.page"
:page-sizes="[10, 20, 50, 100, 200]"
:page-size="example.size"
layout="total, sizes, prev, pager, next, jumper"
:total="data.total"
/>
</div>
</div>
</template>
<script lang="tsx" setup generic="T">
import { ref, computed, watch, onMounted, onUnmounted, toRef, useSlots, h } from "vue";
import { useStore } from "vuex";
import { useI18n } from "vue3-i18n";
import * as lodash from "lodash-es";
import type { ListTableColumn, ListTableProps, PageResponse } from "./types";
import { usePretextColumnWidths, type ColumnFlexConfig } from "./usePretextColumnWidths";
import { usePretextRowHeights, type RowHeightEntry } from "./usePretextRowHeights";
import { useVirtualRows } from "./useVirtualRows";
const slots = useSlots();
const { t } = useI18n();
const { state } = useStore();
// =============================================================================
// Container refs and dimensions
// =============================================================================
const tableContainerRef = ref<HTMLElement | null>(null);
const scrollBodyRef = ref<HTMLElement | null>(null);
const containerWidth = ref(800);
const viewportHeight = ref(400);
let resizeObserver: ResizeObserver | null = null;
// =============================================================================
// Props - same interface as original list-table-v2.vue
// =============================================================================
interface Props {
data?: T[] | PageResponse<T>;
columns?: ListTableColumn<T>[];
page?: boolean;
height?: number | string;
minHeight?: number | string;
maxHeight?: number | string;
example?: { page?: number; size?: number };
rowKey?: string;
selectKey?: string;
treeProps?: any;
lazy?: boolean;
border?: boolean;
timestampFormat?: string;
rowHeight?: number;
estimatedRowHeight?: number;
headerHeight?: number;
debug?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
columns: () => [],
page: false,
height: undefined,
minHeight: 300,
maxHeight: undefined,
example: () => ({}),
rowKey: "id",
selectKey: undefined,
treeProps: undefined,
lazy: false,
border: false,
timestampFormat: "YYYY-MM-DD HH:mm:ss",
rowHeight: undefined,
estimatedRowHeight: undefined,
headerHeight: 44,
debug: false,
});
const emit = defineEmits<{
(e: "query"): void;
(e: "selection-change", selection: any[]): void;
(e: "row-click", row: T): void;
(e: "cell-click", row: T, column: ListTableColumn<T>, ...args: any[]): void;
}>();
// =============================================================================
// Data and columns as computed refs for hooks
// =============================================================================
const pageData = computed<T[]>(() => {
if (!props.data) return [];
if (props.page && (props.data as PageResponse<T>).data) {
return (props.data as PageResponse<T>).data;
}
if (Array.isArray(props.data)) {
return props.data;
}
return [];
});
// Helper function for template to safely access row data
function getRow(index: number): T | undefined {
return pageData.value[index];
}
const columnsRef = toRef(props, "columns");
// Pass computed pageData to hooks
const pageDataRef = toRef(pageData);
// =============================================================================
// Wire hooks
// =============================================================================
// Column width computation via pretext
const { computedConfigs, totalFlexBasis } = usePretextColumnWidths(pageDataRef, columnsRef, containerWidth, {
font: "14px Inter, sans-serif",
headerFont: "bold 14px Inter, sans-serif",
});
// Column widths as array of numbers (for usePretextRowHeights)
const columnWidths = computed<number[]>(() => {
return computedConfigs.value.map((c) => c.flexBasis);
});
// Row heights via pretext
const { rowHeights, totalHeight } = usePretextRowHeights(pageDataRef, columnsRef, columnWidths, {
font: "14px Inter, sans-serif",
lineHeight: 20,
rowPadding: 12,
});
// Virtualizer (needs rowHeights as number[] and viewportHeight)
const virtualizer = useVirtualRows(
computed(() => rowHeights.value.map((r) => r.height)),
viewportHeight,
{ overscan: 5 }
);
// Destructure for template auto-unwrap (Vue 3 auto-unwraps top-level refs)
const { visibleRows, totalHeight: virtualTotalHeight, onScroll } = virtualizer;
// =============================================================================
// Visible columns (non-fixed columns only for now - fixed columns in PR4)
// =============================================================================
const visibleColumns = computed(() => {
return props.columns.filter((col) => !col.fixed);
});
// =============================================================================
// Computed styles
// =============================================================================
const tableContainerStyle = computed(() => {
const style: Record<string, string> = {
display: "flex",
flexDirection: "column" as const,
width: "100%",
overflow: "hidden" as const,
};
if (props.height !== undefined) {
const h = String(props.height);
style.maxHeight = h.endsWith("px") ? h : `${h}px`;
}
// if (props.maxHeight !== undefined) {
// const mh = String(props.maxHeight);
// style.maxHeight = mh.endsWith("px") ? mh : `${mh}px`;
// }
return style;
});
// =============================================================================
// Column styling
// =============================================================================
function getColumnStyle(col: ListTableColumn<T>): Record<string, string> {
const config = computedConfigs.value.find((c) => c.key === col.key);
if (!config) {
return { flex: "1 1 100px", minWidth: "50px", maxWidth: "300px" };
}
return {
flex: `${config.flexGrow} ${config.flexShrink} ${config.flexBasis}px`,
minWidth: `${config.minWidth}px`,
maxWidth: `${config.maxWidth}px`,
};
}
// =============================================================================
// Header height resolution
// =============================================================================
const resolvedHeaderHeight = computed(() => {
return props.headerHeight ?? 44;
});
// =============================================================================
// Cell formatting
// =============================================================================
const getHeaderText = (col: ListTableColumn<T>): string => {
if (col.name) return col.name;
if (col.i18n) return t(col.i18n);
return col.key;
};
const formatCellValue = (row: T, col: ListTableColumn<T>): string => {
const value = (row as any)[col.dataKey || col.key];
if (col.dict) {
return formatterByDist(col.dict, value);
}
if (col.timestamp) {
return formatStamp(value);
}
if (col.filesize) {
return formatFileSize(value);
}
if (value === undefined || value === null || value === "") {
return "--";
}
return String(value);
};
const getValue = (value: any): string => {
if ((typeof value === "undefined" || value === null || value === "") && value !== 0) {
return "--";
}
return value;
};
const formatStamp = (value: any): string => {
const date = new Date(value * 1000);
const month = date.getMonth() < 9 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1;
const day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
const hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
const minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
const second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
return `${date.getFullYear()}-${month}-${day} ${hour}:${minute}:${second}`;
};
const formatFileSize = (value: any): string => {
const k = value / 1024;
if (k < 1) return value + "B";
const m = k / 1024;
if (m < 1) return k.toFixed(2) + "K";
const g = m / 1024;
if (g < 1) return m.toFixed(2) + "M";
return g.toFixed(2) + "G";
};
const formatterByDist = (dictKey: string, cellData: any): string => {
if (!dictKey) return getValue(cellData);
const mapping = (state as any).dict[dictKey];
if (mapping == null) return getValue(cellData);
return mapping[cellData] == null ? cellData : mapping[cellData];
};
// =============================================================================
// Scroll handling
// =============================================================================
function handleScroll(event: Event) {
const target = event.target as HTMLElement;
onScroll(target.scrollTop);
}
// =============================================================================
// Event handlers
// =============================================================================
function handleRowClick(index: number) {
const row = pageData.value[index];
if (row) {
emit("row-click", row);
}
}
function handleCellClick(index: number, col: ListTableColumn<T>) {
const row = pageData.value[index];
if (row) {
emit("cell-click", row, col, null, null);
}
}
const handleSizeChange = (val: number) => {
props.example.size = val;
emit("query");
};
const handleCurrentChange = (val: number) => {
props.example.page = val;
emit("query");
};
// =============================================================================
// ResizeObserver for container dimensions
// =============================================================================
onMounted(() => {
// Initial measurement
if (tableContainerRef.value) {
const rect = tableContainerRef.value.getBoundingClientRect();
containerWidth.value = rect.width;
viewportHeight.value = rect.height - resolvedHeaderHeight.value;
}
// ResizeObserver for container width changes
resizeObserver = new ResizeObserver(
lodash.debounce((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
containerWidth.value = width;
viewportHeight.value = height - resolvedHeaderHeight.value;
}
}, 50)
);
if (tableContainerRef.value) {
resizeObserver.observe(tableContainerRef.value);
}
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
</script>
<style lang="scss" scoped>
.debug-info {
font-size: 9px;
padding: 4px 8px;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
font-family: monospace;
white-space: pre;
}
.list-table-v2 {
display: flex;
flex-direction: column;
flex: 0 0 auto;
overflow: hidden;
position: relative;
}
.table-container {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
overflow: hidden;
}
.table-header {
display: flex;
flex-shrink: 0;
background: v-bind("state.style.tableHeadBg");
border-bottom: 1px solid v-bind("state.style.tableBorderColor");
box-sizing: border-box;
}
.header-cell {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 4px;
box-sizing: border-box;
border-right: 1px solid v-bind("state.style.tableBorderColor");
overflow: hidden;
word-break: break-word;
&:last-child {
border-right: none;
}
}
.header-cell-text {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
color: v-bind("state.style.tableColor");
font-weight: bold;
font-size: var(--el-font-size-base);
}
.table-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background: v-bind("state.style.tableBg");
position: relative;
}
.table-spacer {
position: relative;
width: 100%;
}
.table-row {
position: absolute;
left: 0;
right: 0;
display: flex;
border-bottom: 1px solid v-bind("state.style.tableBorderColor");
box-sizing: border-box;
background: v-bind("state.style.tableBg");
transition: background-color 0.15s;
&:hover {
background: v-bind("state.style.tableCurBg");
}
}
.table-cell {
display: flex;
align-items: center;
padding: 8px 4px;
box-sizing: border-box;
border-right: 1px solid v-bind("state.style.tableBorderColor");
overflow: hidden;
word-break: break-word;
min-height: 100%;
&:last-child {
border-right: none;
}
}
.cell-text {
display: block;
width: 100%;
overflow: visible;
text-overflow: clip;
white-space: normal;
word-break: break-word;
color: v-bind("state.style.tableColor");
font-size: var(--el-font-size-base);
}
.my-pagination {
display: flex;
justify-content: center;
padding-top: 5px;
flex-shrink: 0;
}
.my-pagination * {
--el-pagination-bg-color: v-bind("state.style.bodyBg") !important;
--el-pagination-disabled-bg-color: v-bind("state.style.bodyBg") !important;
--el-pagination-text-color: v-bind("state.style.color") !important;
--el-pagination-button-color: v-bind("state.style.color") !important;
--el-pagination-button-disabled-bg-color: v-bind("state.style.bodyBg") !important;
}
</style>

16
packages/base/data/list-table-v2/useVirtualRows.ts

@ -105,14 +105,24 @@ export function useVirtualRows(
// Get visible rows with their positions // Get visible rows with their positions
const visibleRows = computed<VirtualRow[]>(() => { const visibleRows = computed<VirtualRow[]>(() => {
const { startIndex, endIndex, offsetY } = range.value; const { startIndex, endIndex } = range.value;
const rows: VirtualRow[] = []; const rows: VirtualRow[] = [];
// Safety check: ensure indices are valid
if (startIndex < 0 || endIndex < startIndex) {
return rows;
}
for (let i = startIndex; i < endIndex; i++) { for (let i = startIndex; i < endIndex; i++) {
const offsetY = offsets.value[i];
const height = rowHeights.value[i];
if (offsetY === undefined || height === undefined) {
continue; // Skip invalid entries
}
rows.push({ rows.push({
index: i, index: i,
offsetY: offsets.value[i], offsetY,
height: rowHeights.value[i], height,
}); });
} }

2
packages/base/index.ts

@ -11,7 +11,7 @@ import TzDateTime from "./item/tzDateTime.vue";
import WsMonitorToggle from "./item/ws-monitor-toggle.vue"; import WsMonitorToggle from "./item/ws-monitor-toggle.vue";
import SearchRow from "./data/search-row.vue"; import SearchRow from "./data/search-row.vue";
import ListTable from "./data/list-table.vue"; import ListTable from "./data/list-table.vue";
import ListTableV2 from "./data/list-table-v2.vue"; import ListTableV2 from "./data/list-table-v2/list-table-v2.vue";
import Infomation from "./data/infomation.vue"; import Infomation from "./data/infomation.vue";
import ModifyForm from "./data/modify-form.vue"; import ModifyForm from "./data/modify-form.vue";
import Descriptions from "./data/descriptions.vue"; import Descriptions from "./data/descriptions.vue";

Loading…
Cancel
Save