Browse Source

fix(table-v2): extract timestamp display and align text

dev
hechang27-sprt 3 months ago
parent
commit
e7d26a4991
  1. 93
      packages/base/data/list-table-v2/list-table-v2.vue
  2. 32
      packages/base/data/list-table-v2/types.ts
  3. 19
      packages/base/data/list-table-v2/usePretextColumnWidths.ts
  4. 5
      packages/base/data/list-table-v2/usePretextRowHeights.ts
  5. 59
      packages/base/item/tzDateTime.vue
  6. 1
      plugs/composables/index.ts
  7. 123
      plugs/composables/useTimestampDisplay.ts

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

@ -19,13 +19,7 @@
<div>Column widths: {{ columnWidths.map((w) => Math.round(w)).join(",") }}</div> <div>Column widths: {{ columnWidths.map((w) => Math.round(w)).join(",") }}</div>
<div> <div>
Column configs: Column configs:
{{ {{ computedConfigs }}
computedConfigs.map((c) => ({
key: c.key,
flexBasis: Math.round(c.flexBasis),
flexGrow: c.flexGrow.toFixed(1),
}))
}}
</div> </div>
</div> </div>
@ -35,7 +29,7 @@
<div class="table-header" :style="{ height: `${resolvedHeaderHeight}px` }"> <div class="table-header" :style="{ height: `${resolvedHeaderHeight}px` }">
<div v-for="col in visibleColumns" :key="col.key" class="header-cell" :style="getColumnStyle(col)"> <div v-for="col in visibleColumns" :key="col.key" class="header-cell" :style="getColumnStyle(col)">
<slot :name="`header-${col.key}`"> <slot :name="`header-${col.key}`">
<span class="header-cell-text">{{ getHeaderText(col) }}</span> <span class="header-cell-text" :style="getTextStyle(col)">{{ getHeaderText(col) }}</span>
</slot> </slot>
</div> </div>
</div> </div>
@ -63,7 +57,9 @@
@click="handleCellClick(row.index, col)" @click="handleCellClick(row.index, col)"
> >
<slot :name="col.key" :row="getRow(row.index)"> <slot :name="col.key" :row="getRow(row.index)">
<span class="cell-text">{{ getRow(row.index) ? formatCellValue(getRow(row.index)!, col) : '--' }}</span> <span class="cell-text" :style="getTextStyle(col)">{{
getRow(row.index) ? formatCellValue(getRow(row.index)!, col) : "--"
}}</span>
</slot> </slot>
</div> </div>
</div> </div>
@ -88,18 +84,20 @@
</template> </template>
<script lang="tsx" setup generic="T"> <script lang="tsx" setup generic="T">
import { ref, computed, watch, onMounted, onUnmounted, toRef, useSlots, h } from "vue"; import { ref, computed, onMounted, onUnmounted, toRef, StyleValue, CSSProperties } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import { useI18n } from "vue3-i18n"; import { useI18n } from "vue3-i18n";
import * as lodash from "lodash-es"; import * as lodash from "lodash-es";
import type { ListTableColumn, ListTableProps, PageResponse } from "./types"; import type { ListTableColumn, PageResponse } from "./types";
import { usePretextColumnWidths, type ColumnFlexConfig } from "./usePretextColumnWidths"; import { usePretextColumnWidths } from "./usePretextColumnWidths";
import { resolveRowHeights, type RowHeightEntry } from "./usePretextRowHeights"; import { resolveRowHeights } from "./usePretextRowHeights";
import { useVirtualRows } from "./useVirtualRows"; import { useVirtualRows } from "./useVirtualRows";
import { formatTimestampFromValue } from "../../../../plugs/composables";
import { match } from "ts-pattern";
const DEFAULT_FONT = "14px sans-serif"; const DEFAULT_FONT = "14px sans-serif";
const DEFAULT_TEXT_MAX_WIDTH = 400;
const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const { state } = useStore(); const { state } = useStore();
@ -128,7 +126,6 @@ interface Props {
treeProps?: any; treeProps?: any;
lazy?: boolean; lazy?: boolean;
border?: boolean; border?: boolean;
timestampFormat?: string;
rowHeight?: number; rowHeight?: number;
estimatedRowHeight?: number; estimatedRowHeight?: number;
headerHeight?: number; headerHeight?: number;
@ -149,7 +146,6 @@ const props = withDefaults(defineProps<Props>(), {
treeProps: undefined, treeProps: undefined,
lazy: false, lazy: false,
border: false, border: false,
timestampFormat: "YYYY-MM-DD HH:mm:ss",
rowHeight: undefined, rowHeight: undefined,
estimatedRowHeight: undefined, estimatedRowHeight: undefined,
headerHeight: 44, headerHeight: 44,
@ -204,7 +200,10 @@ const formatCellValue = (row: T, col: ListTableColumn<T>): string => {
return formatterByDist(col.dict, value); return formatterByDist(col.dict, value);
} }
if (col.timestamp) { if (col.timestamp) {
return formatStamp(value); if ((typeof value !== "string" && typeof value !== "number") || value === "") {
return "--";
}
return formatTimestampFromValue(value, col.timestamp);
} }
if (col.filesize) { if (col.filesize) {
return formatFileSize(value); return formatFileSize(value);
@ -223,16 +222,6 @@ const getValue = (value: any): string => {
return value; 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 formatFileSize = (value: any): string => {
const k = value / 1024; const k = value / 1024;
if (k < 1) return value + "B"; if (k < 1) return value + "B";
@ -267,12 +256,19 @@ const { computedConfigs, totalFlexBasis, columnWidths } = usePretextColumnWidths
); );
// Row heights via pretext - use actual column widths from flexbox algorithm // Row heights via pretext - use actual column widths from flexbox algorithm
const { rowHeights, cellHeights } = resolveRowHeights(pageDataRef, columnsRef, columnWidths, formatCellValue, { const { rowHeights, cellHeights } = resolveRowHeights(
font: props.font, pageDataRef,
lineHeight: 20, columnsRef,
debug: props.debug, columnWidths,
fixedRowHeight: props.rowHeight, DEFAULT_TEXT_MAX_WIDTH,
}); formatCellValue,
{
font: props.font,
lineHeight: 20,
debug: props.debug,
fixedRowHeight: props.rowHeight,
}
);
// Virtualizer (needs rowHeights as number[] and viewportHeight) // Virtualizer (needs rowHeights as number[] and viewportHeight)
const virtualizer = useVirtualRows(rowHeights, viewportHeight, { overscan: 5 }); const virtualizer = useVirtualRows(rowHeights, viewportHeight, { overscan: 5 });
@ -291,7 +287,7 @@ const visibleColumns = computed(() => {
// Computed styles // Computed styles
// ============================================================================= // =============================================================================
const tableContainerStyle = computed(() => { const tableContainerStyle = computed(() => {
const style: Record<string, string> = { const style: CSSProperties = {
display: "flex", display: "flex",
flexDirection: "column" as const, flexDirection: "column" as const,
width: "100%", width: "100%",
@ -311,15 +307,24 @@ const tableContainerStyle = computed(() => {
// ============================================================================= // =============================================================================
// Column styling // Column styling
// ============================================================================= // =============================================================================
function getColumnStyle(col: ListTableColumn<T>): Record<string, string> { function getColumnStyle(col: ListTableColumn<T>): CSSProperties {
const config = computedConfigs.value.find((c) => c.key === col.key); const config = computedConfigs.value.find((c) => c.key === col.key);
const explicitWidth = col.width !== undefined && col.width !== "auto" ? Number(col.width) : undefined; const explicitWidth = col.width !== undefined && col.width !== "auto" ? Number(col.width) : undefined;
const explicitMaxWidth =
col.maxWidth !== undefined && Number.isFinite(Number(col.maxWidth)) ? Number(col.maxWidth) : undefined;
const align = match(col.align)
.returnType<CSSProperties>()
.with("left", () => ({ alignSelf: "flex-start", textAlign: "left" }))
.with("right", () => ({ alignSelf: "flex-end", textAlign: "right" }))
.otherwise(() => ({ textAlign: "center" }));
if (explicitWidth !== undefined && Number.isFinite(explicitWidth) && explicitWidth > 0) { if (explicitWidth !== undefined && Number.isFinite(explicitWidth) && explicitWidth > 0) {
return { return {
flex: `0 0 ${explicitWidth}px`, flex: `0 0 ${explicitWidth}px`,
width: `${explicitWidth}px`, width: `${explicitWidth}px`,
minWidth: `${explicitWidth}px`, minWidth: `${explicitWidth}px`,
maxWidth: `${explicitWidth}px`, ...align,
...(explicitMaxWidth !== undefined ? { maxWidth: `${explicitMaxWidth}px` } : {}),
}; };
} }
@ -334,7 +339,8 @@ function getColumnStyle(col: ListTableColumn<T>): Record<string, string> {
flexShrink: "1.1", flexShrink: "1.1",
flexBasis: `${fallbackBasis}px`, flexBasis: `${fallbackBasis}px`,
minWidth: `${Math.max(fallbackBasis, 50)}px`, minWidth: `${Math.max(fallbackBasis, 50)}px`,
maxWidth: "300px", ...align,
...(explicitMaxWidth !== undefined ? { maxWidth: `${explicitMaxWidth}px` } : {}),
}; };
} }
@ -346,7 +352,14 @@ function getColumnStyle(col: ListTableColumn<T>): Record<string, string> {
flexShrink: `${config.flexShrink}`, flexShrink: `${config.flexShrink}`,
flexBasis: `${flexBasis}px`, flexBasis: `${flexBasis}px`,
minWidth: `${config.minWidth}px`, minWidth: `${config.minWidth}px`,
maxWidth: `${config.maxWidth}px`, ...align,
...(explicitMaxWidth !== undefined ? { maxWidth: `${explicitMaxWidth}px` } : {}),
};
}
function getTextStyle(col: ListTableColumn<T>): CSSProperties {
return {
maxWidth: `${col.maxTextWidth ?? DEFAULT_TEXT_MAX_WIDTH}px`,
}; };
} }
@ -484,7 +497,6 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: center;
color: v-bind("state.style.tableColor"); color: v-bind("state.style.tableColor");
font-weight: bold; font-weight: bold;
font-size: var(--el-font-size-base); font-size: var(--el-font-size-base);
@ -521,7 +533,9 @@ onUnmounted(() => {
.table-cell { .table-cell {
display: flex; display: flex;
flex: 1 1 120px; flex: 1 1 120px;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
padding: 8px 4px; padding: 8px 4px;
box-sizing: border-box; box-sizing: border-box;
border-right: 1px solid v-bind("state.style.tableBorderColor"); border-right: 1px solid v-bind("state.style.tableBorderColor");
@ -542,7 +556,6 @@ onUnmounted(() => {
text-overflow: clip; text-overflow: clip;
white-space: normal; white-space: normal;
word-break: break-word; word-break: break-word;
text-align: center;
color: v-bind("state.style.tableColor"); color: v-bind("state.style.tableColor");
font: v-bind("props.font"); font: v-bind("props.font");
} }

32
packages/base/data/list-table-v2/types.ts

@ -2,6 +2,7 @@
* Type definitions for list-table-v2 component * Type definitions for list-table-v2 component
*/ */
import type { CellRenderer, HeaderCellRenderer } from "element-plus/es/components/table-v2/src/types.mjs"; import type { CellRenderer, HeaderCellRenderer } from "element-plus/es/components/table-v2/src/types.mjs";
import type { TimestampDisplayConfig, TimestampSimpleConfig } from "../../../../plugs/composables";
// ============================================================================= // =============================================================================
// Column Types // Column Types
@ -29,9 +30,12 @@ export interface ListTableColumn<T = any> {
/** Minimum column width */ /** Minimum column width */
minWidth?: string | number; minWidth?: string | number;
/** Maximum column width (default 300px) */ /** Maximum column width for the cell container */
maxWidth?: string | number; maxWidth?: string | number;
/** Maximum width in px for header/body text content before wrapping */
maxTextWidth?: number;
/** Flex grow factor (computed from variance if not set) */ /** Flex grow factor (computed from variance if not set) */
flexGrow?: number; flexGrow?: number;
@ -54,7 +58,7 @@ export interface ListTableColumn<T = any> {
dict?: string; dict?: string;
/** Timestamp formatting configuration */ /** Timestamp formatting configuration */
timestamp?: TimestampValue; timestamp?: TimestampSimpleConfig;
/** Format as file size */ /** Format as file size */
filesize?: boolean; filesize?: boolean;
@ -73,25 +77,8 @@ export interface ListTableColumn<T = any> {
// Timestamp Types // Timestamp Types
// ============================================================================= // =============================================================================
export type TimestampValue = export type { TimestampSimpleConfig as TimestampValue };
| undefined export type TzDateTimeConfig = TimestampDisplayConfig;
| boolean
| string
| {
valueFormat?: string;
valueTz?: string;
displayFormat?: string;
locale?: string;
type?: "iso8601" | "unix" | "unixMillis";
};
export interface TzDateTimeConfig {
valueFormat?: string;
valueTz?: string;
displayFormat?: string;
locale?: string;
type?: "iso8601" | "unix" | "unixMillis";
}
// ============================================================================= // =============================================================================
// Table Props // Table Props
@ -137,9 +124,6 @@ export interface ListTableProps<T = any> {
/** Show border */ /** Show border */
border?: boolean; border?: boolean;
/** Default timestamp format */
timestampFormat?: string;
/** Fixed row height (enables fixed-height mode) */ /** Fixed row height (enables fixed-height mode) */
rowHeight?: number; rowHeight?: number;

19
packages/base/data/list-table-v2/usePretextColumnWidths.ts

@ -5,7 +5,7 @@
* - flexBasis: mean (μ) of measured widths + padding * - flexBasis: mean (μ) of measured widths + padding
* - flexGrow/flexShrink: derived from variance (σ²) * - flexGrow/flexShrink: derived from variance (σ²)
* - minWidth: max(μ - 2*σ, 50) + padding * - minWidth: max(μ - 2*σ, 50) + padding
* - maxWidth: 300px * - maxWidth: unconstrained by default
* *
* User can override any parameter via column definition. * User can override any parameter via column definition.
*/ */
@ -17,7 +17,7 @@ const DEFAULT_FONT = "14px Inter, sans-serif";
const DEFAULT_HEADER_FONT = "bold 14px Inter, sans-serif"; const DEFAULT_HEADER_FONT = "bold 14px Inter, sans-serif";
const DEFAULT_PADDING = 16; const DEFAULT_PADDING = 16;
const CELL_PADDING = DEFAULT_PADDING * 2; // left + right const CELL_PADDING = DEFAULT_PADDING * 2; // left + right
const MAX_WIDTH = 300; const MAX_WIDTH = Number.POSITIVE_INFINITY;
const MIN_BASE_WIDTH = 50; const MIN_BASE_WIDTH = 50;
const MIN_AUTO_FLEX = 1.1; const MIN_AUTO_FLEX = 1.1;
const MAX_AUTO_FLEX = 2; const MAX_AUTO_FLEX = 2;
@ -110,7 +110,7 @@ export function computeFlexWidths(configs: ColumnFlexConfig[], containerWidth: n
const totalFlexGrow = configs.reduce((sum, c) => sum + c.flexGrow * c.flexBasis, 0); const totalFlexGrow = configs.reduce((sum, c) => sum + c.flexGrow * c.flexBasis, 0);
if (totalFlexGrow > 0) { if (totalFlexGrow > 0) {
widths = widths.map((w, i) => { widths = widths.map((w, i) => {
const growAmount = (configs[i].flexGrow * configs[i].flexBasis) / totalFlexGrow * remaining; const growAmount = ((configs[i].flexGrow * configs[i].flexBasis) / totalFlexGrow) * remaining;
return w + growAmount; return w + growAmount;
}); });
} }
@ -134,7 +134,7 @@ export function computeFlexWidths(configs: ColumnFlexConfig[], containerWidth: n
// Reduce proportionally based on how much each column can be reduced // Reduce proportionally based on how much each column can be reduced
widths = widths.map((w, i) => { widths = widths.map((w, i) => {
const reduction = reducible[i] / totalReducible * overflow; const reduction = (reducible[i] / totalReducible) * overflow;
return Math.max(w - reduction, configs[i].minWidth); return Math.max(w - reduction, configs[i].minWidth);
}); });
} }
@ -162,7 +162,7 @@ function computeRawFlexWidths(configs: ColumnFlexConfig[], containerWidth: numbe
return configs.map((c) => c.flexBasis); return configs.map((c) => c.flexBasis);
} }
return configs.map((c) => { return configs.map((c) => {
const growAmount = (c.flexGrow * c.flexBasis) / totalGrowFactor * availableSpace; const growAmount = ((c.flexGrow * c.flexBasis) / totalGrowFactor) * availableSpace;
return c.flexBasis + growAmount; return c.flexBasis + growAmount;
}); });
} else { } else {
@ -170,11 +170,11 @@ function computeRawFlexWidths(configs: ColumnFlexConfig[], containerWidth: numbe
const totalShrinkFactor = configs.reduce((sum, c) => sum + c.flexShrink * c.flexBasis, 0); const totalShrinkFactor = configs.reduce((sum, c) => sum + c.flexShrink * c.flexBasis, 0);
if (totalShrinkFactor === 0) { if (totalShrinkFactor === 0) {
// No shrink - distribute loss evenly // No shrink - distribute loss evenly
const evenShrink = (-availableSpace) / configs.length; const evenShrink = -availableSpace / configs.length;
return configs.map((c) => Math.max(0, c.flexBasis - evenShrink)); return configs.map((c) => Math.max(0, c.flexBasis - evenShrink));
} }
return configs.map((c) => { return configs.map((c) => {
const shrinkAmount = (c.flexShrink * c.flexBasis) / totalShrinkFactor * (-availableSpace); const shrinkAmount = ((c.flexShrink * c.flexBasis) / totalShrinkFactor) * -availableSpace;
return Math.max(0, c.flexBasis - shrinkAmount); return Math.max(0, c.flexBasis - shrinkAmount);
}); });
} }
@ -214,7 +214,8 @@ export function usePretextColumnWidths<T>(
const userFlexShrink = col.flexShrink; const userFlexShrink = col.flexShrink;
const userFlexBasis = col.flexBasis !== undefined && col.flexBasis !== "auto" ? Number(col.flexBasis) : undefined; const userFlexBasis = col.flexBasis !== undefined && col.flexBasis !== "auto" ? Number(col.flexBasis) : undefined;
const userMinWidth = col.minWidth !== undefined ? Number(col.minWidth) : undefined; const userMinWidth = col.minWidth !== undefined ? Number(col.minWidth) : undefined;
const userMaxWidth = col.maxWidth !== undefined ? Number(col.maxWidth) : undefined; const userMaxWidth =
col.maxWidth !== undefined && Number.isFinite(Number(col.maxWidth)) ? Number(col.maxWidth) : undefined;
// Measure header width // Measure header width
const headerText = col.name || col.i18n || colKey; const headerText = col.name || col.i18n || colKey;
@ -240,7 +241,7 @@ export function usePretextColumnWidths<T>(
const minWidth = userMinWidth const minWidth = userMinWidth
? Number(userMinWidth) ? Number(userMinWidth)
: Math.max(mean - 2 * Math.sqrt(variance), MIN_BASE_WIDTH) + CELL_PADDING; : Math.max(mean - 2 * Math.sqrt(variance), MIN_BASE_WIDTH) + CELL_PADDING;
const maxWidth = userMaxWidth ? Number(userMaxWidth) : MAX_WIDTH; const maxWidth = userMaxWidth ?? MAX_WIDTH;
const flexGrow = userFlexGrow ?? varianceToFlex(mean, variance); const flexGrow = userFlexGrow ?? varianceToFlex(mean, variance);
const flexShrink = userFlexShrink ?? varianceToFlex(mean, variance); const flexShrink = userFlexShrink ?? varianceToFlex(mean, variance);

5
packages/base/data/list-table-v2/usePretextRowHeights.ts

@ -24,6 +24,7 @@ export function resolveRowHeights<T>(
data: Ref<T[]>, data: Ref<T[]>,
columns: Ref<ListTableColumn<T>[]>, columns: Ref<ListTableColumn<T>[]>,
columnWidths: Ref<number[]>, columnWidths: Ref<number[]>,
lineMaxWidth: number,
formatCellValue: (row: T, col: ListTableColumn<T>) => string, formatCellValue: (row: T, col: ListTableColumn<T>) => string,
options?: { options?: {
font?: string; font?: string;
@ -54,7 +55,7 @@ export function resolveRowHeights<T>(
const col = columns.value[i]; const col = columns.value[i];
// Use flex basis width for height calculation as an approximation // Use flex basis width for height calculation as an approximation
// of actual column width (CSS flex layout determines actual width) // of actual column width (CSS flex layout determines actual width)
const colWidth = columnWidths.value[i] ?? 100; const colWidth = columnWidths.value[i] ?? 120;
// Check if custom renderer exists (we can't measure these with pretext) // Check if custom renderer exists (we can't measure these with pretext)
const isCustomRenderer = !!(col.cellRenderer || col.slot); const isCustomRenderer = !!(col.cellRenderer || col.slot);
@ -76,7 +77,7 @@ export function resolveRowHeights<T>(
if (!cellText) throw "Invalid Cell Text"; if (!cellText) throw "Invalid Cell Text";
// Calculate available width for text (excluding cell padding) // Calculate available width for text (excluding cell padding)
const availableWidth = colWidth - CELL_VERTICAL_PADDING * 2; const availableWidth = Math.min(colWidth - CELL_VERTICAL_PADDING * 2, col.maxTextWidth ?? lineMaxWidth);
if (availableWidth <= 0) throw "Invalid Column Width"; if (availableWidth <= 0) throw "Invalid Column Width";
rowHeightEntry = { rowHeightEntry = {

59
packages/base/item/tzDateTime.vue

@ -3,55 +3,26 @@
<template v-else>{{ display }}</template> <template v-else>{{ display }}</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from "dayjs"; import {
import { computed, toRef } from "vue"; useTimestampDisplay,
import { watchEffectOnce } from "noob-mengyxu"; type TimestampDisplayConfig,
type TimestampDisplayType,
} from "../../../plugs/composables";
interface Props { interface Props extends TimestampDisplayConfig {
value: string | number; value: string | number;
valueFormat?: string; type?: TimestampDisplayType;
valueTz?: string;
displayFormat?: string;
locale?: string;
type?: "iso8601" | "unix" | "unixMillis";
slot?: boolean; slot?: boolean;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
watchEffectOnce(toRef(props, "valueFormat"), async () => { const { display } = useTimestampDisplay(() => ({
const customParseFormat = await import("dayjs/plugin/customParseFormat"); value: props.value,
dayjs.extend(customParseFormat.default); valueFormat: props.valueFormat,
}); valueTz: props.valueTz,
displayFormat: props.displayFormat,
watchEffectOnce(toRef(props, "valueTz"), async () => { locale: props.locale,
const timeZone = await import("dayjs/plugin/timezone"); type: props.type,
dayjs.extend(timeZone.default); }));
});
const parsed = computed(() => {
if (props.type === "unix" || props.type === "unixMillis") {
const value = typeof props.value === "string" ? parseFloat(props.value) : props.value;
return props.type === "unix" ? dayjs.unix(value) : dayjs(value);
} else {
if (props.valueFormat && props.valueTz) {
return dayjs.tz(props.value, props.valueFormat, props.valueTz);
} else if (props.valueTz) {
return dayjs.tz(props.value, props.valueTz);
} else if (props.valueFormat) {
return dayjs(props.value, props.valueFormat);
} else {
return dayjs(props.value);
}
}
});
const display = computed(() => {
let dt = parsed.value;
if (props.locale) {
dt = dt.locale(props.locale);
}
return dt.format(props.displayFormat);
});
</script> </script>

1
plugs/composables/index.ts

@ -5,3 +5,4 @@ export * from "./useModifyForm";
export * from "./useSysDict"; export * from "./useSysDict";
export * from "./useWatchOnce"; export * from "./useWatchOnce";
export * from "./useRefreshFlags"; export * from "./useRefreshFlags";
export * from "./useTimestampDisplay";

123
plugs/composables/useTimestampDisplay.ts

@ -0,0 +1,123 @@
import dayjs, { type Dayjs } from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { match, P } from "ts-pattern";
import { computed, toValue, type MaybeRefOrGetter } from "vue";
dayjs.extend(customParseFormat);
dayjs.extend(utc);
dayjs.extend(timezone);
export type TimestampDisplayType = "iso8601" | "unix" | "unixMillis";
export interface TimestampDisplayConfig {
valueFormat?: string;
valueTz?: string;
displayFormat?: string;
locale?: string;
type?: TimestampDisplayType;
}
export interface TimestampDisplayInput extends TimestampDisplayConfig {
value: string | number;
}
export type TimestampSimpleConfig = undefined | boolean | string | TimestampDisplayConfig;
export const DEFAULT_TIMESTAMP_DISPLAY_FORMAT = "YYYY-MM-DD HH:mm:ss";
function withDefaultDisplayFormat(
config: TimestampDisplayConfig,
defaultDisplayFormat?: string
): TimestampDisplayConfig {
if (!defaultDisplayFormat || config.displayFormat) {
return config;
}
return {
...config,
displayFormat: defaultDisplayFormat,
};
}
export function normalizeTimestampValue(
timestamp: TimestampSimpleConfig,
defaultDisplayFormat?: string
): TimestampDisplayConfig | null {
return match(timestamp)
.with(P.nullish, () => null)
.with(true, () => withDefaultDisplayFormat({ type: "unixMillis" }, defaultDisplayFormat))
.with(P.union("iso8601", "unix", "unixMillis"), (type) => withDefaultDisplayFormat({ type }, defaultDisplayFormat))
.with(P.string, (valueFormat) => withDefaultDisplayFormat({ valueFormat }, defaultDisplayFormat))
.otherwise((config) => withDefaultDisplayFormat({ ...config }, defaultDisplayFormat));
}
export function parseTimestampValue(value: string | number, config: TimestampDisplayConfig): Dayjs {
const numericValue = match(value)
.with(P.string, parseFloat)
.otherwise((n) => n);
if (!isNaN(numericValue)) {
if (config.type == "unix") return dayjs.unix(numericValue);
if (config.type == "unixMillis") return dayjs(numericValue);
}
if (config.valueFormat && config.valueTz) {
return dayjs.tz(value, config.valueFormat, config.valueTz);
}
if (config.valueTz) {
return dayjs.tz(value, config.valueTz);
}
if (config.valueFormat) {
return dayjs(value, config.valueFormat);
}
return dayjs(value);
}
export function formatTimestampDisplay(value: string | number, config: TimestampDisplayConfig): string {
let parsed = parseTimestampValue(value, config);
if (!parsed.isValid()) {
return String(value);
}
if (config.locale) {
parsed = parsed.locale(config.locale);
}
return config.displayFormat ? parsed.format(config.displayFormat) : parsed.format();
}
export function formatTimestampFromValue(
value: string | number,
timestamp: TimestampSimpleConfig,
defaultDisplayFormat = DEFAULT_TIMESTAMP_DISPLAY_FORMAT
): string {
const config = normalizeTimestampValue(timestamp, defaultDisplayFormat);
if (!config) {
return String(value);
}
return formatTimestampDisplay(value, config);
}
export function useTimestampDisplay(input: MaybeRefOrGetter<TimestampDisplayInput>) {
const parsed = computed(() => {
const resolved = toValue(input);
return parseTimestampValue(resolved.value, resolved);
});
const display = computed(() => {
const resolved = toValue(input);
return formatTimestampDisplay(resolved.value, resolved);
});
return {
parsed,
display,
};
}
Loading…
Cancel
Save