@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.
- package/package.json +86 -0
- package/src/client.tsx +31 -0
- package/src/components/admin/admin-user-menu.tsx +90 -0
- package/src/components/admin/developer-log.tsx +356 -0
- package/src/components/ai-provider/model-list-editor.tsx +147 -0
- package/src/components/ai-provider/provider-form.tsx +434 -0
- package/src/components/ai-provider/provider-icon.tsx +43 -0
- package/src/components/ai-provider/provider-logo.tsx +87 -0
- package/src/components/ai-provider/provider-table.tsx +258 -0
- package/src/components/auth/admin-login-form.tsx +214 -0
- package/src/components/auth/auth-brand-panel.tsx +60 -0
- package/src/components/auth/auth-header.tsx +38 -0
- package/src/components/auth/auth-layout.tsx +66 -0
- package/src/components/auth/auth-providers.tsx +34 -0
- package/src/components/auth/icon-input.tsx +46 -0
- package/src/components/auth/oauth-section.tsx +51 -0
- package/src/components/auth/password-input.tsx +67 -0
- package/src/components/common/page-header.tsx +42 -0
- package/src/components/common/placeholder-page.tsx +42 -0
- package/src/components/component/component-form-dialog.tsx +348 -0
- package/src/components/component/component-import-dialog.tsx +235 -0
- package/src/components/component/component-table.tsx +153 -0
- package/src/components/data-table/data-table-pagination.tsx +114 -0
- package/src/components/data-table/data-table.tsx +218 -0
- package/src/components/data-table/table-detail-layout.tsx +99 -0
- package/src/components/dependency/changelog-dialog.tsx +108 -0
- package/src/components/dependency/columns.tsx +232 -0
- package/src/components/dependency/dependency-table.tsx +368 -0
- package/src/components/dependency/edit-dialog.tsx +200 -0
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
- package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
- package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
- package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
- package/src/components/layout/sidebar-02/index.tsx +33 -0
- package/src/components/layout/sidebar-02/logo.tsx +40 -0
- package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
- package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
- package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
- package/src/components/magicui/animated-theme-toggler.tsx +272 -0
- package/src/components/magicui/rainbow-button.tsx +78 -0
- package/src/components/market/columns.tsx +151 -0
- package/src/components/market/market-app-form.tsx +237 -0
- package/src/components/market/market-app-list.tsx +156 -0
- package/src/components/monitoring/error-list.tsx +143 -0
- package/src/components/monitoring/health-cards.tsx +152 -0
- package/src/components/monitoring/metrics-stats.tsx +184 -0
- package/src/components/monitoring/request-log-table.tsx +179 -0
- package/src/components/providers/theme-toggle.tsx +59 -0
- package/src/components/settings/avatar/avatar-config.ts +46 -0
- package/src/components/ui/alert-dialog.tsx +216 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/avatar.tsx +129 -0
- package/src/components/ui/badge.tsx +66 -0
- package/src/components/ui/button.tsx +84 -0
- package/src/components/ui/card.tsx +120 -0
- package/src/components/ui/checkbox.tsx +50 -0
- package/src/components/ui/collapsible.tsx +50 -0
- package/src/components/ui/dialog.tsx +185 -0
- package/src/components/ui/dropdown-menu.tsx +286 -0
- package/src/components/ui/field.tsx +255 -0
- package/src/components/ui/input-group.tsx +171 -0
- package/src/components/ui/input.tsx +36 -0
- package/src/components/ui/label.tsx +41 -0
- package/src/components/ui/pagination.tsx +146 -0
- package/src/components/ui/select.tsx +209 -0
- package/src/components/ui/separator.tsx +45 -0
- package/src/components/ui/sheet.tsx +164 -0
- package/src/components/ui/sidebar.tsx +719 -0
- package/src/components/ui/skeleton.tsx +30 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +48 -0
- package/src/components/ui/table.tsx +133 -0
- package/src/components/ui/textarea.tsx +35 -0
- package/src/components/ui/tooltip.tsx +74 -0
- package/src/components/user/columns.tsx +153 -0
- package/src/components/user/settings-activity-log.tsx +358 -0
- package/src/components/user/settings-profile/constants.ts +42 -0
- package/src/components/user/settings-profile/types.ts +47 -0
- package/src/components/user/settings-profile.tsx +664 -0
- package/src/components/user/user-create-dialog.tsx +137 -0
- package/src/components/user/user-filters.tsx +117 -0
- package/src/hooks/use-mobile.ts +36 -0
- package/src/instrument.client.ts +34 -0
- package/src/lib/anyhub/capabilities.ts +59 -0
- package/src/lib/anyhub/types.ts +43 -0
- package/src/lib/auth-client.ts +83 -0
- package/src/lib/auth-utils.ts +24 -0
- package/src/lib/auth.config.ts +264 -0
- package/src/lib/avatar.ts +41 -0
- package/src/lib/i18n/client.ts +32 -0
- package/src/lib/i18n/config.ts +40 -0
- package/src/lib/i18n/detect.ts +43 -0
- package/src/lib/i18n/locales/en/admin-ai.json +128 -0
- package/src/lib/i18n/locales/en/admin-billing.json +56 -0
- package/src/lib/i18n/locales/en/admin-users.json +88 -0
- package/src/lib/i18n/locales/en/common.json +877 -0
- package/src/lib/i18n/locales/en/enterprise.json +208 -0
- package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
- package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
- package/src/lib/i18n/locales/zh/admin-users.json +88 -0
- package/src/lib/i18n/locales/zh/common.json +877 -0
- package/src/lib/i18n/locales/zh/enterprise.json +208 -0
- package/src/lib/i18n/server.ts +18 -0
- package/src/lib/i18n/types.ts +5 -0
- package/src/lib/monitoring-benchmarks.ts +85 -0
- package/src/lib/monitoring-descriptions.ts +78 -0
- package/src/lib/prisma.ts +18 -0
- package/src/lib/router-context.ts +38 -0
- package/src/lib/session.ts +38 -0
- package/src/lib/utils.ts +23 -0
- package/src/providers/index.tsx +45 -0
- package/src/routeTree.gen.ts +1199 -0
- package/src/router.tsx +51 -0
- package/src/routes/__root.tsx +170 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
- package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
- package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
- package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
- package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
- package/src/routes/_nav/ai-providers/route.tsx +31 -0
- package/src/routes/_nav/apps/route.lazy.tsx +30 -0
- package/src/routes/_nav/apps/route.tsx +20 -0
- package/src/routes/_nav/audit/route.lazy.tsx +100 -0
- package/src/routes/_nav/audit/route.tsx +25 -0
- package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
- package/src/routes/_nav/billing/invoices/route.tsx +25 -0
- package/src/routes/_nav/billing/route.lazy.tsx +48 -0
- package/src/routes/_nav/billing/route.tsx +27 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
- package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
- package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
- package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
- package/src/routes/_nav/compliance/route.tsx +25 -0
- package/src/routes/_nav/components/route.lazy.tsx +184 -0
- package/src/routes/_nav/components/route.tsx +40 -0
- package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
- package/src/routes/_nav/dashboard/route.tsx +20 -0
- package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
- package/src/routes/_nav/dependencies/route.tsx +32 -0
- package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
- package/src/routes/_nav/developer-logs/route.tsx +14 -0
- package/src/routes/_nav/docs/route.lazy.tsx +30 -0
- package/src/routes/_nav/docs/route.tsx +20 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
- package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
- package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
- package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
- package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
- package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
- package/src/routes/_nav/licenses/route.tsx +25 -0
- package/src/routes/_nav/market/route.lazy.tsx +31 -0
- package/src/routes/_nav/market/route.tsx +20 -0
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
- package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
- package/src/routes/_nav/monitoring/route.tsx +20 -0
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
- package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
- package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
- package/src/routes/_nav/permissions/route.tsx +26 -0
- package/src/routes/_nav/route.lazy.tsx +31 -0
- package/src/routes/_nav/route.tsx +34 -0
- package/src/routes/_nav/settings/route.lazy.tsx +30 -0
- package/src/routes/_nav/settings/route.tsx +20 -0
- package/src/routes/_nav/sso/route.lazy.tsx +145 -0
- package/src/routes/_nav/sso/route.tsx +25 -0
- package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
- package/src/routes/_nav/users/$userId/route.tsx +27 -0
- package/src/routes/_nav/users/-components/users-table.tsx +38 -0
- package/src/routes/_nav/users/route.lazy.tsx +74 -0
- package/src/routes/_nav/users/route.tsx +43 -0
- package/src/routes/api/compliance-download/$jobId.ts +35 -0
- package/src/routes/api/observability/errors.ts +41 -0
- package/src/routes/api/observability/health.ts +30 -0
- package/src/routes/api.auth.$.ts +31 -0
- package/src/routes/api.sentry-test.ts +33 -0
- package/src/routes/index/route.lazy.tsx +22 -0
- package/src/routes/index/route.tsx +24 -0
- package/src/routes/login/route.lazy.tsx +46 -0
- package/src/routes/login/route.tsx +28 -0
- package/src/routes/sentry-tunnel.ts +61 -0
- package/src/routes/setup/route.lazy.tsx +94 -0
- package/src/routes/setup/route.tsx +28 -0
- package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
- package/src/server/__tests__/licenses-issue.test.ts +98 -0
- package/src/server/admin-ai-alerts.ts +177 -0
- package/src/server/admin-ai-usage.ts +134 -0
- package/src/server/auth-helpers.ts +51 -0
- package/src/server/entitlements.ts +24 -0
- package/src/server/knowledge-public/canonical-id.ts +55 -0
- package/src/server/knowledge-public/github-sync.ts +86 -0
- package/src/server/license-issue.ts +78 -0
- package/src/server/middlewares/request.ts +26 -0
- package/src/server/observability/debug-middleware.ts +39 -0
- package/src/server/observability/error-reporter.ts +99 -0
- package/src/server/observability/health-check.ts +140 -0
- package/src/server/observability/index.ts +33 -0
- package/src/server/observability/logger.ts +49 -0
- package/src/server/observability/metrics.ts +105 -0
- package/src/server/observability/request-middleware.ts +125 -0
- package/src/server/observability/timing.ts +42 -0
- package/src/server/pro-services.ts +32 -0
- package/src/server/serverFns/admin-billing.ts +94 -0
- package/src/server/serverFns/ai-provider/create.ts +92 -0
- package/src/server/serverFns/ai-provider/delete.ts +42 -0
- package/src/server/serverFns/ai-provider/get.ts +35 -0
- package/src/server/serverFns/ai-provider/list.ts +92 -0
- package/src/server/serverFns/ai-provider/update.ts +73 -0
- package/src/server/serverFns/anyhub/market.ts +64 -0
- package/src/server/serverFns/audit.ts +57 -0
- package/src/server/serverFns/auth/session.ts +26 -0
- package/src/server/serverFns/auth/setup.ts +73 -0
- package/src/server/serverFns/compliance.ts +85 -0
- package/src/server/serverFns/components/import.ts +197 -0
- package/src/server/serverFns/components/list.ts +51 -0
- package/src/server/serverFns/components/mutations.ts +84 -0
- package/src/server/serverFns/dependency/helpers.ts +245 -0
- package/src/server/serverFns/dependency/list.ts +127 -0
- package/src/server/serverFns/dependency/sync.ts +123 -0
- package/src/server/serverFns/dependency/update.ts +51 -0
- package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
- package/src/server/serverFns/developer-logs.ts +132 -0
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
- package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
- package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
- package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
- package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
- package/src/server/serverFns/licenses.ts +92 -0
- package/src/server/serverFns/locale.ts +10 -0
- package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
- package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
- package/src/server/serverFns/monitoring/get-errors.ts +43 -0
- package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
- package/src/server/serverFns/monitoring/get-health.ts +55 -0
- package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
- package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
- package/src/server/serverFns/monitoring/report-error.ts +55 -0
- package/src/server/serverFns/monitoring/reset.ts +70 -0
- package/src/server/serverFns/permissions.ts +214 -0
- package/src/server/serverFns/sentry/config.ts +88 -0
- package/src/server/serverFns/sso-config.ts +83 -0
- package/src/server/serverFns/user/create.ts +40 -0
- package/src/server/serverFns/user/delete.ts +29 -0
- package/src/server/serverFns/user/detail.ts +45 -0
- package/src/server/serverFns/user/list.ts +56 -0
- package/src/server/serverFns/user/reset-password.ts +28 -0
- package/src/server/serverFns/user/update-role.ts +26 -0
- package/src/server/serverFns/user/update-status.ts +26 -0
- package/src/server.ts +27 -0
- package/src/start.ts +85 -0
- package/src/styles/_keyframe-animations.scss +91 -0
- package/src/styles/_variables.scss +296 -0
- package/src/styles/globals.css +200 -0
- 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
|
+
}
|