@allbluecn/admin-app 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.
Files changed (276) hide show
  1. package/package.json +86 -0
  2. package/src/client.tsx +31 -0
  3. package/src/components/admin/admin-user-menu.tsx +90 -0
  4. package/src/components/admin/developer-log.tsx +356 -0
  5. package/src/components/ai-provider/model-list-editor.tsx +147 -0
  6. package/src/components/ai-provider/provider-form.tsx +434 -0
  7. package/src/components/ai-provider/provider-icon.tsx +43 -0
  8. package/src/components/ai-provider/provider-logo.tsx +87 -0
  9. package/src/components/ai-provider/provider-table.tsx +258 -0
  10. package/src/components/auth/admin-login-form.tsx +214 -0
  11. package/src/components/auth/auth-brand-panel.tsx +60 -0
  12. package/src/components/auth/auth-header.tsx +38 -0
  13. package/src/components/auth/auth-layout.tsx +66 -0
  14. package/src/components/auth/auth-providers.tsx +34 -0
  15. package/src/components/auth/icon-input.tsx +46 -0
  16. package/src/components/auth/oauth-section.tsx +51 -0
  17. package/src/components/auth/password-input.tsx +67 -0
  18. package/src/components/common/page-header.tsx +42 -0
  19. package/src/components/common/placeholder-page.tsx +42 -0
  20. package/src/components/component/component-form-dialog.tsx +348 -0
  21. package/src/components/component/component-import-dialog.tsx +235 -0
  22. package/src/components/component/component-table.tsx +153 -0
  23. package/src/components/data-table/data-table-pagination.tsx +114 -0
  24. package/src/components/data-table/data-table.tsx +218 -0
  25. package/src/components/data-table/table-detail-layout.tsx +99 -0
  26. package/src/components/dependency/changelog-dialog.tsx +108 -0
  27. package/src/components/dependency/columns.tsx +232 -0
  28. package/src/components/dependency/dependency-table.tsx +368 -0
  29. package/src/components/dependency/edit-dialog.tsx +200 -0
  30. package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
  31. package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
  32. package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
  33. package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
  34. package/src/components/layout/sidebar-02/index.tsx +33 -0
  35. package/src/components/layout/sidebar-02/logo.tsx +40 -0
  36. package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
  37. package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
  38. package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
  39. package/src/components/magicui/animated-theme-toggler.tsx +272 -0
  40. package/src/components/magicui/rainbow-button.tsx +78 -0
  41. package/src/components/market/columns.tsx +151 -0
  42. package/src/components/market/market-app-form.tsx +237 -0
  43. package/src/components/market/market-app-list.tsx +156 -0
  44. package/src/components/monitoring/error-list.tsx +143 -0
  45. package/src/components/monitoring/health-cards.tsx +152 -0
  46. package/src/components/monitoring/metrics-stats.tsx +184 -0
  47. package/src/components/monitoring/request-log-table.tsx +179 -0
  48. package/src/components/providers/theme-toggle.tsx +59 -0
  49. package/src/components/settings/avatar/avatar-config.ts +46 -0
  50. package/src/components/ui/alert-dialog.tsx +216 -0
  51. package/src/components/ui/alert.tsx +76 -0
  52. package/src/components/ui/avatar.tsx +129 -0
  53. package/src/components/ui/badge.tsx +66 -0
  54. package/src/components/ui/button.tsx +84 -0
  55. package/src/components/ui/card.tsx +120 -0
  56. package/src/components/ui/checkbox.tsx +50 -0
  57. package/src/components/ui/collapsible.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +185 -0
  59. package/src/components/ui/dropdown-menu.tsx +286 -0
  60. package/src/components/ui/field.tsx +255 -0
  61. package/src/components/ui/input-group.tsx +171 -0
  62. package/src/components/ui/input.tsx +36 -0
  63. package/src/components/ui/label.tsx +41 -0
  64. package/src/components/ui/pagination.tsx +146 -0
  65. package/src/components/ui/select.tsx +209 -0
  66. package/src/components/ui/separator.tsx +45 -0
  67. package/src/components/ui/sheet.tsx +164 -0
  68. package/src/components/ui/sidebar.tsx +719 -0
  69. package/src/components/ui/skeleton.tsx +30 -0
  70. package/src/components/ui/sonner.tsx +37 -0
  71. package/src/components/ui/switch.tsx +48 -0
  72. package/src/components/ui/table.tsx +133 -0
  73. package/src/components/ui/textarea.tsx +35 -0
  74. package/src/components/ui/tooltip.tsx +74 -0
  75. package/src/components/user/columns.tsx +153 -0
  76. package/src/components/user/settings-activity-log.tsx +358 -0
  77. package/src/components/user/settings-profile/constants.ts +42 -0
  78. package/src/components/user/settings-profile/types.ts +47 -0
  79. package/src/components/user/settings-profile.tsx +664 -0
  80. package/src/components/user/user-create-dialog.tsx +137 -0
  81. package/src/components/user/user-filters.tsx +117 -0
  82. package/src/hooks/use-mobile.ts +36 -0
  83. package/src/instrument.client.ts +34 -0
  84. package/src/lib/anyhub/capabilities.ts +59 -0
  85. package/src/lib/anyhub/types.ts +43 -0
  86. package/src/lib/auth-client.ts +83 -0
  87. package/src/lib/auth-utils.ts +24 -0
  88. package/src/lib/auth.config.ts +264 -0
  89. package/src/lib/avatar.ts +41 -0
  90. package/src/lib/i18n/client.ts +32 -0
  91. package/src/lib/i18n/config.ts +40 -0
  92. package/src/lib/i18n/detect.ts +43 -0
  93. package/src/lib/i18n/locales/en/admin-ai.json +128 -0
  94. package/src/lib/i18n/locales/en/admin-billing.json +56 -0
  95. package/src/lib/i18n/locales/en/admin-users.json +88 -0
  96. package/src/lib/i18n/locales/en/common.json +877 -0
  97. package/src/lib/i18n/locales/en/enterprise.json +208 -0
  98. package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
  99. package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
  100. package/src/lib/i18n/locales/zh/admin-users.json +88 -0
  101. package/src/lib/i18n/locales/zh/common.json +877 -0
  102. package/src/lib/i18n/locales/zh/enterprise.json +208 -0
  103. package/src/lib/i18n/server.ts +18 -0
  104. package/src/lib/i18n/types.ts +5 -0
  105. package/src/lib/monitoring-benchmarks.ts +85 -0
  106. package/src/lib/monitoring-descriptions.ts +78 -0
  107. package/src/lib/prisma.ts +18 -0
  108. package/src/lib/router-context.ts +38 -0
  109. package/src/lib/session.ts +38 -0
  110. package/src/lib/utils.ts +23 -0
  111. package/src/providers/index.tsx +45 -0
  112. package/src/routeTree.gen.ts +1199 -0
  113. package/src/router.tsx +51 -0
  114. package/src/routes/__root.tsx +170 -0
  115. package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
  116. package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
  117. package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
  118. package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
  119. package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
  120. package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
  121. package/src/routes/_nav/ai-providers/route.tsx +31 -0
  122. package/src/routes/_nav/apps/route.lazy.tsx +30 -0
  123. package/src/routes/_nav/apps/route.tsx +20 -0
  124. package/src/routes/_nav/audit/route.lazy.tsx +100 -0
  125. package/src/routes/_nav/audit/route.tsx +25 -0
  126. package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
  127. package/src/routes/_nav/billing/invoices/route.tsx +25 -0
  128. package/src/routes/_nav/billing/route.lazy.tsx +48 -0
  129. package/src/routes/_nav/billing/route.tsx +27 -0
  130. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
  131. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
  132. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
  133. package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
  134. package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
  135. package/src/routes/_nav/compliance/route.tsx +25 -0
  136. package/src/routes/_nav/components/route.lazy.tsx +184 -0
  137. package/src/routes/_nav/components/route.tsx +40 -0
  138. package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
  139. package/src/routes/_nav/dashboard/route.tsx +20 -0
  140. package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
  141. package/src/routes/_nav/dependencies/route.tsx +32 -0
  142. package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
  143. package/src/routes/_nav/developer-logs/route.tsx +14 -0
  144. package/src/routes/_nav/docs/route.lazy.tsx +30 -0
  145. package/src/routes/_nav/docs/route.tsx +20 -0
  146. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
  147. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
  148. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
  149. package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
  150. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
  151. package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
  152. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
  153. package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
  154. package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
  155. package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
  156. package/src/routes/_nav/licenses/route.tsx +25 -0
  157. package/src/routes/_nav/market/route.lazy.tsx +31 -0
  158. package/src/routes/_nav/market/route.tsx +20 -0
  159. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
  160. package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
  161. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
  162. package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
  163. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
  164. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
  165. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
  166. package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
  167. package/src/routes/_nav/monitoring/route.tsx +20 -0
  168. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
  169. package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
  170. package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
  171. package/src/routes/_nav/permissions/route.tsx +26 -0
  172. package/src/routes/_nav/route.lazy.tsx +31 -0
  173. package/src/routes/_nav/route.tsx +34 -0
  174. package/src/routes/_nav/settings/route.lazy.tsx +30 -0
  175. package/src/routes/_nav/settings/route.tsx +20 -0
  176. package/src/routes/_nav/sso/route.lazy.tsx +145 -0
  177. package/src/routes/_nav/sso/route.tsx +25 -0
  178. package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
  179. package/src/routes/_nav/users/$userId/route.tsx +27 -0
  180. package/src/routes/_nav/users/-components/users-table.tsx +38 -0
  181. package/src/routes/_nav/users/route.lazy.tsx +74 -0
  182. package/src/routes/_nav/users/route.tsx +43 -0
  183. package/src/routes/api/compliance-download/$jobId.ts +35 -0
  184. package/src/routes/api/observability/errors.ts +41 -0
  185. package/src/routes/api/observability/health.ts +30 -0
  186. package/src/routes/api.auth.$.ts +31 -0
  187. package/src/routes/api.sentry-test.ts +33 -0
  188. package/src/routes/index/route.lazy.tsx +22 -0
  189. package/src/routes/index/route.tsx +24 -0
  190. package/src/routes/login/route.lazy.tsx +46 -0
  191. package/src/routes/login/route.tsx +28 -0
  192. package/src/routes/sentry-tunnel.ts +61 -0
  193. package/src/routes/setup/route.lazy.tsx +94 -0
  194. package/src/routes/setup/route.tsx +28 -0
  195. package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
  196. package/src/server/__tests__/licenses-issue.test.ts +98 -0
  197. package/src/server/admin-ai-alerts.ts +177 -0
  198. package/src/server/admin-ai-usage.ts +134 -0
  199. package/src/server/auth-helpers.ts +51 -0
  200. package/src/server/entitlements.ts +24 -0
  201. package/src/server/knowledge-public/canonical-id.ts +55 -0
  202. package/src/server/knowledge-public/github-sync.ts +86 -0
  203. package/src/server/license-issue.ts +78 -0
  204. package/src/server/middlewares/request.ts +26 -0
  205. package/src/server/observability/debug-middleware.ts +39 -0
  206. package/src/server/observability/error-reporter.ts +99 -0
  207. package/src/server/observability/health-check.ts +140 -0
  208. package/src/server/observability/index.ts +33 -0
  209. package/src/server/observability/logger.ts +49 -0
  210. package/src/server/observability/metrics.ts +105 -0
  211. package/src/server/observability/request-middleware.ts +125 -0
  212. package/src/server/observability/timing.ts +42 -0
  213. package/src/server/pro-services.ts +32 -0
  214. package/src/server/serverFns/admin-billing.ts +94 -0
  215. package/src/server/serverFns/ai-provider/create.ts +92 -0
  216. package/src/server/serverFns/ai-provider/delete.ts +42 -0
  217. package/src/server/serverFns/ai-provider/get.ts +35 -0
  218. package/src/server/serverFns/ai-provider/list.ts +92 -0
  219. package/src/server/serverFns/ai-provider/update.ts +73 -0
  220. package/src/server/serverFns/anyhub/market.ts +64 -0
  221. package/src/server/serverFns/audit.ts +57 -0
  222. package/src/server/serverFns/auth/session.ts +26 -0
  223. package/src/server/serverFns/auth/setup.ts +73 -0
  224. package/src/server/serverFns/compliance.ts +85 -0
  225. package/src/server/serverFns/components/import.ts +197 -0
  226. package/src/server/serverFns/components/list.ts +51 -0
  227. package/src/server/serverFns/components/mutations.ts +84 -0
  228. package/src/server/serverFns/dependency/helpers.ts +245 -0
  229. package/src/server/serverFns/dependency/list.ts +127 -0
  230. package/src/server/serverFns/dependency/sync.ts +123 -0
  231. package/src/server/serverFns/dependency/update.ts +51 -0
  232. package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
  233. package/src/server/serverFns/developer-logs.ts +132 -0
  234. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
  235. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
  236. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
  237. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
  238. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
  239. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
  240. package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
  241. package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
  242. package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
  243. package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
  244. package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
  245. package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
  246. package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
  247. package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
  248. package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
  249. package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
  250. package/src/server/serverFns/licenses.ts +92 -0
  251. package/src/server/serverFns/locale.ts +10 -0
  252. package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
  253. package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
  254. package/src/server/serverFns/monitoring/get-errors.ts +43 -0
  255. package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
  256. package/src/server/serverFns/monitoring/get-health.ts +55 -0
  257. package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
  258. package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
  259. package/src/server/serverFns/monitoring/report-error.ts +55 -0
  260. package/src/server/serverFns/monitoring/reset.ts +70 -0
  261. package/src/server/serverFns/permissions.ts +214 -0
  262. package/src/server/serverFns/sentry/config.ts +88 -0
  263. package/src/server/serverFns/sso-config.ts +83 -0
  264. package/src/server/serverFns/user/create.ts +40 -0
  265. package/src/server/serverFns/user/delete.ts +29 -0
  266. package/src/server/serverFns/user/detail.ts +45 -0
  267. package/src/server/serverFns/user/list.ts +56 -0
  268. package/src/server/serverFns/user/reset-password.ts +28 -0
  269. package/src/server/serverFns/user/update-role.ts +26 -0
  270. package/src/server/serverFns/user/update-status.ts +26 -0
  271. package/src/server.ts +27 -0
  272. package/src/start.ts +85 -0
  273. package/src/styles/_keyframe-animations.scss +91 -0
  274. package/src/styles/_variables.scss +296 -0
  275. package/src/styles/globals.css +200 -0
  276. package/src/types/start-authjs.d.ts +27 -0
@@ -0,0 +1,348 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { useState, useEffect } from 'react'
19
+ import { useTranslation } from 'react-i18next'
20
+ import { Button } from '@/components/ui/button'
21
+ import { Input } from '@/components/ui/input'
22
+ import { Label } from '@/components/ui/label'
23
+ import { Textarea } from '@/components/ui/textarea'
24
+ import { Switch } from '@/components/ui/switch'
25
+ import {
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ SelectValue,
31
+ } from '@/components/ui/select'
32
+ import {
33
+ Dialog,
34
+ DialogContent,
35
+ DialogFooter,
36
+ DialogHeader,
37
+ DialogTitle,
38
+ } from '@/components/ui/dialog'
39
+ import { getComponent, ComponentPreview } from '@allbluecn/web-app/prototype'
40
+ import { createComponentFn, updateComponentFn } from '@/server/serverFns/components/mutations'
41
+ import { toast } from '@/components/ui/sonner'
42
+ import type { ComponentItem } from './component-table'
43
+
44
+ const CATEGORY_KEYS = ['base', 'block', 'template'] as const
45
+
46
+ interface ComponentFormDialogProps {
47
+ open: boolean
48
+ onOpenChange: (open: boolean) => void
49
+ component?: ComponentItem | null
50
+ onSuccess: () => void
51
+ }
52
+
53
+ export function ComponentFormDialog({ open, onOpenChange, component, onSuccess }: ComponentFormDialogProps) {
54
+ const { t } = useTranslation('common')
55
+ const isEdit = !!component
56
+ const [name, setName] = useState('')
57
+ const [description, setDescription] = useState('')
58
+ const [category, setCategory] = useState<string>('base')
59
+ const [type, setType] = useState<string>('component')
60
+ const [subCategory, setSubCategory] = useState('')
61
+ const [defaultWidth, setDefaultWidth] = useState(200)
62
+ const [defaultHeight, setDefaultHeight] = useState(40)
63
+ const [tags, setTags] = useState('')
64
+ const [isHidden, setIsHidden] = useState(false)
65
+ const [sortOrder, setSortOrder] = useState(0)
66
+ const [saving, setSaving] = useState(false)
67
+ const [path, setPath] = useState('')
68
+ const [propsState, setPropsState] = useState<Record<string, any>>({})
69
+
70
+ useEffect(() => {
71
+ if (!open) return
72
+ setName(component?.name ?? '')
73
+ setDescription(component?.description ?? '')
74
+ setCategory(categoryFromKey(component?.category) ?? 'base')
75
+ setType(component?.type ?? 'component')
76
+ setSubCategory(component?.subCategory ?? '')
77
+ setPath(component?.path ?? '')
78
+ setDefaultWidth(component?.defaultWidth ?? 200)
79
+ setDefaultHeight(component?.defaultHeight ?? 40)
80
+ setTags((component?.tags ?? []).join(', '))
81
+ setIsHidden(component?.isHidden ?? false)
82
+ setSortOrder(component?.sortOrder ?? 0)
83
+ setPropsState((component?.defaultProps as Record<string, any>) ?? {})
84
+ }, [open, component])
85
+
86
+ async function handleSave() {
87
+ if (!name.trim()) {
88
+ toast.error(t('component.form.nameRequired'))
89
+ return
90
+ }
91
+ setSaving(true)
92
+ try {
93
+ const categoryValue = categoryToDisplay(category)
94
+ if (isEdit && component) {
95
+ await updateComponentFn({
96
+ data: {
97
+ id: component.id,
98
+ name: name.trim(),
99
+ description: description.trim() || undefined,
100
+ category: categoryValue,
101
+ type,
102
+ subCategory: subCategory.trim() || undefined,
103
+ path: path.trim() || undefined,
104
+ defaultWidth,
105
+ defaultHeight,
106
+ tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean),
107
+ isHidden,
108
+ sortOrder,
109
+ defaultProps: propsState,
110
+ },
111
+ })
112
+ toast.success(t('component.form.updated'))
113
+ } else {
114
+ await createComponentFn({
115
+ data: {
116
+ name: name.trim(),
117
+ type,
118
+ category: categoryValue,
119
+ description: description.trim() || undefined,
120
+ },
121
+ })
122
+ toast.success(t('component.form.created'))
123
+ }
124
+ onSuccess()
125
+ onOpenChange(false)
126
+ } catch (e) {
127
+ toast.error(e instanceof Error ? e.message : t('component.form.saveFailed'))
128
+ } finally {
129
+ setSaving(false)
130
+ }
131
+ }
132
+
133
+ function categoryToDisplay(k: string): string {
134
+ const map: Record<string, string> = {
135
+ base: t('component.categories.base'),
136
+ block: t('component.categories.block'),
137
+ template: t('component.categories.template'),
138
+ }
139
+ return map[k] ?? k
140
+ }
141
+
142
+ function categoryFromKey(label: string | null | undefined): string | undefined {
143
+ if (!label) return undefined
144
+ const zhBase = t('component.categories.base')
145
+ const zhBlock = t('component.categories.block')
146
+ const zhTemplate = t('component.categories.template')
147
+ if (label === zhBase) return 'base'
148
+ if (label === zhBlock) return 'block'
149
+ if (label === zhTemplate) return 'template'
150
+ return undefined
151
+ }
152
+
153
+ function renderPreview() {
154
+ if (!component) {
155
+ return <span className="text-xs text-muted-foreground">{t('component.form.previewUnavailable')}</span>
156
+ }
157
+ const config = getComponent(component.prototypeKey ?? '')
158
+ const Comp = config?.component
159
+ if (Comp) {
160
+ return (
161
+ <div className="pointer-events-none flex h-full w-full items-center justify-center overflow-hidden p-3">
162
+ <Comp props={propsState} style={(component.defaultStyle as Record<string, any>) ?? {}} />
163
+ </div>
164
+ )
165
+ }
166
+ return (
167
+ <ComponentPreview
168
+ type={component.type}
169
+ name={component.name}
170
+ defaultProps={propsState}
171
+ defaultStyle={component.defaultStyle as Record<string, any>}
172
+ size="md"
173
+ />
174
+ )
175
+ }
176
+
177
+ function renderPropsEditor() {
178
+ if (!component) return null
179
+ const schema = (component.config as { props?: Array<{ key: string; type: string; label: string; default: unknown; options?: Array<{ label: string; value: unknown }> }> } | null)?.props ?? []
180
+ if (schema.length === 0) return null
181
+ return (
182
+ <div className="col-span-2 mt-2 space-y-2 border-t pt-3">
183
+ <Label className="text-muted-foreground">{t('component.form.defaultProps')}</Label>
184
+ <div className="grid grid-cols-2 gap-3">
185
+ {schema.map((prop) => {
186
+ const val = propsState[prop.key] ?? prop.default
187
+ const setVal = (v: unknown) => setPropsState((s) => ({ ...s, [prop.key]: v }))
188
+ return (
189
+ <div key={prop.key} className="space-y-1">
190
+ <Label className="text-xs text-muted-foreground">{prop.label}</Label>
191
+ {prop.type === 'boolean' ? (
192
+ <Switch checked={!!val} onCheckedChange={setVal} />
193
+ ) : prop.type === 'number' ? (
194
+ <Input type="number" value={Number(val)} onChange={(e) => setVal(Number(e.target.value))} />
195
+ ) : prop.type === 'color' ? (
196
+ <div className="flex gap-2">
197
+ <input type="color" value={String(val)} onChange={(e) => setVal(e.target.value)} className="size-8 shrink-0 rounded border" />
198
+ <Input value={String(val)} onChange={(e) => setVal(e.target.value)} className="font-mono text-xs" />
199
+ </div>
200
+ ) : prop.type === 'select' && prop.options ? (
201
+ <Select value={String(val)} onValueChange={setVal}>
202
+ <SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
203
+ <SelectContent>
204
+ {prop.options.map((o) => (
205
+ <SelectItem key={String(o.value)} value={String(o.value)}>{o.label}</SelectItem>
206
+ ))}
207
+ </SelectContent>
208
+ </Select>
209
+ ) : (
210
+ <Input value={String(val ?? '')} onChange={(e) => setVal(e.target.value)} />
211
+ )}
212
+ </div>
213
+ )
214
+ })}
215
+ </div>
216
+ </div>
217
+ )
218
+ }
219
+
220
+ return (
221
+ <Dialog open={open} onOpenChange={onOpenChange}>
222
+ <DialogContent className="sm:max-w-4xl">
223
+ <DialogHeader>
224
+ <DialogTitle>{isEdit ? t('component.form.edit') : t('component.form.create')}</DialogTitle>
225
+ </DialogHeader>
226
+
227
+ <div className="flex min-h-[300px] gap-4">
228
+ {/* 左侧预览区 */}
229
+ <div className="flex w-60 shrink-0 flex-col gap-1.5">
230
+ <Label className="text-muted-foreground">{t('component.form.preview')}</Label>
231
+ <div className="flex flex-1 items-center justify-center overflow-hidden rounded-lg border bg-muted/30">
232
+ {renderPreview()}
233
+ </div>
234
+ {isEdit && component && (
235
+ <div className="space-y-0.5 text-[10px] text-muted-foreground">
236
+ <div className="truncate">prototypeKey: <code className="font-mono">{component.prototypeKey ?? '-'}</code></div>
237
+ <div>source: <code className="font-mono">{component.source}</code> · v{component.version}</div>
238
+ </div>
239
+ )}
240
+ </div>
241
+
242
+ {/* 右侧表单 */}
243
+ <div className="grid flex-1 grid-cols-2 gap-x-4 gap-y-3">
244
+ <div className="col-span-2 space-y-1.5">
245
+ <Label>{t('component.form.name')}</Label>
246
+ <Input value={name} onChange={(e) => setName(e.target.value)} placeholder={t('component.form.namePlaceholder')} />
247
+ </div>
248
+
249
+ <div className="col-span-2 space-y-1.5">
250
+ <Label>{t('component.form.description')}</Label>
251
+ <Textarea
252
+ value={description}
253
+ onChange={(e) => setDescription(e.target.value)}
254
+ placeholder={t('component.form.descriptionPlaceholder')}
255
+ rows={2}
256
+ />
257
+ </div>
258
+
259
+ <div className="space-y-1.5">
260
+ <Label>{t('component.form.category')}</Label>
261
+ <Select value={category} onValueChange={setCategory}>
262
+ <SelectTrigger className="w-full">
263
+ <SelectValue />
264
+ </SelectTrigger>
265
+ <SelectContent>
266
+ <SelectItem key="base" value="base">{t('component.categories.base')}</SelectItem>
267
+ <SelectItem key="block" value="block">{t('component.categories.block')}</SelectItem>
268
+ <SelectItem key="template" value="template">{t('component.categories.template')}</SelectItem>
269
+ </SelectContent>
270
+ </Select>
271
+ </div>
272
+
273
+ <div className="space-y-1.5">
274
+ <Label>{t('component.form.type')}</Label>
275
+ <Select value={type} onValueChange={setType}>
276
+ <SelectTrigger className="w-full">
277
+ <SelectValue />
278
+ </SelectTrigger>
279
+ <SelectContent>
280
+ <SelectItem key="component" value="component">{t('component.types.base')}</SelectItem>
281
+ <SelectItem key="block" value="block">{t('component.types.block')}</SelectItem>
282
+ <SelectItem key="template" value="template">{t('component.types.template')}</SelectItem>
283
+ </SelectContent>
284
+ </Select>
285
+ </div>
286
+
287
+ <div className="space-y-1.5">
288
+ <Label>{t('component.form.subCategory')}</Label>
289
+ <Input value={subCategory} onChange={(e) => setSubCategory(e.target.value)} placeholder={t('component.form.subCategoryPlaceholder')} />
290
+ </div>
291
+
292
+ <div className="space-y-1.5">
293
+ <Label>{t('component.form.tags')}</Label>
294
+ <Input value={tags} onChange={(e) => setTags(e.target.value)} placeholder={t('component.form.tagsPlaceholder')} />
295
+ </div>
296
+
297
+ <div className="col-span-2 space-y-1.5">
298
+ <Label>{t('component.form.path')}</Label>
299
+ <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder={t('component.form.pathPlaceholder')} className="font-mono text-xs" />
300
+ <p className="text-[10px] text-muted-foreground">{t('component.form.pathHint')}</p>
301
+ </div>
302
+
303
+ <div className="space-y-1.5">
304
+ <Label>{t('component.form.defaultWidth')}</Label>
305
+ <Input
306
+ type="number"
307
+ value={defaultWidth}
308
+ onChange={(e) => setDefaultWidth(Number(e.target.value))}
309
+ />
310
+ </div>
311
+
312
+ <div className="space-y-1.5">
313
+ <Label>{t('component.form.defaultHeight')}</Label>
314
+ <Input
315
+ type="number"
316
+ value={defaultHeight}
317
+ onChange={(e) => setDefaultHeight(Number(e.target.value))}
318
+ />
319
+ </div>
320
+
321
+ <div className="space-y-1.5">
322
+ <Label>{t('component.form.sortOrder')}</Label>
323
+ <Input
324
+ type="number"
325
+ value={sortOrder}
326
+ onChange={(e) => setSortOrder(Number(e.target.value))}
327
+ />
328
+ </div>
329
+
330
+ <div className="flex items-center gap-2 pt-6">
331
+ <Switch checked={isHidden} onCheckedChange={setIsHidden} />
332
+ <Label className="cursor-pointer" onClick={() => setIsHidden(!isHidden)}>{t('component.form.hidden')}</Label>
333
+ </div>
334
+
335
+ {renderPropsEditor()}
336
+ </div>
337
+ </div>
338
+
339
+ <DialogFooter>
340
+ <Button variant="outline" onClick={() => onOpenChange(false)}>{t('common.cancel')}</Button>
341
+ <Button onClick={handleSave} disabled={saving}>
342
+ {saving ? t('status.saving') : t('common.save')}
343
+ </Button>
344
+ </DialogFooter>
345
+ </DialogContent>
346
+ </Dialog>
347
+ )
348
+ }
@@ -0,0 +1,235 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { useState } from 'react'
19
+ import { Button } from '@/components/ui/button'
20
+ import { Input } from '@/components/ui/input'
21
+ import { Label } from '@/components/ui/label'
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '@/components/ui/select'
29
+ import {
30
+ Dialog,
31
+ DialogContent,
32
+ DialogDescription,
33
+ DialogFooter,
34
+ DialogHeader,
35
+ DialogTitle,
36
+ } from '@/components/ui/dialog'
37
+ import { importComponentFn } from '@/server/serverFns/components/import'
38
+ import { toast } from '@/components/ui/sonner'
39
+ import { Loader2, Sparkles } from 'lucide-react'
40
+ import { cn } from '@/lib/utils'
41
+
42
+ const CATEGORIES = [
43
+ { value: '基础组件', label: '基础组件', type: 'component' },
44
+ { value: '内容区块', label: '内容区块', type: 'block' },
45
+ { value: '页面模版', label: '页面模版', type: 'template' },
46
+ ] as const
47
+
48
+ interface ComponentImportDialogProps {
49
+ open: boolean
50
+ onOpenChange: (open: boolean) => void
51
+ onSuccess: () => void
52
+ }
53
+
54
+ export function ComponentImportDialog({ open, onOpenChange, onSuccess }: ComponentImportDialogProps) {
55
+ const [source, setSource] = useState<'npm' | 'local'>('npm')
56
+ const [packageName, setPackageName] = useState('')
57
+ const [importPath, setImportPath] = useState('')
58
+ const [exportName, setExportName] = useState('default')
59
+ const [componentKey, setComponentKey] = useState('')
60
+ const [displayName, setDisplayName] = useState('')
61
+ const [category, setCategory] = useState<string>('基础组件')
62
+ const [importing, setImporting] = useState(false)
63
+
64
+ function suggestKey() {
65
+ const raw = source === 'npm' ? packageName : importPath
66
+ if (!raw) return
67
+ const lastPart = raw.split('/').pop() ?? raw
68
+ const key = lastPart
69
+ .replace(/[^a-z0-9-]/gi, '-')
70
+ .replace(/-+/g, '-')
71
+ .replace(/^-|-$/g, '')
72
+ .toLowerCase()
73
+ setComponentKey(key)
74
+ if (!displayName) setDisplayName(lastPart)
75
+ }
76
+
77
+ async function handleImport() {
78
+ const idField = source === 'npm' ? packageName : importPath
79
+ if (!idField.trim() || !componentKey.trim() || !displayName.trim()) {
80
+ toast.error('请填写所有必填字段')
81
+ return
82
+ }
83
+ setImporting(true)
84
+ try {
85
+ const cat = CATEGORIES.find((c) => c.value === category) ?? CATEGORIES[0]!
86
+ const result = await importComponentFn({
87
+ data: {
88
+ source,
89
+ packageName: source === 'npm' ? packageName.trim() : undefined,
90
+ importPath: source === 'local' ? importPath.trim() : undefined,
91
+ exportName: exportName.trim() || 'default',
92
+ componentKey: componentKey.trim(),
93
+ displayName: displayName.trim(),
94
+ category: cat.value,
95
+ type: cat.type,
96
+ },
97
+ })
98
+ toast.success(result.message)
99
+ onSuccess()
100
+ onOpenChange(false)
101
+ setPackageName('')
102
+ setImportPath('')
103
+ setExportName('default')
104
+ setComponentKey('')
105
+ setDisplayName('')
106
+ } catch (e) {
107
+ toast.error(e instanceof Error ? e.message : '导入失败')
108
+ } finally {
109
+ setImporting(false)
110
+ }
111
+ }
112
+
113
+ return (
114
+ <Dialog open={open} onOpenChange={onOpenChange}>
115
+ <DialogContent className="sm:max-w-xl">
116
+ <DialogHeader>
117
+ <DialogTitle>导入组件</DialogTitle>
118
+ <DialogDescription>
119
+ {source === 'npm'
120
+ ? '从 npm 安装第三方组件包(自动 pnpm add + 生成 wrapper)。'
121
+ : '输入 registry 路径(如 @magicui/safari,自动 shadcn add 安装)或已安装组件的 @/ 路径。'}
122
+ </DialogDescription>
123
+ </DialogHeader>
124
+
125
+ <div className="space-y-3">
126
+ <div className="flex gap-1 rounded-lg bg-muted/40 p-1">
127
+ {(['npm', 'local'] as const).map((s) => (
128
+ <button
129
+ key={s}
130
+ onClick={() => setSource(s)}
131
+ className={cn(
132
+ 'flex-1 rounded-md px-3 py-1.5 text-sm transition-colors',
133
+ source === s ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground',
134
+ )}
135
+ >
136
+ {s === 'npm' ? 'npm 包' : '本地组件'}
137
+ </button>
138
+ ))}
139
+ </div>
140
+
141
+ {source === 'npm' ? (
142
+ <div className="space-y-1.5">
143
+ <Label>npm 包名 *</Label>
144
+ <div className="flex gap-2">
145
+ <Input
146
+ value={packageName}
147
+ onChange={(e) => setPackageName(e.target.value)}
148
+ placeholder="如:lucide-react"
149
+ className="font-mono text-xs"
150
+ />
151
+ <Button variant="outline" size="sm" onClick={suggestKey} disabled={!packageName}>
152
+ <Sparkles className="mr-1 size-3.5" />
153
+ 生成
154
+ </Button>
155
+ </div>
156
+ </div>
157
+ ) : (
158
+ <div className="space-y-1.5">
159
+ <Label>registry 路径或 @/ 路径 *</Label>
160
+ <div className="flex gap-2">
161
+ <Input
162
+ value={importPath}
163
+ onChange={(e) => setImportPath(e.target.value)}
164
+ placeholder="@magicui/safari 或 @/components/magicui/safari"
165
+ className="font-mono text-xs"
166
+ />
167
+ <Button variant="outline" size="sm" onClick={suggestKey} disabled={!importPath}>
168
+ <Sparkles className="mr-1 size-3.5" />
169
+ 生成
170
+ </Button>
171
+ </div>
172
+ <p className="text-[10px] text-muted-foreground">
173
+ registry 路径自动 <code className="font-mono">shadcn add</code> 安装;<code className="font-mono">@/</code> 路径直接引用已安装组件。
174
+ </p>
175
+ </div>
176
+ )}
177
+
178
+ <div className="grid grid-cols-2 gap-3">
179
+ <div className="space-y-1.5">
180
+ <Label>导出名</Label>
181
+ <Input
182
+ value={exportName}
183
+ onChange={(e) => setExportName(e.target.value)}
184
+ placeholder="default 或具名"
185
+ className="font-mono text-xs"
186
+ />
187
+ </div>
188
+ <div className="space-y-1.5">
189
+ <Label>组件标识 *</Label>
190
+ <Input
191
+ value={componentKey}
192
+ onChange={(e) => setComponentKey(e.target.value)}
193
+ placeholder="如:my-button"
194
+ className="font-mono text-xs"
195
+ />
196
+ </div>
197
+ </div>
198
+
199
+ <div className="grid grid-cols-2 gap-3">
200
+ <div className="space-y-1.5">
201
+ <Label>显示名称 *</Label>
202
+ <Input
203
+ value={displayName}
204
+ onChange={(e) => setDisplayName(e.target.value)}
205
+ placeholder="如:我的按钮"
206
+ />
207
+ </div>
208
+ <div className="space-y-1.5">
209
+ <Label>分类</Label>
210
+ <Select value={category} onValueChange={setCategory}>
211
+ <SelectTrigger className="w-full">
212
+ <SelectValue />
213
+ </SelectTrigger>
214
+ <SelectContent>
215
+ {CATEGORIES.map((c) => (
216
+ <SelectItem key={c.value} value={c.value}>{c.label}</SelectItem>
217
+ ))}
218
+ </SelectContent>
219
+ </Select>
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ <DialogFooter>
225
+ <Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
226
+ <Button onClick={handleImport} disabled={importing}>
227
+ {importing ? (
228
+ <><Loader2 className="mr-1 size-4 animate-spin" />处理中...</>
229
+ ) : '导入'}
230
+ </Button>
231
+ </DialogFooter>
232
+ </DialogContent>
233
+ </Dialog>
234
+ )
235
+ }