@kine-design/crud 0.0.1-beta.2 → 0.0.1-beta.21

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 (118) hide show
  1. package/.vlaude/last-session-id +1 -0
  2. package/components/crudPage/KCrudPage.tsx +178 -0
  3. package/components/crudPage/crudPage.css +64 -0
  4. package/components/crudPage/index.ts +10 -0
  5. package/components/editableTable/KEditableTable.tsx +281 -0
  6. package/components/editableTable/editableTable.css +268 -0
  7. package/components/editableTable/index.ts +10 -0
  8. package/components/formPage/KApprovalDialog.tsx +142 -0
  9. package/components/formPage/KFormCard.tsx +65 -0
  10. package/components/formPage/KFormPage.tsx +128 -0
  11. package/components/formPage/KMasterDetailPage.tsx +205 -0
  12. package/components/formPage/KStickyActionBar.tsx +33 -0
  13. package/components/formPage/formPage.css +629 -0
  14. package/components/formPage/index.ts +14 -0
  15. package/components/layout/KContent.tsx +20 -0
  16. package/components/layout/KHeader.tsx +37 -0
  17. package/components/layout/KLayout.tsx +82 -0
  18. package/components/layout/KSider.tsx +80 -0
  19. package/components/layout/index.ts +18 -0
  20. package/components/layout/layout.css +262 -0
  21. package/components/login/KLoginPage.tsx +129 -0
  22. package/components/login/index.ts +10 -0
  23. package/components/login/login.css +118 -0
  24. package/components/navMenu/KNavMenu.tsx +175 -0
  25. package/components/navMenu/index.ts +2 -0
  26. package/components/navMenu/navMenu.css +197 -0
  27. package/components/pageHeader/KPageHeader.tsx +85 -0
  28. package/components/pageHeader/index.ts +9 -0
  29. package/components/pageHeader/pageHeader.css +93 -0
  30. package/components/searchTable/KSearchTable.tsx +138 -0
  31. package/components/searchTable/index.ts +10 -0
  32. package/components/searchTable/searchTable.css +121 -0
  33. package/components/upload/KFileList.tsx +95 -0
  34. package/components/upload/KImageUpload.tsx +286 -0
  35. package/components/upload/KUpload.tsx +206 -0
  36. package/components/upload/index.ts +13 -0
  37. package/components/upload/types.ts +26 -0
  38. package/components/upload/upload.css +345 -0
  39. package/composables/auth/authGuard.ts +128 -0
  40. package/composables/auth/index.ts +23 -0
  41. package/composables/auth/types.ts +109 -0
  42. package/composables/auth/useAuth.ts +278 -0
  43. package/composables/auth/vCan.ts +95 -0
  44. package/composables/defineRepository.ts +224 -0
  45. package/composables/error/createErrorHandler.ts +46 -0
  46. package/composables/error/defaultFeedbackHandler.ts +76 -0
  47. package/composables/error/dispatchError.ts +70 -0
  48. package/composables/error/index.ts +32 -0
  49. package/composables/error/types.ts +57 -0
  50. package/composables/error/useErrorHandler.ts +41 -0
  51. package/composables/form/index.ts +18 -0
  52. package/composables/form/renderFormField.tsx +119 -0
  53. package/composables/form/types.ts +129 -0
  54. package/composables/form/useFormPage.ts +183 -0
  55. package/composables/index.ts +62 -0
  56. package/composables/page/index.ts +11 -0
  57. package/composables/page/types.ts +62 -0
  58. package/composables/page/useCrudPage.ts +88 -0
  59. package/composables/request/composables.ts +206 -0
  60. package/composables/request/controlGate.ts +143 -0
  61. package/composables/request/createRequest.ts +173 -0
  62. package/composables/request/index.ts +71 -0
  63. package/composables/request/orchestrator.ts +145 -0
  64. package/composables/request/requestBuilder.ts +418 -0
  65. package/composables/request/transport/fetchTransport.ts +79 -0
  66. package/composables/request/transport/xhrTransport.ts +100 -0
  67. package/composables/request/types.ts +226 -0
  68. package/composables/request/upload.ts +146 -0
  69. package/composables/router/createRouterGuard.ts +134 -0
  70. package/composables/router/defineCrudRoutes.ts +116 -0
  71. package/composables/router/index.ts +22 -0
  72. package/composables/router/types.ts +128 -0
  73. package/composables/router/useMenuFromRoutes.ts +109 -0
  74. package/composables/router/useTabStore.ts +183 -0
  75. package/composables/search/index.ts +11 -0
  76. package/composables/search/useAutoCompleteSearch.ts +161 -0
  77. package/composables/setupCrud.ts +43 -0
  78. package/composables/storage/createStorageAdapter.ts +72 -0
  79. package/composables/storage/index.ts +13 -0
  80. package/composables/storage/types.ts +30 -0
  81. package/composables/storage/useStorage.ts +108 -0
  82. package/composables/store/defineUserStore.ts +122 -0
  83. package/composables/store/index.ts +11 -0
  84. package/composables/types.ts +118 -0
  85. package/dist/components/crudPage/KCrudPage.d.ts +14 -0
  86. package/dist/components/crudPage/index.d.ts +9 -0
  87. package/dist/components/editableTable/KEditableTable.d.ts +146 -0
  88. package/dist/components/editableTable/index.d.ts +10 -0
  89. package/dist/components/formPage/KApprovalDialog.d.ts +99 -0
  90. package/dist/components/formPage/KFormCard.d.ts +49 -0
  91. package/dist/components/formPage/KFormPage.d.ts +14 -0
  92. package/dist/components/formPage/KMasterDetailPage.d.ts +14 -0
  93. package/dist/components/formPage/KStickyActionBar.d.ts +16 -0
  94. package/dist/components/formPage/index.d.ts +14 -0
  95. package/dist/components/layout/KLayout.d.ts +7 -4
  96. package/dist/composables/auth/useAuth.d.ts +5 -5
  97. package/dist/composables/error/types.d.ts +2 -1
  98. package/dist/composables/form/index.d.ts +12 -0
  99. package/dist/composables/form/renderFormField.d.ts +11 -0
  100. package/dist/composables/form/types.d.ts +104 -0
  101. package/dist/composables/form/useFormPage.d.ts +38 -0
  102. package/dist/composables/index.d.ts +2 -0
  103. package/dist/composables/page/index.d.ts +10 -0
  104. package/dist/composables/page/types.d.ts +61 -0
  105. package/dist/composables/page/useCrudPage.d.ts +14 -0
  106. package/dist/composables/request/createRequest.d.ts +2 -0
  107. package/dist/composables/request/requestBuilder.d.ts +2 -0
  108. package/dist/composables/search/index.d.ts +10 -0
  109. package/dist/composables/search/useAutoCompleteSearch.d.ts +50 -0
  110. package/dist/crud.css +2499 -663
  111. package/dist/crud.js +11512 -2910
  112. package/dist/index.d.ts +11 -0
  113. package/dist/setup.d.ts +2 -2
  114. package/index.ts +144 -0
  115. package/package.json +20 -19
  116. package/setup.ts +288 -0
  117. package/tsconfig.json +12 -0
  118. package/vite.config.build.ts +52 -0
@@ -0,0 +1 @@
1
+ 594292cc-8321-4629-97d9-91ad432e0646
@@ -0,0 +1,178 @@
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 KTableColumn from 'kine-ui/components/tableColumn/KTableColumn.tsx';
13
+ import KTag from 'kine-ui/components/tag/KTag.tsx';
14
+ import KImage from 'kine-ui/components/image/KImage.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 {
35
+ page, pageSize, total, list, loading,
36
+ filters, onPageChange, onSearch, onReset,
37
+ } = useCrudPage(props.config);
38
+
39
+ /** 格式化日期 */
40
+ const formatDate = (val: unknown) => {
41
+ if (!val) return '';
42
+ const d = new Date(val as string);
43
+ return isNaN(d.getTime()) ? String(val) : d.toLocaleDateString('zh-CN');
44
+ };
45
+
46
+ /** 格式化日期时间 */
47
+ const formatDateTime = (val: unknown) => {
48
+ if (!val) return '';
49
+ const d = new Date(val as string);
50
+ return isNaN(d.getTime()) ? String(val) : d.toLocaleString('zh-CN');
51
+ };
52
+
53
+ /** 渲染状态标签 */
54
+ const renderStatus = (val: unknown) => {
55
+ const statusMap = props.config.statusMap;
56
+ if (!statusMap || !val) return String(val ?? '');
57
+ const mapped = statusMap[val as string];
58
+ if (!mapped) return String(val);
59
+ return <KTag type={mapped.type ?? 'default'} size="small">{mapped.label}</KTag>;
60
+ };
61
+
62
+ /** 按列类型渲染单元格 */
63
+ const renderCell = (col: CrudColumnConfig, row: Record<string, unknown>) => {
64
+ const val = row[col.param];
65
+ switch (col.type) {
66
+ case 'status': return renderStatus(val);
67
+ case 'date': return formatDate(val);
68
+ case 'datetime': return formatDateTime(val);
69
+ case 'image': {
70
+ if (!val) {
71
+ return (
72
+ <div class="k-image" style={{ width: '40px', height: '40px' }}>
73
+ <div class="k-image-placeholder k-image-empty">
74
+ <svg viewBox="0 0 24 24" fill="none" class="k-image-placeholder-icon">
75
+ <rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" />
76
+ <circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" opacity="0.5" />
77
+ <path d="M3 15l5-5 4 4 3-3 6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
78
+ </svg>
79
+ </div>
80
+ </div>
81
+ );
82
+ }
83
+ const raw = String(val);
84
+ const resolver = props.config.imageResolver;
85
+ const src = resolver ? resolver(raw) : raw;
86
+ return (
87
+ <KImage
88
+ src={src}
89
+ previewSrcList={[src]}
90
+ width={40}
91
+ height={40}
92
+ fit="cover"
93
+ lazy
94
+ />
95
+ );
96
+ }
97
+ default: return val != null ? String(val) : '';
98
+ }
99
+ };
100
+
101
+ /** 渲染筛选区表单项 */
102
+ const renderFilters = () => {
103
+ if (!props.config.filters?.length) return null;
104
+ return props.config.filters.map(f => (
105
+ <div class="k-crud-page-filter-item" key={f.param}>
106
+ <label class="k-crud-page-filter-label">{f.label}</label>
107
+ {f.type === 'select' ? (
108
+ <KSelect
109
+ class="k-crud-page-filter-control"
110
+ modelValue={(filters[f.param] as string) ?? ''}
111
+ options={f.options ?? []}
112
+ optionParam="label"
113
+ valueParam="value"
114
+ placeholder={f.placeholder ?? '全部'}
115
+ onUpdate:modelValue={(v: unknown) => {
116
+ filters[f.param] = (v as string) || undefined;
117
+ }}
118
+ />
119
+ ) : (
120
+ <KInput
121
+ class="k-crud-page-filter-control"
122
+ placeholder={f.placeholder ?? `请输入${f.label}`}
123
+ modelValue={(filters[f.param] as string) ?? ''}
124
+ onUpdate:modelValue={(v: string) => {
125
+ filters[f.param] = v || undefined;
126
+ }}
127
+ />
128
+ )}
129
+ </div>
130
+ ));
131
+ };
132
+
133
+ return () => (
134
+ <div class="k-crud-page">
135
+ <KPageHeader title={props.config.title}>
136
+ {{ extra: slots.headerExtra }}
137
+ </KPageHeader>
138
+
139
+ <KSearchTable
140
+ data={list.value}
141
+ loading={loading.value}
142
+ total={total.value}
143
+ page={page.value}
144
+ pageSize={pageSize.value}
145
+ searchable={!!props.config.filters?.length}
146
+ onUpdate:page={onPageChange}
147
+ onSearch={onSearch}
148
+ onReset={onReset}
149
+ >
150
+ {{
151
+ search: renderFilters,
152
+ toolbar: slots.toolbar,
153
+ default: () => props.config.columns.map(col => {
154
+ const customSlot = slots[`column-${col.param}`];
155
+ return (
156
+ <KTableColumn
157
+ key={col.param}
158
+ param={col.param}
159
+ label={col.label}
160
+ width={col.width ?? ''}
161
+ >
162
+ {{
163
+ default: customSlot
164
+ ? (scope: { row: Record<string, unknown>; index: number }) => customSlot(scope)
165
+ : col.type
166
+ ? (scope: { row: Record<string, unknown> }) => renderCell(col, scope.row)
167
+ : undefined,
168
+ }}
169
+ </KTableColumn>
170
+ );
171
+ }),
172
+ empty: slots.empty,
173
+ }}
174
+ </KSearchTable>
175
+ </div>
176
+ );
177
+ },
178
+ });
@@ -0,0 +1,64 @@
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 .k-image-placeholder {
39
+ min-width: unset;
40
+ min-height: unset;
41
+ }
42
+
43
+ .k-image-empty {
44
+ color: var(--kine-color-text-muted);
45
+ opacity: 0.4;
46
+ }
47
+
48
+ /* ================================================================
49
+ 移动端(<768px)
50
+ ================================================================ */
51
+
52
+ @media (max-width: 767px) {
53
+ .k-crud-page {
54
+ padding: var(--kine-spacing-4);
55
+ gap: var(--kine-spacing-4);
56
+ }
57
+
58
+ /* 筛选项自动换行,撑满宽度 */
59
+ .k-crud-page-filter-item {
60
+ min-width: 0;
61
+ flex: 1 1 100%;
62
+ }
63
+ }
64
+
@@ -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,281 @@
1
+ /**
2
+ * @description KEditableTable — 可编辑行表格组件
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.1.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ *
9
+ * 所有行永远可编辑,不区分查看/编辑态。
10
+ * - 每个 cell 直接渲染为 input
11
+ * - 删除:内联确认
12
+ * - 新增:底部按钮
13
+ * - 汇总行:底部加粗合计
14
+ */
15
+ import { defineComponent, ref, type PropType } from 'vue';
16
+ import KButton from 'kine-ui/components/button/KButton.tsx';
17
+ import KInput from 'kine-ui/components/input/KInput.tsx';
18
+ import KSelect from 'kine-ui/components/select/KSelect.tsx';
19
+ import './editableTable.css';
20
+
21
+ // ────────────────────────────────────────────────────────────────────────────
22
+ // 类型
23
+ // ────────────────────────────────────────────────────────────────────────────
24
+
25
+ export interface EditableColumn {
26
+ /** 字段名 */
27
+ param: string;
28
+ /** 列标题 */
29
+ label: string;
30
+ /** 列宽 */
31
+ width?: string;
32
+ /** 输入类型 */
33
+ editType?: 'text' | 'number' | 'select' | 'search' | 'readonly';
34
+ /** select 类型的选项 */
35
+ options?: Array<{ label: string; value: string | number }>;
36
+ /** 对齐方式 */
37
+ align?: 'left' | 'center' | 'right';
38
+ /** 是否在汇总行中求和 */
39
+ summary?: boolean;
40
+ /** 汇总行格式化 */
41
+ summaryFormat?: (sum: number) => string;
42
+ }
43
+
44
+ export interface EditableTableRow {
45
+ /** 行唯一标识 */
46
+ _id: string | number;
47
+ /** 行数据 */
48
+ [key: string]: unknown;
49
+ }
50
+
51
+ // ────────────────────────────────────────────────────────────────────────────
52
+ // 组件
53
+ // ────────────────────────────────────────────────────────────────────────────
54
+
55
+ export default defineComponent({
56
+ name: 'KEditableTable',
57
+ props: {
58
+ /** 列定义 */
59
+ columns: { type: Array as PropType<EditableColumn[]>, required: true },
60
+ /** 行数据(v-model) */
61
+ modelValue: { type: Array as PropType<EditableTableRow[]>, default: () => [] },
62
+ /** 标题 */
63
+ title: { type: String, default: '' },
64
+ /** 是否显示序号列 */
65
+ showIndex: { type: Boolean, default: true },
66
+ /** 是否显示操作列 */
67
+ showActions: { type: Boolean, default: true },
68
+ /** 是否显示汇总行 */
69
+ showSummary: { type: Boolean, default: false },
70
+ /** 汇总行标签 */
71
+ summaryLabel: { type: String, default: '合计' },
72
+ /** 新增行按钮文本 */
73
+ addText: { type: String, default: '+ 添加行' },
74
+ /** 是否可新增 */
75
+ addable: { type: Boolean, default: true },
76
+ /** 币种前缀(用于金额汇总展示) */
77
+ currencyPrefix: { type: String, default: '' },
78
+ },
79
+ emits: ['update:modelValue', 'add', 'remove'],
80
+ setup(props, { emit, slots }) {
81
+ const deletingId = ref<string | number | null>(null);
82
+
83
+ // ── 更新某行某字段 ──────────────────────────────
84
+
85
+ const updateCell = (rowIdx: number, param: string, value: unknown) => {
86
+ const rows = [...props.modelValue];
87
+ rows[rowIdx] = { ...rows[rowIdx], [param]: value };
88
+ emit('update:modelValue', rows);
89
+ };
90
+
91
+ // ── 删除 ────────────────────────────────────────
92
+
93
+ const startDelete = (row: EditableTableRow) => {
94
+ deletingId.value = row._id;
95
+ };
96
+
97
+ const confirmDelete = () => {
98
+ if (deletingId.value === null) return;
99
+ const rows = props.modelValue.filter(r => r._id !== deletingId.value);
100
+ emit('update:modelValue', rows);
101
+ emit('remove', deletingId.value);
102
+ deletingId.value = null;
103
+ };
104
+
105
+ const cancelDelete = () => {
106
+ deletingId.value = null;
107
+ };
108
+
109
+ const addRow = () => {
110
+ emit('add');
111
+ };
112
+
113
+ // ── 汇总计算 ──────────────────────────────────
114
+
115
+ const getSummary = (col: EditableColumn): string => {
116
+ if (!col.summary) return '';
117
+ const sum = props.modelValue.reduce((acc, row) => {
118
+ const val = Number(row[col.param]);
119
+ return acc + (isNaN(val) ? 0 : val);
120
+ }, 0);
121
+ if (col.summaryFormat) return col.summaryFormat(sum);
122
+ return sum.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
123
+ };
124
+
125
+ // ── 渲染单元格(永远是输入框) ──────────────────
126
+
127
+ const renderCell = (col: EditableColumn, row: EditableTableRow, rowIdx: number) => {
128
+ const editSlot = slots[`edit-${col.param}`];
129
+ if (editSlot) return editSlot({ row, value: row[col.param], update: (v: unknown) => updateCell(rowIdx, col.param, v) });
130
+
131
+ const customSlot = slots[`column-${col.param}`];
132
+ if (customSlot) return customSlot({ row, value: row[col.param] });
133
+
134
+ const type = col.editType ?? 'text';
135
+
136
+ if (type === 'readonly') {
137
+ return <span class="k-et-cell-readonly">{row[col.param] as string ?? ''}</span>;
138
+ }
139
+
140
+ if (type === 'select' && col.options) {
141
+ return (
142
+ <KSelect
143
+ class="k-et-select-wrap"
144
+ modelValue={row[col.param]}
145
+ options={col.options}
146
+ optionParam="label"
147
+ valueParam="value"
148
+ onUpdate:modelValue={(v: unknown) => updateCell(rowIdx, col.param, v)}
149
+ />
150
+ );
151
+ }
152
+
153
+ return (
154
+ <KInput
155
+ class="k-et-input-wrap"
156
+ type={type === 'number' ? 'number' : 'text'}
157
+ modelValue={row[col.param] as string | number}
158
+ onUpdate:modelValue={(v: string) => updateCell(rowIdx, col.param, v)}
159
+ />
160
+ );
161
+ };
162
+
163
+ return () => {
164
+ const rows = props.modelValue;
165
+ const cols = props.columns;
166
+ const isEmpty = rows.length === 0;
167
+
168
+ return (
169
+ <div class="k-editable-table">
170
+ {/* 标题栏 */}
171
+ {(props.title || slots.header) && (
172
+ <div class="k-et-header">
173
+ <div class="k-et-header-left">
174
+ {props.title && <span class="k-et-title">{props.title}</span>}
175
+ {rows.length > 0 && <span class="k-et-count">{rows.length} items</span>}
176
+ </div>
177
+ <div class="k-et-header-right">
178
+ {slots.headerExtra?.()}
179
+ {props.addable && (
180
+ <KButton plain text={props.addText} onClick={addRow} />
181
+ )}
182
+ </div>
183
+ </div>
184
+ )}
185
+
186
+ {/* 表格 */}
187
+ <table class="k-et-table">
188
+ <thead class="k-et-thead">
189
+ <tr>
190
+ {props.showIndex && <th class="k-et-th k-et-th-index">#</th>}
191
+ {cols.map(col => (
192
+ <th
193
+ key={col.param}
194
+ class={['k-et-th', col.align && `k-et-align-${col.align}`]}
195
+ style={col.width ? { width: col.width } : undefined}
196
+ >
197
+ {col.label}
198
+ </th>
199
+ ))}
200
+ {props.showActions && <th class="k-et-th k-et-th-actions" />}
201
+ </tr>
202
+ </thead>
203
+
204
+ <tbody class="k-et-tbody">
205
+ {rows.map((row, i) => {
206
+ const isDeleting = deletingId.value === row._id;
207
+
208
+ if (isDeleting) {
209
+ return (
210
+ <tr key={row._id} class="k-et-tr k-et-tr--deleting">
211
+ {props.showIndex && <td class="k-et-td k-et-td-index">{String(i + 1).padStart(2, '0')}</td>}
212
+ <td class="k-et-td" colspan={cols.length + (props.showActions ? 1 : 0)}>
213
+ <span class="k-et-delete-confirm">
214
+ <s class="k-et-deleted-text">{row[cols[0].param] as string ?? ''}</s>
215
+ <span class="k-et-delete-ask">确定删除这一行?</span>
216
+ <KButton type="danger" size="small" text="删除" onClick={confirmDelete} />
217
+ <KButton plain size="small" text="取消" onClick={cancelDelete} />
218
+ </span>
219
+ </td>
220
+ </tr>
221
+ );
222
+ }
223
+
224
+ return (
225
+ <tr key={row._id} class="k-et-tr k-et-tr--editing">
226
+ {props.showIndex && <td class="k-et-td k-et-td-index">{String(i + 1).padStart(2, '0')}</td>}
227
+ {cols.map(col => (
228
+ <td key={col.param} class={['k-et-td', col.align && `k-et-align-${col.align}`]}>
229
+ {renderCell(col, row, i)}
230
+ </td>
231
+ ))}
232
+ {props.showActions && (
233
+ <td class="k-et-td k-et-td-actions">
234
+ <KButton plain size="small" text="×" onClick={() => startDelete(row)} />
235
+ </td>
236
+ )}
237
+ </tr>
238
+ );
239
+ })}
240
+ </tbody>
241
+
242
+ {/* 汇总行 */}
243
+ {props.showSummary && rows.length > 0 && (
244
+ <tfoot class="k-et-tfoot">
245
+ <tr class="k-et-tr k-et-tr--summary">
246
+ {props.showIndex && <td class="k-et-td" />}
247
+ {cols.map((col, ci) => (
248
+ <td key={col.param} class={['k-et-td k-et-td-summary', col.align && `k-et-align-${col.align}`]}>
249
+ {ci === 0 ? props.summaryLabel : ''}
250
+ {col.summary ? (
251
+ <span class="k-et-summary-value">
252
+ {props.currencyPrefix && col.summaryFormat === undefined && ci === cols.length - 1 ? `${props.currencyPrefix} ` : ''}
253
+ {getSummary(col)}
254
+ </span>
255
+ ) : ''}
256
+ </td>
257
+ ))}
258
+ {props.showActions && <td class="k-et-td" />}
259
+ </tr>
260
+ </tfoot>
261
+ )}
262
+ </table>
263
+
264
+ {/* 空态 */}
265
+ {isEmpty && (
266
+ <div class="k-et-empty">
267
+ {slots.empty?.() ?? (
268
+ <>
269
+ <span class="k-et-empty-text">暂无数据</span>
270
+ {props.addable && (
271
+ <KButton type="primary" text="添加第一行" onClick={addRow} />
272
+ )}
273
+ </>
274
+ )}
275
+ </div>
276
+ )}
277
+ </div>
278
+ );
279
+ };
280
+ },
281
+ });