diff --git a/packages/components/src/__tests__/data-table-inline-edit.test.tsx b/packages/components/src/__tests__/data-table-inline-edit.test.tsx
new file mode 100644
index 000000000..ab5a16738
--- /dev/null
+++ b/packages/components/src/__tests__/data-table-inline-edit.test.tsx
@@ -0,0 +1,98 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * Regression: inline cell editing on an `editable` data-table must be usable
+ * for per-row data entry (e.g. 生产报工 where each row gets its own actual
+ * date). Two bugs made it unusable:
+ *
+ * A) Clicking an editable cell entered edit mode AND bubbled up to the row's
+ * onClick → onRowClick, which in ObjectGrid opens the record-detail drawer.
+ * The edit cell must stopPropagation so the drawer never opens.
+ * B) The inline editor was a hardcoded text for every field type, so
+ * date columns could only be typed by hand. A `type: 'date'` column must
+ * render a native date picker ().
+ */
+import { describe, it, expect, vi, beforeAll } from 'vitest';
+import { fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import React from 'react';
+import { renderComponent } from './test-utils';
+
+beforeAll(async () => {
+ await import('../renderers');
+}, 30000);
+
+const editableSchema = {
+ type: 'data-table' as const,
+ editable: true,
+ singleClickEdit: true,
+ columns: [
+ { header: '工序', accessorKey: 'name', editable: false },
+ { header: '实际开始时间', accessorKey: 'actual_start', type: 'date' },
+ { header: '报工数量', accessorKey: 'qty', type: 'number' },
+ ],
+ data: [{ id: '1', name: '将军柱下料', actual_start: '', qty: '' }],
+} as any;
+
+describe('data-table — inline edit is per-row usable', () => {
+ it('A) clicking an editable cell does NOT fire onRowClick (no detail drawer)', () => {
+ const onRowClick = vi.fn();
+ const { container } = renderComponent({ ...editableSchema, onRowClick });
+
+ const cell = Array.from(container.querySelectorAll('td')).find((td) =>
+ td.textContent?.includes('将军柱下料')
+ ) as HTMLElement;
+ // sanity: the readonly name cell is editable:false, so it should still
+ // behave like a normal row click.
+ expect(cell).toBeTruthy();
+
+ // Click the editable date cell (3rd column has no text yet → locate by index).
+ const cells = container.querySelectorAll('tbody td');
+ const dateCell = cells[1] as HTMLElement; // 实际开始时间
+ fireEvent.click(dateCell);
+
+ expect(onRowClick).not.toHaveBeenCalled();
+ });
+
+ it('A2) clicking a readonly (editable:false) cell still fires onRowClick (sanity)', () => {
+ const onRowClick = vi.fn();
+ const { container } = renderComponent({ ...editableSchema, onRowClick });
+
+ const nameCell = Array.from(container.querySelectorAll('tbody td')).find((td) =>
+ td.textContent?.includes('将军柱下料')
+ ) as HTMLElement;
+ fireEvent.click(nameCell);
+
+ expect(onRowClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('B) a date column renders a native date picker, not a free-text input', () => {
+ const { container } = renderComponent(editableSchema);
+
+ const cells = container.querySelectorAll('tbody td');
+ const dateCell = cells[1] as HTMLElement; // 实际开始时间
+ fireEvent.click(dateCell);
+
+ const input = dateCell.querySelector('input') as HTMLInputElement;
+ expect(input).toBeTruthy();
+ expect(input.type).toBe('date');
+ });
+
+ it('B2) a number column renders a numeric input', () => {
+ const { container } = renderComponent(editableSchema);
+
+ const cells = container.querySelectorAll('tbody td');
+ const qtyCell = cells[2] as HTMLElement; // 报工数量
+ fireEvent.click(qtyCell);
+
+ const input = qtyCell.querySelector('input') as HTMLInputElement;
+ expect(input).toBeTruthy();
+ expect(input.type).toBe('number');
+ });
+});
diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx
index a0cf306d2..e8c1fe958 100644
--- a/packages/components/src/renderers/complex/data-table.tsx
+++ b/packages/components/src/renderers/complex/data-table.tsx
@@ -61,6 +61,45 @@ import {
type SortDirection = 'asc' | 'desc' | null;
+/**
+ * Inline-edit helpers: convert a stored cell value to the string a native
+ * `` / `` expects, and back.
+ *
+ * Native date inputs require `yyyy-MM-dd`; datetime-local requires
+ * `yyyy-MM-ddTHH:mm`. We pad to the LOCAL wall-clock so the picker shows the
+ * same day the user sees, then convert back on change. A `date` field stays a
+ * plain `yyyy-MM-dd` string; a `datetime` field round-trips through an ISO
+ * string (matching how display/format code already treats ISO datetimes).
+ */
+function pad2(n: number): string {
+ return String(n).padStart(2, '0');
+}
+
+function toDateInputValue(value: unknown): string {
+ if (value == null || value === '') return '';
+ // A bare yyyy-MM-dd (or its leading slice of an ISO string) is already in the
+ // exact shape the native control wants. Pass it through verbatim — parsing it
+ // through `new Date()` would interpret it as UTC midnight and can shift the
+ // displayed day by one in negative-offset timezones.
+ if (typeof value === 'string') {
+ const m = value.match(/^(\d{4}-\d{2}-\d{2})/);
+ if (m) return m[1];
+ }
+ const d = value instanceof Date ? value : new Date(String(value));
+ if (Number.isNaN(d.getTime())) return '';
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
+}
+
+function toDateTimeInputValue(value: unknown): string {
+ if (value == null || value === '') return '';
+ const d = value instanceof Date ? value : new Date(String(value));
+ if (Number.isNaN(d.getTime())) return '';
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
+}
+
+// Field types that should edit as a numeric ``.
+const NUMERIC_EDIT_TYPES = new Set(['number', 'currency', 'percent', 'int', 'integer', 'float', 'double']);
+
// Default English fallback translations for the data table
const TABLE_DEFAULT_TRANSLATIONS: Record = {
'table.rowsPerPage': 'Rows per page',
@@ -1062,19 +1101,96 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
maxWidth: columnWidth,
...(isFrozen && { left: frozenOffset }),
}}
- onDoubleClick={() => isEditable && !singleClickEdit && startEdit(rowIndex, col.accessorKey)}
- onClick={() => isEditable && singleClickEdit && startEdit(rowIndex, col.accessorKey)}
+ onDoubleClick={(e) => {
+ // Entering edit mode must NOT also fire the row's
+ // onRowClick (record-detail drawer). The row heuristic
+ // can't see the editor yet (the only renders
+ // next frame), so stop propagation here explicitly.
+ if (isEditable && !singleClickEdit) {
+ e.stopPropagation();
+ startEdit(rowIndex, col.accessorKey);
+ }
+ }}
+ onClick={(e) => {
+ if (isEditable && singleClickEdit) {
+ e.stopPropagation();
+ startEdit(rowIndex, col.accessorKey);
+ }
+ }}
onKeyDown={(e) => handleCellKeyDown(e, rowIndex, col.accessorKey)}
tabIndex={0}
>
{isEditing ? (
- setEditValue(e.target.value)}
- onKeyDown={handleEditKeyDown}
- className="h-8 px-2 py-1"
- />
+ (() => {
+ // Type-aware inline editor. `col.type` is forwarded
+ // from ObjectGrid's column inference. Keep this a small,
+ // readable switch that's easy to extend.
+ const editType = (col as any).type as string | undefined;
+
+ if (editType === 'date') {
+ return (
+ setEditValue(e.target.value)}
+ onKeyDown={handleEditKeyDown}
+ className="h-8 px-2 py-1"
+ />
+ );
+ }
+
+ if (editType === 'datetime' || editType === 'datetime-local') {
+ return (
+ {
+ const v = e.target.value;
+ const d = v ? new Date(v) : null;
+ setEditValue(d && !Number.isNaN(d.getTime()) ? d.toISOString() : v);
+ }}
+ onKeyDown={handleEditKeyDown}
+ className="h-8 px-2 py-1"
+ />
+ );
+ }
+
+ if (editType && NUMERIC_EDIT_TYPES.has(editType)) {
+ return (
+ setEditValue(e.target.value)}
+ onKeyDown={handleEditKeyDown}
+ className="h-8 px-2 py-1"
+ />
+ );
+ }
+
+ // NOTE: `select`/`boolean` editors are intentionally not
+ // wired here — the data-table column does not currently
+ // carry option metadata. Extension point: when `col.options`
+ // is forwarded from ObjectGrid, render a