refactor(grid): 提取 cellValue 格式化到共享模块并增加测试 (#218)

将 CellValue 类型和 displayCellValue(原 formatCell)提取到
src/lib/cellValue.ts,DataGrid.vue 和 useDataGridExport.ts 统一引用,
移除 composable 中冗余的 formatCell 参数传递。
This commit is contained in:
Abeautifulsnow 2026-05-11 18:56:08 +08:00 committed by GitHub
parent e4c1d6639b
commit dddb813fc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 74 additions and 13 deletions

View File

@ -63,6 +63,7 @@ import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql"
import { isHiddenGridColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { formatGridSqlLiteral } from "@/lib/dataGridSql";
import { matchesRowStatusFilter, type RowStatus, type RowStatusFilter } from "@/lib/gridRowStatus";
import { displayCellValue, type CellValue } from "@/lib/cellValue";
import { useToast } from "@/composables/useToast";
import { useDataGridExport } from "@/composables/useDataGridExport";
@ -817,7 +818,6 @@ function changePageSize(size: number) {
}
// --- Editing (composable) ---
type CellValue = string | number | boolean | null;
interface RowItem {
id: number;
@ -1041,7 +1041,7 @@ const activeCellDetail = computed(() => {
const column = props.result.columns[cell.col];
if (!item || !column) return null;
const value = item.data[cell.col] ?? null;
const rawValue = formatCell(value);
const rawValue = displayCellValue(value);
const valueText = value === null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
const trimmed = valueText.trim();
const maybeJson = typeof value === "string" && (trimmed.startsWith("{") || trimmed.startsWith("["));
@ -1339,7 +1339,6 @@ const {
selectedCells,
contextCell: exportContextCell,
getRowItem: (rowId: number) => visibleDisplayItems.value.find((item) => item.id === rowId),
formatCell,
quoteIdent,
escapeVal,
});
@ -1419,8 +1418,10 @@ async function onGridKeydown(event: KeyboardEvent) {
}
function copyDetailValue() {
if (!activeCellDetail.value) return;
copyText(activeCellDetail.value.rawValue);
const detail = activeCellDetail.value;
if (!detail) return;
const text = detail.value === null ? "" : displayCellValue(detail.value);
copyText(text);
}
function copyDetailColumnName() {
@ -2326,7 +2327,8 @@ defineExpose({
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
<pre
class="max-h-72 overflow-auto rounded border bg-muted/20 p-2 font-mono text-xs whitespace-pre-wrap break-words"
>{{ activeCellDetail.formattedJson }}</pre
>
{{ activeCellDetail.formattedJson }}</pre
>
</div>
</div>
@ -2618,9 +2620,11 @@ defineExpose({
color: oklch(0.6 0.15 250);
font-weight: 600;
}
.ddl-code :deep(.ddl-ident) {
color: oklch(0.65 0.15 150);
}
.ddl-code :deep(.ddl-str) {
color: oklch(0.65 0.15 50);
}

View File

@ -10,8 +10,7 @@ import {
type SelectionData,
} from "@/lib/gridSelection";
import { useToast } from "@/composables/useToast";
type CellValue = string | number | boolean | null;
import { displayCellValue, type CellValue } from "@/lib/cellValue";
interface RowItem {
id: number;
@ -36,7 +35,6 @@ export interface UseDataGridExportOptions {
| Ref<{ rowId: number; rowIndex: number; col: number } | null>
| ComputedRef<{ rowId: number; rowIndex: number; col: number } | null>;
getRowItem: (rowId: number) => RowItem | undefined;
formatCell: (value: CellValue) => string;
quoteIdent: (name: string) => string;
escapeVal: (value: CellValue) => string;
}
@ -54,7 +52,6 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
selectedCells,
contextCell,
getRowItem,
formatCell,
quoteIdent,
escapeVal,
} = options;
@ -90,7 +87,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
if (!contextCell.value || contextCell.value.col < 0) return;
const item = getRowItem(contextCell.value.rowId);
const val = item?.data[contextCell.value.col] ?? null;
copyText(formatCell(val));
copyText(displayCellValue(val));
}
function copyRow() {
@ -118,7 +115,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
function copyAll() {
const header = columns.value.join("\t");
const body = displayItems.value.map((item) => item.data.map((c) => formatCell(c)).join("\t")).join("\n");
const body = displayItems.value.map((item) => item.data.map((c) => displayCellValue(c)).join("\t")).join("\n");
copyText(`${header}\n${body}`);
}
@ -184,7 +181,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
// --- Export functions ---
async function exportCsv() {
try {
const rows = displayItems.value.map((item) => item.data.map((c) => formatCell(c)));
const rows = displayItems.value.map((item) => item.data.map((c) => displayCellValue(c)));
if (await saveFileContent(formatCsv(columns.value, rows), "export.csv", "CSV", "csv")) {
toast(t("grid.exported"));
}

8
src/lib/cellValue.ts Normal file
View File

@ -0,0 +1,8 @@
export type CellValue = string | number | boolean | null;
export function displayCellValue(value: CellValue): string {
if (value === null) return "NULL";
if (typeof value === "boolean") return value ? "true" : "false";
if (typeof value === "object") return JSON.stringify(value);
return String(value);
}

52
tests/cellValue.test.ts Normal file
View File

@ -0,0 +1,52 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import { displayCellValue } from "../src/lib/cellValue.ts";
test("displayCellValue returns NULL for null", () => {
assert.equal(displayCellValue(null), "NULL");
});
test("displayCellValue returns string representation for booleans", () => {
assert.equal(displayCellValue(true), "true");
assert.equal(displayCellValue(false), "false");
});
test("displayCellValue returns String for numbers", () => {
assert.equal(displayCellValue(42), "42");
assert.equal(displayCellValue(-3.14), "-3.14");
assert.equal(displayCellValue(0), "0");
});
test("displayCellValue returns String for strings", () => {
assert.equal(displayCellValue("hello"), "hello");
assert.equal(displayCellValue(""), "");
});
test("displayCellValue does not truncate long strings", () => {
const long = "a".repeat(1000);
assert.equal(displayCellValue(long), long);
});
test("displayCellValue serializes JSON strings containing complex nested objects", () => {
const payload = '{"metadata":{"version":"2.1.0","tags":["alpha","beta"],"flags":{"enabled":true,"nested":{"depth":3,"items":[{"id":1,"label":"a"},{"id":2,"label":"b"}]}}},"data":{"records":[{"key":"k-001","value":42},{"key":"k-002","value":-999}],"summary":{"total":3,"valid":2}}}';
assert.equal(displayCellValue(payload), payload);
});
test("displayCellValue handles strings with unicode and special characters", () => {
const unicodeStr = "你好世界 🚀🎉 <div>test</div> Line1\nLine2\tTabbed";
assert.equal(displayCellValue(unicodeStr), unicodeStr);
});
test("displayCellValue handles large integer and scientific notation as strings", () => {
const bigIntStr = "9007199254740991";
const sciStr = "1e-10";
assert.equal(displayCellValue(bigIntStr), bigIntStr);
assert.equal(displayCellValue(sciStr), sciStr);
});
test("displayCellValue handles empty JSON-like strings without alteration", () => {
assert.equal(displayCellValue("{}"), "{}");
assert.equal(displayCellValue("[]"), "[]");
assert.equal(displayCellValue('{"key":"value"}'), '{"key":"value"}');
assert.equal(displayCellValue("[1,2,3]"), "[1,2,3]");
});