@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,218 @@
1
+ <!--
2
+ =============================================================================
3
+ @h-ai/ui - AvatarUpload 组件
4
+ =============================================================================
5
+ 头像上传组件(圆形)
6
+
7
+ 使用 Svelte 5 Runes ($props, $state, $derived)
8
+ =============================================================================
9
+ -->
10
+ <script lang='ts'>
11
+ import type { AvatarUploadProps } from '../types.js'
12
+ import { uiM } from '../../../messages.js'
13
+ import { cn } from '../../../utils.js'
14
+ import BareInput from '../../primitives/BareInput.svelte'
15
+
16
+ let {
17
+ value = $bindable(''),
18
+ size = 'lg',
19
+ accept = 'image/*',
20
+ maxSize = 2 * 1024 * 1024, // 2MB
21
+ disabled = false,
22
+ uploadUrl = '',
23
+ presignUrl = '',
24
+ headers = {},
25
+ fallback = '',
26
+ class: className = '',
27
+ onchange,
28
+ onerror,
29
+ }: AvatarUploadProps = $props()
30
+
31
+ let loading = $state(false)
32
+ let inputElement = $state<HTMLInputElement | undefined>(undefined)
33
+
34
+ const sizeClass = $derived({
35
+ 'xs': 'w-8 h-8',
36
+ 'sm': 'w-12 h-12',
37
+ 'md': 'w-16 h-16',
38
+ 'lg': 'w-24 h-24',
39
+ 'xl': 'w-32 h-32',
40
+ '2xl': 'w-36 h-36',
41
+ '3xl': 'w-40 h-40',
42
+ '4xl': 'w-44 h-44',
43
+ }[size])
44
+
45
+ const containerClass = $derived(
46
+ cn(
47
+ 'avatar-upload relative rounded-full overflow-hidden',
48
+ !disabled && 'cursor-pointer group',
49
+ disabled && 'opacity-50 cursor-not-allowed',
50
+ sizeClass,
51
+ className,
52
+ ),
53
+ )
54
+
55
+ // 格式化文件大小
56
+ function formatSize(bytes: number): string {
57
+ if (bytes === 0)
58
+ return '0 B'
59
+ const k = 1024
60
+ const sizes = ['B', 'KB', 'MB', 'GB']
61
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
62
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
63
+ }
64
+
65
+ // 验证文件
66
+ function validateFile(file: File): string | null {
67
+ if (file.size > maxSize) {
68
+ return `${uiM('avatar_upload_size_exceeded')} ${formatSize(maxSize)})`
69
+ }
70
+
71
+ if (!file.type.startsWith('image/')) {
72
+ return uiM('avatar_upload_invalid_type')
73
+ }
74
+
75
+ return null
76
+ }
77
+
78
+ // 上传文件
79
+ async function uploadFile(file: File) {
80
+ loading = true
81
+
82
+ try {
83
+ if (!uploadUrl && !presignUrl) {
84
+ // 没有上传地址,创建本地预览
85
+ value = URL.createObjectURL(file)
86
+ onchange?.(value)
87
+ return
88
+ }
89
+
90
+ let targetUrl = uploadUrl
91
+
92
+ // 如果配置了签名 URL,先获取
93
+ if (presignUrl) {
94
+ const presignResponse = await fetch(presignUrl, {
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ ...headers,
99
+ },
100
+ body: JSON.stringify({
101
+ filename: file.name,
102
+ contentType: file.type,
103
+ size: file.size,
104
+ }),
105
+ })
106
+
107
+ if (!presignResponse.ok) {
108
+ throw new Error(uiM('avatar_upload_get_url_failed'))
109
+ }
110
+
111
+ const data = await presignResponse.json()
112
+ targetUrl = data.url
113
+ }
114
+
115
+ // 上传文件
116
+ const response = await fetch(targetUrl!, {
117
+ method: 'PUT',
118
+ headers: {
119
+ 'Content-Type': file.type,
120
+ ...headers,
121
+ },
122
+ body: file,
123
+ })
124
+
125
+ if (!response.ok) {
126
+ throw new Error(uiM('avatar_upload_failed'))
127
+ }
128
+
129
+ // 获取最终 URL
130
+ let finalUrl = targetUrl!.split('?')[0]
131
+
132
+ try {
133
+ const data = await response.json()
134
+ if (data.url) {
135
+ finalUrl = data.url
136
+ }
137
+ }
138
+ catch {
139
+ // 响应不是 JSON
140
+ }
141
+
142
+ value = finalUrl
143
+ onchange?.(finalUrl)
144
+ }
145
+ catch (error) {
146
+ const message = error instanceof Error ? error.message : uiM('avatar_upload_failed')
147
+ onerror?.(message)
148
+ }
149
+ finally {
150
+ loading = false
151
+ }
152
+ }
153
+
154
+ function handleClick() {
155
+ if (!disabled) {
156
+ inputElement?.click()
157
+ }
158
+ }
159
+
160
+ function handleChange(e: Event & { currentTarget: HTMLInputElement }) {
161
+ const file = e.currentTarget.files?.[0]
162
+ if (file) {
163
+ const error = validateFile(file)
164
+ if (error) {
165
+ onerror?.(error)
166
+ return
167
+ }
168
+ uploadFile(file)
169
+ }
170
+ e.currentTarget.value = ''
171
+ }
172
+ </script>
173
+
174
+ <div
175
+ class={containerClass}
176
+ role='button'
177
+ tabindex='0'
178
+ onclick={handleClick}
179
+ onkeydown={e => e.key === 'Enter' && handleClick()}
180
+ >
181
+ <BareInput
182
+ type='file'
183
+ class='hidden'
184
+ bind:inputRef={inputElement}
185
+ {accept}
186
+ {disabled}
187
+ onchange={handleChange}
188
+ />
189
+
190
+ <!-- 头像图片或占位符 -->
191
+ <div class='w-full h-full bg-neutral flex items-center justify-center text-neutral-content'>
192
+ {#if value}
193
+ <img
194
+ src={value}
195
+ alt={uiM('avatar_upload_alt')}
196
+ class='w-full h-full object-cover'
197
+ />
198
+ {:else}
199
+ <span class='text-2xl font-bold'>
200
+ {fallback || '?'}
201
+ </span>
202
+ {/if}
203
+ </div>
204
+
205
+ <!-- 遮罩层 -->
206
+ {#if !disabled}
207
+ <div class='absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center'>
208
+ {#if loading}
209
+ <span class='loading loading-spinner text-white'></span>
210
+ {:else}
211
+ <svg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6 text-white' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
212
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z' />
213
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15 13a3 3 0 11-6 0 3 3 0 016 0z' />
214
+ </svg>
215
+ {/if}
216
+ </div>
217
+ {/if}
218
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { AvatarUploadProps } from '../types.js';
2
+ declare const AvatarUpload: import("svelte").Component<AvatarUploadProps, {}, "value">;
3
+ type AvatarUpload = ReturnType<typeof AvatarUpload>;
4
+ export default AvatarUpload;
@@ -0,0 +1,260 @@
1
+ <!--
2
+ =============================================================================
3
+ @h-ai/ui - FileList 组件
4
+ =============================================================================
5
+ 文件列表展示组件
6
+
7
+ 使用 Svelte 5 Runes ($props, $state, $derived)
8
+ =============================================================================
9
+ -->
10
+ <script lang='ts'>
11
+ import type { FileItem, FileListProps } from '../types.js'
12
+ import { uiM } from '../../../messages.js'
13
+ import { cn } from '../../../utils.js'
14
+ import Button from '../../primitives/Button.svelte'
15
+ import IconButton from '../../primitives/IconButton.svelte'
16
+
17
+ const {
18
+ files = [],
19
+ loading = false,
20
+ showPreview = true,
21
+ showDownload = true,
22
+ showDelete = true,
23
+ showSize = true,
24
+ showDate = true,
25
+ layout = 'list',
26
+ class: className = '',
27
+ ondownload,
28
+ ondelete,
29
+ onpreview,
30
+ }: FileListProps = $props()
31
+
32
+ let previewFile = $state<FileItem | null>(null)
33
+
34
+ const containerClass = $derived(
35
+ cn(
36
+ 'file-list',
37
+ layout === 'grid' && 'grid grid-cols-2 md:grid-cols-4 gap-4',
38
+ layout === 'list' && 'space-y-2',
39
+ className,
40
+ ),
41
+ )
42
+
43
+ // 格式化文件大小
44
+ function formatSize(bytes: number): string {
45
+ if (bytes === 0)
46
+ return '0 B'
47
+ const k = 1024
48
+ const sizes = ['B', 'KB', 'MB', 'GB']
49
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
50
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
51
+ }
52
+
53
+ // 格式化日期
54
+ function formatDate(date: Date | string): string {
55
+ const d = typeof date === 'string' ? new Date(date) : date
56
+ return d.toLocaleDateString('zh-CN', {
57
+ year: 'numeric',
58
+ month: '2-digit',
59
+ day: '2-digit',
60
+ hour: '2-digit',
61
+ minute: '2-digit',
62
+ })
63
+ }
64
+
65
+ // 获取文件图标
66
+ function getFileIcon(type: string): string {
67
+ if (type.startsWith('image/'))
68
+ return '🖼️'
69
+ if (type.startsWith('video/'))
70
+ return '🎬'
71
+ if (type.startsWith('audio/'))
72
+ return '🎵'
73
+ if (type.includes('pdf'))
74
+ return '📄'
75
+ if (type.includes('word') || type.includes('document'))
76
+ return '📝'
77
+ if (type.includes('excel') || type.includes('spreadsheet'))
78
+ return '📊'
79
+ if (type.includes('zip') || type.includes('rar') || type.includes('archive'))
80
+ return '📦'
81
+ return '📁'
82
+ }
83
+
84
+ // 是否可预览
85
+ function canPreview(file: FileItem): boolean {
86
+ return file.type.startsWith('image/') || file.type.startsWith('video/') || file.type === 'application/pdf'
87
+ }
88
+
89
+ function handlePreview(file: FileItem) {
90
+ if (onpreview) {
91
+ onpreview(file)
92
+ }
93
+ else {
94
+ previewFile = file
95
+ }
96
+ }
97
+
98
+ function handleDownload(file: FileItem) {
99
+ ondownload?.(file)
100
+ }
101
+
102
+ function handleDelete(file: FileItem) {
103
+ ondelete?.(file)
104
+ }
105
+
106
+ function closePreview() {
107
+ previewFile = null
108
+ }
109
+ </script>
110
+
111
+ {#if loading}
112
+ <div class='flex justify-center py-8'>
113
+ <span class='loading loading-spinner loading-lg'></span>
114
+ </div>
115
+ {:else if files.length === 0}
116
+ <div class='text-center py-8 text-base-content/50'>
117
+ <svg xmlns='http://www.w3.org/2000/svg' class='h-16 w-16 mx-auto mb-4 opacity-30' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
118
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z' />
119
+ </svg>
120
+ <p>{uiM('file_list_no_files')}</p>
121
+ </div>
122
+ {:else}
123
+ <div class={containerClass}>
124
+ {#each files as file (file.id)}
125
+ {#if layout === 'grid'}
126
+ <!-- 网格布局 -->
127
+ <div class='card bg-base-200 overflow-hidden'>
128
+ {#if file.type.startsWith('image/') && file.url}
129
+ <figure class='aspect-square bg-base-300'>
130
+ <img src={file.url} alt={file.name} class='object-cover w-full h-full' />
131
+ </figure>
132
+ {:else}
133
+ <figure class='aspect-square bg-base-300 flex items-center justify-center'>
134
+ <span class='text-4xl'>{getFileIcon(file.type)}</span>
135
+ </figure>
136
+ {/if}
137
+ <div class='card-body p-3'>
138
+ <h3 class='card-title text-sm truncate'>{file.name}</h3>
139
+ {#if showSize}
140
+ <p class='text-xs text-base-content/50'>{formatSize(file.size)}</p>
141
+ {/if}
142
+ <div class='card-actions justify-end mt-2'>
143
+ {#if showPreview && canPreview(file)}
144
+ <Button variant='ghost' size='xs' onclick={() => handlePreview(file)}>
145
+ {uiM('file_list_preview')}
146
+ </Button>
147
+ {/if}
148
+ {#if showDownload}
149
+ <Button variant='ghost' size='xs' onclick={() => handleDownload(file)}>
150
+ {uiM('file_list_download')}
151
+ </Button>
152
+ {/if}
153
+ {#if showDelete}
154
+ <Button variant='ghost' size='xs' class='text-error' onclick={() => handleDelete(file)}>
155
+ {uiM('file_list_delete')}
156
+ </Button>
157
+ {/if}
158
+ </div>
159
+ </div>
160
+ </div>
161
+ {:else}
162
+ <!-- 列表布局 -->
163
+ <div class='flex items-center gap-3 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors'>
164
+ <!-- 文件图标/缩略图 -->
165
+ <div class='w-12 h-12 flex items-center justify-center bg-base-300 rounded'>
166
+ {#if file.type.startsWith('image/') && file.url}
167
+ <img src={file.url} alt={file.name} class='w-full h-full object-cover rounded' />
168
+ {:else}
169
+ <span class='text-2xl'>{getFileIcon(file.type)}</span>
170
+ {/if}
171
+ </div>
172
+
173
+ <!-- 文件信息 -->
174
+ <div class='flex-1 min-w-0'>
175
+ <h3 class='font-medium truncate'>{file.name}</h3>
176
+ <div class='flex gap-3 text-xs text-base-content/50'>
177
+ {#if showSize}
178
+ <span>{formatSize(file.size)}</span>
179
+ {/if}
180
+ {#if showDate && file.updatedAt}
181
+ <span>{formatDate(file.updatedAt)}</span>
182
+ {/if}
183
+ </div>
184
+ </div>
185
+
186
+ <!-- 操作按钮 -->
187
+ <div class='flex gap-1'>
188
+ {#if showPreview && canPreview(file)}
189
+ <IconButton
190
+ variant='ghost'
191
+ size='sm'
192
+ onclick={() => handlePreview(file)}
193
+ ariaLabel={uiM('file_list_preview')}
194
+ >
195
+ <svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
196
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15 12a3 3 0 11-6 0 3 3 0 016 0z' />
197
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z' />
198
+ </svg>
199
+ </IconButton>
200
+ {/if}
201
+ {#if showDownload}
202
+ <IconButton
203
+ variant='ghost'
204
+ size='sm'
205
+ onclick={() => handleDownload(file)}
206
+ ariaLabel={uiM('file_list_download')}
207
+ >
208
+ <svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
209
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' />
210
+ </svg>
211
+ </IconButton>
212
+ {/if}
213
+ {#if showDelete}
214
+ <IconButton
215
+ variant='ghost'
216
+ size='sm'
217
+ class='text-error'
218
+ onclick={() => handleDelete(file)}
219
+ ariaLabel={uiM('file_list_delete')}
220
+ >
221
+ <svg xmlns='http://www.w3.org/2000/svg' class='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
222
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' />
223
+ </svg>
224
+ </IconButton>
225
+ {/if}
226
+ </div>
227
+ </div>
228
+ {/if}
229
+ {/each}
230
+ </div>
231
+ {/if}
232
+
233
+ <!-- 预览模态框 -->
234
+ {#if previewFile}
235
+ <dialog class='modal modal-open' onclick={closePreview}>
236
+ <div
237
+ class='modal-box max-w-4xl'
238
+ role='dialog'
239
+ tabindex='-1'
240
+ onclick={e => e.stopPropagation()}
241
+ onkeydown={e => e.stopPropagation()}
242
+ >
243
+ <IconButton variant='ghost' size='sm' class='absolute right-2 top-2' onclick={closePreview} ariaLabel={uiM('file_list_close')}>✕</IconButton>
244
+ <h3 class='font-bold text-lg mb-4'>{previewFile.name}</h3>
245
+
246
+ {#if previewFile.type.startsWith('image/')}
247
+ <img src={previewFile.url} alt={previewFile.name} class='max-w-full max-h-[70vh] mx-auto' />
248
+ {:else if previewFile.type.startsWith('video/')}
249
+ <video src={previewFile.url} controls class='max-w-full max-h-[70vh] mx-auto'>
250
+ <track kind='captions' />
251
+ </video>
252
+ {:else if previewFile.type === 'application/pdf'}
253
+ <iframe src={previewFile.url} class='w-full h-[70vh]' title={previewFile.name}></iframe>
254
+ {/if}
255
+ </div>
256
+ <form method='dialog' class='modal-backdrop'>
257
+ <Button>{uiM('file_list_close')}</Button>
258
+ </form>
259
+ </dialog>
260
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { FileListProps } from '../types.js';
2
+ declare const FileList: import("svelte").Component<FileListProps, {}, "">;
3
+ type FileList = ReturnType<typeof FileList>;
4
+ export default FileList;