@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,268 @@
1
+ /**
2
+ * @description KEditableTable 样式 — 对标 AIDesigner 设计规范
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 设计要点:
8
+ * - 编辑行:左侧 3px 靛蓝竖线
9
+ * - 删除行:红色文字 + 删线 + 内联确认
10
+ * - 新增行:底部空行带 placeholder
11
+ * - 汇总行:加粗金额
12
+ */
13
+
14
+ .k-editable-table {
15
+ width: 100%;
16
+ }
17
+
18
+ /* ── 标题栏 ──────────────────────────── */
19
+
20
+ .k-et-header {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ padding: 0 0 var(--kine-spacing-6);
25
+ }
26
+
27
+ .k-et-header-left {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: var(--kine-spacing-4);
31
+ }
32
+
33
+ .k-et-title {
34
+ font-size: var(--kine-font-size-xl);
35
+ font-weight: var(--kine-font-weight-semibold);
36
+ color: var(--kine-color-text-primary, #111827);
37
+ }
38
+
39
+ .k-et-count {
40
+ font-size: var(--kine-font-size-sm);
41
+ padding: 1px 7px;
42
+ border-radius: 100px;
43
+ background: var(--kine-color-bg-inset, #f3f4f6);
44
+ color: var(--kine-color-text-secondary, #6b7280);
45
+ font-weight: var(--kine-font-weight-medium);
46
+ }
47
+
48
+ .k-et-header-right {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--kine-spacing-4);
52
+ }
53
+
54
+ /* ── 表格 ──────────────────────────── */
55
+
56
+ .k-et-table {
57
+ width: 100%;
58
+ border-collapse: collapse;
59
+ }
60
+
61
+ .k-et-thead {
62
+ border-bottom: 1px solid var(--kine-color-border-default, #e5e7eb);
63
+ }
64
+
65
+ .k-et-th {
66
+ padding: var(--kine-spacing-4) var(--kine-spacing-6);
67
+ font-size: var(--kine-font-size-sm);
68
+ font-weight: var(--kine-font-weight-medium);
69
+ color: var(--kine-color-text-muted, #9ca3af);
70
+ text-transform: uppercase;
71
+ letter-spacing: 0.05em;
72
+ text-align: left;
73
+ white-space: nowrap;
74
+ }
75
+
76
+ .k-et-th-index {
77
+ width: 40px;
78
+ }
79
+
80
+ .k-et-th-actions {
81
+ width: 70px;
82
+ }
83
+
84
+ .k-et-td {
85
+ padding: var(--kine-spacing-5) var(--kine-spacing-6);
86
+ font-size: var(--kine-font-size-lg);
87
+ color: var(--kine-color-text-primary, #111827);
88
+ border-bottom: 1px solid var(--kine-color-border-subtle, #f3f4f6);
89
+ vertical-align: middle;
90
+ }
91
+
92
+ .k-et-td-index {
93
+ color: var(--kine-color-text-muted, #9ca3af);
94
+ font-size: var(--kine-font-size-md);
95
+ font-variant-numeric: tabular-nums;
96
+ }
97
+
98
+ /* 对齐 */
99
+ .k-et-align-center { text-align: center; }
100
+ .k-et-align-right { text-align: right; }
101
+
102
+ /* 行 hover */
103
+ .k-et-tr {
104
+ transition: background var(--kine-motion-duration-fast) var(--kine-motion-easing-default);
105
+ }
106
+
107
+ .k-et-tr:hover {
108
+ background: var(--kine-color-bg-hover, rgba(0, 0, 0, 0.02));
109
+ }
110
+
111
+ /* ── 编辑行 ──────────────────────────── */
112
+
113
+ .k-et-tr--editing {
114
+ background: rgba(79, 70, 229, 0.02);
115
+ border-left: 3px solid var(--kine-color-accent-default, #4f46e5);
116
+ }
117
+
118
+ .k-et-tr--editing:hover {
119
+ background: rgba(79, 70, 229, 0.03);
120
+ }
121
+
122
+ .k-et-tr--editing .k-et-td:first-child {
123
+ padding-left: var(--kine-spacing-4); /* ~8px,视觉上约等于 12 - 3px border */
124
+ }
125
+
126
+ /* 行内编辑态 KInput / KSelect 包裹容器,宽度撑满单元格 */
127
+ .k-et-input-wrap,
128
+ .k-et-select-wrap {
129
+ width: 100%;
130
+ }
131
+
132
+ .k-et-cell-readonly {
133
+ color: var(--kine-color-text-muted, #9ca3af);
134
+ }
135
+
136
+ /* ── 删除确认行 ──────────────────────────── */
137
+
138
+ .k-et-tr--deleting {
139
+ background: rgba(220, 38, 38, 0.02);
140
+ }
141
+
142
+ .k-et-tr--deleting:hover {
143
+ background: rgba(220, 38, 38, 0.03);
144
+ }
145
+
146
+ .k-et-delete-confirm {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: var(--kine-spacing-6);
150
+ font-size: var(--kine-font-size-lg);
151
+ }
152
+
153
+ .k-et-deleted-text {
154
+ color: var(--kine-color-semantic-error, #dc2626);
155
+ text-decoration: line-through;
156
+ }
157
+
158
+ .k-et-delete-ask {
159
+ color: var(--kine-color-semantic-error, #dc2626);
160
+ font-size: var(--kine-font-size-md);
161
+ }
162
+
163
+ /* ── 操作图标列 ──────────────────────────── */
164
+
165
+ .k-et-td-actions {
166
+ text-align: right;
167
+ white-space: nowrap;
168
+ }
169
+
170
+ /* ── 汇总行 ──────────────────────────── */
171
+
172
+ .k-et-tfoot {
173
+ border-top: 1px solid var(--kine-color-border-default, #e5e7eb);
174
+ }
175
+
176
+ .k-et-tr--summary {
177
+ background: transparent;
178
+ }
179
+
180
+ .k-et-tr--summary:hover {
181
+ background: transparent;
182
+ }
183
+
184
+ .k-et-td-summary {
185
+ font-weight: var(--kine-font-weight-semibold);
186
+ color: var(--kine-color-text-primary, #111827);
187
+ font-size: var(--kine-font-size-lg);
188
+ }
189
+
190
+ .k-et-summary-value {
191
+ font-variant-numeric: tabular-nums;
192
+ letter-spacing: -0.01em;
193
+ }
194
+
195
+ /* ── 空态 ──────────────────────────── */
196
+
197
+ .k-et-empty {
198
+ display: flex;
199
+ flex-direction: column;
200
+ align-items: center;
201
+ gap: var(--kine-spacing-6);
202
+ padding: var(--kine-spacing-24) var(--kine-spacing-12);
203
+ }
204
+
205
+ .k-et-empty-text {
206
+ font-size: var(--kine-font-size-lg);
207
+ color: var(--kine-color-text-muted, #9ca3af);
208
+ }
209
+
210
+ /* ================================================================
211
+ Dark Mode
212
+ ================================================================ */
213
+
214
+ @media (prefers-color-scheme: dark) {
215
+ .k-et-thead {
216
+ border-bottom-color: var(--kine-color-border-default, #2e2e3e);
217
+ }
218
+
219
+ .k-et-td {
220
+ color: var(--kine-color-text-primary, #e5e5e5);
221
+ border-bottom-color: var(--kine-color-border-subtle, #252535);
222
+ }
223
+
224
+ .k-et-tr:hover {
225
+ background: rgba(255, 255, 255, 0.03);
226
+ }
227
+
228
+ .k-et-tr--editing {
229
+ background: rgba(79, 70, 229, 0.06);
230
+ }
231
+
232
+ .k-et-tfoot {
233
+ border-top-color: var(--kine-color-border-default, #2e2e3e);
234
+ }
235
+
236
+ .k-et-td-summary {
237
+ color: var(--kine-color-text-primary, #e5e5e5);
238
+ }
239
+
240
+ .k-et-icon-btn:hover {
241
+ background: rgba(255, 255, 255, 0.06);
242
+ }
243
+
244
+ .k-et-count {
245
+ background: var(--kine-color-bg-inset, #252535);
246
+ }
247
+ }
248
+
249
+ .dark .k-et-thead,
250
+ [data-theme="dark"] .k-et-thead {
251
+ border-bottom-color: var(--kine-color-border-default, #2e2e3e);
252
+ }
253
+
254
+ .dark .k-et-td,
255
+ [data-theme="dark"] .k-et-td {
256
+ color: var(--kine-color-text-primary, #e5e5e5);
257
+ border-bottom-color: var(--kine-color-border-subtle, #252535);
258
+ }
259
+
260
+ .dark .k-et-tfoot,
261
+ [data-theme="dark"] .k-et-tfoot {
262
+ border-top-color: var(--kine-color-border-default, #2e2e3e);
263
+ }
264
+
265
+ .dark .k-et-td-summary,
266
+ [data-theme="dark"] .k-et-td-summary {
267
+ color: var(--kine-color-text-primary, #e5e5e5);
268
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @description editableTable barrel export
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ export { default as KEditableTable } from './KEditableTable';
10
+ export type { EditableColumn, EditableTableRow } from './KEditableTable';
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @description KApprovalDialog — 审批操作弹窗(提交/通过/驳回)
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ *
9
+ * 配置驱动,传入 type 自动切换视觉风格:
10
+ * - submit: 靛蓝确认按钮
11
+ * - approve: 绿色确认按钮
12
+ * - reject: 红色确认按钮,备注必填
13
+ */
14
+ import { defineComponent, ref, type PropType } from 'vue';
15
+ import KButton from 'kine-ui/components/button/KButton.tsx';
16
+ import KInput from 'kine-ui/components/input/KInput.tsx';
17
+ import './formPage.css';
18
+
19
+ export type ApprovalType = 'submit' | 'approve' | 'reject';
20
+
21
+ const CONFIG = {
22
+ submit: {
23
+ title: '提交审批',
24
+ confirmText: '确认提交',
25
+ btnType: 'primary' as const,
26
+ },
27
+ approve: {
28
+ title: '审批通过',
29
+ confirmText: '确认通过',
30
+ btnType: 'success' as const,
31
+ },
32
+ reject: {
33
+ title: '驳回申请',
34
+ confirmText: '确认驳回',
35
+ btnType: 'danger' as const,
36
+ },
37
+ } as const;
38
+
39
+ export default defineComponent({
40
+ name: 'KApprovalDialog',
41
+ props: {
42
+ /** 弹窗是否可见 */
43
+ visible: { type: Boolean, default: false },
44
+ /** 审批类型 */
45
+ type: { type: String as PropType<ApprovalType>, default: 'submit' },
46
+ /** 自定义标题,不传则使用默认 */
47
+ title: { type: String, default: '' },
48
+ /** 描述文字 */
49
+ description: { type: String, default: '' },
50
+ /** 摘要信息,如 "PO-2024-0087 | 供应商: 三星纺织 | ¥156,200" */
51
+ summary: { type: String, default: '' },
52
+ /** 备注输入框占位文本 */
53
+ remarkPlaceholder: { type: String, default: '' },
54
+ /** 备注是否必填(reject 默认必填) */
55
+ remarkRequired: { type: Boolean, default: undefined },
56
+ /** 确认中加载态 */
57
+ loading: { type: Boolean, default: false },
58
+ },
59
+ emits: ['update:visible', 'confirm', 'cancel'],
60
+ setup(props, { emit, slots }) {
61
+ const remark = ref('');
62
+
63
+ const close = () => {
64
+ emit('update:visible', false);
65
+ emit('cancel');
66
+ };
67
+
68
+ const confirm = () => {
69
+ emit('confirm', { remark: remark.value });
70
+ };
71
+
72
+ const isRemarkRequired = () => {
73
+ if (props.remarkRequired !== undefined) return props.remarkRequired;
74
+ return props.type === 'reject';
75
+ };
76
+
77
+ const canConfirm = () => {
78
+ if (isRemarkRequired() && !remark.value.trim()) return false;
79
+ return !props.loading;
80
+ };
81
+
82
+ return () => {
83
+ if (!props.visible) return null;
84
+
85
+ const cfg = CONFIG[props.type];
86
+ const displayTitle = props.title || cfg.title;
87
+ const placeholder = props.remarkPlaceholder
88
+ || (props.type === 'reject' ? '请输入驳回原因...' : '添加备注(可选)...');
89
+
90
+ return (
91
+ <div class="k-approval-overlay" onClick={close}>
92
+ <div class="k-approval-dialog" onClick={(e: Event) => e.stopPropagation()}>
93
+ {/* 头部 */}
94
+ <div class="k-approval-dialog-header">
95
+ <span class="k-approval-dialog-title">{displayTitle}</span>
96
+ <KButton plain text="×" onClick={close} />
97
+ </div>
98
+
99
+ {/* 内容 */}
100
+ <div class="k-approval-dialog-body">
101
+ {slots.default?.()}
102
+
103
+ {props.summary && (
104
+ <div class="k-approval-dialog-summary">{props.summary}</div>
105
+ )}
106
+
107
+ {props.description && (
108
+ <p class="k-approval-dialog-desc">{props.description}</p>
109
+ )}
110
+
111
+ <KInput
112
+ type="textarea"
113
+ class={[
114
+ 'k-approval-dialog-remark',
115
+ isRemarkRequired() && 'k-approval-dialog-remark--required',
116
+ ].filter(Boolean)}
117
+ placeholder={placeholder}
118
+ modelValue={remark.value}
119
+ onUpdate:modelValue={(v: string) => { remark.value = v; }}
120
+ />
121
+ {isRemarkRequired() && (
122
+ <span class="k-approval-dialog-required-hint">* 必填</span>
123
+ )}
124
+ </div>
125
+
126
+ {/* 底部 */}
127
+ <div class="k-approval-dialog-footer">
128
+ <KButton text="取消" onClick={close} />
129
+ <KButton
130
+ type={cfg.btnType}
131
+ text={props.loading ? '处理中...' : cfg.confirmText}
132
+ disabled={!canConfirm()}
133
+ loading={props.loading}
134
+ onClick={confirm}
135
+ />
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ };
141
+ },
142
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @description KFormCard — 表单区块卡片容器,带标题和可选折叠
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ *
9
+ * 用于 SimpleFormPage / MasterDetailFormPage 中的分区展示。
10
+ * 提供标题、副标题、右侧操作区 slot、可折叠能力。
11
+ */
12
+ import { defineComponent, ref } from 'vue';
13
+ import './formPage.css';
14
+
15
+ export default defineComponent({
16
+ name: 'KFormCard',
17
+ props: {
18
+ /** 区块标题 */
19
+ title: { type: String, default: '' },
20
+ /** 副标题 */
21
+ subtitle: { type: String, default: '' },
22
+ /** 是否可折叠 */
23
+ collapsible: { type: Boolean, default: false },
24
+ /** 初始是否折叠 */
25
+ defaultCollapsed: { type: Boolean, default: false },
26
+ },
27
+ setup(props, { slots }) {
28
+ const collapsed = ref(props.defaultCollapsed);
29
+
30
+ const toggle = () => {
31
+ if (props.collapsible) {
32
+ collapsed.value = !collapsed.value;
33
+ }
34
+ };
35
+
36
+ return () => (
37
+ <div class={['k-form-card', collapsed.value && 'k-form-card--collapsed']}>
38
+ {(props.title || slots.header) && (
39
+ <div
40
+ class={['k-form-card-header', props.collapsible && 'k-form-card-header--clickable']}
41
+ onClick={toggle}
42
+ >
43
+ <div class="k-form-card-header-left">
44
+ {props.title && <h3 class="k-form-card-title">{props.title}</h3>}
45
+ {props.subtitle && <span class="k-form-card-subtitle">{props.subtitle}</span>}
46
+ </div>
47
+ <div class="k-form-card-header-right">
48
+ {slots.extra?.()}
49
+ {props.collapsible && (
50
+ <span class={['k-form-card-collapse-icon', collapsed.value && 'k-form-card-collapse-icon--collapsed']}>
51
+
52
+ </span>
53
+ )}
54
+ </div>
55
+ </div>
56
+ )}
57
+ {!collapsed.value && (
58
+ <div class="k-form-card-body">
59
+ {slots.default?.()}
60
+ </div>
61
+ )}
62
+ </div>
63
+ );
64
+ },
65
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @description KFormPage — 配置驱动的简单表单页组件
3
+ * @author 阿怪
4
+ * @date 2026/3/22
5
+ * @version v0.0.1
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ *
9
+ * 一个配置出一整个表单页:页头 + 字段网格 + 校验 + 底部操作栏。
10
+ * 业务页面只需 <KFormPage :config="config" /> 即可。
11
+ */
12
+ import { defineComponent, type PropType } from 'vue';
13
+ import { useFormPage, renderFormField } from '../../composables/form';
14
+ import type { FormPageConfig } from '../../composables/form/types';
15
+ import KFormCard from './KFormCard';
16
+ import KStickyActionBar from './KStickyActionBar';
17
+ import KButton from 'kine-ui/components/button/KButton.tsx';
18
+ import './formPage.css';
19
+
20
+ export default defineComponent({
21
+ name: 'KFormPage',
22
+ props: {
23
+ config: { type: Object as PropType<FormPageConfig>, required: true },
24
+ },
25
+ setup(props, { slots }) {
26
+ const {
27
+ formData,
28
+ isEdit,
29
+ loading,
30
+ submitting,
31
+ errors,
32
+ validateField,
33
+ submit,
34
+ saveDraft,
35
+ goBack,
36
+ } = useFormPage({
37
+ api: props.config.api,
38
+ fields: props.config.fields,
39
+ rowKey: props.config.rowKey,
40
+ redirectPath: props.config.redirectPath,
41
+ });
42
+
43
+ const cols = props.config.columns ?? 2;
44
+
45
+ // ── 渲染 ──────────────────────────────────────
46
+
47
+ return () => {
48
+ if (loading.value) {
49
+ return <div class="k-fp-loading">加载中...</div>;
50
+ }
51
+
52
+ const title = isEdit.value ? `编辑${props.config.title}` : `新建${props.config.title}`;
53
+
54
+ return (
55
+ <div class="k-form-page-wrapper">
56
+ <div class="k-form-page">
57
+ {/* 页头 */}
58
+ <div class="k-fp-header">
59
+ <div class="k-fp-header-left">
60
+ <KButton text="←" onClick={goBack} />
61
+ <h1 class="k-fp-title">{title}</h1>
62
+ </div>
63
+ <div class="k-fp-header-right">
64
+ {slots.headerExtra?.()}
65
+ </div>
66
+ </div>
67
+
68
+ {/* 表单 */}
69
+ <KFormCard>
70
+ {slots.beforeFields?.({ formData })}
71
+
72
+ <div class={`k-fp-grid k-fp-grid--${cols}`}>
73
+ {props.config.fields.map(field => {
74
+ const span = field.span === 'full' ? cols : (field.span ?? 1);
75
+ return (
76
+ <div
77
+ key={field.param}
78
+ class="k-fp-field"
79
+ style={span > 1 ? { gridColumn: `span ${span}` } : undefined}
80
+ >
81
+ <label class="k-fp-label">
82
+ {field.label}
83
+ {field.required && <span class="k-fp-required">*</span>}
84
+ </label>
85
+ {renderFormField(field, formData, errors.value, validateField, slots)}
86
+ {errors.value[field.param] && (
87
+ <span class="k-fp-error">{errors.value[field.param]}</span>
88
+ )}
89
+ </div>
90
+ );
91
+ })}
92
+ </div>
93
+
94
+ {slots.afterFields?.({ formData })}
95
+ </KFormCard>
96
+
97
+ {slots.default?.({ formData })}
98
+ </div>
99
+
100
+ {/* 底部操作栏 — 在 max-width 容器外,撑满内容区 */}
101
+ <KStickyActionBar>
102
+ {{
103
+ default: () => slots.actions?.({ formData, submit, saveDraft, submitting }) ?? (
104
+ <>
105
+ <KButton text="取消" onClick={goBack} />
106
+ {props.config.showDraft && (
107
+ <KButton
108
+ text="保存草稿"
109
+ disabled={submitting.value}
110
+ onClick={() => saveDraft()}
111
+ />
112
+ )}
113
+ <KButton
114
+ type="primary"
115
+ text={submitting.value ? '保存中...' : (props.config.submitText ?? '保存')}
116
+ disabled={submitting.value}
117
+ loading={submitting.value}
118
+ onClick={() => submit()}
119
+ />
120
+ </>
121
+ ),
122
+ }}
123
+ </KStickyActionBar>
124
+ </div>
125
+ );
126
+ };
127
+ },
128
+ });