@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,200 @@
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 { useTranslation } from 'react-i18next'
20
+ import { toast } from '@/components/ui/sonner'
21
+ import { Loader2, Pencil } from "lucide-react"
22
+ import {
23
+ DEPENDENCY_CATEGORIES,
24
+ DEPENDENCY_PRIORITIES,
25
+ type DependencyCategory,
26
+ type ThirdPartyDependencyRow,
27
+ } from "@allbluecn/kernel"
28
+ import { updateDependencyFn } from "@/server/serverFns/dependency/update"
29
+ import { Button } from "@/components/ui/button"
30
+ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
31
+ import { Input } from "@/components/ui/input"
32
+ import { Label } from "@/components/ui/label"
33
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
34
+ import { Textarea } from "@/components/ui/textarea"
35
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
36
+
37
+ export function EditDependencyDialog({
38
+ row,
39
+ onSaved,
40
+ }: {
41
+ row: ThirdPartyDependencyRow
42
+ onSaved?: () => void
43
+ }) {
44
+ const [open, setOpen] = useState(false)
45
+ const [form, setForm] = useState({
46
+ displayName: row.displayName,
47
+ description: row.description ?? "",
48
+ homepage: row.homepage ?? "",
49
+ category: row.category,
50
+ isWatched: row.isWatched,
51
+ priority: row.priority,
52
+ notes: row.notes ?? "",
53
+ sortOrder: row.sortOrder,
54
+ })
55
+ const [saving, setSaving] = useState(false)
56
+ const { t } = useTranslation('common')
57
+
58
+ async function handleSave() {
59
+ setSaving(true)
60
+ try {
61
+ await updateDependencyFn({
62
+ data: {
63
+ id: row.id.startsWith("untracked:") ? "" : row.id,
64
+ displayName: form.displayName,
65
+ description: form.description || null,
66
+ homepage: form.homepage || null,
67
+ category: form.category,
68
+ isWatched: form.isWatched,
69
+ priority: form.priority,
70
+ notes: form.notes || null,
71
+ sortOrder: form.sortOrder,
72
+ },
73
+ })
74
+ toast.success(t('dependency.edit.saved'))
75
+ setOpen(false)
76
+ onSaved?.()
77
+ } catch (e) {
78
+ toast.error(t('dependency.edit.saveFailed'), { description: e instanceof Error ? e.message : String(e) })
79
+ } finally {
80
+ setSaving(false)
81
+ }
82
+ }
83
+
84
+ if (row.id.startsWith("untracked:")) {
85
+ return (
86
+ <Tooltip>
87
+ <TooltipTrigger asChild>
88
+ <span>
89
+ <Pencil className="size-3.5 text-muted-foreground/50" />
90
+ </span>
91
+ </TooltipTrigger>
92
+ <TooltipContent>{t('dependency.edit.untracked')}</TooltipContent>
93
+ </Tooltip>
94
+ )
95
+ }
96
+
97
+ return (
98
+ <Dialog open={open} onOpenChange={setOpen}>
99
+ <DialogTrigger asChild>
100
+ <Button size="sm" variant="ghost" className="size-7 p-0" title={t('common.edit')}>
101
+ <Pencil className="size-3.5" />
102
+ </Button>
103
+ </DialogTrigger>
104
+ <DialogContent className="max-w-md">
105
+ <DialogHeader>
106
+ <DialogTitle>{t('dependency.edit.dialogTitle', { name: row.name })}</DialogTitle>
107
+ </DialogHeader>
108
+ <div className="space-y-3">
109
+ <div className="space-y-1.5">
110
+ <Label>{t('dependency.edit.displayName')}</Label>
111
+ <Input value={form.displayName} onChange={(e) => setForm({ ...form, displayName: e.target.value })} />
112
+ </div>
113
+ <div className="space-y-1.5">
114
+ <Label>{t('dependency.edit.description')}</Label>
115
+ <Textarea
116
+ value={form.description}
117
+ onChange={(e) => setForm({ ...form, description: e.target.value })}
118
+ placeholder={t('dependency.edit.descriptionPlaceholder')}
119
+ rows={2}
120
+ />
121
+ </div>
122
+ <div className="space-y-1.5">
123
+ <Label>{t('dependency.edit.homepage')}</Label>
124
+ <Input
125
+ value={form.homepage}
126
+ onChange={(e) => setForm({ ...form, homepage: e.target.value })}
127
+ placeholder="https://"
128
+ />
129
+ </div>
130
+ <div className="grid grid-cols-2 gap-3">
131
+ <div className="space-y-1.5">
132
+ <Label>{t('dependency.edit.category')}</Label>
133
+ <Select
134
+ value={form.category}
135
+ onValueChange={(v) => setForm({ ...form, category: v as DependencyCategory })}
136
+ >
137
+ <SelectTrigger size="sm">
138
+ <SelectValue />
139
+ </SelectTrigger>
140
+ <SelectContent>
141
+ {DEPENDENCY_CATEGORIES.map((c) => (
142
+ <SelectItem key={c.value} value={c.value}>
143
+ {c.label}
144
+ </SelectItem>
145
+ ))}
146
+ </SelectContent>
147
+ </Select>
148
+ </div>
149
+ <div className="space-y-1.5">
150
+ <Label>{t('dependency.edit.priority')}</Label>
151
+ <Select
152
+ value={form.priority}
153
+ onValueChange={(v) => setForm({ ...form, priority: v as ThirdPartyDependencyRow["priority"] })}
154
+ >
155
+ <SelectTrigger size="sm">
156
+ <SelectValue />
157
+ </SelectTrigger>
158
+ <SelectContent>
159
+ {DEPENDENCY_PRIORITIES.map((p) => (
160
+ <SelectItem key={p.value} value={p.value}>
161
+ {p.label}
162
+ </SelectItem>
163
+ ))}
164
+ </SelectContent>
165
+ </Select>
166
+ </div>
167
+ </div>
168
+ <div className="flex items-center gap-2">
169
+ <input
170
+ type="checkbox"
171
+ id="isWatched"
172
+ checked={form.isWatched}
173
+ onChange={(e) => setForm({ ...form, isWatched: e.target.checked })}
174
+ className="size-4"
175
+ />
176
+ <Label htmlFor="isWatched">{t('dependency.edit.watched')}</Label>
177
+ </div>
178
+ <div className="space-y-1.5">
179
+ <Label>{t('dependency.edit.notes')}</Label>
180
+ <Textarea
181
+ value={form.notes}
182
+ onChange={(e) => setForm({ ...form, notes: e.target.value })}
183
+ placeholder={t('dependency.edit.notesPlaceholder')}
184
+ rows={3}
185
+ />
186
+ </div>
187
+ </div>
188
+ <DialogFooter>
189
+ <Button variant="outline" onClick={() => setOpen(false)} disabled={saving}>
190
+ {t('common.cancel')}
191
+ </Button>
192
+ <Button onClick={handleSave} disabled={saving}>
193
+ {saving && <Loader2 className="mr-1 size-3.5 animate-spin" />}
194
+ {t('common.save')}
195
+ </Button>
196
+ </DialogFooter>
197
+ </DialogContent>
198
+ </Dialog>
199
+ )
200
+ }
@@ -0,0 +1,215 @@
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 { Loader2 } from 'lucide-react'
20
+ import { toast } from '@/components/ui/sonner'
21
+ import { Button } from '@/components/ui/button'
22
+ import { Input } from '@/components/ui/input'
23
+ import { Label } from '@/components/ui/label'
24
+ import { Textarea } from '@/components/ui/textarea'
25
+ import {
26
+ Dialog,
27
+ DialogContent,
28
+ DialogFooter,
29
+ DialogHeader,
30
+ DialogTitle,
31
+ } from '@/components/ui/dialog'
32
+ import { adminCreateBookmarkFn } from '@/server/serverFns/knowledge-public/bookmarks/create'
33
+ import { adminUpdateBookmarkFn } from '@/server/serverFns/knowledge-public/bookmarks/update'
34
+ import { adminGetBookmarkDetailFn } from '@/server/serverFns/knowledge-public/bookmarks/detail'
35
+
36
+ export interface BookmarkRow {
37
+ id: string
38
+ canonicalId: string
39
+ title: string
40
+ url: string
41
+ categoryId: string | null
42
+ summary: string | null
43
+ faviconUrl: string | null
44
+ categoryIcon?: string
45
+ isGithub: boolean
46
+ githubStars: number | null
47
+ githubLatestVersion: string | null
48
+ githubVersionDate: Date | null
49
+ githubRepoUpdatedAt: Date | null
50
+ githubSyncedAt: Date | null
51
+ tags: string[]
52
+ visibility: string
53
+ userId: string
54
+ createdAt: Date
55
+ }
56
+
57
+ interface BookmarkFormDialogProps {
58
+ open: boolean
59
+ onOpenChange: (open: boolean) => void
60
+ item?: BookmarkRow | null
61
+ onSuccess: () => void
62
+ }
63
+
64
+ const EMPTY = {
65
+ title: '',
66
+ url: '',
67
+ categoryId: '',
68
+ summary: '',
69
+ tags: '',
70
+ }
71
+
72
+ export function BookmarkFormDialog({ open, onOpenChange, item, onSuccess }: BookmarkFormDialogProps) {
73
+ const isEdit = !!item
74
+ const [form, setForm] = useState(EMPTY)
75
+ const [saving, setSaving] = useState(false)
76
+ const [loading, setLoading] = useState(false)
77
+
78
+ useEffect(() => {
79
+ if (!open) return
80
+ if (item) {
81
+ setLoading(true)
82
+ adminGetBookmarkDetailFn({ data: { id: item.id } })
83
+ .then((d) => {
84
+ setForm({
85
+ title: d.title ?? '',
86
+ url: d.url ?? '',
87
+ categoryId: d.categoryId ?? '',
88
+ summary: d.summary ?? '',
89
+ tags: ((d.tags as string[]) ?? []).join(', '),
90
+ })
91
+ })
92
+ .catch((e) => toast.error(e instanceof Error ? e.message : '加载失败'))
93
+ .finally(() => setLoading(false))
94
+ } else {
95
+ setForm(EMPTY)
96
+ }
97
+ }, [open, item])
98
+
99
+ async function handleSubmit(e: React.FormEvent) {
100
+ e.preventDefault()
101
+ if (!form.title.trim()) {
102
+ toast.error('请输入标题')
103
+ return
104
+ }
105
+ if (!form.url.trim()) {
106
+ toast.error('请输入网址')
107
+ return
108
+ }
109
+ setSaving(true)
110
+ try {
111
+ const payload = {
112
+ title: form.title.trim(),
113
+ url: form.url.trim(),
114
+ categoryId: form.categoryId.trim() || undefined,
115
+ summary: form.summary.trim() || undefined,
116
+ tags: form.tags.split(',').map((t) => t.trim()).filter(Boolean),
117
+ }
118
+ if (isEdit && item) {
119
+ await adminUpdateBookmarkFn({
120
+ data: {
121
+ id: item.id,
122
+ title: payload.title,
123
+ url: payload.url,
124
+ categoryId: payload.categoryId ?? null,
125
+ summary: payload.summary ?? null,
126
+ tags: payload.tags,
127
+ },
128
+ })
129
+ toast.success('网站收藏已更新')
130
+ } else {
131
+ await adminCreateBookmarkFn({ data: payload })
132
+ toast.success('网站收藏已创建')
133
+ }
134
+ onSuccess()
135
+ onOpenChange(false)
136
+ } catch (err) {
137
+ toast.error(err instanceof Error ? err.message : '保存失败')
138
+ } finally {
139
+ setSaving(false)
140
+ }
141
+ }
142
+
143
+ return (
144
+ <Dialog open={open} onOpenChange={onOpenChange}>
145
+ <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
146
+ <DialogHeader>
147
+ <DialogTitle>{isEdit ? '编辑网站收藏' : '新建网站收藏'}</DialogTitle>
148
+ </DialogHeader>
149
+ {loading ? (
150
+ <div className="flex h-40 items-center justify-center">
151
+ <Loader2 className="size-6 animate-spin text-muted-foreground" />
152
+ </div>
153
+ ) : (
154
+ <form onSubmit={handleSubmit}>
155
+ <div className="grid gap-4 py-1">
156
+ <div className="grid gap-2">
157
+ <Label>标题 *</Label>
158
+ <Input
159
+ value={form.title}
160
+ onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}
161
+ placeholder="网站名称"
162
+ />
163
+ </div>
164
+ <div className="grid gap-2">
165
+ <Label>网址 *</Label>
166
+ <Input
167
+ type="url"
168
+ value={form.url}
169
+ onChange={(e) => setForm((f) => ({ ...f, url: e.target.value }))}
170
+ placeholder="https://"
171
+ />
172
+ <p className="text-xs text-muted-foreground">
173
+ GitHub 仓库地址将自动识别并启用同步。
174
+ </p>
175
+ </div>
176
+ <div className="grid gap-2">
177
+ <Label>分类 ID</Label>
178
+ <Input
179
+ value={form.categoryId}
180
+ onChange={(e) => setForm((f) => ({ ...f, categoryId: e.target.value }))}
181
+ placeholder="可选,留空为未分类"
182
+ />
183
+ </div>
184
+ <div className="grid gap-2">
185
+ <Label>摘要</Label>
186
+ <Textarea
187
+ value={form.summary}
188
+ onChange={(e) => setForm((f) => ({ ...f, summary: e.target.value }))}
189
+ rows={2}
190
+ placeholder="一句话简介"
191
+ />
192
+ </div>
193
+ <div className="grid gap-2">
194
+ <Label>标签(逗号分隔)</Label>
195
+ <Input
196
+ value={form.tags}
197
+ onChange={(e) => setForm((f) => ({ ...f, tags: e.target.value }))}
198
+ placeholder="如:工具, AI"
199
+ />
200
+ </div>
201
+ </div>
202
+ <DialogFooter>
203
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
204
+ 取消
205
+ </Button>
206
+ <Button type="submit" disabled={saving}>
207
+ {saving ? '保存中...' : '保存'}
208
+ </Button>
209
+ </DialogFooter>
210
+ </form>
211
+ )}
212
+ </DialogContent>
213
+ </Dialog>
214
+ )
215
+ }