@fcurd/core 0.1.0

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.
package/dist/index.mjs ADDED
@@ -0,0 +1,702 @@
1
+ import { ref, reactive, computed, shallowRef, watch, onScopeDispose } from 'vue';
2
+
3
+ // src/hooks/useCrudActions.ts
4
+ function useCrudActions(options = {}) {
5
+ const { actions: initialActions = [] } = options;
6
+ const actions = ref([...initialActions]);
7
+ function getByArea(area) {
8
+ return actions.value.filter((a) => a.area === area).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
9
+ }
10
+ function register(action) {
11
+ const existing = actions.value.findIndex((a) => a.id === action.id);
12
+ if (existing >= 0) {
13
+ actions.value[existing] = action;
14
+ } else {
15
+ actions.value.push(action);
16
+ }
17
+ }
18
+ function unregister(id) {
19
+ const idx = actions.value.findIndex((a) => a.id === id);
20
+ if (idx >= 0) {
21
+ actions.value.splice(idx, 1);
22
+ }
23
+ }
24
+ return {
25
+ actions,
26
+ getByArea,
27
+ register,
28
+ unregister
29
+ };
30
+ }
31
+ function createAction(options) {
32
+ return {
33
+ id: "create",
34
+ label: options.label ?? "\u65B0\u589E",
35
+ type: "primary",
36
+ area: "toolbar",
37
+ order: 0,
38
+ onClick: options.onClick
39
+ };
40
+ }
41
+ function editAction(options) {
42
+ return {
43
+ id: "edit",
44
+ label: options.label ?? "\u7F16\u8F91",
45
+ type: "default",
46
+ area: "row",
47
+ order: 0,
48
+ onClick: (ctx) => {
49
+ if (ctx.row) {
50
+ options.onClick(ctx.row);
51
+ }
52
+ }
53
+ };
54
+ }
55
+ function deleteAction(options) {
56
+ const { adapter, getId = (row) => row?.id, confirm = true } = options;
57
+ return {
58
+ id: "delete",
59
+ label: options.label ?? "\u5220\u9664",
60
+ type: "error",
61
+ area: "row",
62
+ order: 10,
63
+ confirm: confirm === true ? "\u786E\u5B9A\u8981\u5220\u9664\u6B64\u8BB0\u5F55\u5417\uFF1F" : confirm,
64
+ onClick: async (ctx) => {
65
+ if (!ctx.row || !adapter.remove)
66
+ return;
67
+ try {
68
+ const id = getId(ctx.row);
69
+ await adapter.remove(id);
70
+ await ctx.refresh();
71
+ options.onSuccess?.();
72
+ } catch (err) {
73
+ options.onError?.(err);
74
+ }
75
+ }
76
+ };
77
+ }
78
+ function batchDeleteAction(options) {
79
+ const { adapter, getId = (row) => row?.id, confirm = true } = options;
80
+ return {
81
+ id: "batchDelete",
82
+ label: options.label ?? "\u6279\u91CF\u5220\u9664",
83
+ type: "error",
84
+ area: "batch",
85
+ order: 0,
86
+ confirm: confirm === true ? "\u786E\u5B9A\u8981\u5220\u9664\u9009\u4E2D\u7684\u8BB0\u5F55\u5417\uFF1F" : confirm,
87
+ visible: (ctx) => ctx.selectedIds.length > 0,
88
+ onClick: async (ctx) => {
89
+ if (!adapter.remove || ctx.selectedIds.length === 0)
90
+ return;
91
+ try {
92
+ await Promise.all(ctx.selectedIds.map((id) => adapter.remove(id)));
93
+ ctx.clearSelection();
94
+ await ctx.refresh();
95
+ options.onSuccess?.();
96
+ } catch (err) {
97
+ options.onError?.(err);
98
+ }
99
+ }
100
+ };
101
+ }
102
+ function exportAction(options) {
103
+ const { adapter } = options;
104
+ return {
105
+ id: "export",
106
+ label: options.label ?? "\u5BFC\u51FA",
107
+ type: "default",
108
+ area: "toolbar",
109
+ order: 100,
110
+ visible: () => typeof adapter.export === "function",
111
+ onClick: async (ctx) => {
112
+ if (!adapter.export)
113
+ return;
114
+ try {
115
+ const query = ctx.query ?? {};
116
+ const sort = ctx.sort ?? null;
117
+ const result = await adapter.export({
118
+ query,
119
+ sort
120
+ });
121
+ options.handleExport?.(result, options.filename);
122
+ options.onSuccess?.();
123
+ } catch (err) {
124
+ options.onError?.(err);
125
+ }
126
+ }
127
+ };
128
+ }
129
+ var presetActions = {
130
+ create: createAction,
131
+ edit: editAction,
132
+ delete: deleteAction,
133
+ batchDelete: batchDeleteAction,
134
+ export: exportAction
135
+ };
136
+ function useCrudForm(options) {
137
+ const { fields, initialData } = options;
138
+ const model = reactive({});
139
+ const mode = ref("create");
140
+ const initialSnapshot = ref({});
141
+ function initModel(data) {
142
+ Object.keys(model).forEach((key) => {
143
+ delete model[key];
144
+ });
145
+ if (data) {
146
+ Object.assign(model, data);
147
+ }
148
+ initialSnapshot.value = { ...model };
149
+ }
150
+ if (initialData) {
151
+ initModel(initialData);
152
+ }
153
+ const changedKeys = computed(() => {
154
+ const before = initialSnapshot.value ?? {};
155
+ const after = model;
156
+ const keys = /* @__PURE__ */ new Set([
157
+ ...Object.keys(before),
158
+ ...Object.keys(after)
159
+ ]);
160
+ const changed = [];
161
+ keys.forEach((key) => {
162
+ const a = before[key];
163
+ const b = after[key];
164
+ if (a !== b)
165
+ changed.push(key);
166
+ });
167
+ return changed;
168
+ });
169
+ const changedData = computed(() => {
170
+ const after = model;
171
+ const data = {};
172
+ for (const key of changedKeys.value)
173
+ data[key] = after[key];
174
+ return data;
175
+ });
176
+ const dirty = computed(() => changedKeys.value.length > 0);
177
+ const visibleFields = computed(() => {
178
+ return fields.filter((field) => {
179
+ const formVisible = field.visibleIn?.form;
180
+ if (formVisible === void 0 || formVisible === true)
181
+ return true;
182
+ if (formVisible === false)
183
+ return false;
184
+ if (typeof formVisible === "function") {
185
+ const ctx = {
186
+ surface: "form",
187
+ formModel: model
188
+ };
189
+ return formVisible(ctx);
190
+ }
191
+ return true;
192
+ });
193
+ });
194
+ function reset(data) {
195
+ initModel(data);
196
+ }
197
+ function setMode(newMode) {
198
+ mode.value = newMode;
199
+ }
200
+ function getSubmitData() {
201
+ if (mode.value === "create") {
202
+ return { ...model };
203
+ }
204
+ return changedData.value;
205
+ }
206
+ return {
207
+ model,
208
+ mode,
209
+ dirty,
210
+ changedKeys,
211
+ changedData,
212
+ visibleFields,
213
+ reset,
214
+ setMode,
215
+ getSubmitData
216
+ };
217
+ }
218
+ function useCrudList(options) {
219
+ const {
220
+ adapter,
221
+ initialQuery,
222
+ initialPage = 1,
223
+ initialPageSize = 20,
224
+ autoFetch = true,
225
+ debounceMs = 0,
226
+ dedupe = true,
227
+ onError
228
+ } = options;
229
+ const rows = shallowRef([]);
230
+ const total = ref(0);
231
+ const loading = ref(false);
232
+ const error = ref(null);
233
+ const query = shallowRef(initialQuery ?? {});
234
+ const sort = shallowRef(null);
235
+ const page = ref(initialPage);
236
+ const pageSize = ref(initialPageSize);
237
+ let fetchTimer = null;
238
+ let requestSeq = 0;
239
+ let activeSeq = 0;
240
+ let lastKey = null;
241
+ let activeKey = null;
242
+ let abortController = null;
243
+ function buildKey() {
244
+ try {
245
+ return JSON.stringify({
246
+ page: page.value,
247
+ pageSize: pageSize.value,
248
+ query: query.value,
249
+ sort: sort.value
250
+ });
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+ function isAbortError(err) {
256
+ if (!err || typeof err !== "object")
257
+ return false;
258
+ const obj = err;
259
+ return obj.name === "AbortError" || obj.code === "ABORT_ERR";
260
+ }
261
+ function isPlainObject(value) {
262
+ return Object.prototype.toString.call(value) === "[object Object]";
263
+ }
264
+ function pruneEmptyDeep(value) {
265
+ if (value === void 0 || value === null)
266
+ return void 0;
267
+ if (typeof value === "string" && value === "")
268
+ return void 0;
269
+ if (Array.isArray(value)) {
270
+ const next = value.map((v) => pruneEmptyDeep(v)).filter((v) => v !== void 0);
271
+ return next.length === 0 ? void 0 : next;
272
+ }
273
+ if (isPlainObject(value)) {
274
+ const out = {};
275
+ for (const [k, v] of Object.entries(value)) {
276
+ const pv = pruneEmptyDeep(v);
277
+ if (pv !== void 0)
278
+ out[k] = pv;
279
+ }
280
+ return Object.keys(out).length === 0 ? void 0 : out;
281
+ }
282
+ return value;
283
+ }
284
+ async function fetchList(opts) {
285
+ if (!adapter || typeof adapter.list !== "function")
286
+ return;
287
+ const force = Boolean(opts?.force);
288
+ const key = buildKey();
289
+ if (!force && dedupe && key) {
290
+ if (key === lastKey || key === activeKey)
291
+ return;
292
+ }
293
+ if (abortController) {
294
+ try {
295
+ abortController.abort();
296
+ } catch {
297
+ }
298
+ }
299
+ abortController = typeof AbortController !== "undefined" ? new AbortController() : null;
300
+ activeKey = key;
301
+ const seq = requestSeq += 1;
302
+ activeSeq = seq;
303
+ loading.value = true;
304
+ error.value = null;
305
+ try {
306
+ const result = await adapter.list({
307
+ page: page.value,
308
+ pageSize: pageSize.value,
309
+ query: query.value,
310
+ sort: sort.value,
311
+ signal: abortController?.signal
312
+ });
313
+ if (seq !== activeSeq)
314
+ return;
315
+ rows.value = result.items;
316
+ total.value = result.total;
317
+ if (key)
318
+ lastKey = key;
319
+ } catch (err) {
320
+ if (isAbortError(err))
321
+ return;
322
+ if (seq !== activeSeq)
323
+ return;
324
+ error.value = err;
325
+ if (onError)
326
+ onError(err);
327
+ } finally {
328
+ if (seq === activeSeq) {
329
+ loading.value = false;
330
+ activeKey = null;
331
+ }
332
+ }
333
+ }
334
+ function scheduleFetch() {
335
+ if (!autoFetch)
336
+ return;
337
+ if (fetchTimer) {
338
+ clearTimeout(fetchTimer);
339
+ fetchTimer = null;
340
+ }
341
+ if (debounceMs > 0) {
342
+ fetchTimer = setTimeout(() => {
343
+ fetchTimer = null;
344
+ void fetchList({ force: false });
345
+ }, debounceMs);
346
+ return;
347
+ }
348
+ void fetchList({ force: false });
349
+ }
350
+ watch([query, sort, page, pageSize], () => {
351
+ scheduleFetch();
352
+ }, { deep: false });
353
+ function setQuery(partial, options2) {
354
+ const mode = options2?.mode ?? "merge";
355
+ const clearKeys = options2?.clearKeys ?? [];
356
+ const shouldPrune = Boolean(options2?.pruneEmpty);
357
+ const base = mode === "replace" ? {} : { ...query.value };
358
+ for (const k of clearKeys)
359
+ delete base[k];
360
+ const merged = mode === "replace" ? { ...partial } : { ...base, ...partial };
361
+ const next = shouldPrune ? pruneEmptyDeep(merged) ?? {} : merged;
362
+ query.value = next;
363
+ page.value = 1;
364
+ }
365
+ function setPage(nextPage) {
366
+ if (nextPage <= 0)
367
+ return;
368
+ page.value = nextPage;
369
+ }
370
+ function setPageSize(size) {
371
+ if (size <= 0)
372
+ return;
373
+ pageSize.value = size;
374
+ page.value = 1;
375
+ }
376
+ function setSort(nextSort) {
377
+ sort.value = nextSort;
378
+ page.value = 1;
379
+ }
380
+ function reset() {
381
+ query.value = initialQuery ?? {};
382
+ sort.value = null;
383
+ page.value = initialPage;
384
+ pageSize.value = initialPageSize;
385
+ void fetchList({ force: true });
386
+ }
387
+ async function refresh() {
388
+ await fetchList({ force: true });
389
+ }
390
+ scheduleFetch();
391
+ onScopeDispose(() => {
392
+ if (fetchTimer) {
393
+ clearTimeout(fetchTimer);
394
+ fetchTimer = null;
395
+ }
396
+ if (abortController) {
397
+ try {
398
+ abortController.abort();
399
+ } catch {
400
+ }
401
+ abortController = null;
402
+ }
403
+ activeSeq = 0;
404
+ activeKey = null;
405
+ });
406
+ return {
407
+ // State
408
+ rows,
409
+ total,
410
+ loading,
411
+ error,
412
+ query,
413
+ sort,
414
+ page,
415
+ pageSize,
416
+ // Actions
417
+ refresh,
418
+ setQuery,
419
+ setPage,
420
+ setPageSize,
421
+ setSort,
422
+ reset
423
+ };
424
+ }
425
+ function useCrudRouteSync(options) {
426
+ const {
427
+ query,
428
+ setQuery,
429
+ router,
430
+ route,
431
+ queryKey = "q",
432
+ serialize = (q) => JSON.stringify(q),
433
+ deserialize = (s) => JSON.parse(s),
434
+ debounceMs = 300,
435
+ syncFromRouteMode = "replace"
436
+ } = options;
437
+ let syncTimer = null;
438
+ let isSyncingFromRoute = false;
439
+ let releaseSyncTimer = null;
440
+ function isPlainObject(value) {
441
+ return Object.prototype.toString.call(value) === "[object Object]";
442
+ }
443
+ function pruneEmptyDeep(value) {
444
+ if (value === void 0 || value === null)
445
+ return void 0;
446
+ if (typeof value === "string" && value === "")
447
+ return void 0;
448
+ if (Array.isArray(value)) {
449
+ const next = value.map((v) => pruneEmptyDeep(v)).filter((v) => v !== void 0);
450
+ return next.length === 0 ? void 0 : next;
451
+ }
452
+ if (isPlainObject(value)) {
453
+ const out = {};
454
+ for (const [k, v] of Object.entries(value)) {
455
+ const pv = pruneEmptyDeep(v);
456
+ if (pv !== void 0)
457
+ out[k] = pv;
458
+ }
459
+ return Object.keys(out).length === 0 ? void 0 : out;
460
+ }
461
+ return value;
462
+ }
463
+ function syncFromRoute() {
464
+ if (!route)
465
+ return;
466
+ const raw = route.query?.[queryKey];
467
+ if (!raw || typeof raw !== "string")
468
+ return;
469
+ try {
470
+ isSyncingFromRoute = true;
471
+ const parsed = deserialize(raw);
472
+ if (parsed && typeof parsed === "object") {
473
+ setQuery(parsed, {
474
+ mode: syncFromRouteMode,
475
+ pruneEmpty: true
476
+ });
477
+ }
478
+ } catch {
479
+ } finally {
480
+ if (releaseSyncTimer)
481
+ clearTimeout(releaseSyncTimer);
482
+ releaseSyncTimer = setTimeout(() => {
483
+ releaseSyncTimer = null;
484
+ isSyncingFromRoute = false;
485
+ }, 0);
486
+ }
487
+ }
488
+ function syncToRoute() {
489
+ if (!router || !route || isSyncingFromRoute)
490
+ return;
491
+ const currentQuery = query.value;
492
+ const normalized = pruneEmptyDeep(currentQuery);
493
+ const isEmpty = normalized === void 0 || isPlainObject(normalized) && Object.keys(normalized).length === 0;
494
+ const newRouteQuery = { ...route.query };
495
+ if (isEmpty) {
496
+ delete newRouteQuery[queryKey];
497
+ } else {
498
+ newRouteQuery[queryKey] = serialize(normalized);
499
+ }
500
+ const currentStr = route.query?.[queryKey] ?? "";
501
+ const newStr = newRouteQuery[queryKey] ?? "";
502
+ if (currentStr !== newStr) {
503
+ void Promise.resolve(router.replace({ query: newRouteQuery })).catch(() => {
504
+ });
505
+ }
506
+ }
507
+ watch(
508
+ query,
509
+ () => {
510
+ if (isSyncingFromRoute)
511
+ return;
512
+ if (syncTimer) {
513
+ clearTimeout(syncTimer);
514
+ }
515
+ syncTimer = setTimeout(() => {
516
+ syncTimer = null;
517
+ syncToRoute();
518
+ }, debounceMs);
519
+ },
520
+ { deep: true }
521
+ );
522
+ if (route) {
523
+ syncFromRoute();
524
+ }
525
+ onScopeDispose(() => {
526
+ if (syncTimer) {
527
+ clearTimeout(syncTimer);
528
+ syncTimer = null;
529
+ }
530
+ if (releaseSyncTimer) {
531
+ clearTimeout(releaseSyncTimer);
532
+ releaseSyncTimer = null;
533
+ }
534
+ });
535
+ return {
536
+ syncFromRoute,
537
+ syncToRoute
538
+ };
539
+ }
540
+ function useCrudSelection(options) {
541
+ const { rows, getId = (row) => row?.id } = options;
542
+ const selectedIds = shallowRef(/* @__PURE__ */ new Set());
543
+ const selectedRowMap = shallowRef(/* @__PURE__ */ new Map());
544
+ const selectedRows = computed(() => {
545
+ const ids = selectedIds.value;
546
+ const map = selectedRowMap.value;
547
+ const result = [];
548
+ ids.forEach((id) => {
549
+ const row = map.get(id);
550
+ if (row !== void 0)
551
+ result.push(row);
552
+ });
553
+ return result;
554
+ });
555
+ const selectedCount = computed(() => selectedIds.value.size);
556
+ function syncCacheFromRows(nextRows) {
557
+ const ids = selectedIds.value;
558
+ if (ids.size === 0)
559
+ return;
560
+ const map = new Map(selectedRowMap.value);
561
+ for (const row of nextRows) {
562
+ const id = getId(row);
563
+ if (ids.has(id))
564
+ map.set(id, row);
565
+ }
566
+ selectedRowMap.value = map;
567
+ }
568
+ watch(
569
+ rows,
570
+ (newRows) => {
571
+ syncCacheFromRows(newRows);
572
+ },
573
+ { immediate: true }
574
+ );
575
+ watch(
576
+ selectedIds,
577
+ (next) => {
578
+ const map = new Map(selectedRowMap.value);
579
+ for (const id of map.keys()) {
580
+ if (!next.has(id))
581
+ map.delete(id);
582
+ }
583
+ selectedRowMap.value = map;
584
+ syncCacheFromRows(rows.value);
585
+ },
586
+ { deep: false }
587
+ );
588
+ function setSelectedIds(ids) {
589
+ selectedIds.value = new Set(ids);
590
+ }
591
+ function select(id) {
592
+ const newSet = new Set(selectedIds.value);
593
+ newSet.add(id);
594
+ selectedIds.value = newSet;
595
+ const row = rows.value.find((r) => getId(r) === id);
596
+ if (row !== void 0) {
597
+ const map = new Map(selectedRowMap.value);
598
+ map.set(id, row);
599
+ selectedRowMap.value = map;
600
+ }
601
+ }
602
+ function deselect(id) {
603
+ const newSet = new Set(selectedIds.value);
604
+ newSet.delete(id);
605
+ selectedIds.value = newSet;
606
+ const map = new Map(selectedRowMap.value);
607
+ map.delete(id);
608
+ selectedRowMap.value = map;
609
+ }
610
+ function toggle(id) {
611
+ if (selectedIds.value.has(id)) {
612
+ deselect(id);
613
+ } else {
614
+ select(id);
615
+ }
616
+ }
617
+ function selectAll() {
618
+ const map = new Map(selectedRowMap.value);
619
+ const allIds = [];
620
+ for (const row of rows.value) {
621
+ const id = getId(row);
622
+ allIds.push(id);
623
+ map.set(id, row);
624
+ }
625
+ selectedIds.value = new Set(allIds);
626
+ selectedRowMap.value = map;
627
+ }
628
+ function clear() {
629
+ selectedIds.value = /* @__PURE__ */ new Set();
630
+ selectedRowMap.value = /* @__PURE__ */ new Map();
631
+ }
632
+ function isSelected(id) {
633
+ return selectedIds.value.has(id);
634
+ }
635
+ return {
636
+ selectedIds,
637
+ selectedRows,
638
+ selectedCount,
639
+ setSelectedIds,
640
+ select,
641
+ deselect,
642
+ toggle,
643
+ selectAll,
644
+ clear,
645
+ isSelected
646
+ };
647
+ }
648
+
649
+ // src/utils/fields.ts
650
+ function defineFields(fields) {
651
+ return fields;
652
+ }
653
+ function filterFieldsBySurface(fields, surface, ctx) {
654
+ return fields.filter((field) => {
655
+ const visibility = field.visibleIn?.[surface];
656
+ if (visibility === void 0 || visibility === true)
657
+ return true;
658
+ if (visibility === false)
659
+ return false;
660
+ if (typeof visibility === "function") {
661
+ const fullCtx = {
662
+ surface,
663
+ row: ctx?.row,
664
+ formModel: ctx?.formModel,
665
+ query: ctx?.query
666
+ };
667
+ return visibility(fullCtx);
668
+ }
669
+ return true;
670
+ });
671
+ }
672
+ function getFieldLabel(field) {
673
+ if (typeof field.label === "function") {
674
+ return field.label();
675
+ }
676
+ return field.label;
677
+ }
678
+
679
+ // src/utils/columns.ts
680
+ function defineColumns(columns) {
681
+ return columns;
682
+ }
683
+ function createColumnsFromFields(fields, options) {
684
+ const { overrides = {}, defaults = {}, filter } = options ?? {};
685
+ const visibleFields = filter ? fields.filter(filter) : fields.filter((f) => {
686
+ const tableVisible = f.visibleIn?.table;
687
+ return tableVisible !== false;
688
+ });
689
+ return visibleFields.map((field) => {
690
+ const override = overrides[field.key] ?? {};
691
+ return {
692
+ key: field.key,
693
+ label: () => getFieldLabel(field),
694
+ ...defaults,
695
+ ...override
696
+ };
697
+ });
698
+ }
699
+
700
+ export { batchDeleteAction, createAction, createColumnsFromFields, defineColumns, defineFields, deleteAction, editAction, exportAction, filterFieldsBySurface, getFieldLabel, presetActions, useCrudActions, useCrudForm, useCrudList, useCrudRouteSync, useCrudSelection };
701
+ //# sourceMappingURL=index.mjs.map
702
+ //# sourceMappingURL=index.mjs.map