@h-ai/ui 0.1.0-alpha5

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 (234) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +488 -0
  3. package/auto-import.d.ts +96 -0
  4. package/auto-import.js +259 -0
  5. package/dist/components/compounds/Accordion.svelte +142 -0
  6. package/dist/components/compounds/Accordion.svelte.d.ts +25 -0
  7. package/dist/components/compounds/ActionSheet.svelte +116 -0
  8. package/dist/components/compounds/ActionSheet.svelte.d.ts +22 -0
  9. package/dist/components/compounds/Alert.svelte +67 -0
  10. package/dist/components/compounds/Alert.svelte.d.ts +8 -0
  11. package/dist/components/compounds/AppBar.svelte +77 -0
  12. package/dist/components/compounds/AppBar.svelte.d.ts +20 -0
  13. package/dist/components/compounds/BottomNav.svelte +75 -0
  14. package/dist/components/compounds/BottomNav.svelte.d.ts +18 -0
  15. package/dist/components/compounds/Breadcrumb.svelte +49 -0
  16. package/dist/components/compounds/Breadcrumb.svelte.d.ts +4 -0
  17. package/dist/components/compounds/Calendar.svelte +116 -0
  18. package/dist/components/compounds/Calendar.svelte.d.ts +47 -0
  19. package/dist/components/compounds/Card.svelte +77 -0
  20. package/dist/components/compounds/Card.svelte.d.ts +8 -0
  21. package/dist/components/compounds/Combobox.svelte +327 -0
  22. package/dist/components/compounds/Combobox.svelte.d.ts +69 -0
  23. package/dist/components/compounds/Confirm.svelte +90 -0
  24. package/dist/components/compounds/Confirm.svelte.d.ts +4 -0
  25. package/dist/components/compounds/DataTable.svelte +133 -0
  26. package/dist/components/compounds/DataTable.svelte.d.ts +69 -0
  27. package/dist/components/compounds/DatePicker.svelte +135 -0
  28. package/dist/components/compounds/DatePicker.svelte.d.ts +46 -0
  29. package/dist/components/compounds/Drawer.svelte +113 -0
  30. package/dist/components/compounds/Drawer.svelte.d.ts +4 -0
  31. package/dist/components/compounds/Dropdown.svelte +91 -0
  32. package/dist/components/compounds/Dropdown.svelte.d.ts +8 -0
  33. package/dist/components/compounds/Empty.svelte +83 -0
  34. package/dist/components/compounds/Empty.svelte.d.ts +4 -0
  35. package/dist/components/compounds/Form.svelte +96 -0
  36. package/dist/components/compounds/Form.svelte.d.ts +7 -0
  37. package/dist/components/compounds/FormField.svelte +56 -0
  38. package/dist/components/compounds/FormField.svelte.d.ts +4 -0
  39. package/dist/components/compounds/InfiniteScroll.svelte +82 -0
  40. package/dist/components/compounds/InfiniteScroll.svelte.d.ts +22 -0
  41. package/dist/components/compounds/Modal.svelte +123 -0
  42. package/dist/components/compounds/Modal.svelte.d.ts +8 -0
  43. package/dist/components/compounds/PageHeader.svelte +44 -0
  44. package/dist/components/compounds/PageHeader.svelte.d.ts +28 -0
  45. package/dist/components/compounds/Pagination.svelte +157 -0
  46. package/dist/components/compounds/Pagination.svelte.d.ts +4 -0
  47. package/dist/components/compounds/Popover.svelte +137 -0
  48. package/dist/components/compounds/Popover.svelte.d.ts +4 -0
  49. package/dist/components/compounds/PullRefresh.svelte +98 -0
  50. package/dist/components/compounds/PullRefresh.svelte.d.ts +16 -0
  51. package/dist/components/compounds/Result.svelte +84 -0
  52. package/dist/components/compounds/Result.svelte.d.ts +4 -0
  53. package/dist/components/compounds/SafeArea.svelte +37 -0
  54. package/dist/components/compounds/SafeArea.svelte.d.ts +12 -0
  55. package/dist/components/compounds/Skeleton.svelte +75 -0
  56. package/dist/components/compounds/Skeleton.svelte.d.ts +8 -0
  57. package/dist/components/compounds/Steps.svelte +111 -0
  58. package/dist/components/compounds/Steps.svelte.d.ts +4 -0
  59. package/dist/components/compounds/SwipeCell.svelte +115 -0
  60. package/dist/components/compounds/SwipeCell.svelte.d.ts +19 -0
  61. package/dist/components/compounds/Tabs.svelte +79 -0
  62. package/dist/components/compounds/Tabs.svelte.d.ts +8 -0
  63. package/dist/components/compounds/TagInput.svelte +108 -0
  64. package/dist/components/compounds/TagInput.svelte.d.ts +4 -0
  65. package/dist/components/compounds/Timeline.svelte +167 -0
  66. package/dist/components/compounds/Timeline.svelte.d.ts +25 -0
  67. package/dist/components/compounds/ToastContainer.svelte +59 -0
  68. package/dist/components/compounds/ToastContainer.svelte.d.ts +3 -0
  69. package/dist/components/compounds/Tooltip.svelte +41 -0
  70. package/dist/components/compounds/Tooltip.svelte.d.ts +4 -0
  71. package/dist/components/compounds/accordion-types.d.ts +16 -0
  72. package/dist/components/compounds/accordion-types.js +5 -0
  73. package/dist/components/compounds/action-sheet-types.d.ts +14 -0
  74. package/dist/components/compounds/action-sheet-types.js +5 -0
  75. package/dist/components/compounds/bottom-nav-types.d.ts +17 -0
  76. package/dist/components/compounds/bottom-nav-types.js +5 -0
  77. package/dist/components/compounds/index.d.ts +44 -0
  78. package/dist/components/compounds/index.js +54 -0
  79. package/dist/components/compounds/swipe-cell-types.d.ts +14 -0
  80. package/dist/components/compounds/swipe-cell-types.js +5 -0
  81. package/dist/components/compounds/timeline-types.d.ts +20 -0
  82. package/dist/components/compounds/timeline-types.js +5 -0
  83. package/dist/components/index.d.ts +9 -0
  84. package/dist/components/index.js +12 -0
  85. package/dist/components/primitives/Avatar.svelte +102 -0
  86. package/dist/components/primitives/Avatar.svelte.d.ts +8 -0
  87. package/dist/components/primitives/Badge.svelte +33 -0
  88. package/dist/components/primitives/Badge.svelte.d.ts +8 -0
  89. package/dist/components/primitives/BareButton.svelte +45 -0
  90. package/dist/components/primitives/BareButton.svelte.d.ts +4 -0
  91. package/dist/components/primitives/BareInput.svelte +101 -0
  92. package/dist/components/primitives/BareInput.svelte.d.ts +4 -0
  93. package/dist/components/primitives/Button.svelte +60 -0
  94. package/dist/components/primitives/Button.svelte.d.ts +8 -0
  95. package/dist/components/primitives/Checkbox.svelte +58 -0
  96. package/dist/components/primitives/Checkbox.svelte.d.ts +4 -0
  97. package/dist/components/primitives/IconButton.svelte +105 -0
  98. package/dist/components/primitives/IconButton.svelte.d.ts +4 -0
  99. package/dist/components/primitives/Input.svelte +130 -0
  100. package/dist/components/primitives/Input.svelte.d.ts +8 -0
  101. package/dist/components/primitives/Progress.svelte +58 -0
  102. package/dist/components/primitives/Progress.svelte.d.ts +4 -0
  103. package/dist/components/primitives/Radio.svelte +60 -0
  104. package/dist/components/primitives/Radio.svelte.d.ts +25 -0
  105. package/dist/components/primitives/Range.svelte +117 -0
  106. package/dist/components/primitives/Range.svelte.d.ts +27 -0
  107. package/dist/components/primitives/Rating.svelte +148 -0
  108. package/dist/components/primitives/Rating.svelte.d.ts +27 -0
  109. package/dist/components/primitives/Select.svelte +95 -0
  110. package/dist/components/primitives/Select.svelte.d.ts +25 -0
  111. package/dist/components/primitives/Spinner.svelte +54 -0
  112. package/dist/components/primitives/Spinner.svelte.d.ts +4 -0
  113. package/dist/components/primitives/Switch.svelte +53 -0
  114. package/dist/components/primitives/Switch.svelte.d.ts +4 -0
  115. package/dist/components/primitives/Tag.svelte +64 -0
  116. package/dist/components/primitives/Tag.svelte.d.ts +4 -0
  117. package/dist/components/primitives/Textarea.svelte +92 -0
  118. package/dist/components/primitives/Textarea.svelte.d.ts +4 -0
  119. package/dist/components/primitives/ToggleCheckbox.svelte +36 -0
  120. package/dist/components/primitives/ToggleCheckbox.svelte.d.ts +4 -0
  121. package/dist/components/primitives/ToggleInput.svelte +36 -0
  122. package/dist/components/primitives/ToggleInput.svelte.d.ts +4 -0
  123. package/dist/components/primitives/ToggleRadio.svelte +36 -0
  124. package/dist/components/primitives/ToggleRadio.svelte.d.ts +4 -0
  125. package/dist/components/primitives/index.d.ts +26 -0
  126. package/dist/components/primitives/index.js +30 -0
  127. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +387 -0
  128. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte.d.ts +38 -0
  129. package/dist/components/scenes/ai/AiDocumentEditor.svelte +2259 -0
  130. package/dist/components/scenes/ai/AiDocumentEditor.svelte.d.ts +4 -0
  131. package/dist/components/scenes/ai/MarkdownRenderer.svelte +460 -0
  132. package/dist/components/scenes/ai/MarkdownRenderer.svelte.d.ts +4 -0
  133. package/dist/components/scenes/ai/document-download.d.ts +40 -0
  134. package/dist/components/scenes/ai/document-download.js +300 -0
  135. package/dist/components/scenes/ai/document-parse.d.ts +28 -0
  136. package/dist/components/scenes/ai/document-parse.js +243 -0
  137. package/dist/components/scenes/ai/document-types.d.ts +164 -0
  138. package/dist/components/scenes/ai/document-types.js +1 -0
  139. package/dist/components/scenes/ai/index.d.ts +17 -0
  140. package/dist/components/scenes/ai/index.js +13 -0
  141. package/dist/components/scenes/ai/markdown-parse.d.ts +32 -0
  142. package/dist/components/scenes/ai/markdown-parse.js +161 -0
  143. package/dist/components/scenes/app/FeedbackModal.svelte +160 -0
  144. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +47 -0
  145. package/dist/components/scenes/app/LanguageSwitch.svelte +136 -0
  146. package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +50 -0
  147. package/dist/components/scenes/app/SettingsModal.svelte +174 -0
  148. package/dist/components/scenes/app/SettingsModal.svelte.d.ts +31 -0
  149. package/dist/components/scenes/app/ThemeSelector.svelte +176 -0
  150. package/dist/components/scenes/app/ThemeSelector.svelte.d.ts +31 -0
  151. package/dist/components/scenes/app/ThemeToggle.svelte +76 -0
  152. package/dist/components/scenes/app/ThemeToggle.svelte.d.ts +29 -0
  153. package/dist/components/scenes/app/index.d.ts +11 -0
  154. package/dist/components/scenes/app/index.js +11 -0
  155. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte +39 -0
  156. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte.d.ts +18 -0
  157. package/dist/components/scenes/crud/CrudDetailDrawer.svelte +155 -0
  158. package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +38 -0
  159. package/dist/components/scenes/crud/CrudEditDrawer.svelte +222 -0
  160. package/dist/components/scenes/crud/CrudEditDrawer.svelte.d.ts +48 -0
  161. package/dist/components/scenes/crud/CrudFilterBar.svelte +96 -0
  162. package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +31 -0
  163. package/dist/components/scenes/crud/CrudPage.svelte +499 -0
  164. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +114 -0
  165. package/dist/components/scenes/crud/crud-types.d.ts +135 -0
  166. package/dist/components/scenes/crud/crud-types.js +7 -0
  167. package/dist/components/scenes/crud/index.d.ts +12 -0
  168. package/dist/components/scenes/crud/index.js +11 -0
  169. package/dist/components/scenes/crypto/EncryptedInput.svelte +156 -0
  170. package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +32 -0
  171. package/dist/components/scenes/crypto/HashDisplay.svelte +97 -0
  172. package/dist/components/scenes/crypto/HashDisplay.svelte.d.ts +4 -0
  173. package/dist/components/scenes/crypto/SignatureDisplay.svelte +161 -0
  174. package/dist/components/scenes/crypto/SignatureDisplay.svelte.d.ts +4 -0
  175. package/dist/components/scenes/crypto/index.d.ts +9 -0
  176. package/dist/components/scenes/crypto/index.js +11 -0
  177. package/dist/components/scenes/iam/ChangePasswordForm.svelte +134 -0
  178. package/dist/components/scenes/iam/ChangePasswordForm.svelte.d.ts +8 -0
  179. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +147 -0
  180. package/dist/components/scenes/iam/ForgotPasswordForm.svelte.d.ts +4 -0
  181. package/dist/components/scenes/iam/LoginForm.svelte +191 -0
  182. package/dist/components/scenes/iam/LoginForm.svelte.d.ts +8 -0
  183. package/dist/components/scenes/iam/PasswordInput.svelte +203 -0
  184. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +49 -0
  185. package/dist/components/scenes/iam/PermGuard.svelte +69 -0
  186. package/dist/components/scenes/iam/PermGuard.svelte.d.ts +16 -0
  187. package/dist/components/scenes/iam/RegisterForm.svelte +269 -0
  188. package/dist/components/scenes/iam/RegisterForm.svelte.d.ts +8 -0
  189. package/dist/components/scenes/iam/ResetPasswordForm.svelte +153 -0
  190. package/dist/components/scenes/iam/ResetPasswordForm.svelte.d.ts +8 -0
  191. package/dist/components/scenes/iam/UserProfile.svelte +251 -0
  192. package/dist/components/scenes/iam/UserProfile.svelte.d.ts +4 -0
  193. package/dist/components/scenes/iam/index.d.ts +16 -0
  194. package/dist/components/scenes/iam/index.js +18 -0
  195. package/dist/components/scenes/iam/password-utils.d.ts +15 -0
  196. package/dist/components/scenes/iam/password-utils.js +19 -0
  197. package/dist/components/scenes/iam/permission-context.svelte.d.ts +90 -0
  198. package/dist/components/scenes/iam/permission-context.svelte.js +114 -0
  199. package/dist/components/scenes/index.d.ts +13 -0
  200. package/dist/components/scenes/index.js +20 -0
  201. package/dist/components/scenes/storage/AvatarUpload.svelte +218 -0
  202. package/dist/components/scenes/storage/AvatarUpload.svelte.d.ts +4 -0
  203. package/dist/components/scenes/storage/FileList.svelte +260 -0
  204. package/dist/components/scenes/storage/FileList.svelte.d.ts +4 -0
  205. package/dist/components/scenes/storage/FileUpload.svelte +400 -0
  206. package/dist/components/scenes/storage/FileUpload.svelte.d.ts +7 -0
  207. package/dist/components/scenes/storage/ImageUpload.svelte +285 -0
  208. package/dist/components/scenes/storage/ImageUpload.svelte.d.ts +4 -0
  209. package/dist/components/scenes/storage/index.d.ts +10 -0
  210. package/dist/components/scenes/storage/index.js +12 -0
  211. package/dist/components/scenes/types.d.ts +583 -0
  212. package/dist/components/scenes/types.js +7 -0
  213. package/dist/i18n.svelte.d.ts +44 -0
  214. package/dist/i18n.svelte.js +108 -0
  215. package/dist/index.d.ts +13 -0
  216. package/dist/index.js +19 -0
  217. package/dist/messages/en-US.json +242 -0
  218. package/dist/messages/zh-CN.json +242 -0
  219. package/dist/messages.d.ts +29 -0
  220. package/dist/messages.js +42 -0
  221. package/dist/styles/design-tokens.css +58 -0
  222. package/dist/styles/global.css +218 -0
  223. package/dist/styles/mobile.css +105 -0
  224. package/dist/theme-config.d.ts +92 -0
  225. package/dist/theme-config.js +122 -0
  226. package/dist/toast.svelte.d.ts +45 -0
  227. package/dist/toast.svelte.js +77 -0
  228. package/dist/types.d.ts +944 -0
  229. package/dist/types.js +7 -0
  230. package/dist/utils/platform.svelte.d.ts +54 -0
  231. package/dist/utils/platform.svelte.js +87 -0
  232. package/dist/utils.d.ts +46 -0
  233. package/dist/utils.js +100 -0
  234. package/package.json +69 -0
@@ -0,0 +1,31 @@
1
+ type FilterFieldDef = {
2
+ id: string;
3
+ label: string | (() => string);
4
+ type: string;
5
+ options?: Array<{
6
+ label: string;
7
+ value: string | number | boolean;
8
+ }> | (() => Array<{
9
+ label: string;
10
+ value: string | number | boolean;
11
+ }>);
12
+ };
13
+ type $$ComponentProps = {
14
+ searchable?: boolean;
15
+ searchPlaceholder?: string;
16
+ searchValue?: string;
17
+ filterFields?: FilterFieldDef[];
18
+ filterValues?: Record<string, unknown>;
19
+ total?: number;
20
+ onsearch?: (search: string) => void;
21
+ onfilterchange?: (filters: Record<string, unknown>) => void;
22
+ };
23
+ /**
24
+ * CrudFilterBar
25
+ * CRUD 搜索 + 过滤栏组件
26
+ *
27
+ * 使用 Svelte 5 Runes ($props, $state)
28
+ */
29
+ declare const CrudFilterBar: import("svelte").Component<$$ComponentProps, {}, "searchValue" | "filterValues">;
30
+ type CrudFilterBar = ReturnType<typeof CrudFilterBar>;
31
+ export default CrudFilterBar;
@@ -0,0 +1,499 @@
1
+ <!--
2
+ @component CrudPage
3
+ 通用 CRUD 页面组件
4
+
5
+ 基于声明式资源定义,自动生成列表 + 搜索过滤 + 分页 + 详情抽屉 + 编辑/新建抽屉 + 删除确认。
6
+
7
+ 使用 Svelte 5 Runes ($props, $state, $derived, $effect)
8
+ 使用 compounds 组件:Card, DataTable, PageHeader, Pagination, Drawer
9
+
10
+ @example
11
+ <CrudPage crud={userCrud} data={data} permissions={{ create: true, update: true, delete: true }} />
12
+ -->
13
+ <script lang='ts'>
14
+ import type { Snippet } from 'svelte'
15
+ import type { Size } from '../../../types.js'
16
+ import { goto, invalidateAll } from '$app/navigation'
17
+ import { page } from '$app/state'
18
+ import { uiM } from '../../../messages.js'
19
+ import Card from '../../compounds/Card.svelte'
20
+ import DataTable from '../../compounds/DataTable.svelte'
21
+ import PageHeader from '../../compounds/PageHeader.svelte'
22
+ import Pagination from '../../compounds/Pagination.svelte'
23
+ import Button from '../../primitives/Button.svelte'
24
+ import IconButton from '../../primitives/IconButton.svelte'
25
+ import CrudDeleteConfirm from './CrudDeleteConfirm.svelte'
26
+ import CrudDetailDrawer from './CrudDetailDrawer.svelte'
27
+ import CrudEditDrawer from './CrudEditDrawer.svelte'
28
+ import CrudFilterBar from './CrudFilterBar.svelte'
29
+
30
+ // ─── 类型定义 ───
31
+
32
+ type FieldDef = {
33
+ id: string
34
+ label: string | (() => string)
35
+ type: string
36
+ inList?: boolean
37
+ filterable?: boolean
38
+ readonly?: boolean
39
+ inCreate?: boolean
40
+ inEdit?: boolean
41
+ inDetail?: boolean
42
+ options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
43
+ validation?: { required?: boolean, min?: number, max?: number, pattern?: string, message?: string }
44
+ width?: string
45
+ align?: 'left' | 'center' | 'right'
46
+ render?: (value: unknown, item: Record<string, unknown>) => string
47
+ placeholder?: string | (() => string)
48
+ defaultValue?: unknown
49
+ order?: number
50
+ }
51
+
52
+ type AnyApiFunc = (...args: unknown[]) => Promise<Record<string, unknown> | unknown> | Record<string, unknown> | unknown
53
+
54
+ type CrudDef = {
55
+ resource: {
56
+ name: string
57
+ label: string | (() => string)
58
+ keyField?: string
59
+ fields: FieldDef[]
60
+ defaultPageSize?: number
61
+ searchable?: boolean
62
+ searchPlaceholder?: string | (() => string)
63
+ api: {
64
+ list: AnyApiFunc
65
+ get?: AnyApiFunc
66
+ create?: AnyApiFunc
67
+ update?: AnyApiFunc
68
+ remove?: AnyApiFunc
69
+ }
70
+ }
71
+ getListFields: () => FieldDef[]
72
+ getFilterFields: () => FieldDef[]
73
+ getCreateFields: () => FieldDef[]
74
+ getEditFields: () => FieldDef[]
75
+ getDetailFields: () => FieldDef[]
76
+ toTableColumns: () => Array<{ key: string, label: string, width?: string, align?: string, render?: (item: Record<string, unknown>) => string }>
77
+ getDefaultValues: () => Record<string, unknown>
78
+ api: {
79
+ list: AnyApiFunc
80
+ get?: AnyApiFunc
81
+ create?: AnyApiFunc
82
+ update?: AnyApiFunc
83
+ remove?: AnyApiFunc
84
+ }
85
+ }
86
+
87
+ // ─── Props ───
88
+
89
+ const {
90
+ crud,
91
+ data,
92
+ permissions = {},
93
+ drawerSize = '2xl' as Size,
94
+ rowClickDetail = true,
95
+ listItemActions,
96
+ editFormExtra,
97
+ detailExtra,
98
+ headerActions,
99
+ onbeforedelete,
100
+ onaftersubmit,
101
+ onafterdelete,
102
+ onerror,
103
+ basePath = '',
104
+ class: className = '',
105
+ }: {
106
+ crud: CrudDef
107
+ data: { items: Record<string, unknown>[], total: number, page: number, pageSize: number, filters?: Record<string, unknown> }
108
+ permissions?: { create?: boolean, update?: boolean, delete?: boolean }
109
+ drawerSize?: Size
110
+ rowClickDetail?: boolean
111
+ listItemActions?: Snippet<[Record<string, unknown>]>
112
+ editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>
113
+ detailExtra?: Snippet<[Record<string, unknown>]>
114
+ headerActions?: Snippet
115
+ onbeforedelete?: (item: Record<string, unknown>) => Promise<boolean> | boolean
116
+ onaftersubmit?: (item: Record<string, unknown>, mode: 'create' | 'edit') => void
117
+ onafterdelete?: (item: Record<string, unknown>) => void
118
+ onerror?: (error: string) => void
119
+ basePath?: string
120
+ class?: string
121
+ } = $props()
122
+
123
+ // ─── 状态 ───
124
+
125
+ const keyField = $derived(crud.resource.keyField ?? 'id')
126
+ const resourceLabel = $derived(resolveText(crud.resource.label))
127
+ const canCreate = $derived(permissions.create !== false && Boolean(crud.api.create))
128
+ const canUpdate = $derived(permissions.update !== false && Boolean(crud.api.update))
129
+ const canDelete = $derived(permissions.delete !== false && Boolean(crud.api.remove))
130
+ const searchable = $derived(crud.resource.searchable !== false)
131
+
132
+ // 视图状态
133
+ let drawerMode = $state<'detail' | 'edit' | 'create' | null>(null)
134
+ let selectedItem = $state<Record<string, unknown> | null>(null)
135
+ let formData = $state<Record<string, unknown>>({})
136
+ let submitting = $state(false)
137
+ let formError = $state('')
138
+ let deleteConfirmOpen = $state(false)
139
+ let deletingItem = $state<Record<string, unknown> | null>(null)
140
+ let deleting = $state(false)
141
+
142
+ // 过滤状态
143
+ let searchValue = $state('')
144
+ let filterValues = $state<Record<string, unknown>>({})
145
+
146
+ // 基础路径
147
+ const currentBasePath = $derived(basePath || page.url.pathname)
148
+
149
+ // ─── 工具函数 ───
150
+
151
+ function resolveText(text: string | (() => string)): string {
152
+ return typeof text === 'function' ? text() : text
153
+ }
154
+
155
+ // 同步 data.filters → 本地过滤状态
156
+ $effect(() => {
157
+ if (data.filters) {
158
+ searchValue = String(data.filters.search ?? '')
159
+ const fv: Record<string, unknown> = {}
160
+ for (const f of crud.getFilterFields()) {
161
+ if (data.filters[f.id] !== undefined) {
162
+ fv[f.id] = data.filters[f.id]
163
+ }
164
+ }
165
+ filterValues = fv
166
+ }
167
+ })
168
+
169
+ // ─── 表格列 ───
170
+
171
+ const tableColumns = $derived(crud.toTableColumns())
172
+
173
+ // ─── 导航 ───
174
+
175
+ function navigateWithParams(overrides: Record<string, string | number>) {
176
+ const params = new URLSearchParams()
177
+ const merged: Record<string, string | number> = {
178
+ search: searchValue,
179
+ page: data.page,
180
+ pageSize: data.pageSize,
181
+ ...Object.fromEntries(
182
+ Object.entries(filterValues)
183
+ .filter(([, v]) => v !== undefined && v !== '')
184
+ .map(([k, v]) => [k, String(v)]),
185
+ ),
186
+ ...overrides,
187
+ }
188
+
189
+ for (const [key, val] of Object.entries(merged)) {
190
+ const strVal = String(val)
191
+ if (strVal && strVal !== '' && !(key === 'page' && strVal === '1') && !(key === 'pageSize' && strVal === String(crud.resource.defaultPageSize ?? 20))) {
192
+ params.set(key, strVal)
193
+ }
194
+ }
195
+
196
+ const qs = params.toString()
197
+ goto(`${currentBasePath}${qs ? `?${qs}` : ''}`, { invalidateAll: true })
198
+ }
199
+
200
+ function handleSearch(search: string) {
201
+ navigateWithParams({ search, page: 1 })
202
+ }
203
+
204
+ function handleFilterChange(filters: Record<string, unknown>) {
205
+ const overrides: Record<string, string | number> = { page: 1 }
206
+ for (const [k, v] of Object.entries(filters)) {
207
+ overrides[k] = String(v ?? '')
208
+ }
209
+ navigateWithParams(overrides)
210
+ }
211
+
212
+ function handlePageChange(newPage: number) {
213
+ navigateWithParams({ page: newPage })
214
+ }
215
+
216
+ // ─── 打开/关闭抽屉 ───
217
+
218
+ function openDetail(item: Record<string, unknown>) {
219
+ selectedItem = item
220
+ drawerMode = 'detail'
221
+ }
222
+
223
+ function openCreate() {
224
+ selectedItem = null
225
+ formData = { ...crud.getDefaultValues() }
226
+ formError = ''
227
+ drawerMode = 'create'
228
+ }
229
+
230
+ function openEdit(item: Record<string, unknown>) {
231
+ selectedItem = item
232
+ formData = { ...item }
233
+ formError = ''
234
+ drawerMode = 'edit'
235
+ }
236
+
237
+ function closeDrawer() {
238
+ drawerMode = null
239
+ selectedItem = null
240
+ formError = ''
241
+ }
242
+
243
+ function switchToEdit() {
244
+ if (selectedItem) {
245
+ openEdit(selectedItem)
246
+ }
247
+ }
248
+
249
+ // ─── 提交 ───
250
+
251
+ async function handleSubmit(submitData: Record<string, unknown>) {
252
+ formError = ''
253
+ submitting = true
254
+
255
+ try {
256
+ if (drawerMode === 'create' && crud.api.create) {
257
+ const result = await crud.api.create(submitData)
258
+ const submitted = (result ?? {}) as Record<string, unknown>
259
+ closeDrawer()
260
+ onaftersubmit?.(submitted, 'create')
261
+ await invalidateAll()
262
+ }
263
+ else if (drawerMode === 'edit' && crud.api.update && selectedItem) {
264
+ const id = String(selectedItem[keyField])
265
+ const result = await crud.api.update(id, submitData)
266
+ const submitted = (result ?? {}) as Record<string, unknown>
267
+ closeDrawer()
268
+ onaftersubmit?.(submitted, 'edit')
269
+ await invalidateAll()
270
+ }
271
+ }
272
+ catch (e) {
273
+ const msg = e instanceof Error ? e.message : uiM('crud_submit_failed')
274
+ formError = msg
275
+ onerror?.(msg)
276
+ }
277
+ finally {
278
+ submitting = false
279
+ }
280
+ }
281
+
282
+ // ─── 删除 ───
283
+
284
+ function requestDelete(item: Record<string, unknown>) {
285
+ deletingItem = item
286
+ deleteConfirmOpen = true
287
+ }
288
+
289
+ async function confirmDelete() {
290
+ if (!deletingItem || !crud.api.remove)
291
+ return
292
+
293
+ if (onbeforedelete) {
294
+ const proceed = await onbeforedelete(deletingItem)
295
+ if (!proceed) {
296
+ deleteConfirmOpen = false
297
+ deletingItem = null
298
+ return
299
+ }
300
+ }
301
+
302
+ deleting = true
303
+ try {
304
+ const id = String(deletingItem[keyField])
305
+ await crud.api.remove(id)
306
+ const deleted = deletingItem
307
+ deleteConfirmOpen = false
308
+ deletingItem = null
309
+ // 如果删除的是当前详情/编辑中的项,关闭抽屉
310
+ if (selectedItem && String(selectedItem[keyField]) === id) {
311
+ closeDrawer()
312
+ }
313
+ onafterdelete?.(deleted)
314
+ await invalidateAll()
315
+ }
316
+ catch (e) {
317
+ const msg = e instanceof Error ? e.message : uiM('crud_delete_failed')
318
+ onerror?.(msg)
319
+ }
320
+ finally {
321
+ deleting = false
322
+ }
323
+ }
324
+
325
+ function cancelDelete() {
326
+ deleteConfirmOpen = false
327
+ deletingItem = null
328
+ }
329
+
330
+ // 搜索占位符
331
+ const searchPlaceholderText = $derived(
332
+ crud.resource.searchPlaceholder
333
+ ? resolveText(crud.resource.searchPlaceholder)
334
+ : uiM('crud_search_placeholder'),
335
+ )
336
+
337
+ // 构建编辑/新建字段
338
+ const createFields = $derived(crud.getCreateFields() as FieldDef[])
339
+ const editFields = $derived(crud.getEditFields() as FieldDef[])
340
+ const detailFields = $derived(crud.getDetailFields() as FieldDef[])
341
+ const filterFields = $derived(crud.getFilterFields() as FieldDef[])
342
+
343
+ // 抽屉标题
344
+ const drawerTitle = $derived(
345
+ drawerMode === 'create'
346
+ ? `${uiM('crud_create')}${resourceLabel}`
347
+ : drawerMode === 'edit'
348
+ ? `${uiM('crud_edit')}${resourceLabel}`
349
+ : `${resourceLabel}${uiM('crud_detail')}`,
350
+ )
351
+
352
+ // 抽屉 open 状态
353
+ const detailOpen = $derived(drawerMode === 'detail')
354
+ const editOpen = $derived(drawerMode === 'create' || drawerMode === 'edit')
355
+
356
+ // 用于 DataTable 的 columns(含自定义渲染)
357
+ const dtColumns = $derived(
358
+ tableColumns.map(col => ({
359
+ key: col.key,
360
+ label: col.label,
361
+ width: col.width,
362
+ align: col.align as 'left' | 'center' | 'right' | undefined,
363
+ render: col.render
364
+ ? (item: Record<string, unknown>) => col.render!(item)
365
+ : undefined,
366
+ })),
367
+ )
368
+ </script>
369
+
370
+ <div class='space-y-4 {className}'>
371
+ <!-- 页面标题 -->
372
+ <PageHeader title={resourceLabel}>
373
+ {#snippet actions()}
374
+ {#if headerActions}
375
+ {@render headerActions()}
376
+ {/if}
377
+ {#if canCreate}
378
+ <Button variant='primary' onclick={openCreate}>
379
+ <span class='icon-[tabler--plus] size-4.5 mr-1'></span>
380
+ {uiM('crud_create')}
381
+ </Button>
382
+ {/if}
383
+ {/snippet}
384
+ </PageHeader>
385
+
386
+ <!-- 搜索 + 过滤栏 -->
387
+ {#if searchable || filterFields.length > 0}
388
+ <Card>
389
+ <CrudFilterBar
390
+ {searchable}
391
+ searchPlaceholder={searchPlaceholderText}
392
+ bind:searchValue
393
+ {filterFields}
394
+ bind:filterValues
395
+ total={data.total}
396
+ onsearch={handleSearch}
397
+ onfilterchange={handleFilterChange}
398
+ />
399
+ </Card>
400
+ {/if}
401
+
402
+ <!-- 数据列表 -->
403
+ <Card>
404
+ <DataTable
405
+ data={data.items}
406
+ columns={dtColumns}
407
+ keyField={keyField}
408
+ loading={false}
409
+ >
410
+ {#snippet actions(item)}
411
+ {#if rowClickDetail}
412
+ <IconButton
413
+ variant='ghost'
414
+ size='sm'
415
+ ariaLabel={uiM('crud_detail')}
416
+ onclick={() => openDetail(item)}
417
+ >
418
+ <span class='icon-[tabler--eye] size-4'></span>
419
+ </IconButton>
420
+ {/if}
421
+ {#if canUpdate}
422
+ <IconButton
423
+ variant='ghost'
424
+ size='sm'
425
+ ariaLabel={uiM('crud_edit')}
426
+ onclick={() => openEdit(item)}
427
+ >
428
+ <span class='icon-[tabler--edit] size-4'></span>
429
+ </IconButton>
430
+ {/if}
431
+ {#if canDelete}
432
+ <IconButton
433
+ variant='ghost'
434
+ size='sm'
435
+ ariaLabel={uiM('crud_delete')}
436
+ onclick={() => requestDelete(item)}
437
+ class='hover:text-error'
438
+ >
439
+ <span class='icon-[tabler--trash] size-4'></span>
440
+ </IconButton>
441
+ {/if}
442
+ {#if listItemActions}
443
+ {@render listItemActions(item)}
444
+ {/if}
445
+ {/snippet}
446
+ </DataTable>
447
+
448
+ <!-- 分页 -->
449
+ {#if data.total > data.pageSize}
450
+ <div class='flex justify-center p-4 border-t border-base-content/5'>
451
+ <Pagination
452
+ page={data.page}
453
+ total={data.total}
454
+ pageSize={data.pageSize}
455
+ size='sm'
456
+ showTotal
457
+ onchange={handlePageChange}
458
+ />
459
+ </div>
460
+ {/if}
461
+ </Card>
462
+ </div>
463
+
464
+ <!-- 详情抽屉 -->
465
+ <CrudDetailDrawer
466
+ open={detailOpen}
467
+ item={selectedItem}
468
+ fields={detailFields}
469
+ title={drawerTitle}
470
+ size={drawerSize}
471
+ canEdit={canUpdate}
472
+ onedit={switchToEdit}
473
+ onclose={closeDrawer}
474
+ {detailExtra}
475
+ />
476
+
477
+ <!-- 编辑/新建抽屉 -->
478
+ <CrudEditDrawer
479
+ open={editOpen}
480
+ mode={drawerMode === 'create' ? 'create' : 'edit'}
481
+ fields={drawerMode === 'create' ? createFields : editFields}
482
+ bind:formData
483
+ title={drawerTitle}
484
+ size={drawerSize}
485
+ {submitting}
486
+ error={formError}
487
+ onsubmit={handleSubmit}
488
+ onclose={closeDrawer}
489
+ {editFormExtra}
490
+ editingItem={selectedItem}
491
+ />
492
+
493
+ <!-- 删除确认对话框 -->
494
+ <CrudDeleteConfirm
495
+ bind:open={deleteConfirmOpen}
496
+ loading={deleting}
497
+ onconfirm={confirmDelete}
498
+ oncancel={cancelDelete}
499
+ />
@@ -0,0 +1,114 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Size } from '../../../types.js';
3
+ type FieldDef = {
4
+ id: string;
5
+ label: string | (() => string);
6
+ type: string;
7
+ inList?: boolean;
8
+ filterable?: boolean;
9
+ readonly?: boolean;
10
+ inCreate?: boolean;
11
+ inEdit?: boolean;
12
+ inDetail?: boolean;
13
+ options?: Array<{
14
+ label: string;
15
+ value: string | number | boolean;
16
+ }> | (() => Array<{
17
+ label: string;
18
+ value: string | number | boolean;
19
+ }>);
20
+ validation?: {
21
+ required?: boolean;
22
+ min?: number;
23
+ max?: number;
24
+ pattern?: string;
25
+ message?: string;
26
+ };
27
+ width?: string;
28
+ align?: 'left' | 'center' | 'right';
29
+ render?: (value: unknown, item: Record<string, unknown>) => string;
30
+ placeholder?: string | (() => string);
31
+ defaultValue?: unknown;
32
+ order?: number;
33
+ };
34
+ type AnyApiFunc = (...args: unknown[]) => Promise<Record<string, unknown> | unknown> | Record<string, unknown> | unknown;
35
+ type CrudDef = {
36
+ resource: {
37
+ name: string;
38
+ label: string | (() => string);
39
+ keyField?: string;
40
+ fields: FieldDef[];
41
+ defaultPageSize?: number;
42
+ searchable?: boolean;
43
+ searchPlaceholder?: string | (() => string);
44
+ api: {
45
+ list: AnyApiFunc;
46
+ get?: AnyApiFunc;
47
+ create?: AnyApiFunc;
48
+ update?: AnyApiFunc;
49
+ remove?: AnyApiFunc;
50
+ };
51
+ };
52
+ getListFields: () => FieldDef[];
53
+ getFilterFields: () => FieldDef[];
54
+ getCreateFields: () => FieldDef[];
55
+ getEditFields: () => FieldDef[];
56
+ getDetailFields: () => FieldDef[];
57
+ toTableColumns: () => Array<{
58
+ key: string;
59
+ label: string;
60
+ width?: string;
61
+ align?: string;
62
+ render?: (item: Record<string, unknown>) => string;
63
+ }>;
64
+ getDefaultValues: () => Record<string, unknown>;
65
+ api: {
66
+ list: AnyApiFunc;
67
+ get?: AnyApiFunc;
68
+ create?: AnyApiFunc;
69
+ update?: AnyApiFunc;
70
+ remove?: AnyApiFunc;
71
+ };
72
+ };
73
+ type $$ComponentProps = {
74
+ crud: CrudDef;
75
+ data: {
76
+ items: Record<string, unknown>[];
77
+ total: number;
78
+ page: number;
79
+ pageSize: number;
80
+ filters?: Record<string, unknown>;
81
+ };
82
+ permissions?: {
83
+ create?: boolean;
84
+ update?: boolean;
85
+ delete?: boolean;
86
+ };
87
+ drawerSize?: Size;
88
+ rowClickDetail?: boolean;
89
+ listItemActions?: Snippet<[Record<string, unknown>]>;
90
+ editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>;
91
+ detailExtra?: Snippet<[Record<string, unknown>]>;
92
+ headerActions?: Snippet;
93
+ onbeforedelete?: (item: Record<string, unknown>) => Promise<boolean> | boolean;
94
+ onaftersubmit?: (item: Record<string, unknown>, mode: 'create' | 'edit') => void;
95
+ onafterdelete?: (item: Record<string, unknown>) => void;
96
+ onerror?: (error: string) => void;
97
+ basePath?: string;
98
+ class?: string;
99
+ };
100
+ /**
101
+ * CrudPage
102
+ * 通用 CRUD 页面组件
103
+ *
104
+ * 基于声明式资源定义,自动生成列表 + 搜索过滤 + 分页 + 详情抽屉 + 编辑/新建抽屉 + 删除确认。
105
+ *
106
+ * 使用 Svelte 5 Runes ($props, $state, $derived, $effect)
107
+ * 使用 compounds 组件:Card, DataTable, PageHeader, Pagination, Drawer
108
+ *
109
+ * @example
110
+ * <CrudPage crud={userCrud} data={data} permissions={{ create: true, update: true, delete: true }} />
111
+ */
112
+ declare const CrudPage: import("svelte").Component<$$ComponentProps, {}, "">;
113
+ type CrudPage = ReturnType<typeof CrudPage>;
114
+ export default CrudPage;