@h-ai/ui 0.1.0-alpha.48 → 0.1.0-alpha.50
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/README.md +20 -3
- package/dist/components/compounds/Pagination.svelte +12 -6
- package/dist/components/compounds/Popover.svelte +104 -18
- package/dist/components/compounds/Tabs.svelte +20 -1
- package/dist/components/compounds/Tooltip.svelte +127 -6
- package/dist/components/scenes/crud/CrudPage.svelte +167 -90
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +13 -0
- package/dist/components/scenes/crud/crud-controller.svelte.d.ts +213 -0
- package/dist/components/scenes/crud/crud-controller.svelte.js +265 -0
- package/dist/components/scenes/crud/index.d.ts +5 -1
- package/dist/components/scenes/crud/index.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/internal/browser-safety.d.ts +3 -2
- package/dist/internal/browser-safety.js +26 -5
- package/dist/messages/en-US.json +3 -1
- package/dist/messages/zh-CN.json +3 -1
- package/dist/toast.svelte.d.ts +46 -0
- package/dist/toast.svelte.js +61 -0
- package/dist/types.d.ts +27 -4
- package/package.json +2 -2
|
@@ -159,6 +159,11 @@
|
|
|
159
159
|
onerror,
|
|
160
160
|
basePath = '',
|
|
161
161
|
nav = createBrowserNavAdapter(),
|
|
162
|
+
loading = false,
|
|
163
|
+
loaded = true,
|
|
164
|
+
error,
|
|
165
|
+
onRetry,
|
|
166
|
+
keepPreviousData = true,
|
|
162
167
|
class: className = '',
|
|
163
168
|
...restProps
|
|
164
169
|
}: {
|
|
@@ -219,12 +224,42 @@
|
|
|
219
224
|
* 以获得客户端跳转 + invalidateAll 体验。
|
|
220
225
|
*/
|
|
221
226
|
nav?: NavAdapter
|
|
227
|
+
/** 列表是否正在加载;由外部控制器(`createCrudController`)提供,用于展示查询中占位。 */
|
|
228
|
+
loading?: boolean
|
|
229
|
+
/**
|
|
230
|
+
* 是否至少成功加载过一次。仅当 `loaded && total === 0` 时展示空状态,
|
|
231
|
+
* 避免首次请求未回时误显示“暂无数据”。默认 true 以兼容 SvelteKit SSR(数据随页面就绪)。
|
|
232
|
+
*/
|
|
233
|
+
loaded?: boolean
|
|
234
|
+
/** 最新一轮加载错误;无可用数据时展示错误信息与重试按钮。 */
|
|
235
|
+
error?: Error | string
|
|
236
|
+
/** 点击“重试”回调;通常传入控制器的 `reload`。 */
|
|
237
|
+
onRetry?: () => void
|
|
238
|
+
/** 失败或刷新时是否保留旧数据(默认 true);为 false 时加载中展示查询中占位而非旧列表。 */
|
|
239
|
+
keepPreviousData?: boolean
|
|
222
240
|
/** 根节点自定义 class。 */
|
|
223
241
|
class?: string
|
|
224
242
|
} & DataAttributes = $props()
|
|
225
243
|
|
|
226
244
|
/** 透传到根节点的 data-* 属性。 */
|
|
227
245
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
246
|
+
|
|
247
|
+
// ─── 加载 / 空 / 错误展示态 ───
|
|
248
|
+
|
|
249
|
+
/** 归一化错误文案;error 允许是 Error 或字符串。 */
|
|
250
|
+
const errorMessage = $derived(error ? (typeof error === 'string' ? error : error.message) : '')
|
|
251
|
+
/** 是否已有可展示的数据行。 */
|
|
252
|
+
const hasItems = $derived(data.items.length > 0)
|
|
253
|
+
/** 加载失败且无可保留数据:整块替换为错误信息 + 重试。 */
|
|
254
|
+
const showErrorState = $derived(!!error && (!hasItems || !keepPreviousData))
|
|
255
|
+
/** 首次查询(或不保留旧数据时)尚无数据:展示查询中占位,避免误显示空状态。 */
|
|
256
|
+
const showQuerying = $derived(loading && !showErrorState && (!hasItems || !keepPreviousData))
|
|
257
|
+
/** 常规列表区块(含空状态);仅此区块内才可能出现“暂无数据”。 */
|
|
258
|
+
const showListRegion = $derived(!showErrorState && !showQuerying)
|
|
259
|
+
/** 列表可见但仍在刷新(保留旧数据):顶部细进度条提示。 */
|
|
260
|
+
const showRefreshing = $derived(showListRegion && loading && hasItems)
|
|
261
|
+
/** 保留旧数据时的错误细条 + 重试,不打断浏览已有列表。 */
|
|
262
|
+
const showErrorBanner = $derived(showListRegion && !!error && hasItems)
|
|
228
263
|
// ─── 状态 ───
|
|
229
264
|
|
|
230
265
|
/** 表格行的主键字段。 */
|
|
@@ -655,7 +690,7 @@
|
|
|
655
690
|
{/if}
|
|
656
691
|
|
|
657
692
|
<!-- 搜索 + 过滤栏 -->
|
|
658
|
-
{#if searchable || filterFields.length > 0}
|
|
693
|
+
{#if searchable || filterFields.length > 0 || toolbarLeading || toolbarActions}
|
|
659
694
|
{#if toolbarStyle === 'toolbar'}
|
|
660
695
|
<TableToolbar
|
|
661
696
|
{searchable}
|
|
@@ -697,101 +732,143 @@
|
|
|
697
732
|
|
|
698
733
|
<!-- 数据列表 -->
|
|
699
734
|
<Card padding='none' class={listCardClass}>
|
|
700
|
-
{#if
|
|
701
|
-
<!--
|
|
702
|
-
<div class='min-h-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
735
|
+
{#if showQuerying}
|
|
736
|
+
<!-- 首次查询:展示查询中占位,避免请求未回时误显示“暂无数据” -->
|
|
737
|
+
<div class='flex min-h-40 flex-1 flex-col items-center justify-center gap-2 px-4 py-10 text-sm text-base-content/55'>
|
|
738
|
+
<span class='loading loading-spinner loading-md text-base-content/40'></span>
|
|
739
|
+
<span>{uiM('crud_loading')}</span>
|
|
740
|
+
</div>
|
|
741
|
+
{:else if showErrorState}
|
|
742
|
+
<!-- 加载失败且无可保留数据:错误信息 + 重试 -->
|
|
743
|
+
<div class='flex min-h-40 flex-1 flex-col items-center justify-center gap-3 px-4 py-10 text-sm'>
|
|
744
|
+
<span class='icon-[tabler--alert-triangle] size-8 text-error/70'></span>
|
|
745
|
+
<p class='max-w-md text-center text-base-content/70'>{errorMessage || uiM('crud_load_failed')}</p>
|
|
746
|
+
{#if onRetry}
|
|
747
|
+
<Button size={actionButtonSize} onclick={() => onRetry?.()}>
|
|
748
|
+
<span class='icon-[tabler--refresh] mr-1 size-4'></span>
|
|
749
|
+
{uiM('crud_retry')}
|
|
750
|
+
</Button>
|
|
713
751
|
{/if}
|
|
714
752
|
</div>
|
|
715
753
|
{:else}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
sortDir={sortBy ? sortDirection : null}
|
|
724
|
-
onsort={handleSort}
|
|
725
|
-
class='min-h-0 min-w-0 flex-1 overflow-auto'
|
|
726
|
-
{density}
|
|
727
|
-
cell={tableCell}
|
|
728
|
-
{onrowclick}
|
|
729
|
-
>
|
|
730
|
-
{#snippet actions(item)}
|
|
731
|
-
{#if rowClickDetail}
|
|
732
|
-
<IconButton
|
|
733
|
-
variant='ghost'
|
|
734
|
-
size={actionButtonSize}
|
|
735
|
-
ariaLabel={uiM('crud_detail')}
|
|
736
|
-
onclick={(event) => {
|
|
737
|
-
event.stopPropagation()
|
|
738
|
-
openDetail(item)
|
|
739
|
-
}}
|
|
740
|
-
>
|
|
741
|
-
<span class='icon-[tabler--eye] {actionIconClass}'></span>
|
|
742
|
-
</IconButton>
|
|
743
|
-
{/if}
|
|
744
|
-
{#if canUpdate}
|
|
745
|
-
<IconButton
|
|
746
|
-
variant='ghost'
|
|
747
|
-
size={actionButtonSize}
|
|
748
|
-
ariaLabel={uiM('crud_edit')}
|
|
749
|
-
onclick={(event) => {
|
|
750
|
-
event.stopPropagation()
|
|
751
|
-
openEdit(item)
|
|
752
|
-
}}
|
|
753
|
-
>
|
|
754
|
-
<span class='icon-[tabler--edit] {actionIconClass}'></span>
|
|
755
|
-
</IconButton>
|
|
756
|
-
{/if}
|
|
757
|
-
{#if canDelete}
|
|
758
|
-
<IconButton
|
|
759
|
-
variant='ghost'
|
|
760
|
-
size={actionButtonSize}
|
|
761
|
-
ariaLabel={uiM('crud_delete')}
|
|
762
|
-
onclick={(event) => {
|
|
763
|
-
event.stopPropagation()
|
|
764
|
-
requestDelete(item)
|
|
765
|
-
}}
|
|
766
|
-
class='hover:text-error'
|
|
767
|
-
>
|
|
768
|
-
<span class='icon-[tabler--trash] {actionIconClass}'></span>
|
|
769
|
-
</IconButton>
|
|
754
|
+
{#if showErrorBanner}
|
|
755
|
+
<!-- 保留旧数据时的错误细条:不打断浏览,仍提供重试 -->
|
|
756
|
+
<div role='alert' class='flex items-center gap-2 border-b border-error/20 bg-error/10 px-3 py-2 text-xs text-error'>
|
|
757
|
+
<span class='icon-[tabler--alert-triangle] size-4 shrink-0'></span>
|
|
758
|
+
<span class='min-w-0 flex-1 truncate'>{errorMessage || uiM('crud_load_failed')}</span>
|
|
759
|
+
{#if onRetry}
|
|
760
|
+
<button type='button' class='shrink-0 font-medium underline underline-offset-2' onclick={() => onRetry?.()}>{uiM('crud_retry')}</button>
|
|
770
761
|
{/if}
|
|
771
|
-
|
|
772
|
-
|
|
762
|
+
</div>
|
|
763
|
+
{/if}
|
|
764
|
+
{#if showRefreshing}
|
|
765
|
+
<!-- 保留旧数据的刷新态:顶部细进度提示 -->
|
|
766
|
+
<div class='h-0.5 w-full overflow-hidden'>
|
|
767
|
+
<div class='h-full w-1/3 animate-pulse rounded-full bg-primary/60'></div>
|
|
768
|
+
</div>
|
|
769
|
+
{/if}
|
|
770
|
+
{#if card}
|
|
771
|
+
<!-- 卡片视图使用独立滚动容器,分页栏始终保留在容器外。 -->
|
|
772
|
+
<div class='min-h-0 flex-1 overflow-auto pt-2'>
|
|
773
|
+
{#if hasItems}
|
|
774
|
+
<div class='grid min-h-full grid-cols-[repeat(auto-fill,minmax(min(100%,18rem),1fr))] content-start gap-3'>
|
|
775
|
+
{#each data.items as item (String(item[keyField]))}
|
|
776
|
+
{@render card(item)}
|
|
777
|
+
{/each}
|
|
778
|
+
</div>
|
|
779
|
+
{:else if loaded}
|
|
780
|
+
<div class='flex min-h-full items-center justify-center px-4 text-sm text-base-content/55'>
|
|
781
|
+
{uiM('data_table_empty')}
|
|
782
|
+
</div>
|
|
783
|
+
{:else}
|
|
784
|
+
<!-- 惰性页签尚未激活:既未加载也无数据,留白不显示空状态 -->
|
|
785
|
+
<div class='min-h-full'></div>
|
|
773
786
|
{/if}
|
|
774
|
-
|
|
775
|
-
|
|
787
|
+
</div>
|
|
788
|
+
{:else if loaded || hasItems}
|
|
789
|
+
<!-- DataTable 自身同时承接横向和纵向滚动,确保 sticky 表头与数据行共享滚动上下文。 -->
|
|
790
|
+
<DataTable
|
|
791
|
+
data={data.items}
|
|
792
|
+
columns={dtColumns}
|
|
793
|
+
keyField={keyField}
|
|
794
|
+
loading={false}
|
|
795
|
+
sortKey={sortBy || undefined}
|
|
796
|
+
sortDir={sortBy ? sortDirection : null}
|
|
797
|
+
onsort={handleSort}
|
|
798
|
+
class='min-h-0 min-w-0 flex-1 overflow-auto'
|
|
799
|
+
{density}
|
|
800
|
+
cell={tableCell}
|
|
801
|
+
{onrowclick}
|
|
802
|
+
>
|
|
803
|
+
{#snippet actions(item)}
|
|
804
|
+
{#if rowClickDetail}
|
|
805
|
+
<IconButton
|
|
806
|
+
variant='ghost'
|
|
807
|
+
size={actionButtonSize}
|
|
808
|
+
ariaLabel={uiM('crud_detail')}
|
|
809
|
+
onclick={(event) => {
|
|
810
|
+
event.stopPropagation()
|
|
811
|
+
openDetail(item)
|
|
812
|
+
}}
|
|
813
|
+
>
|
|
814
|
+
<span class='icon-[tabler--eye] {actionIconClass}'></span>
|
|
815
|
+
</IconButton>
|
|
816
|
+
{/if}
|
|
817
|
+
{#if canUpdate}
|
|
818
|
+
<IconButton
|
|
819
|
+
variant='ghost'
|
|
820
|
+
size={actionButtonSize}
|
|
821
|
+
ariaLabel={uiM('crud_edit')}
|
|
822
|
+
onclick={(event) => {
|
|
823
|
+
event.stopPropagation()
|
|
824
|
+
openEdit(item)
|
|
825
|
+
}}
|
|
826
|
+
>
|
|
827
|
+
<span class='icon-[tabler--edit] {actionIconClass}'></span>
|
|
828
|
+
</IconButton>
|
|
829
|
+
{/if}
|
|
830
|
+
{#if canDelete}
|
|
831
|
+
<IconButton
|
|
832
|
+
variant='ghost'
|
|
833
|
+
size={actionButtonSize}
|
|
834
|
+
ariaLabel={uiM('crud_delete')}
|
|
835
|
+
onclick={(event) => {
|
|
836
|
+
event.stopPropagation()
|
|
837
|
+
requestDelete(item)
|
|
838
|
+
}}
|
|
839
|
+
class='hover:text-error'
|
|
840
|
+
>
|
|
841
|
+
<span class='icon-[tabler--trash] {actionIconClass}'></span>
|
|
842
|
+
</IconButton>
|
|
843
|
+
{/if}
|
|
844
|
+
{#if listItemActions}
|
|
845
|
+
{@render listItemActions(item)}
|
|
846
|
+
{/if}
|
|
847
|
+
{/snippet}
|
|
848
|
+
</DataTable>
|
|
849
|
+
{:else}
|
|
850
|
+
<!-- 惰性页签未激活:既未加载也无数据,留白不显示空状态 -->
|
|
851
|
+
<div class='min-h-40 flex-1'></div>
|
|
852
|
+
{/if}
|
|
853
|
+
|
|
854
|
+
<!-- 分页栏:始终显示,支持每页条数选择与跳页(shadcn table 风格) -->
|
|
855
|
+
<div class={paginationBarClass}>
|
|
856
|
+
<Pagination
|
|
857
|
+
page={data.page}
|
|
858
|
+
total={data.total}
|
|
859
|
+
pageSize={data.pageSize}
|
|
860
|
+
size={paginationSize}
|
|
861
|
+
showTotal={paginationShowTotal}
|
|
862
|
+
showJumper={paginationShowJumper}
|
|
863
|
+
showSizeChanger={paginationShowSizeChanger}
|
|
864
|
+
showPageInfo={paginationShowPageInfo}
|
|
865
|
+
showFirstLast={paginationShowFirstLast}
|
|
866
|
+
pageSizeOptions={paginationPageSizeOptions}
|
|
867
|
+
onchange={handlePageChange}
|
|
868
|
+
onpagesizechange={handlePageSizeChange}
|
|
869
|
+
/>
|
|
870
|
+
</div>
|
|
776
871
|
{/if}
|
|
777
|
-
|
|
778
|
-
<!-- 分页栏:始终显示,支持每页条数选择与跳页(shadcn table 风格) -->
|
|
779
|
-
<div class={paginationBarClass}>
|
|
780
|
-
<Pagination
|
|
781
|
-
page={data.page}
|
|
782
|
-
total={data.total}
|
|
783
|
-
pageSize={data.pageSize}
|
|
784
|
-
size={paginationSize}
|
|
785
|
-
showTotal={paginationShowTotal}
|
|
786
|
-
showJumper={paginationShowJumper}
|
|
787
|
-
showSizeChanger={paginationShowSizeChanger}
|
|
788
|
-
showPageInfo={paginationShowPageInfo}
|
|
789
|
-
showFirstLast={paginationShowFirstLast}
|
|
790
|
-
pageSizeOptions={paginationPageSizeOptions}
|
|
791
|
-
onchange={handlePageChange}
|
|
792
|
-
onpagesizechange={handlePageSizeChange}
|
|
793
|
-
/>
|
|
794
|
-
</div>
|
|
795
872
|
</Card>
|
|
796
873
|
</div>
|
|
797
874
|
|
|
@@ -188,6 +188,19 @@ type $$ComponentProps = {
|
|
|
188
188
|
* 以获得客户端跳转 + invalidateAll 体验。
|
|
189
189
|
*/
|
|
190
190
|
nav?: NavAdapter;
|
|
191
|
+
/** 列表是否正在加载;由外部控制器(`createCrudController`)提供,用于展示查询中占位。 */
|
|
192
|
+
loading?: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* 是否至少成功加载过一次。仅当 `loaded && total === 0` 时展示空状态,
|
|
195
|
+
* 避免首次请求未回时误显示“暂无数据”。默认 true 以兼容 SvelteKit SSR(数据随页面就绪)。
|
|
196
|
+
*/
|
|
197
|
+
loaded?: boolean;
|
|
198
|
+
/** 最新一轮加载错误;无可用数据时展示错误信息与重试按钮。 */
|
|
199
|
+
error?: Error | string;
|
|
200
|
+
/** 点击“重试”回调;通常传入控制器的 `reload`。 */
|
|
201
|
+
onRetry?: () => void;
|
|
202
|
+
/** 失败或刷新时是否保留旧数据(默认 true);为 false 时加载中展示查询中占位而非旧列表。 */
|
|
203
|
+
keepPreviousData?: boolean;
|
|
191
204
|
/** 根节点自定义 class。 */
|
|
192
205
|
class?: string;
|
|
193
206
|
} & DataAttributes;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @h-ai/ui — CRUD 控制器(SPA 数据 + 导航托管)
|
|
3
|
+
*
|
|
4
|
+
* 为 `CrudPage` 提供两个框架级适配器,业务应用无需在各自项目里重复实现:
|
|
5
|
+
* 1. `defineCrud`:由声明式资源定义生成 `CrudPage` 所需的 `crud` 对象(字段筛选、列映射、默认值),
|
|
6
|
+
* 不依赖 `@h-ai/kit`,纯 SPA / Tauri / Capacitor 均可直接使用。
|
|
7
|
+
* 2. `createCrudController`:在纯客户端(无 SvelteKit SSR)下托管 `CrudPage` 的 `data` 与 `nav`,
|
|
8
|
+
* 把 `CrudPage` 通过 URL query 同步的搜索/过滤/分页参数解析回本地状态并重新拉取列表。
|
|
9
|
+
*
|
|
10
|
+
* 控制器特性:latest-wins(seq + AbortSignal 双保险)、失败保留旧数据、惰性加载、并发去重、
|
|
11
|
+
* 查询串回填地址栏(保留 pathname 与 hash,不整页跳转)。
|
|
12
|
+
*
|
|
13
|
+
* @module crud-controller
|
|
14
|
+
*/
|
|
15
|
+
import type { PaginatedResult } from '@h-ai/core';
|
|
16
|
+
import type { NavAdapter } from './nav-adapter.js';
|
|
17
|
+
/** 字段展示形式;与 `CrudEditPanel` / `CrudFilterBar` 支持的类型对齐,保留 `string` 以兼容 `@h-ai/kit`。 */
|
|
18
|
+
export type CrudFieldType = 'text' | 'string' | 'textarea' | 'select' | 'radio' | 'multi-select' | 'boolean' | 'checkbox' | 'email' | 'url' | 'tel' | 'password' | 'number' | 'date' | 'datetime' | 'date-range' | 'custom';
|
|
19
|
+
/** 可解析为字符串的文本值(支持 i18n 函数)。 */
|
|
20
|
+
export type CrudText = string | (() => string);
|
|
21
|
+
/** 选项项(select / radio / multi-select)。 */
|
|
22
|
+
export interface CrudOption {
|
|
23
|
+
label: string;
|
|
24
|
+
value: string | number | boolean;
|
|
25
|
+
}
|
|
26
|
+
/** 字段定义(与 `CrudPage` 的字段模型对齐)。 */
|
|
27
|
+
export interface CrudFieldDef {
|
|
28
|
+
/** 字段 id(对应数据对象的属性名)。 */
|
|
29
|
+
id: string;
|
|
30
|
+
/** 字段标签(支持 i18n 函数)。 */
|
|
31
|
+
label: CrudText;
|
|
32
|
+
/** 字段类型。 */
|
|
33
|
+
type: CrudFieldType;
|
|
34
|
+
/** 是否在列表展示(默认 true)。 */
|
|
35
|
+
inList?: boolean;
|
|
36
|
+
/** 是否可过滤(默认 false)。 */
|
|
37
|
+
filterable?: boolean;
|
|
38
|
+
/** 是否只读(编辑态禁用)。 */
|
|
39
|
+
readonly?: boolean;
|
|
40
|
+
/** 是否在新建表单出现(默认 true)。 */
|
|
41
|
+
inCreate?: boolean;
|
|
42
|
+
/** 是否在编辑表单出现(默认 true)。 */
|
|
43
|
+
inEdit?: boolean;
|
|
44
|
+
/** 是否在详情出现(默认 true)。 */
|
|
45
|
+
inDetail?: boolean;
|
|
46
|
+
/** 选项(select / radio / multi-select),支持工厂函数。 */
|
|
47
|
+
options?: CrudOption[] | (() => CrudOption[]);
|
|
48
|
+
/** 校验规则。 */
|
|
49
|
+
validation?: {
|
|
50
|
+
required?: boolean;
|
|
51
|
+
min?: number;
|
|
52
|
+
max?: number;
|
|
53
|
+
pattern?: string;
|
|
54
|
+
message?: string;
|
|
55
|
+
};
|
|
56
|
+
/** 列宽(CSS)。 */
|
|
57
|
+
width?: string;
|
|
58
|
+
/** 列对齐。 */
|
|
59
|
+
align?: 'left' | 'center' | 'right';
|
|
60
|
+
/** 列表单元格自定义渲染。 */
|
|
61
|
+
render?: (value: unknown, item: Record<string, unknown>) => string;
|
|
62
|
+
/** 占位文本。 */
|
|
63
|
+
placeholder?: CrudText;
|
|
64
|
+
/** 日期范围起始值 key(仅 date-range,默认 `${id}Start`)。 */
|
|
65
|
+
startKey?: string;
|
|
66
|
+
/** 日期范围结束值 key(仅 date-range,默认 `${id}End`)。 */
|
|
67
|
+
endKey?: string;
|
|
68
|
+
/** 默认值(新建时)。 */
|
|
69
|
+
defaultValue?: unknown;
|
|
70
|
+
/** 排序权重(升序)。 */
|
|
71
|
+
order?: number;
|
|
72
|
+
}
|
|
73
|
+
/** 列表请求附加项;用于 latest-wins 取消旧请求。 */
|
|
74
|
+
export interface CrudListOptions {
|
|
75
|
+
/** 取消信号;旧请求被新请求取代时触发 abort。 */
|
|
76
|
+
signal?: AbortSignal;
|
|
77
|
+
}
|
|
78
|
+
/** CRUD API 集合。列表返回标准 `PaginatedResult<T>`,避免各页面重复 page/pageSize/total 映射。 */
|
|
79
|
+
export interface CrudApi<T> {
|
|
80
|
+
/** 分页列表;第二参数携带取消信号,实现方可透传给 fetch。 */
|
|
81
|
+
list: (params: Record<string, unknown>, options?: CrudListOptions) => Promise<PaginatedResult<T>>;
|
|
82
|
+
/** 按主键获取单条。 */
|
|
83
|
+
get?: (id: string) => Promise<T | null>;
|
|
84
|
+
/** 创建一条记录。 */
|
|
85
|
+
create?: (data: Partial<T>) => Promise<T>;
|
|
86
|
+
/** 更新一条记录。 */
|
|
87
|
+
update?: (id: string, data: Partial<T>) => Promise<T>;
|
|
88
|
+
/** 删除一条记录。 */
|
|
89
|
+
remove?: (id: string) => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
/** 资源定义。 */
|
|
92
|
+
export interface CrudResource<T> {
|
|
93
|
+
/** 资源名称(用于识别业务实体)。 */
|
|
94
|
+
name: string;
|
|
95
|
+
/** 资源标题(支持 i18n 函数)。 */
|
|
96
|
+
label: CrudText;
|
|
97
|
+
/** 主键字段(默认 `id`)。 */
|
|
98
|
+
keyField?: string;
|
|
99
|
+
/** 字段定义列表。 */
|
|
100
|
+
fields: CrudFieldDef[];
|
|
101
|
+
/** 默认分页条数(默认 10)。 */
|
|
102
|
+
defaultPageSize?: number;
|
|
103
|
+
/** 是否显示搜索框(默认 true)。 */
|
|
104
|
+
searchable?: boolean;
|
|
105
|
+
/** 搜索框占位文案。 */
|
|
106
|
+
searchPlaceholder?: CrudText;
|
|
107
|
+
/** 资源 CRUD API。 */
|
|
108
|
+
api: CrudApi<T>;
|
|
109
|
+
}
|
|
110
|
+
/** DataTable 列定义(适配 `CrudPage`)。 */
|
|
111
|
+
export interface CrudTableColumn {
|
|
112
|
+
key: string;
|
|
113
|
+
label: string;
|
|
114
|
+
width?: string;
|
|
115
|
+
align?: 'left' | 'center' | 'right';
|
|
116
|
+
render?: (item: Record<string, unknown>) => string;
|
|
117
|
+
}
|
|
118
|
+
/** `CrudPage` 所需的 crud 对象形状(`defineCrud` 的返回值)。 */
|
|
119
|
+
export interface CrudDef<T> {
|
|
120
|
+
resource: CrudResource<T>;
|
|
121
|
+
getListFields: () => CrudFieldDef[];
|
|
122
|
+
getFilterFields: () => CrudFieldDef[];
|
|
123
|
+
getCreateFields: () => CrudFieldDef[];
|
|
124
|
+
getEditFields: () => CrudFieldDef[];
|
|
125
|
+
getDetailFields: () => CrudFieldDef[];
|
|
126
|
+
toTableColumns: () => CrudTableColumn[];
|
|
127
|
+
getDefaultValues: () => Record<string, unknown>;
|
|
128
|
+
api: CrudApi<T>;
|
|
129
|
+
}
|
|
130
|
+
/** `CrudPage` 的 `data` 形状。 */
|
|
131
|
+
export interface CrudData<T> {
|
|
132
|
+
items: T[];
|
|
133
|
+
total: number;
|
|
134
|
+
page: number;
|
|
135
|
+
pageSize: number;
|
|
136
|
+
filters: Record<string, unknown>;
|
|
137
|
+
}
|
|
138
|
+
/** CRUD 控制器:托管列表数据与 SPA 导航适配器。 */
|
|
139
|
+
export interface CrudController<T> {
|
|
140
|
+
/** 传给 `<CrudPage data={...}>` 的响应式数据对象。 */
|
|
141
|
+
readonly data: CrudData<T>;
|
|
142
|
+
/** 传给 `<CrudPage nav={...}>` 的导航适配器(仅同步 query,不整页跳转)。 */
|
|
143
|
+
readonly nav: NavAdapter;
|
|
144
|
+
/** 首次或按需拉取当前参数;惰性页签用它在首次激活时启动查询。 */
|
|
145
|
+
load: () => Promise<void>;
|
|
146
|
+
/** 主动按当前参数重新拉取(写操作后或重试触发)。 */
|
|
147
|
+
reload: () => Promise<void>;
|
|
148
|
+
/** 当前是否有最新一轮列表请求尚未结束。 */
|
|
149
|
+
readonly loading: boolean;
|
|
150
|
+
/** 是否至少成功取得过一次列表响应。 */
|
|
151
|
+
readonly loaded: boolean;
|
|
152
|
+
/** 最新一轮请求错误;已有成功数据时仍保留原列表。 */
|
|
153
|
+
readonly error: Error | undefined;
|
|
154
|
+
}
|
|
155
|
+
/** CRUD 控制器创建选项。 */
|
|
156
|
+
export interface CrudControllerOptions {
|
|
157
|
+
/** 是否在创建后立即加载;不可见 / 惰性页签应设为 false(默认 true)。 */
|
|
158
|
+
autoLoad?: boolean;
|
|
159
|
+
/** 请求失败或重新拉取时是否保留已有数据(默认 true,避免闪现空列表)。 */
|
|
160
|
+
keepPreviousData?: boolean;
|
|
161
|
+
/** 是否把查询串回填浏览器地址栏(保留 pathname 与 hash,默认 true)。 */
|
|
162
|
+
syncUrl?: boolean;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 映射分页结果的 `items`,保留 total / page / pageSize。
|
|
166
|
+
*
|
|
167
|
+
* @description 标准 `PaginatedResult<T>` 适配器:业务页面只需转换条目本身,
|
|
168
|
+
* 无需重复搬运分页元数据,配合 `CrudApi.list` 消除 page/pageSize/total 样板。
|
|
169
|
+
*
|
|
170
|
+
* @param result - 后端返回的分页结果。
|
|
171
|
+
* @param map - 单条映射函数。
|
|
172
|
+
* @returns 条目被映射、分页元数据不变的新分页结果。
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* list: async (params) => mapPaginated(await api.listUsers(params), toRow)
|
|
176
|
+
*/
|
|
177
|
+
export declare function mapPaginated<T, R>(result: PaginatedResult<T>, map: (item: T, index: number) => R): PaginatedResult<R>;
|
|
178
|
+
/**
|
|
179
|
+
* 由声明式资源定义生成 `CrudPage` 所需的 `crud` 对象。
|
|
180
|
+
*
|
|
181
|
+
* @description 仅做字段筛选、列映射与默认值收集,不依赖 `@h-ai/kit`,可在纯 SPA 直接使用。
|
|
182
|
+
*
|
|
183
|
+
* @param resource - 资源定义(字段、标签、API)。
|
|
184
|
+
* @returns `CrudPage` 可直接消费的 `crud` 对象。
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const userCrud = defineCrud<User>({
|
|
188
|
+
* name: 'user',
|
|
189
|
+
* label: () => m.users_title(),
|
|
190
|
+
* fields: [{ id: 'name', label: () => m.user_name(), type: 'text', filterable: true }],
|
|
191
|
+
* api: { list: params => listUsers(params) },
|
|
192
|
+
* })
|
|
193
|
+
*/
|
|
194
|
+
export declare function defineCrud<T extends Record<string, unknown>>(resource: CrudResource<T>): CrudDef<T>;
|
|
195
|
+
/**
|
|
196
|
+
* 在纯客户端(无 SvelteKit SSR)下为 `CrudPage` 创建数据 + 导航控制器。
|
|
197
|
+
*
|
|
198
|
+
* @description `CrudPage` 通过 `nav.navigate(url)` 把搜索/过滤/分页写进 URL query,再调用 `nav.refresh()`
|
|
199
|
+
* 触发列表刷新。本控制器把 query 解析回参数、调用 `crud.api.list` 更新本地 `data`,并把查询串回填地址栏,
|
|
200
|
+
* 不触发整页跳转(避免刷新与登录态丢失)。连续请求只允许最新一轮更新状态(seq + AbortSignal 双保险),
|
|
201
|
+
* 失败时保留已有数据并暴露错误供页面重试。
|
|
202
|
+
*
|
|
203
|
+
* @param crud - `defineCrud` 的返回值。
|
|
204
|
+
* @param options - 加载策略;默认立即加载、失败保留旧数据、回填地址栏。
|
|
205
|
+
* @returns 含响应式 `data`、`nav` 适配器与 `load` / `reload` 的控制器。
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* const controller = createCrudController(userCrud)
|
|
209
|
+
* // <CrudPage crud={userCrud} data={controller.data} nav={controller.nav}
|
|
210
|
+
* // loading={controller.loading} loaded={controller.loaded}
|
|
211
|
+
* // error={controller.error} onRetry={controller.reload} />
|
|
212
|
+
*/
|
|
213
|
+
export declare function createCrudController<T extends Record<string, unknown>>(crud: CrudDef<T>, options?: CrudControllerOptions): CrudController<T>;
|