@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,87 @@
|
|
|
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
|
+
// apps/admin/src/components/ai-provider/provider-logo.tsx
|
|
19
|
+
import { useEffect, useState } from "react"
|
|
20
|
+
import type { AiProviderMeta } from "@allbluecn/kernel"
|
|
21
|
+
import { LOBE_ICON_MAP } from "@allbluecn/kernel"
|
|
22
|
+
|
|
23
|
+
type LobeIconComponent = React.ComponentType<{ size?: number; className?: string }>
|
|
24
|
+
|
|
25
|
+
async function loadLobeIcon(lobeName: string): Promise<LobeIconComponent | null> {
|
|
26
|
+
try {
|
|
27
|
+
const mod = await import("@lobehub/icons")
|
|
28
|
+
return (mod as unknown as Record<string, LobeIconComponent>)[lobeName] ?? null
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function InitialFallback({
|
|
35
|
+
brandName,
|
|
36
|
+
size,
|
|
37
|
+
}: {
|
|
38
|
+
brandName: string
|
|
39
|
+
size: number
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
style={{
|
|
44
|
+
width: size,
|
|
45
|
+
height: size,
|
|
46
|
+
display: "inline-flex",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
fontSize: size * 0.5,
|
|
50
|
+
fontWeight: 700,
|
|
51
|
+
color: "currentColor",
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{brandName.charAt(0).toUpperCase()}
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ProviderLogo({
|
|
60
|
+
provider,
|
|
61
|
+
size = 24,
|
|
62
|
+
}: {
|
|
63
|
+
provider: Pick<AiProviderMeta, "providerId" | "brandName">
|
|
64
|
+
size?: number
|
|
65
|
+
}) {
|
|
66
|
+
const [IconComponent, setIconComponent] = useState<LobeIconComponent | null>(null)
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const lobeName = LOBE_ICON_MAP[provider.providerId]
|
|
70
|
+
if (!lobeName) {
|
|
71
|
+
setIconComponent(null)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
let cancelled = false
|
|
75
|
+
loadLobeIcon(lobeName).then((icon) => {
|
|
76
|
+
if (!cancelled) setIconComponent(icon)
|
|
77
|
+
})
|
|
78
|
+
return () => { cancelled = true }
|
|
79
|
+
}, [provider.providerId])
|
|
80
|
+
|
|
81
|
+
// admin 使用单色版本(默认导出即 mono)
|
|
82
|
+
if (IconComponent) {
|
|
83
|
+
return <IconComponent size={size} />
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return <InitialFallback brandName={provider.brandName} size={size} />
|
|
87
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
// apps/admin/src/components/ai-provider/provider-table.tsx
|
|
19
|
+
import { useState } from "react"
|
|
20
|
+
import { useTranslation } from 'react-i18next'
|
|
21
|
+
import { useMutation } from "@tanstack/react-query"
|
|
22
|
+
import { Link, useRouter } from "@tanstack/react-router"
|
|
23
|
+
import { Edit, Plus, Trash2, Users } from "lucide-react"
|
|
24
|
+
import { toast } from '@/components/ui/sonner'
|
|
25
|
+
import { ADAPTER_KINDS, PROVIDER_BADGE_STYLES, type AiProviderRow } from "@allbluecn/kernel"
|
|
26
|
+
import { Button } from "@/components/ui/button"
|
|
27
|
+
import { Switch } from "@/components/ui/switch"
|
|
28
|
+
import { Badge } from "@/components/ui/badge"
|
|
29
|
+
import {
|
|
30
|
+
AlertDialog,
|
|
31
|
+
AlertDialogAction,
|
|
32
|
+
AlertDialogCancel,
|
|
33
|
+
AlertDialogContent,
|
|
34
|
+
AlertDialogDescription,
|
|
35
|
+
AlertDialogFooter,
|
|
36
|
+
AlertDialogHeader,
|
|
37
|
+
AlertDialogTitle,
|
|
38
|
+
} from "@/components/ui/alert-dialog"
|
|
39
|
+
import { DataTable, type AdminColumnDef } from "@/components/data-table/data-table"
|
|
40
|
+
import { ProviderIcon } from "./provider-icon"
|
|
41
|
+
import { updateAiProviderAdminFn } from "@/server/serverFns/ai-provider/update"
|
|
42
|
+
import { deleteAiProviderAdminFn } from "@/server/serverFns/ai-provider/delete"
|
|
43
|
+
|
|
44
|
+
const ADAPTER_LABEL_MAP = new Map(ADAPTER_KINDS.map((k) => [k.value, k.label]))
|
|
45
|
+
|
|
46
|
+
export function ProviderTable({ data }: { data: AiProviderRow[] }) {
|
|
47
|
+
const { t } = useTranslation('admin-ai')
|
|
48
|
+
const router = useRouter()
|
|
49
|
+
const [pendingDelete, setPendingDelete] = useState<AiProviderRow | null>(null)
|
|
50
|
+
|
|
51
|
+
const toggleMutation = useMutation({
|
|
52
|
+
mutationFn: (vars: { id: string; enabled: boolean }) =>
|
|
53
|
+
updateAiProviderAdminFn({
|
|
54
|
+
data: { id: vars.id, enabled: vars.enabled },
|
|
55
|
+
}),
|
|
56
|
+
onSuccess: async () => {
|
|
57
|
+
toast.success(t('table.toast.updated'))
|
|
58
|
+
await router.invalidate()
|
|
59
|
+
},
|
|
60
|
+
onError: (err) =>
|
|
61
|
+
toast.error(err instanceof Error ? err.message : t('market.form.operationFailed')),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const deleteMutation = useMutation({
|
|
65
|
+
mutationFn: (id: string) => deleteAiProviderAdminFn({ data: { id } }),
|
|
66
|
+
onSuccess: async () => {
|
|
67
|
+
toast.success(t('table.toast.deleted'))
|
|
68
|
+
setPendingDelete(null)
|
|
69
|
+
await router.invalidate()
|
|
70
|
+
},
|
|
71
|
+
onError: (err) =>
|
|
72
|
+
toast.error(err instanceof Error ? err.message : t('table.toast.deleteFailed')),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const columns: AdminColumnDef<AiProviderRow>[] = [
|
|
76
|
+
{
|
|
77
|
+
accessorKey: "displayName",
|
|
78
|
+
header: t('table.columns.provider'),
|
|
79
|
+
cell: ({ row }) => (
|
|
80
|
+
<div className="flex items-center gap-3">
|
|
81
|
+
<ProviderIcon provider={row.original} size={36} />
|
|
82
|
+
<div>
|
|
83
|
+
<div className="font-medium">{row.original.displayName}</div>
|
|
84
|
+
<div className="font-mono text-xs text-muted-foreground">
|
|
85
|
+
{row.original.providerId}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
accessorKey: "adapterKind",
|
|
93
|
+
header: t('table.columns.adapterKind'),
|
|
94
|
+
cell: ({ row }) => (
|
|
95
|
+
<Badge variant="outline">
|
|
96
|
+
{ADAPTER_LABEL_MAP.get(row.original.adapterKind) ??
|
|
97
|
+
row.original.adapterKind}
|
|
98
|
+
</Badge>
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
accessorKey: "category",
|
|
103
|
+
header: t('table.columns.category'),
|
|
104
|
+
cell: ({ row }) => (
|
|
105
|
+
<Badge variant="secondary">{row.original.category}</Badge>
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
accessorKey: "badge",
|
|
110
|
+
header: t('table.columns.badge'),
|
|
111
|
+
cell: ({ row }) => {
|
|
112
|
+
const b = row.original.badge
|
|
113
|
+
if (!b) return <span className="text-muted-foreground">—</span>
|
|
114
|
+
const style = PROVIDER_BADGE_STYLES[b] ?? "bg-muted text-muted-foreground"
|
|
115
|
+
return (
|
|
116
|
+
<span className={`inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium ${style}`}>
|
|
117
|
+
{b}
|
|
118
|
+
</span>
|
|
119
|
+
)
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
accessorKey: "models",
|
|
124
|
+
header: t('table.columns.modelCount'),
|
|
125
|
+
cell: ({ row }) => (
|
|
126
|
+
<span className="text-sm">
|
|
127
|
+
{Array.isArray(row.original.models) ? row.original.models.length : 0}
|
|
128
|
+
</span>
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
accessorKey: "configCount",
|
|
133
|
+
header: () => (
|
|
134
|
+
<span className="inline-flex items-center gap-1">
|
|
135
|
+
<Users className="size-3.5" />
|
|
136
|
+
{t('table.columns.userConfigCount')}
|
|
137
|
+
</span>
|
|
138
|
+
),
|
|
139
|
+
cell: ({ row }) => (
|
|
140
|
+
<span
|
|
141
|
+
className={
|
|
142
|
+
row.original.configCount > 0
|
|
143
|
+
? "font-medium"
|
|
144
|
+
: "text-muted-foreground"
|
|
145
|
+
}
|
|
146
|
+
>
|
|
147
|
+
{row.original.configCount}
|
|
148
|
+
</span>
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
accessorKey: "enabled",
|
|
153
|
+
header: t('table.columns.platformEnabled'),
|
|
154
|
+
cell: ({ row }) => (
|
|
155
|
+
<Switch
|
|
156
|
+
checked={row.original.enabled}
|
|
157
|
+
onCheckedChange={(checked) =>
|
|
158
|
+
toggleMutation.mutate({
|
|
159
|
+
id: row.original.id,
|
|
160
|
+
enabled: checked,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
disabled={toggleMutation.isPending}
|
|
164
|
+
aria-label={t('table.enabledToggle')}
|
|
165
|
+
/>
|
|
166
|
+
),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "actions",
|
|
170
|
+
header: t('table.columns.actions'),
|
|
171
|
+
cell: ({ row }) => {
|
|
172
|
+
const canDelete = row.original.configCount === 0
|
|
173
|
+
return (
|
|
174
|
+
<div className="flex items-center gap-1">
|
|
175
|
+
<Button asChild variant="ghost" size="sm">
|
|
176
|
+
<Link
|
|
177
|
+
to="/ai-providers/$id/edit"
|
|
178
|
+
params={{ id: row.original.id }}
|
|
179
|
+
>
|
|
180
|
+
<Edit className="size-4" />
|
|
181
|
+
{t('table.edit')}
|
|
182
|
+
</Link>
|
|
183
|
+
</Button>
|
|
184
|
+
{canDelete ? (
|
|
185
|
+
<Button
|
|
186
|
+
variant="ghost"
|
|
187
|
+
size="sm"
|
|
188
|
+
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
189
|
+
onClick={() => setPendingDelete(row.original)}
|
|
190
|
+
>
|
|
191
|
+
<Trash2 className="size-4" />
|
|
192
|
+
{t('table.delete')}
|
|
193
|
+
</Button>
|
|
194
|
+
) : (
|
|
195
|
+
<Button
|
|
196
|
+
variant="ghost"
|
|
197
|
+
size="sm"
|
|
198
|
+
disabled
|
|
199
|
+
title={t('table.deleteDisabled', { count: row.original.configCount })}
|
|
200
|
+
>
|
|
201
|
+
<Trash2 className="size-4" />
|
|
202
|
+
{t('table.delete')}
|
|
203
|
+
</Button>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div className="space-y-4">
|
|
213
|
+
<div className="flex items-center justify-between">
|
|
214
|
+
<div>
|
|
215
|
+
<h2 className="text-lg font-semibold">{t('table.pageTitle')}</h2>
|
|
216
|
+
<p className="text-sm text-muted-foreground">
|
|
217
|
+
{t('table.pageDescription', { count: data.length })}
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
<Button asChild>
|
|
221
|
+
<Link to="/ai-providers/new">
|
|
222
|
+
<Plus className="size-4" />
|
|
223
|
+
{t('table.addProvider')}
|
|
224
|
+
</Link>
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
<DataTable columns={columns} data={data} />
|
|
228
|
+
|
|
229
|
+
<AlertDialog
|
|
230
|
+
open={!!pendingDelete}
|
|
231
|
+
onOpenChange={(open) => !open && setPendingDelete(null)}
|
|
232
|
+
>
|
|
233
|
+
<AlertDialogContent>
|
|
234
|
+
<AlertDialogHeader>
|
|
235
|
+
<AlertDialogTitle>
|
|
236
|
+
{t('table.deleteConfirm.title', { name: pendingDelete?.displayName })}
|
|
237
|
+
</AlertDialogTitle>
|
|
238
|
+
<AlertDialogDescription>
|
|
239
|
+
{t('table.deleteConfirm.description')}
|
|
240
|
+
</AlertDialogDescription>
|
|
241
|
+
</AlertDialogHeader>
|
|
242
|
+
<AlertDialogFooter>
|
|
243
|
+
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
|
244
|
+
<AlertDialogAction
|
|
245
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
246
|
+
onClick={() =>
|
|
247
|
+
pendingDelete && deleteMutation.mutate(pendingDelete.id)
|
|
248
|
+
}
|
|
249
|
+
disabled={deleteMutation.isPending}
|
|
250
|
+
>
|
|
251
|
+
{t('table.deleteConfirm.confirm')}
|
|
252
|
+
</AlertDialogAction>
|
|
253
|
+
</AlertDialogFooter>
|
|
254
|
+
</AlertDialogContent>
|
|
255
|
+
</AlertDialog>
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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 { LogOut, User, Mail, Users, BarChart3, Shield, Settings, ArrowRight, ArrowLeft, Target } from "lucide-react";
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { useTranslation } from 'react-i18next'
|
|
21
|
+
import { signIn, signOut } from "@/lib/auth-client";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
24
|
+
import { Field, FieldLabel } from "@/components/ui/field";
|
|
25
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
26
|
+
import { toast } from '@/components/ui/sonner';
|
|
27
|
+
import { UserAvatar } from "@allbluecn/web-app/user-avatar";
|
|
28
|
+
import type { AvatarConfig, ShapeType } from "@allbluecn/kernel";
|
|
29
|
+
import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from "@/components/auth/auth-layout";
|
|
30
|
+
import { AuthHeader } from "@/components/auth/auth-header";
|
|
31
|
+
import { AuthBrandPanel } from "@/components/auth/auth-brand-panel";
|
|
32
|
+
import { IconInput } from "@/components/auth/icon-input";
|
|
33
|
+
import { PasswordInput } from "@/components/auth/password-input";
|
|
34
|
+
import { RainbowButton } from "@/components/magicui/rainbow-button";
|
|
35
|
+
|
|
36
|
+
interface AdminLoginFormProps {
|
|
37
|
+
user?: {
|
|
38
|
+
name?: string | null;
|
|
39
|
+
email?: string | null;
|
|
40
|
+
avatarConfig?: AvatarConfig;
|
|
41
|
+
bgShape?: string;
|
|
42
|
+
bgColor?: string;
|
|
43
|
+
} | null;
|
|
44
|
+
isAdmin: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const brandPanel = (
|
|
48
|
+
<AuthBrandPanel title="ProtoX 管理后台" subtitle="高效管理您的产品工作台" features={[
|
|
49
|
+
{ icon: Users, label: "团队管理", desc: "成员与角色" },
|
|
50
|
+
{ icon: BarChart3, label: "数据看板", desc: "系统概览" },
|
|
51
|
+
{ icon: Shield, label: "权限控制", desc: "安全策略" },
|
|
52
|
+
{ icon: Settings, label: "系统设置", desc: "配置管理" },
|
|
53
|
+
]} />
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
function maskEmail(email: string): string {
|
|
57
|
+
const [local, domain] = email.split("@");
|
|
58
|
+
if (!domain) return email;
|
|
59
|
+
const prefix = local.length > 3 ? local.slice(0, 3) : local;
|
|
60
|
+
return `${prefix}***@${domain}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default function AdminLoginForm({ user, isAdmin }: AdminLoginFormProps) {
|
|
64
|
+
const { t } = useTranslation('common')
|
|
65
|
+
const [password, setPassword] = useState("");
|
|
66
|
+
const [email, setEmail] = useState("");
|
|
67
|
+
const [stayLoggedIn, setStayLoggedIn] = useState(false);
|
|
68
|
+
const [loading, setLoading] = useState(false);
|
|
69
|
+
|
|
70
|
+
async function handleLogin(e: React.FormEvent) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
|
|
73
|
+
if (!isAdmin && !email) {
|
|
74
|
+
toast.error(t('login.error.enterEmail'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!isAdmin && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) {
|
|
78
|
+
toast.error(t('login.error.validEmail'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!password) {
|
|
82
|
+
toast.error(t('login.error.enterPassword'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setLoading(true);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// signIn 内部用 form.submit() 触发浏览器原生 POST + 302 全页刷新,
|
|
90
|
+
// 浏览器自动处理 Set-Cookie 和重定向到 callbackUrl。
|
|
91
|
+
// 禁止在 signIn 后调用 router.navigate——router context 中 session 仍是旧值(空),
|
|
92
|
+
// _nav beforeLoad 会因 session 校验失败重定向回 /login(反模式,见 memrec 885bdd10)。
|
|
93
|
+
await signIn("credentials", {
|
|
94
|
+
redirectTo: "/dashboard",
|
|
95
|
+
email: isAdmin ? (user?.email || email) : email,
|
|
96
|
+
password,
|
|
97
|
+
remember: stayLoggedIn,
|
|
98
|
+
});
|
|
99
|
+
// form.submit() 已触发导航,以下代码在页面卸载前执行
|
|
100
|
+
// 若 csrf fetch 失败会抛错进入 catch
|
|
101
|
+
} catch {
|
|
102
|
+
toast.error(t('login.error.invalidCredentials'));
|
|
103
|
+
setLoading(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleLogout() {
|
|
108
|
+
await signOut();
|
|
109
|
+
window.location.reload();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleSwitchAccount() {
|
|
113
|
+
await signOut();
|
|
114
|
+
window.location.reload();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isAdmin && user?.email) {
|
|
118
|
+
return (
|
|
119
|
+
<AuthLayout>
|
|
120
|
+
<AuthLayoutLeft>
|
|
121
|
+
<div className="space-y-4 text-center">
|
|
122
|
+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-lg bg-primary">
|
|
123
|
+
<Target className="h-10 w-10 text-primary-foreground" />
|
|
124
|
+
</div>
|
|
125
|
+
<h1 className="text-3xl font-semibold">{t('login.adminPanel')}</h1>
|
|
126
|
+
<Card className="h-16 w-full bg-muted/50 border">
|
|
127
|
+
<CardContent className="flex h-full items-center gap-3 px-3 py-3">
|
|
128
|
+
<UserAvatar
|
|
129
|
+
avatarConfig={user?.avatarConfig}
|
|
130
|
+
bgShape={user?.bgShape as ShapeType | undefined}
|
|
131
|
+
bgColor={user?.bgColor}
|
|
132
|
+
name={user?.name || user?.email || ""}
|
|
133
|
+
size="lg"
|
|
134
|
+
/>
|
|
135
|
+
<div className="min-w-0 text-left">
|
|
136
|
+
<p className="truncate text-base font-medium">{user?.name || user?.email || t('login.admin')}</p>
|
|
137
|
+
<p className="truncate text-xm text-muted-foreground">{maskEmail(user?.email || "")}</p>
|
|
138
|
+
</div>
|
|
139
|
+
</CardContent>
|
|
140
|
+
</Card>
|
|
141
|
+
</div>
|
|
142
|
+
<form onSubmit={handleLogin} className="space-y-6">
|
|
143
|
+
<PasswordInput label={t('login.password.label')} placeholder={t('login.password.placeholder')} value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
144
|
+
<Field orientation="horizontal" className="gap-2">
|
|
145
|
+
<FieldLabel>
|
|
146
|
+
<Checkbox checked={stayLoggedIn} onCheckedChange={(v) => setStayLoggedIn(v === true)} />
|
|
147
|
+
<span className="text-sm">{t('login.rememberMe')}</span>
|
|
148
|
+
</FieldLabel>
|
|
149
|
+
</Field>
|
|
150
|
+
<RainbowButton type="submit" className="w-full" disabled={loading}>
|
|
151
|
+
{loading ? t('login.loading') : t('login.loginAdmin')}
|
|
152
|
+
<ArrowRight className="ml-2 h-4 w-4" />
|
|
153
|
+
</RainbowButton>
|
|
154
|
+
<div className="flex gap-2">
|
|
155
|
+
<Button type="button" variant="outline" className="flex-1" onClick={handleSwitchAccount}>
|
|
156
|
+
<Users className="w-4 h-4 mr-2" />{t('login.switchAccount')}
|
|
157
|
+
</Button>
|
|
158
|
+
<Button type="button" variant="outline" className="flex-1" onClick={handleLogout}>
|
|
159
|
+
<LogOut className="w-4 h-4 mr-2" />{t('login.logout')}
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</form>
|
|
163
|
+
</AuthLayoutLeft>
|
|
164
|
+
<AuthLayoutRight>{brandPanel}</AuthLayoutRight>
|
|
165
|
+
</AuthLayout>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (user && !isAdmin) {
|
|
170
|
+
return (
|
|
171
|
+
<AuthLayout>
|
|
172
|
+
<AuthLayoutLeft>
|
|
173
|
+
<div className="space-y-4 text-center">
|
|
174
|
+
<AuthHeader title={t('login.noPermission')} titleClassName="text-destructive" />
|
|
175
|
+
<p className="text-muted-foreground">
|
|
176
|
+
{t('login.notAdmin', { name: user.name || user.email })}
|
|
177
|
+
</p>
|
|
178
|
+
<Button className="w-full" onClick={() => window.open(`${import.meta.env.VITE_WEB_URL ?? '/'}dashboard`, "_self")}>
|
|
179
|
+
<ArrowLeft className="h-4 w-4" />
|
|
180
|
+
{t('login.backToWorkspace')}
|
|
181
|
+
</Button>
|
|
182
|
+
<Button variant="outline" className="w-full" onClick={handleSwitchAccount}>
|
|
183
|
+
{t('login.switchAndLogin')}
|
|
184
|
+
</Button>
|
|
185
|
+
</div>
|
|
186
|
+
</AuthLayoutLeft>
|
|
187
|
+
<AuthLayoutRight>{brandPanel}</AuthLayoutRight>
|
|
188
|
+
</AuthLayout>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<AuthLayout>
|
|
194
|
+
<AuthLayoutLeft>
|
|
195
|
+
<AuthHeader title={t('login.adminPanel')} subtitle={t('login.subtitle')} />
|
|
196
|
+
<form onSubmit={handleLogin} className="space-y-6">
|
|
197
|
+
<IconInput icon={Mail} label={t('login.email.label')} placeholder={t('login.email.placeholder')} value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
198
|
+
<PasswordInput label={t('login.password.label')} placeholder={t('login.password.placeholder')} value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
199
|
+
<Field orientation="horizontal" className="gap-2">
|
|
200
|
+
<FieldLabel>
|
|
201
|
+
<Checkbox checked={stayLoggedIn} onCheckedChange={(v) => setStayLoggedIn(v === true)} />
|
|
202
|
+
<span className="text-sm">{t('login.rememberMe')}</span>
|
|
203
|
+
</FieldLabel>
|
|
204
|
+
</Field>
|
|
205
|
+
<RainbowButton type="submit" className="w-full" disabled={loading}>
|
|
206
|
+
{loading ? t('login.loggingIn') : t('login.loginAdmin')}
|
|
207
|
+
<ArrowRight className="ml-2 h-4 w-4" />
|
|
208
|
+
</RainbowButton>
|
|
209
|
+
</form>
|
|
210
|
+
</AuthLayoutLeft>
|
|
211
|
+
<AuthLayoutRight>{brandPanel}</AuthLayoutRight>
|
|
212
|
+
</AuthLayout>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 type { LucideIcon } from 'lucide-react'
|
|
19
|
+
import type { ReactNode } from 'react'
|
|
20
|
+
|
|
21
|
+
interface BrandFeature {
|
|
22
|
+
icon: LucideIcon
|
|
23
|
+
label: string
|
|
24
|
+
desc: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type { BrandFeature }
|
|
28
|
+
|
|
29
|
+
interface AuthBrandPanelProps {
|
|
30
|
+
title: string
|
|
31
|
+
subtitle?: string
|
|
32
|
+
features?: BrandFeature[]
|
|
33
|
+
footer?: ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AuthBrandPanel({ title, subtitle, features, footer }: AuthBrandPanelProps) {
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<h1 className="text-4xl font-bold tracking-tight">{title}</h1>
|
|
40
|
+
{subtitle && (
|
|
41
|
+
<p className="text-lg text-muted-foreground">{subtitle}</p>
|
|
42
|
+
)}
|
|
43
|
+
{features && features.length > 0 && (
|
|
44
|
+
<div className="grid grid-cols-2 gap-4 pt-8">
|
|
45
|
+
{features.map((feature) => {
|
|
46
|
+
const Icon = feature.icon
|
|
47
|
+
return (
|
|
48
|
+
<div key={feature.label} className="text-center p-4 rounded-lg bg-background/50">
|
|
49
|
+
<Icon className="h-8 w-8 mx-auto mb-2 text-primary" />
|
|
50
|
+
<p className="font-semibold">{feature.label}</p>
|
|
51
|
+
<p className="text-xs text-muted-foreground">{feature.desc}</p>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
})}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
{footer && <div className="pt-4">{footer}</div>}
|
|
58
|
+
</>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { Target } from 'lucide-react'
|
|
19
|
+
|
|
20
|
+
interface AuthHeaderProps {
|
|
21
|
+
title: string
|
|
22
|
+
subtitle?: string
|
|
23
|
+
titleClassName?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AuthHeader({ title, subtitle, titleClassName }: AuthHeaderProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="space-y-2 text-center">
|
|
29
|
+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-lg bg-primary">
|
|
30
|
+
<Target className="h-10 w-10 text-primary-foreground" />
|
|
31
|
+
</div>
|
|
32
|
+
<h1 className={`text-3xl font-semibold ${titleClassName || ''}`}>{title}</h1>
|
|
33
|
+
{subtitle && (
|
|
34
|
+
<p className="text-muted-foreground">{subtitle}</p>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|