@kine-design/crud 0.0.1-beta.1 → 0.0.1-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/components/crudPage/KCrudPage.tsx +175 -0
  2. package/components/crudPage/crudPage.css +40 -0
  3. package/components/crudPage/index.ts +10 -0
  4. package/components/editableTable/KEditableTable.tsx +335 -0
  5. package/components/editableTable/editableTable.css +268 -0
  6. package/components/editableTable/index.ts +10 -0
  7. package/components/formPage/KApprovalDialog.tsx +142 -0
  8. package/components/formPage/KFormCard.tsx +65 -0
  9. package/components/formPage/KFormPage.tsx +126 -0
  10. package/components/formPage/KMasterDetailPage.tsx +205 -0
  11. package/components/formPage/KStickyActionBar.tsx +33 -0
  12. package/components/formPage/formPage.css +577 -0
  13. package/components/formPage/index.ts +14 -0
  14. package/components/layout/KContent.tsx +20 -0
  15. package/components/layout/KHeader.tsx +29 -0
  16. package/components/layout/KLayout.tsx +69 -0
  17. package/components/layout/KSider.tsx +77 -0
  18. package/components/layout/index.ts +18 -0
  19. package/components/layout/layout.css +149 -0
  20. package/components/login/KLoginPage.tsx +129 -0
  21. package/components/login/index.ts +10 -0
  22. package/components/login/login.css +118 -0
  23. package/components/navMenu/KNavMenu.tsx +175 -0
  24. package/components/navMenu/index.ts +2 -0
  25. package/components/navMenu/navMenu.css +197 -0
  26. package/components/pageHeader/KPageHeader.tsx +85 -0
  27. package/components/pageHeader/index.ts +9 -0
  28. package/components/pageHeader/pageHeader.css +93 -0
  29. package/components/searchTable/KSearchTable.tsx +138 -0
  30. package/components/searchTable/index.ts +10 -0
  31. package/components/searchTable/searchTable.css +93 -0
  32. package/components/upload/KFileList.tsx +95 -0
  33. package/components/upload/KImageUpload.tsx +286 -0
  34. package/components/upload/KUpload.tsx +206 -0
  35. package/components/upload/index.ts +13 -0
  36. package/components/upload/types.ts +26 -0
  37. package/components/upload/upload.css +345 -0
  38. package/composables/auth/authGuard.ts +128 -0
  39. package/composables/auth/index.ts +23 -0
  40. package/composables/auth/types.ts +109 -0
  41. package/composables/auth/useAuth.ts +258 -0
  42. package/composables/auth/vCan.ts +95 -0
  43. package/composables/defineRepository.ts +224 -0
  44. package/composables/error/createErrorHandler.ts +46 -0
  45. package/composables/error/defaultFeedbackHandler.ts +76 -0
  46. package/composables/error/dispatchError.ts +70 -0
  47. package/composables/error/index.ts +32 -0
  48. package/composables/error/types.ts +57 -0
  49. package/composables/error/useErrorHandler.ts +41 -0
  50. package/composables/form/index.ts +18 -0
  51. package/composables/form/renderFormField.tsx +119 -0
  52. package/composables/form/types.ts +129 -0
  53. package/composables/form/useFormPage.ts +183 -0
  54. package/composables/index.ts +62 -0
  55. package/composables/page/index.ts +11 -0
  56. package/composables/page/types.ts +60 -0
  57. package/composables/page/useCrudPage.ts +88 -0
  58. package/composables/request/composables.ts +206 -0
  59. package/composables/request/controlGate.ts +143 -0
  60. package/composables/request/createRequest.ts +168 -0
  61. package/composables/request/index.ts +71 -0
  62. package/composables/request/orchestrator.ts +145 -0
  63. package/composables/request/requestBuilder.ts +379 -0
  64. package/composables/request/transport/fetchTransport.ts +79 -0
  65. package/composables/request/transport/xhrTransport.ts +97 -0
  66. package/composables/request/types.ts +226 -0
  67. package/composables/request/upload.ts +146 -0
  68. package/composables/router/createRouterGuard.ts +134 -0
  69. package/composables/router/defineCrudRoutes.ts +116 -0
  70. package/composables/router/index.ts +22 -0
  71. package/composables/router/types.ts +128 -0
  72. package/composables/router/useMenuFromRoutes.ts +109 -0
  73. package/composables/router/useTabStore.ts +183 -0
  74. package/composables/search/index.ts +11 -0
  75. package/composables/search/useAutoCompleteSearch.ts +161 -0
  76. package/composables/setupCrud.ts +43 -0
  77. package/composables/storage/createStorageAdapter.ts +72 -0
  78. package/composables/storage/index.ts +13 -0
  79. package/composables/storage/types.ts +30 -0
  80. package/composables/storage/useStorage.ts +108 -0
  81. package/composables/store/defineUserStore.ts +122 -0
  82. package/composables/store/index.ts +11 -0
  83. package/composables/types.ts +118 -0
  84. package/dist/components/crudPage/KCrudPage.d.ts +14 -0
  85. package/dist/components/crudPage/index.d.ts +9 -0
  86. package/dist/components/editableTable/KEditableTable.d.ts +149 -0
  87. package/dist/components/editableTable/index.d.ts +10 -0
  88. package/dist/components/formPage/KApprovalDialog.d.ts +99 -0
  89. package/dist/components/formPage/KFormCard.d.ts +49 -0
  90. package/dist/components/formPage/KFormPage.d.ts +14 -0
  91. package/dist/components/formPage/KMasterDetailPage.d.ts +14 -0
  92. package/dist/components/formPage/KStickyActionBar.d.ts +16 -0
  93. package/dist/components/formPage/index.d.ts +14 -0
  94. package/dist/components/layout/KLayout.d.ts +5 -4
  95. package/dist/composables/auth/useAuth.d.ts +5 -5
  96. package/dist/composables/error/types.d.ts +2 -1
  97. package/dist/composables/form/index.d.ts +12 -0
  98. package/dist/composables/form/renderFormField.d.ts +11 -0
  99. package/dist/composables/form/types.d.ts +104 -0
  100. package/dist/composables/form/useFormPage.d.ts +38 -0
  101. package/dist/composables/index.d.ts +2 -0
  102. package/dist/composables/page/index.d.ts +10 -0
  103. package/dist/composables/page/types.d.ts +59 -0
  104. package/dist/composables/page/useCrudPage.d.ts +14 -0
  105. package/dist/composables/search/index.d.ts +10 -0
  106. package/dist/composables/search/useAutoCompleteSearch.d.ts +50 -0
  107. package/dist/crud.css +1621 -316
  108. package/dist/crud.js +11064 -2910
  109. package/dist/index.d.ts +11 -0
  110. package/dist/setup.d.ts +2 -2
  111. package/index.ts +144 -0
  112. package/package.json +20 -19
  113. package/setup.ts +288 -0
  114. package/tsconfig.json +12 -0
  115. package/vite.config.build.ts +52 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @description KCrudPage 配置驱动的 ERP 列表页组件
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ *
9
+ * 一个配置出一整个页面:标题 + 筛选 + 表格 + 分页 + 状态标签。
10
+ */
11
+ import { defineComponent, type PropType } from 'vue';
12
+ import { useRouter } from 'vue-router';
13
+ import KTableColumn from 'kine-ui/components/tableColumn/KTableColumn.tsx';
14
+ import KTag from 'kine-ui/components/tag/KTag.tsx';
15
+ import KInput from 'kine-ui/components/input/KInput.tsx';
16
+ import KSelect from 'kine-ui/components/select/KSelect.tsx';
17
+ import KPageHeader from '../pageHeader/KPageHeader.tsx';
18
+ import KSearchTable from '../searchTable/KSearchTable.tsx';
19
+ import { useCrudPage } from '../../composables/page/useCrudPage';
20
+ import type { CrudPageConfig, CrudColumnConfig } from '../../composables/page/types';
21
+ import './crudPage.css';
22
+
23
+ export default defineComponent({
24
+ name: 'KCrudPage',
25
+
26
+ props: {
27
+ config: {
28
+ type: Object as PropType<CrudPageConfig>,
29
+ required: true,
30
+ },
31
+ },
32
+
33
+ setup(props, { slots }) {
34
+ const router = useRouter();
35
+ const {
36
+ page, pageSize, total, list, loading,
37
+ filters, onPageChange, onSearch, onReset,
38
+ } = useCrudPage(props.config);
39
+
40
+ /** 格式化日期 */
41
+ const formatDate = (val: unknown) => {
42
+ if (!val) return '';
43
+ const d = new Date(val as string);
44
+ return isNaN(d.getTime()) ? String(val) : d.toLocaleDateString('zh-CN');
45
+ };
46
+
47
+ /** 格式化日期时间 */
48
+ const formatDateTime = (val: unknown) => {
49
+ if (!val) return '';
50
+ const d = new Date(val as string);
51
+ return isNaN(d.getTime()) ? String(val) : d.toLocaleString('zh-CN');
52
+ };
53
+
54
+ /** 渲染状态标签 */
55
+ const renderStatus = (val: unknown) => {
56
+ const statusMap = props.config.statusMap;
57
+ if (!statusMap || !val) return String(val ?? '');
58
+ const mapped = statusMap[val as string];
59
+ if (!mapped) return String(val);
60
+ return <KTag type={mapped.type ?? 'default'} size="small">{mapped.label}</KTag>;
61
+ };
62
+
63
+ /** 按列类型渲染单元格 */
64
+ const renderCell = (col: CrudColumnConfig, row: Record<string, unknown>) => {
65
+ const val = row[col.param];
66
+ switch (col.type) {
67
+ case 'status': return renderStatus(val);
68
+ case 'date': return formatDate(val);
69
+ case 'datetime': return formatDateTime(val);
70
+ default: return val != null ? String(val) : '';
71
+ }
72
+ };
73
+
74
+ /** 行点击 → 跳转详情 */
75
+ const onRowClick = (e: MouseEvent) => {
76
+ const detailPath = props.config.detailPath;
77
+ if (!detailPath) return;
78
+
79
+ const tr = (e.target as HTMLElement).closest?.('tr');
80
+ if (!tr) return;
81
+ const tbody = tr.closest('tbody');
82
+ if (!tbody || tbody.classList.contains('k-table-empty')) return;
83
+
84
+ const rows = Array.from(tbody.querySelectorAll('tr'));
85
+ const index = rows.indexOf(tr as HTMLTableRowElement);
86
+ if (index < 0 || index >= list.value.length) return;
87
+
88
+ const row = list.value[index];
89
+ const key = props.config.rowKey ?? 'id';
90
+ const id = row[key];
91
+ if (id != null) {
92
+ router.push(`${detailPath}/${id}`);
93
+ }
94
+ };
95
+
96
+ /** 渲染筛选区表单项 */
97
+ const renderFilters = () => {
98
+ if (!props.config.filters?.length) return null;
99
+ return props.config.filters.map(f => (
100
+ <div class="k-crud-page-filter-item" key={f.param}>
101
+ <label class="k-crud-page-filter-label">{f.label}</label>
102
+ {f.type === 'select' ? (
103
+ <KSelect
104
+ class="k-crud-page-filter-control"
105
+ modelValue={(filters[f.param] as string) ?? ''}
106
+ options={f.options ?? []}
107
+ optionParam="label"
108
+ valueParam="value"
109
+ placeholder={f.placeholder ?? '全部'}
110
+ onUpdate:modelValue={(v: unknown) => {
111
+ filters[f.param] = (v as string) || undefined;
112
+ }}
113
+ />
114
+ ) : (
115
+ <KInput
116
+ class="k-crud-page-filter-control"
117
+ placeholder={f.placeholder ?? `请输入${f.label}`}
118
+ modelValue={(filters[f.param] as string) ?? ''}
119
+ onUpdate:modelValue={(v: string) => {
120
+ filters[f.param] = v || undefined;
121
+ }}
122
+ />
123
+ )}
124
+ </div>
125
+ ));
126
+ };
127
+
128
+ return () => (
129
+ <div class={['k-crud-page', props.config.detailPath ? 'k-crud-page--clickable' : '']}>
130
+ <KPageHeader title={props.config.title}>
131
+ {{ extra: slots.headerExtra }}
132
+ </KPageHeader>
133
+
134
+ <div onClick={onRowClick}>
135
+ <KSearchTable
136
+ data={list.value}
137
+ loading={loading.value}
138
+ total={total.value}
139
+ page={page.value}
140
+ pageSize={pageSize.value}
141
+ searchable={!!props.config.filters?.length}
142
+ onUpdate:page={onPageChange}
143
+ onSearch={onSearch}
144
+ onReset={onReset}
145
+ >
146
+ {{
147
+ search: renderFilters,
148
+ toolbar: slots.toolbar,
149
+ default: () => props.config.columns.map(col => {
150
+ const customSlot = slots[`column-${col.param}`];
151
+ return (
152
+ <KTableColumn
153
+ key={col.param}
154
+ param={col.param}
155
+ label={col.label}
156
+ width={col.width ?? ''}
157
+ >
158
+ {{
159
+ default: customSlot
160
+ ? (scope: { row: Record<string, unknown>; index: number }) => customSlot(scope)
161
+ : col.type
162
+ ? (scope: { row: Record<string, unknown> }) => renderCell(col, scope.row)
163
+ : undefined,
164
+ }}
165
+ </KTableColumn>
166
+ );
167
+ }),
168
+ empty: slots.empty,
169
+ }}
170
+ </KSearchTable>
171
+ </div>
172
+ </div>
173
+ );
174
+ },
175
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @description KCrudPage 样式
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ */
7
+
8
+ /* ===== 根容器 ===== */
9
+ .k-crud-page {
10
+ display: flex;
11
+ flex-direction: column;
12
+ height: 100%;
13
+ gap: var(--kine-spacing-6);
14
+ padding: var(--kine-spacing-8);
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ /* ===== 筛选项 ===== */
19
+ .k-crud-page-filter-item {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: var(--kine-spacing-2);
23
+ min-width: 140px;
24
+ }
25
+
26
+ .k-crud-page-filter-label {
27
+ font-size: var(--kine-font-size-sm);
28
+ color: var(--kine-color-text-secondary);
29
+ line-height: 1;
30
+ }
31
+
32
+ /* KInput / KSelect 筛选控件宽度适配 */
33
+ .k-crud-page-filter-control {
34
+ width: 100%;
35
+ }
36
+
37
+ /* ===== 行可点击 ===== */
38
+ .k-crud-page--clickable .k-tbody .k-tr {
39
+ cursor: pointer;
40
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @description KCrudPage barrel export
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+
10
+ export { default as KCrudPage } from './KCrudPage';
@@ -0,0 +1,335 @@
1
+ /**
2
+ * @description KEditableTable — 可编辑行表格组件
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ *
9
+ * 支持行内编辑、添加行、删除行、汇总行。
10
+ * 设计参考 AIDesigner 的 EditableDataTable 规范:
11
+ * - 正常行:纯展示
12
+ * - 编辑行:左侧靛蓝竖线,cell 变为 input
13
+ * - 删除确认行:红色高亮 + 内联确认
14
+ * - 新增行:底部空行带搜索输入
15
+ * - 汇总行:底部加粗合计
16
+ */
17
+ import { defineComponent, ref, type PropType } from 'vue';
18
+ import KButton from 'kine-ui/components/button/KButton.tsx';
19
+ import KInput from 'kine-ui/components/input/KInput.tsx';
20
+ import KSelect from 'kine-ui/components/select/KSelect.tsx';
21
+ import './editableTable.css';
22
+
23
+ // ────────────────────────────────────────────────────────────────────────────
24
+ // 类型
25
+ // ────────────────────────────────────────────────────────────────────────────
26
+
27
+ export interface EditableColumn {
28
+ /** 字段名 */
29
+ param: string;
30
+ /** 列标题 */
31
+ label: string;
32
+ /** 列宽 */
33
+ width?: string;
34
+ /** 编辑态下的输入类型 */
35
+ editType?: 'text' | 'number' | 'select' | 'search' | 'readonly';
36
+ /** select 类型的选项 */
37
+ options?: Array<{ label: string; value: string | number }>;
38
+ /** 对齐方式 */
39
+ align?: 'left' | 'center' | 'right';
40
+ /** 是否在汇总行中求和 */
41
+ summary?: boolean;
42
+ /** 汇总行格式化 */
43
+ summaryFormat?: (sum: number) => string;
44
+ }
45
+
46
+ export interface EditableTableRow {
47
+ /** 行唯一标识 */
48
+ _id: string | number;
49
+ /** 行数据 */
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ // ────────────────────────────────────────────────────────────────────────────
54
+ // 组件
55
+ // ────────────────────────────────────────────────────────────────────────────
56
+
57
+ export default defineComponent({
58
+ name: 'KEditableTable',
59
+ props: {
60
+ /** 列定义 */
61
+ columns: { type: Array as PropType<EditableColumn[]>, required: true },
62
+ /** 行数据(v-model) */
63
+ modelValue: { type: Array as PropType<EditableTableRow[]>, default: () => [] },
64
+ /** 标题 */
65
+ title: { type: String, default: '' },
66
+ /** 是否显示序号列 */
67
+ showIndex: { type: Boolean, default: true },
68
+ /** 是否显示操作列 */
69
+ showActions: { type: Boolean, default: true },
70
+ /** 是否显示汇总行 */
71
+ showSummary: { type: Boolean, default: false },
72
+ /** 汇总行标签 */
73
+ summaryLabel: { type: String, default: '合计' },
74
+ /** 新增行按钮文本 */
75
+ addText: { type: String, default: '+ 添加行' },
76
+ /** 是否可新增 */
77
+ addable: { type: Boolean, default: true },
78
+ /** 币种前缀(用于金额汇总展示) */
79
+ currencyPrefix: { type: String, default: '' },
80
+ },
81
+ emits: ['update:modelValue', 'add', 'edit', 'save', 'cancel', 'remove'],
82
+ setup(props, { emit, slots }) {
83
+ const editingId = ref<string | number | null>(null);
84
+ const deletingId = ref<string | number | null>(null);
85
+ const editingData = ref<Record<string, unknown>>({});
86
+
87
+ // ── 行操作 ────────────────────────────────────
88
+
89
+ const startEdit = (row: EditableTableRow) => {
90
+ editingId.value = row._id;
91
+ editingData.value = { ...row };
92
+ deletingId.value = null;
93
+ emit('edit', row);
94
+ };
95
+
96
+ const saveEdit = () => {
97
+ if (editingId.value === null) return;
98
+ const rows = [...props.modelValue];
99
+ const idx = rows.findIndex(r => r._id === editingId.value);
100
+ if (idx >= 0) {
101
+ rows[idx] = { ...editingData.value } as EditableTableRow;
102
+ emit('update:modelValue', rows);
103
+ emit('save', rows[idx]);
104
+ }
105
+ editingId.value = null;
106
+ editingData.value = {};
107
+ };
108
+
109
+ const cancelEdit = () => {
110
+ editingId.value = null;
111
+ editingData.value = {};
112
+ emit('cancel');
113
+ };
114
+
115
+ const startDelete = (row: EditableTableRow) => {
116
+ deletingId.value = row._id;
117
+ editingId.value = null;
118
+ };
119
+
120
+ const confirmDelete = () => {
121
+ if (deletingId.value === null) return;
122
+ const rows = props.modelValue.filter(r => r._id !== deletingId.value);
123
+ emit('update:modelValue', rows);
124
+ emit('remove', deletingId.value);
125
+ deletingId.value = null;
126
+ };
127
+
128
+ const cancelDelete = () => {
129
+ deletingId.value = null;
130
+ };
131
+
132
+ const addRow = () => {
133
+ emit('add');
134
+ };
135
+
136
+ // ── 汇总计算 ──────────────────────────────────
137
+
138
+ const getSummary = (col: EditableColumn): string => {
139
+ if (!col.summary) return '';
140
+ const sum = props.modelValue.reduce((acc, row) => {
141
+ const val = Number(row[col.param]);
142
+ return acc + (isNaN(val) ? 0 : val);
143
+ }, 0);
144
+ if (col.summaryFormat) return col.summaryFormat(sum);
145
+ return sum.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
146
+ };
147
+
148
+ // ── 渲染 ──────────────────────────────────────
149
+
150
+ /** 渲染普通单元格 */
151
+ const renderCell = (col: EditableColumn, row: EditableTableRow) => {
152
+ const customSlot = slots[`column-${col.param}`];
153
+ if (customSlot) return customSlot({ row, value: row[col.param] });
154
+
155
+ const val = row[col.param];
156
+ return val != null ? String(val) : '';
157
+ };
158
+
159
+ /** 渲染编辑态单元格 */
160
+ const renderEditCell = (col: EditableColumn) => {
161
+ const editSlot = slots[`edit-${col.param}`];
162
+ if (editSlot) return editSlot({ data: editingData.value });
163
+
164
+ const type = col.editType ?? 'text';
165
+
166
+ if (type === 'readonly') {
167
+ return <span class="k-et-cell-readonly">{editingData.value[col.param] as string ?? ''}</span>;
168
+ }
169
+
170
+ if (type === 'select' && col.options) {
171
+ return (
172
+ <KSelect
173
+ class="k-et-select-wrap"
174
+ modelValue={editingData.value[col.param]}
175
+ options={col.options}
176
+ optionParam="label"
177
+ valueParam="value"
178
+ onUpdate:modelValue={(v: unknown) => { editingData.value[col.param] = v; }}
179
+ />
180
+ );
181
+ }
182
+
183
+ return (
184
+ <KInput
185
+ class="k-et-input-wrap"
186
+ type={type === 'number' ? 'number' : 'text'}
187
+ modelValue={editingData.value[col.param] as string | number}
188
+ onUpdate:modelValue={(v: string) => { editingData.value[col.param] = v; }}
189
+ />
190
+ );
191
+ };
192
+
193
+ return () => {
194
+ const rows = props.modelValue;
195
+ const cols = props.columns;
196
+ const isEmpty = rows.length === 0;
197
+
198
+ return (
199
+ <div class="k-editable-table">
200
+ {/* 标题栏 */}
201
+ {(props.title || slots.header) && (
202
+ <div class="k-et-header">
203
+ <div class="k-et-header-left">
204
+ {props.title && <span class="k-et-title">{props.title}</span>}
205
+ {rows.length > 0 && <span class="k-et-count">{rows.length} items</span>}
206
+ </div>
207
+ <div class="k-et-header-right">
208
+ {slots.headerExtra?.()}
209
+ {props.addable && (
210
+ <KButton plain text={props.addText} onClick={addRow} />
211
+ )}
212
+ </div>
213
+ </div>
214
+ )}
215
+
216
+ {/* 表格 */}
217
+ <table class="k-et-table">
218
+ <thead class="k-et-thead">
219
+ <tr>
220
+ {props.showIndex && <th class="k-et-th k-et-th-index">#</th>}
221
+ {cols.map(col => (
222
+ <th
223
+ key={col.param}
224
+ class={['k-et-th', col.align && `k-et-align-${col.align}`]}
225
+ style={col.width ? { width: col.width } : undefined}
226
+ >
227
+ {col.label}
228
+ </th>
229
+ ))}
230
+ {props.showActions && <th class="k-et-th k-et-th-actions" />}
231
+ </tr>
232
+ </thead>
233
+
234
+ <tbody class="k-et-tbody">
235
+ {rows.map((row, i) => {
236
+ const isEditing = editingId.value === row._id;
237
+ const isDeleting = deletingId.value === row._id;
238
+
239
+ // 删除确认行
240
+ if (isDeleting) {
241
+ return (
242
+ <tr key={row._id} class="k-et-tr k-et-tr--deleting">
243
+ {props.showIndex && <td class="k-et-td k-et-td-index">{String(i + 1).padStart(2, '0')}</td>}
244
+ <td class="k-et-td" colspan={cols.length + (props.showActions ? 1 : 0)}>
245
+ <span class="k-et-delete-confirm">
246
+ <s class="k-et-deleted-text">{renderCell(cols[0], row)}</s>
247
+ <span class="k-et-delete-ask">Are you sure you want to delete this line?</span>
248
+ <KButton type="danger" size="small" text="Delete" onClick={confirmDelete} />
249
+ <KButton plain size="small" text="Cancel" onClick={cancelDelete} />
250
+ </span>
251
+ </td>
252
+ </tr>
253
+ );
254
+ }
255
+
256
+ // 编辑行
257
+ if (isEditing) {
258
+ return (
259
+ <tr key={row._id} class="k-et-tr k-et-tr--editing">
260
+ {props.showIndex && <td class="k-et-td k-et-td-index">{String(i + 1).padStart(2, '0')}</td>}
261
+ {cols.map(col => (
262
+ <td key={col.param} class={['k-et-td', col.align && `k-et-align-${col.align}`]}>
263
+ {renderEditCell(col)}
264
+ </td>
265
+ ))}
266
+ {props.showActions && (
267
+ <td class="k-et-td k-et-td-actions">
268
+ <KButton plain size="small" text="✓" onClick={saveEdit} />
269
+ <KButton plain size="small" text="×" onClick={cancelEdit} />
270
+ </td>
271
+ )}
272
+ </tr>
273
+ );
274
+ }
275
+
276
+ // 正常行
277
+ return (
278
+ <tr key={row._id} class="k-et-tr">
279
+ {props.showIndex && <td class="k-et-td k-et-td-index">{String(i + 1).padStart(2, '0')}</td>}
280
+ {cols.map(col => (
281
+ <td key={col.param} class={['k-et-td', col.align && `k-et-align-${col.align}`]}>
282
+ {renderCell(col, row)}
283
+ </td>
284
+ ))}
285
+ {props.showActions && (
286
+ <td class="k-et-td k-et-td-actions">
287
+ <KButton plain size="small" text="✎" onClick={() => startEdit(row)} />
288
+ <KButton plain size="small" text="×" onClick={() => startDelete(row)} />
289
+ </td>
290
+ )}
291
+ </tr>
292
+ );
293
+ })}
294
+ </tbody>
295
+
296
+ {/* 汇总行 */}
297
+ {props.showSummary && rows.length > 0 && (
298
+ <tfoot class="k-et-tfoot">
299
+ <tr class="k-et-tr k-et-tr--summary">
300
+ {props.showIndex && <td class="k-et-td" />}
301
+ {cols.map((col, ci) => (
302
+ <td key={col.param} class={['k-et-td k-et-td-summary', col.align && `k-et-align-${col.align}`]}>
303
+ {ci === 0 ? props.summaryLabel : ''}
304
+ {col.summary ? (
305
+ <span class="k-et-summary-value">
306
+ {props.currencyPrefix && col.summaryFormat === undefined && ci === cols.length - 1 ? `${props.currencyPrefix} ` : ''}
307
+ {getSummary(col)}
308
+ </span>
309
+ ) : ''}
310
+ </td>
311
+ ))}
312
+ {props.showActions && <td class="k-et-td" />}
313
+ </tr>
314
+ </tfoot>
315
+ )}
316
+ </table>
317
+
318
+ {/* 空态 */}
319
+ {isEmpty && (
320
+ <div class="k-et-empty">
321
+ {slots.empty?.() ?? (
322
+ <>
323
+ <span class="k-et-empty-text">暂无数据</span>
324
+ {props.addable && (
325
+ <KButton type="primary" text="添加第一行" onClick={addRow} />
326
+ )}
327
+ </>
328
+ )}
329
+ </div>
330
+ )}
331
+ </div>
332
+ );
333
+ };
334
+ },
335
+ });