@ploykit/ui 0.0.0-stage → 0.1.1
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/README.md +40 -2
- package/dist/components/ConfirmDialog.d.ts +13 -0
- package/dist/components/ConfirmDialog.js +29 -0
- package/dist/components/CronInput.d.ts +15 -0
- package/dist/components/CronInput.js +112 -0
- package/dist/components/DataTable.d.ts +22 -0
- package/dist/components/DataTable.js +13 -0
- package/dist/components/ErrorBoundary.d.ts +16 -0
- package/dist/components/ErrorBoundary.js +24 -0
- package/dist/components/FormField.d.ts +10 -0
- package/dist/components/FormField.js +22 -0
- package/dist/components/ImpersonationBanner.d.ts +3 -0
- package/dist/components/ImpersonationBanner.js +10 -0
- package/dist/components/NotificationBell.d.ts +3 -0
- package/dist/components/NotificationBell.js +47 -0
- package/dist/components/OnboardingChecklist.d.ts +12 -0
- package/dist/components/OnboardingChecklist.js +43 -0
- package/dist/components/Page.d.ts +17 -0
- package/dist/components/Page.js +16 -0
- package/dist/components/SecretModal.d.ts +16 -0
- package/dist/components/SecretModal.js +45 -0
- package/dist/components/SettingsCard.d.ts +10 -0
- package/dist/components/SettingsCard.js +6 -0
- package/dist/components/SiteBanner.d.ts +6 -0
- package/dist/components/SiteBanner.js +11 -0
- package/dist/components/SiteBannerLayer.d.ts +4 -0
- package/dist/components/SiteBannerLayer.js +9 -0
- package/dist/components/UserMenu.d.ts +13 -0
- package/dist/components/UserMenu.js +30 -0
- package/dist/components/WorkspaceSwitcher.d.ts +1 -0
- package/dist/components/WorkspaceSwitcher.js +53 -0
- package/dist/components/toast.d.ts +7 -0
- package/dist/components/toast.js +11 -0
- package/dist/components/ui/Badge.d.ts +13 -0
- package/dist/components/ui/Badge.js +21 -0
- package/dist/components/ui/Button.d.ts +10 -0
- package/dist/components/ui/Button.js +33 -0
- package/dist/components/ui/Input.d.ts +3 -0
- package/dist/components/ui/Input.js +10 -0
- package/dist/components/ui/alert-dialog.d.ts +18 -0
- package/dist/components/ui/alert-dialog.js +41 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +22 -0
- package/dist/components/ui/breadcrumb.d.ts +10 -0
- package/dist/components/ui/breadcrumb.js +36 -0
- package/dist/components/ui/card.d.ts +13 -0
- package/dist/components/ui/card.js +24 -0
- package/dist/components/ui/checkbox.d.ts +3 -0
- package/dist/components/ui/checkbox.js +8 -0
- package/dist/components/ui/dialog.d.ts +15 -0
- package/dist/components/ui/dialog.js +26 -0
- package/dist/components/ui/empty.d.ts +13 -0
- package/dist/components/ui/empty.js +36 -0
- package/dist/components/ui/field.d.ts +24 -0
- package/dist/components/ui/field.js +67 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/label.js +6 -0
- package/dist/components/ui/menu.d.ts +29 -0
- package/dist/components/ui/menu.js +50 -0
- package/dist/components/ui/popover.d.ts +9 -0
- package/dist/components/ui/popover.js +22 -0
- package/dist/components/ui/progress.d.ts +7 -0
- package/dist/components/ui/progress.js +19 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +33 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/separator.js +7 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +36 -0
- package/dist/components/ui/sidebar.d.ts +63 -0
- package/dist/components/ui/sidebar.js +265 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.js +6 -0
- package/dist/components/ui/switch.d.ts +5 -0
- package/dist/components/ui/switch.js +7 -0
- package/dist/components/ui/tabs.d.ts +10 -0
- package/dist/components/ui/tabs.js +28 -0
- package/dist/components/ui/tooltip.d.ts +6 -0
- package/dist/components/ui/tooltip.js +16 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/hooks/useApi.d.ts +10 -0
- package/dist/hooks/useApi.js +11 -0
- package/dist/hooks/useAudit.d.ts +39 -0
- package/dist/hooks/useAudit.js +55 -0
- package/dist/hooks/useAuth.d.ts +1 -0
- package/dist/hooks/useAuth.js +4 -0
- package/dist/hooks/useBilling.d.ts +44 -0
- package/dist/hooks/useBilling.js +64 -0
- package/dist/hooks/useLogin.d.ts +10 -0
- package/dist/hooks/useLogin.js +39 -0
- package/dist/hooks/useMembers.d.ts +125 -0
- package/dist/hooks/useMembers.js +127 -0
- package/dist/hooks/useNotificationPrefs.d.ts +20 -0
- package/dist/hooks/useNotificationPrefs.js +42 -0
- package/dist/hooks/usePasswordReset.d.ts +5 -0
- package/dist/hooks/usePasswordReset.js +12 -0
- package/dist/hooks/useSEO.d.ts +6 -0
- package/dist/hooks/useSEO.js +14 -0
- package/dist/hooks/useSessions.d.ts +20 -0
- package/dist/hooks/useSessions.js +38 -0
- package/dist/hooks/useSiteConfig.d.ts +12 -0
- package/dist/hooks/useSiteConfig.js +11 -0
- package/dist/hooks/useTokens.d.ts +36 -0
- package/dist/hooks/useTokens.js +35 -0
- package/dist/hooks/useUsage.d.ts +20 -0
- package/dist/hooks/useUsage.js +21 -0
- package/dist/hooks/useWorkspace.d.ts +26 -0
- package/dist/hooks/useWorkspace.js +32 -0
- package/dist/hooks/useZodForm.d.ts +6 -0
- package/dist/hooks/useZodForm.js +6 -0
- package/dist/hooks/ws.d.ts +4 -0
- package/dist/hooks/ws.js +21 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +94 -0
- package/dist/layouts/AppShell.d.ts +20 -0
- package/dist/layouts/AppShell.js +21 -0
- package/dist/layouts/AuthShell.d.ts +7 -0
- package/dist/layouts/AuthShell.js +4 -0
- package/dist/layouts/SettingsShell.d.ts +19 -0
- package/dist/layouts/SettingsShell.js +21 -0
- package/dist/lib/api-error.d.ts +1 -0
- package/dist/lib/api-error.js +74 -0
- package/dist/lib/perm.d.ts +2 -0
- package/dist/lib/perm.js +7 -0
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.js +84 -0
- package/dist/lib/ws-query-bridge.d.ts +7 -0
- package/dist/lib/ws-query-bridge.js +35 -0
- package/dist/pages/LandingPage.d.ts +22 -0
- package/dist/pages/LandingPage.js +5 -0
- package/dist/pages/LoginPage.d.ts +15 -0
- package/dist/pages/LoginPage.js +87 -0
- package/dist/pages/account/NotificationPreferencesPage.d.ts +13 -0
- package/dist/pages/account/NotificationPreferencesPage.js +41 -0
- package/dist/pages/account/ProfileSettingsPage.d.ts +4 -0
- package/dist/pages/account/ProfileSettingsPage.js +96 -0
- package/dist/pages/account/SecuritySettingsPage.d.ts +1 -0
- package/dist/pages/account/SecuritySettingsPage.js +134 -0
- package/dist/pages/account/TokensPage.d.ts +1 -0
- package/dist/pages/account/TokensPage.js +117 -0
- package/dist/pages/admin/AdminSettingsPage.d.ts +1 -0
- package/dist/pages/admin/AdminSettingsPage.js +210 -0
- package/dist/pages/admin/AdminShell.d.ts +7 -0
- package/dist/pages/admin/AdminShell.js +79 -0
- package/dist/pages/admin/AuditPage.d.ts +1 -0
- package/dist/pages/admin/AuditPage.js +164 -0
- package/dist/pages/admin/OrdersPage.d.ts +1 -0
- package/dist/pages/admin/OrdersPage.js +236 -0
- package/dist/pages/admin/OverviewPage.d.ts +1 -0
- package/dist/pages/admin/OverviewPage.js +200 -0
- package/dist/pages/admin/SSOPage.d.ts +1 -0
- package/dist/pages/admin/SSOPage.js +53 -0
- package/dist/pages/admin/UserDetailPage.d.ts +1 -0
- package/dist/pages/admin/UserDetailPage.js +266 -0
- package/dist/pages/admin/UsersPage.d.ts +4 -0
- package/dist/pages/admin/UsersPage.js +201 -0
- package/dist/pages/admin/WebhookDeliveriesPage.d.ts +1 -0
- package/dist/pages/admin/WebhookDeliveriesPage.js +128 -0
- package/dist/pages/admin/WorkspaceDetailPage.d.ts +1 -0
- package/dist/pages/admin/WorkspaceDetailPage.js +202 -0
- package/dist/pages/admin/WorkspacesPage.d.ts +1 -0
- package/dist/pages/admin/WorkspacesPage.js +133 -0
- package/dist/pages/admin/shared.d.ts +8 -0
- package/dist/pages/admin/shared.js +23 -0
- package/dist/pages/auth/ForgotPasswordPage.d.ts +6 -0
- package/dist/pages/auth/ForgotPasswordPage.js +32 -0
- package/dist/pages/auth/InviteAcceptPage.d.ts +5 -0
- package/dist/pages/auth/InviteAcceptPage.js +40 -0
- package/dist/pages/auth/RegisterPage.d.ts +15 -0
- package/dist/pages/auth/RegisterPage.js +80 -0
- package/dist/pages/auth/ResetPasswordPage.d.ts +4 -0
- package/dist/pages/auth/ResetPasswordPage.js +71 -0
- package/dist/pages/auth/VerifyEmailPage.d.ts +1 -0
- package/dist/pages/auth/VerifyEmailPage.js +45 -0
- package/dist/pages/workspace/BillingPage.d.ts +4 -0
- package/dist/pages/workspace/BillingPage.js +183 -0
- package/dist/pages/workspace/BillingSuccessPage.d.ts +4 -0
- package/dist/pages/workspace/BillingSuccessPage.js +72 -0
- package/dist/pages/workspace/MembersPage.d.ts +3 -0
- package/dist/pages/workspace/MembersPage.js +262 -0
- package/dist/pages/workspace/SchedulesPage.d.ts +4 -0
- package/dist/pages/workspace/SchedulesPage.js +177 -0
- package/dist/pages/workspace/UsagePage.d.ts +15 -0
- package/dist/pages/workspace/UsagePage.js +74 -0
- package/dist/pages/workspace/WebhooksPage.d.ts +3 -0
- package/dist/pages/workspace/WebhooksPage.js +242 -0
- package/dist/pages/workspace/WorkspaceAuditPage.d.ts +3 -0
- package/dist/pages/workspace/WorkspaceAuditPage.js +109 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.d.ts +4 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.js +101 -0
- package/dist/provider/PloykitProvider.d.ts +50 -0
- package/dist/provider/PloykitProvider.js +240 -0
- package/dist/realtime.d.ts +3 -0
- package/dist/realtime.js +11 -0
- package/package.json +61 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { AuthShell } from '../../layouts/AuthShell';
|
|
4
|
+
import { FormField } from '../../components/FormField';
|
|
5
|
+
import { Button } from '../../components/ui/Button';
|
|
6
|
+
import { Input } from '../../components/ui/Input';
|
|
7
|
+
import { usePasswordReset } from '../../hooks/usePasswordReset';
|
|
8
|
+
import { passwordSchema } from './RegisterPage';
|
|
9
|
+
const REDIRECT_DELAY_MS = 3000;
|
|
10
|
+
export function ResetPasswordPage({ onDone }) {
|
|
11
|
+
const { reset } = usePasswordReset();
|
|
12
|
+
const [params, setParams] = useState(null);
|
|
13
|
+
const email = params?.get('email') ?? '';
|
|
14
|
+
const token = params?.get('token') ?? '';
|
|
15
|
+
const [password, setPassword] = useState('');
|
|
16
|
+
const [confirm, setConfirm] = useState('');
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
const [done, setDone] = useState(false);
|
|
19
|
+
const [countdown, setCountdown] = useState(REDIRECT_DELAY_MS / 1000);
|
|
20
|
+
const [confirmError, setConfirmError] = useState(null);
|
|
21
|
+
const [serverError, setServerError] = useState(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setParams(new URLSearchParams(window.location.search));
|
|
24
|
+
}, []);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!done)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
window.history.replaceState(null, '', window.location.pathname);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* replaceState 不可用(极端沙箱)时忽略——令牌已失效,仅剩地址残留 */
|
|
33
|
+
}
|
|
34
|
+
const tick = setInterval(() => setCountdown((c) => c - 1), 1000);
|
|
35
|
+
const jump = setTimeout(() => {
|
|
36
|
+
window.location.href = onDone ?? '/login';
|
|
37
|
+
}, REDIRECT_DELAY_MS);
|
|
38
|
+
return () => {
|
|
39
|
+
clearInterval(tick);
|
|
40
|
+
clearTimeout(jump);
|
|
41
|
+
};
|
|
42
|
+
}, [done, onDone]);
|
|
43
|
+
const handleSubmit = async (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setConfirmError(null);
|
|
46
|
+
setServerError(null);
|
|
47
|
+
if (!password || !confirm)
|
|
48
|
+
return;
|
|
49
|
+
if (password !== confirm) {
|
|
50
|
+
setConfirmError('两次输入的密码不一致');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const pwCheck = passwordSchema.safeParse(password);
|
|
54
|
+
if (!pwCheck.success) {
|
|
55
|
+
setServerError(pwCheck.error.issues[0]?.message ?? '密码不符合要求');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setLoading(true);
|
|
59
|
+
try {
|
|
60
|
+
await reset(email, token, password);
|
|
61
|
+
setDone(true);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
setServerError(err instanceof Error ? err.message : '重置失败,链接可能已失效');
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return (_jsx(AuthShell, { children: _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsx("div", { className: "space-y-1 text-center", children: _jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "\u8BBE\u7F6E\u65B0\u5BC6\u7801" }) }), done ? (_jsxs("div", { className: "space-y-2 text-center", "data-testid": "reset-done", children: [_jsx("p", { className: "text-sm text-emerald-600", children: "\u5BC6\u7801\u5DF2\u91CD\u7F6E\uFF0C\u5168\u90E8\u65E7\u4F1A\u8BDD\u5DF2\u5931\u6548\u3002" }), _jsx("p", { className: "text-sm text-muted-foreground", children: countdown > 0 ? `${countdown} 秒后跳转登录页…` : '正在跳转…' })] })) : params === null ? (_jsx("p", { className: "text-center text-sm text-muted-foreground", children: "\u6B63\u5728\u6253\u5F00\u94FE\u63A5\u2026" })) : !email || !token ? (_jsx("p", { className: "text-center text-sm text-destructive", "data-testid": "reset-invalid", children: "\u94FE\u63A5\u65E0\u6548\uFF1A\u7F3A\u5C11\u90AE\u7BB1\u6216\u4EE4\u724C\u53C2\u6570\uFF0C\u8BF7\u4ECE\u90AE\u4EF6\u4E2D\u7684\u94FE\u63A5\u8FDB\u5165\u3002" })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: ["\u6B63\u5728\u4E3A ", email, " \u8BBE\u7F6E\u65B0\u5BC6\u7801"] }), _jsx(FormField, { label: "\u65B0\u5BC6\u7801", htmlFor: "reset-new", children: _jsx(Input, { id: "reset-new", type: "password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "\u81F3\u5C11 10 \u4F4D\uFF0C\u542B\u4E09\u7C7B\u5B57\u7B26", autoComplete: "new-password", required: true }) }), _jsx(FormField, { label: "\u786E\u8BA4\u65B0\u5BC6\u7801", error: confirmError ?? undefined, htmlFor: "reset-confirm", children: _jsx(Input, { id: "reset-confirm", type: "password", value: confirm, onChange: (e) => setConfirm(e.target.value), placeholder: "\u518D\u8F93\u5165\u4E00\u6B21", autoComplete: "new-password", "aria-invalid": confirmError ? true : undefined, required: true }) }), serverError && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: serverError })), _jsx(Button, { type: "submit", className: "w-full", disabled: loading || !password || !confirm, children: loading ? '提交中…' : '重置密码' })] }))] }) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function VerifyEmailPage(): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { api } from '@ploykit/client';
|
|
4
|
+
import { AuthShell } from '../../layouts/AuthShell';
|
|
5
|
+
import { buttonVariants } from '../../components/ui/Button';
|
|
6
|
+
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyFooter, } from '../../components/ui/empty';
|
|
7
|
+
import { MailCheck, LoaderCircle, CircleX } from 'lucide-react';
|
|
8
|
+
export function VerifyEmailPage() {
|
|
9
|
+
const [state, setState] = useState('verifying');
|
|
10
|
+
const [message, setMessage] = useState(null);
|
|
11
|
+
const fired = useRef(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (fired.current)
|
|
14
|
+
return;
|
|
15
|
+
fired.current = true;
|
|
16
|
+
const params = new URLSearchParams(window.location.search);
|
|
17
|
+
const email = params.get('email') ?? '';
|
|
18
|
+
const token = params.get('token') ?? '';
|
|
19
|
+
if (!email || !token) {
|
|
20
|
+
setState('failed');
|
|
21
|
+
setMessage('链接无效:缺少邮箱或令牌参数');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
api
|
|
25
|
+
.post('/auth/verify-email', { email, token })
|
|
26
|
+
.then(() => {
|
|
27
|
+
try {
|
|
28
|
+
window.history.replaceState(null, '', window.location.pathname);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* replaceState 不可用时忽略——令牌已失效 */
|
|
32
|
+
}
|
|
33
|
+
setState('ok');
|
|
34
|
+
})
|
|
35
|
+
.catch((err) => {
|
|
36
|
+
setState('failed');
|
|
37
|
+
setMessage(err instanceof Error ? err.message : '验证失败,链接可能已失效');
|
|
38
|
+
});
|
|
39
|
+
}, []);
|
|
40
|
+
return (_jsx(AuthShell, { children: _jsxs(Empty, { className: "border-none p-0", children: [_jsxs(EmptyHeader, { children: [_jsxs(EmptyMedia, { variant: "icon", "data-testid": "verify-state", children: [state === 'ok' && _jsx(MailCheck, { className: "text-emerald-600", "aria-hidden": "true" }), state === 'verifying' && _jsx(LoaderCircle, { className: "animate-spin", "aria-hidden": "true" }), state === 'failed' && _jsx(CircleX, { className: "text-destructive", "aria-hidden": "true" })] }), _jsx(EmptyTitle, { children: state === 'ok' ? '邮箱验证成功' : state === 'failed' ? '验证失败' : '正在验证…' }), _jsx(EmptyDescription, { children: state === 'ok'
|
|
41
|
+
? '感谢确认,现在可以正常登录了。'
|
|
42
|
+
: state === 'failed'
|
|
43
|
+
? (message ?? '验证失败')
|
|
44
|
+
: '请稍候,正在校验邮件链接…' })] }), _jsx(EmptyFooter, { children: _jsx("a", { href: "/login", className: buttonVariants({ variant: 'outline' }), children: "\u8FD4\u56DE\u767B\u5F55" }) })] }) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useSearchParams } from 'react-router';
|
|
4
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { PageHeader, PageLoading, PageError, PageEmpty } from '../../components/Page';
|
|
6
|
+
import { DataTable } from '../../components/DataTable';
|
|
7
|
+
import { SettingsCard } from '../../components/SettingsCard';
|
|
8
|
+
import { useConfirm } from '../../components/ConfirmDialog';
|
|
9
|
+
import { toast } from '../../components/toast';
|
|
10
|
+
import { Button } from '../../components/ui/Button';
|
|
11
|
+
import { Badge, StatusBadge } from '../../components/ui/Badge';
|
|
12
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
|
|
13
|
+
import { useApi } from '../../hooks/useApi';
|
|
14
|
+
import { useBilling } from '../../hooks/useBilling';
|
|
15
|
+
import { useSiteConfig } from '../../hooks/useSiteConfig';
|
|
16
|
+
import { useWorkspace } from '../../hooks/useWorkspace';
|
|
17
|
+
import { queryKeys, } from '../../provider/PloykitProvider';
|
|
18
|
+
import { formatDateTime, cn } from '../../lib/utils';
|
|
19
|
+
import { apiErrorMessage } from '../../lib/api-error';
|
|
20
|
+
const INTERVAL_LABEL = { monthly: '按月', yearly: '按年' };
|
|
21
|
+
const INTERVAL_UNIT = { monthly: '月', yearly: '年' };
|
|
22
|
+
const CHANNEL_LABEL = {
|
|
23
|
+
stripe: 'Stripe(银行卡)',
|
|
24
|
+
alipay: '支付宝',
|
|
25
|
+
wechat: '微信支付',
|
|
26
|
+
manual: '线下转账(管理员核销)',
|
|
27
|
+
};
|
|
28
|
+
const channelLabel = (code) => CHANNEL_LABEL[code] ?? code;
|
|
29
|
+
const LIMIT_LABEL = {
|
|
30
|
+
tasks_monthly: '任务数 / 周期',
|
|
31
|
+
workspaces: '工作区数',
|
|
32
|
+
seats: '成员席位',
|
|
33
|
+
webhooks: 'Webhook 订阅',
|
|
34
|
+
};
|
|
35
|
+
function priceCentsOf(plan, interval) {
|
|
36
|
+
const key = interval === 'yearly' ? 'price_yearly_cents' : 'price_monthly_cents';
|
|
37
|
+
return plan.limits[key] ?? 0;
|
|
38
|
+
}
|
|
39
|
+
function fmtCents(cents, currency) {
|
|
40
|
+
if (cents === 0)
|
|
41
|
+
return '免费';
|
|
42
|
+
const cur = (currency ?? 'CNY').toUpperCase();
|
|
43
|
+
const symbol = cur === 'CNY' ? '¥' : `${cur} `;
|
|
44
|
+
return `${symbol}${(cents / 100).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
45
|
+
}
|
|
46
|
+
export function BillingPage({ channel = 'stripe' } = {}) {
|
|
47
|
+
const { current } = useWorkspace();
|
|
48
|
+
const { subscription, plans, loading } = useBilling();
|
|
49
|
+
const api = useApi();
|
|
50
|
+
const confirm = useConfirm();
|
|
51
|
+
const qc = useQueryClient();
|
|
52
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
53
|
+
const [interval, setInterval] = useState(searchParams.get('interval') === 'yearly' ? 'yearly' : 'monthly');
|
|
54
|
+
const [payChannelSel, setPayChannelSel] = useState(searchParams.get('channel') ?? channel);
|
|
55
|
+
const [checkoutPlan, setCheckoutPlan] = useState(null);
|
|
56
|
+
const [manualNote, setManualNote] = useState(null);
|
|
57
|
+
const syncParams = (next) => {
|
|
58
|
+
const p = new URLSearchParams(searchParams);
|
|
59
|
+
if (next.interval)
|
|
60
|
+
p.set('interval', next.interval);
|
|
61
|
+
if (next.channel)
|
|
62
|
+
p.set('channel', next.channel);
|
|
63
|
+
setSearchParams(p, { replace: true });
|
|
64
|
+
};
|
|
65
|
+
const configQ = useSiteConfig();
|
|
66
|
+
const billingChannels = configQ.data?.billing_channels;
|
|
67
|
+
const channelOptions = billingChannels !== undefined ? billingChannels : Object.keys(CHANNEL_LABEL);
|
|
68
|
+
const payChannel = billingChannels !== undefined && billingChannels.length > 0 && !billingChannels.includes(payChannelSel)
|
|
69
|
+
? billingChannels[0]
|
|
70
|
+
: payChannelSel;
|
|
71
|
+
const noChannels = billingChannels !== undefined && billingChannels.length === 0;
|
|
72
|
+
const wsId = current?.id ?? '';
|
|
73
|
+
const subErrQ = useQuery({
|
|
74
|
+
queryKey: queryKeys.billingSubscription(wsId),
|
|
75
|
+
queryFn: () => api.get('/api/billing/subscription'),
|
|
76
|
+
enabled: !!wsId,
|
|
77
|
+
});
|
|
78
|
+
const plansErrQ = useQuery({
|
|
79
|
+
queryKey: queryKeys.billingPlans,
|
|
80
|
+
queryFn: () => api.get('/api/billing/plans'),
|
|
81
|
+
});
|
|
82
|
+
const loadError = subErrQ.isError
|
|
83
|
+
? subErrQ.error
|
|
84
|
+
: plansErrQ.isError
|
|
85
|
+
? plansErrQ.error
|
|
86
|
+
: null;
|
|
87
|
+
const orders = subscription?.orders ?? [];
|
|
88
|
+
const currentPlanCode = subscription?.plan_code ?? null;
|
|
89
|
+
const sortedPlans = [...plans].sort((a, b) => a.sort_no - b.sort_no);
|
|
90
|
+
const checkoutDescription = payChannel === 'manual'
|
|
91
|
+
? '升级立即生效;下单后请联系管理员完成线下转账,订单在管理员核销后生效。'
|
|
92
|
+
: payChannel === 'stripe'
|
|
93
|
+
? '升级立即生效;Stripe 结账完成后自动跳回。'
|
|
94
|
+
: '升级立即生效;在所选渠道的收银台完成支付后自动生效。';
|
|
95
|
+
const refreshBilling = () => qc.invalidateQueries({ queryKey: queryKeys.billing });
|
|
96
|
+
const handleCheckout = async (plan) => {
|
|
97
|
+
if (noChannels || (billingChannels !== undefined && billingChannels.length > 0 && !billingChannels.includes(payChannel))) {
|
|
98
|
+
toast.error('该支付渠道未配置');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
setCheckoutPlan(plan.code);
|
|
102
|
+
setManualNote(null);
|
|
103
|
+
try {
|
|
104
|
+
const res = await api.post('/api/billing/checkout', {
|
|
105
|
+
plan_code: plan.code, interval, channel: payChannel,
|
|
106
|
+
});
|
|
107
|
+
if (res.action_url) {
|
|
108
|
+
window.location.href = res.action_url;
|
|
109
|
+
}
|
|
110
|
+
else if (payChannel === 'manual') {
|
|
111
|
+
setManualNote(`已创建 ${plan.name} 订单(待处理):下单后请联系管理员完成支付,订单在管理员核销后生效。`);
|
|
112
|
+
toast.success('订单已创建,请联系管理员完成支付');
|
|
113
|
+
await refreshBilling();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
toast.error('支付渠道未返回跳转地址');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
toast.error(apiErrorMessage(e, '发起结账失败'));
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
setCheckoutPlan(null);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const handleCancel = async (order) => {
|
|
127
|
+
if (!(await confirm({
|
|
128
|
+
title: '取消订单',
|
|
129
|
+
description: `取消 ${order.plan_code}(${INTERVAL_LABEL[order.interval] ?? order.interval})的待处理订单?`,
|
|
130
|
+
confirmText: '取消订单', danger: true,
|
|
131
|
+
})))
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
await api.post(`/api/billing/orders/${order.id}/cancel`);
|
|
135
|
+
await refreshBilling();
|
|
136
|
+
toast.success('订单已取消');
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
toast.error(apiErrorMessage(e, '取消失败'));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const orderColumns = [
|
|
143
|
+
{ key: 'plan_code', header: '套餐', render: (o) => _jsx("span", { className: "font-medium", children: o.plan_code }) },
|
|
144
|
+
{ key: 'interval', header: '周期', render: (o) => INTERVAL_LABEL[o.interval] ?? o.interval },
|
|
145
|
+
{ key: 'amount_cents', header: '金额', render: (o) => fmtCents(o.amount_cents, o.currency) },
|
|
146
|
+
{ key: 'status', header: '状态', render: (o) => _jsx(StatusBadge, { status: o.status }) },
|
|
147
|
+
{ key: 'created_at', header: '创建时间', render: (o) => _jsx("span", { className: "text-muted-foreground", children: formatDateTime(o.created_at) }) },
|
|
148
|
+
{
|
|
149
|
+
key: 'actions', header: '操作', className: 'text-right',
|
|
150
|
+
render: (o) => o.status === 'pending' ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => handleCancel(o), children: "\u53D6\u6D88" })) : null,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
if (!wsId) {
|
|
154
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u8BA1\u8D39" }), _jsx(PageEmpty, { label: "\u8BF7\u5148\u521B\u5EFA\u5E76\u9009\u62E9\u4E00\u4E2A\u5DE5\u4F5C\u533A" })] }));
|
|
155
|
+
}
|
|
156
|
+
if (loading)
|
|
157
|
+
return _jsx(PageLoading, { label: "\u52A0\u8F7D\u8BA1\u8D39\u4FE1\u606F\u2026" });
|
|
158
|
+
if (loadError) {
|
|
159
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u8BA1\u8D39" }), _jsx(PageError, { message: apiErrorMessage(loadError, '计费信息加载失败'), onRetry: () => {
|
|
160
|
+
void subErrQ.refetch();
|
|
161
|
+
void plansErrQ.refetch();
|
|
162
|
+
} })] }));
|
|
163
|
+
}
|
|
164
|
+
return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u8BA1\u8D39", description: "\u5F53\u524D\u5957\u9910\u3001\u5347\u7EA7\u4E0E\u8BA2\u5355\u7BA1\u7406\uFF08\u6309\u5468\u671F\u8D2D\u4E70\uFF0C\u5230\u671F\u540E\u9700\u518D\u6B21\u8D2D\u4E70\u6216\u7EED\u671F\uFF09" }), _jsx(SettingsCard, { title: "\u5F53\u524D\u5957\u9910", description: "\u5DE5\u4F5C\u533A\u5F53\u524D\u751F\u6548\u7684\u5957\u9910\u4E0E\u5386\u53F2\u8BA2\u5355\u3002", cta: _jsx(Badge, { variant: "outline", children: currentPlanCode ?? '未知' }), children: _jsxs("p", { className: "text-sm text-muted-foreground", children: ["\u5957\u9910 ", _jsx("span", { className: "font-medium text-foreground", children: currentPlanCode ?? '—' }), "\uFF0C\u5171 ", orders.length, " \u7B14\u8BA2\u5355\uFF08\u5F85\u5904\u7406 ", orders.filter((o) => o.status === 'pending').length, " \u7B14\uFF09\u3002"] }) }), _jsxs(SettingsCard, { title: "\u9009\u62E9\u5957\u9910", description: checkoutDescription, children: [_jsxs("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-3", children: [_jsx("div", { className: "w-fit rounded-xl border bg-muted p-1", role: "group", "aria-label": "\u8BA1\u8D39\u5468\u671F", children: ['monthly', 'yearly'].map((it) => (_jsx("button", { type: "button", onClick: () => { setInterval(it); syncParams({ interval: it }); }, className: cn('rounded-lg px-3 py-1.5 text-sm font-medium transition-colors', interval === it
|
|
165
|
+
? 'bg-primary text-primary-foreground shadow-sm'
|
|
166
|
+
: 'text-muted-foreground hover:text-foreground'), children: INTERVAL_LABEL[it] }, it))) }), _jsx("div", { className: "flex items-center gap-2", children: _jsxs(Select, { value: payChannel, onValueChange: (v) => {
|
|
167
|
+
if (typeof v === 'string') {
|
|
168
|
+
setPayChannelSel(v);
|
|
169
|
+
syncParams({ channel: v });
|
|
170
|
+
}
|
|
171
|
+
}, children: [_jsx(SelectTrigger, { size: "sm", className: "w-44", "aria-label": "\u652F\u4ED8\u6E20\u9053", children: _jsx(SelectValue, { children: channelLabel(payChannel) }) }), _jsx(SelectContent, { children: channelOptions.map((code) => (_jsx(SelectItem, { value: code, children: channelLabel(code) }, code))) })] }) })] }), manualNote && (_jsx("p", { role: "note", className: "mb-4 rounded-lg border-(--warning)/40 bg-(--warning)/15 px-3 py-2 text-sm text-(--warning)", children: manualNote })), _jsx("div", { className: "grid gap-4 lg:grid-cols-3", children: sortedPlans.map((plan) => {
|
|
172
|
+
const isCurrent = plan.code === currentPlanCode;
|
|
173
|
+
const price = priceCentsOf(plan, interval);
|
|
174
|
+
const features = Object.entries(plan.limits)
|
|
175
|
+
.filter(([k]) => !k.startsWith('price_') && k !== 'trial_days')
|
|
176
|
+
.map(([k, v]) => `${LIMIT_LABEL[k] ?? k}:${v < 0 ? '不限' : v}`);
|
|
177
|
+
return (_jsxs("div", { className: cn('flex flex-col rounded-xl border bg-card p-5 shadow-xs', isCurrent && 'border-primary/40'), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "font-semibold", children: plan.name }), isCurrent && _jsx(Badge, { children: "\u5F53\u524D\u8BA1\u5212" })] }), _jsxs("p", { className: "mt-3 text-3xl font-bold tabular-nums", children: [fmtCents(price), price > 0 && (_jsx("span", { className: "text-sm font-normal text-muted-foreground", children: ` / ${INTERVAL_UNIT[interval]}` }))] }), _jsx(Button, { className: "mt-4 w-full", variant: isCurrent ? 'secondary' : 'default', disabled: isCurrent || checkoutPlan !== null || noChannels, onClick: () => handleCheckout(plan), children: isCurrent
|
|
178
|
+
? '当前计划'
|
|
179
|
+
: checkoutPlan === plan.code
|
|
180
|
+
? '跳转收银台…'
|
|
181
|
+
: '升级' }), !isCurrent && noChannels && (_jsx("p", { className: "mt-2 text-xs text-muted-foreground", role: "note", children: "\u6682\u4E0D\u53EF\u5347\u7EA7\uFF1A\u7AD9\u70B9\u672A\u914D\u7F6E\u4EFB\u4F55\u652F\u4ED8\u6E20\u9053\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002" })), features.length > 0 && (_jsx("ul", { className: "mt-6 space-y-3 border-t pt-6 text-sm text-muted-foreground", children: features.map((f) => (_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-primary", children: "\u2713" }), " ", f] }, f))) }))] }, plan.code));
|
|
182
|
+
}) })] }), _jsx(SettingsCard, { title: "\u8BA2\u5355", description: "\u5168\u90E8\u8D2D\u4E70\u8BB0\u5F55\uFF1B\u5F85\u5904\u7406\u8BA2\u5355\u53EF\u53D6\u6D88\u3002", bodyClassName: "px-4 pt-4 pb-4", children: _jsx(DataTable, { columns: orderColumns, rows: orders, rowKey: (o) => o.id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u8BA2\u5355", hint: "\u9009\u62E9\u5957\u9910\u5B8C\u6210\u9996\u6B21\u8D2D\u4E70\u540E\uFF0C\u8BA2\u5355\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC" }) }) })] }));
|
|
183
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useNavigate } from 'react-router';
|
|
4
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { Skeleton } from '../../components/ui/skeleton';
|
|
6
|
+
import { toast } from '../../components/toast';
|
|
7
|
+
import { useApi } from '../../hooks/useApi';
|
|
8
|
+
import { useWorkspace } from '../../hooks/useWorkspace';
|
|
9
|
+
import { queryKeys } from '../../provider/PloykitProvider';
|
|
10
|
+
const POLL_MS = 5_000;
|
|
11
|
+
const MAX_TRIES = 12;
|
|
12
|
+
function toBaseline(sub) {
|
|
13
|
+
return {
|
|
14
|
+
plan: sub.plan_code,
|
|
15
|
+
paidIds: sub.orders.filter((o) => o.status === 'paid').map((o) => o.id).sort().join(','),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function BillingSuccessPage({ redirectTo = '/settings/workspace/billing' } = {}) {
|
|
19
|
+
const { current } = useWorkspace();
|
|
20
|
+
const api = useApi();
|
|
21
|
+
const qc = useQueryClient();
|
|
22
|
+
const navigate = useNavigate();
|
|
23
|
+
const wsId = current?.id ?? '';
|
|
24
|
+
const [baseline, setBaseline] = useState(() => {
|
|
25
|
+
if (!wsId)
|
|
26
|
+
return null;
|
|
27
|
+
const cached = qc.getQueryData(queryKeys.billingSubscription(wsId));
|
|
28
|
+
return cached ? toBaseline(cached) : null;
|
|
29
|
+
});
|
|
30
|
+
const tries = useRef(0);
|
|
31
|
+
const done = useRef(false);
|
|
32
|
+
const subQ = useQuery({
|
|
33
|
+
queryKey: queryKeys.billingSubscription(wsId),
|
|
34
|
+
queryFn: () => api.get('/api/billing/subscription'),
|
|
35
|
+
enabled: !!wsId,
|
|
36
|
+
refetchInterval: POLL_MS,
|
|
37
|
+
});
|
|
38
|
+
const finish = (ok) => {
|
|
39
|
+
if (done.current)
|
|
40
|
+
return;
|
|
41
|
+
done.current = true;
|
|
42
|
+
if (ok)
|
|
43
|
+
toast.success('支付成功,套餐已生效');
|
|
44
|
+
else
|
|
45
|
+
toast.info('支付结果确认超时,可稍后在计费页查看');
|
|
46
|
+
navigate(redirectTo);
|
|
47
|
+
};
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const sub = subQ.data;
|
|
50
|
+
if (!sub || !subQ.dataUpdatedAt || done.current)
|
|
51
|
+
return;
|
|
52
|
+
if (baseline === null) {
|
|
53
|
+
setBaseline(toBaseline(sub));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
tries.current += 1;
|
|
57
|
+
const planChanged = sub.plan_code !== baseline.plan;
|
|
58
|
+
const newPaid = toBaseline(sub).paidIds !== baseline.paidIds;
|
|
59
|
+
if (planChanged || newPaid) {
|
|
60
|
+
finish(true);
|
|
61
|
+
}
|
|
62
|
+
else if (tries.current >= MAX_TRIES) {
|
|
63
|
+
finish(false);
|
|
64
|
+
}
|
|
65
|
+
// baseline 有意不入依赖:基线固定后轮询推进只由 dataUpdatedAt 驱动
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, [subQ.dataUpdatedAt]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
68
|
+
if (!wsId) {
|
|
69
|
+
return (_jsxs("div", { className: "flex min-h-[60vh] flex-col items-center justify-center gap-3 p-6 text-center", children: [_jsx("p", { className: "text-sm font-medium", children: "\u6682\u65E0\u6CD5\u786E\u8BA4\u652F\u4ED8\u7ED3\u679C" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u5F53\u524D\u6CA1\u6709\u9009\u4E2D\u7684\u5DE5\u4F5C\u533A\uFF0C\u65E0\u6CD5\u67E5\u8BE2\u8BA2\u9605\u72B6\u6001\u3002" }), _jsx("a", { href: redirectTo, className: "text-sm text-primary underline-offset-4 hover:underline", children: "\u524D\u5F80\u8BA1\u8D39\u9875\u67E5\u770B\u5F53\u524D\u5957\u9910" })] }));
|
|
70
|
+
}
|
|
71
|
+
return (_jsxs("div", { className: "flex min-h-[60vh] flex-col items-center justify-center gap-4 p-6", children: [_jsx("div", { className: "size-8 animate-spin rounded-full border-2 border-muted border-t-primary", "aria-hidden": "true" }), _jsx("p", { className: "text-sm font-medium", children: "\u6B63\u5728\u786E\u8BA4\u652F\u4ED8\u7ED3\u679C\u2026" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u786E\u8BA4\u5B8C\u6210\u540E\u5C06\u81EA\u52A8\u8FD4\u56DE\u8BA1\u8D39\u9875\uFF0C\u8BF7\u52FF\u5173\u95ED\u9875\u9762\u3002" }), _jsxs("div", { className: "mt-4 w-full max-w-md space-y-2", "aria-hidden": "true", children: [_jsx(Skeleton, { className: "h-4 w-full" }), _jsx(Skeleton, { className: "h-4 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2" })] })] }));
|
|
72
|
+
}
|