@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.
Files changed (196) hide show
  1. package/README.md +40 -2
  2. package/dist/components/ConfirmDialog.d.ts +13 -0
  3. package/dist/components/ConfirmDialog.js +29 -0
  4. package/dist/components/CronInput.d.ts +15 -0
  5. package/dist/components/CronInput.js +112 -0
  6. package/dist/components/DataTable.d.ts +22 -0
  7. package/dist/components/DataTable.js +13 -0
  8. package/dist/components/ErrorBoundary.d.ts +16 -0
  9. package/dist/components/ErrorBoundary.js +24 -0
  10. package/dist/components/FormField.d.ts +10 -0
  11. package/dist/components/FormField.js +22 -0
  12. package/dist/components/ImpersonationBanner.d.ts +3 -0
  13. package/dist/components/ImpersonationBanner.js +10 -0
  14. package/dist/components/NotificationBell.d.ts +3 -0
  15. package/dist/components/NotificationBell.js +47 -0
  16. package/dist/components/OnboardingChecklist.d.ts +12 -0
  17. package/dist/components/OnboardingChecklist.js +43 -0
  18. package/dist/components/Page.d.ts +17 -0
  19. package/dist/components/Page.js +16 -0
  20. package/dist/components/SecretModal.d.ts +16 -0
  21. package/dist/components/SecretModal.js +45 -0
  22. package/dist/components/SettingsCard.d.ts +10 -0
  23. package/dist/components/SettingsCard.js +6 -0
  24. package/dist/components/SiteBanner.d.ts +6 -0
  25. package/dist/components/SiteBanner.js +11 -0
  26. package/dist/components/SiteBannerLayer.d.ts +4 -0
  27. package/dist/components/SiteBannerLayer.js +9 -0
  28. package/dist/components/UserMenu.d.ts +13 -0
  29. package/dist/components/UserMenu.js +30 -0
  30. package/dist/components/WorkspaceSwitcher.d.ts +1 -0
  31. package/dist/components/WorkspaceSwitcher.js +53 -0
  32. package/dist/components/toast.d.ts +7 -0
  33. package/dist/components/toast.js +11 -0
  34. package/dist/components/ui/Badge.d.ts +13 -0
  35. package/dist/components/ui/Badge.js +21 -0
  36. package/dist/components/ui/Button.d.ts +10 -0
  37. package/dist/components/ui/Button.js +33 -0
  38. package/dist/components/ui/Input.d.ts +3 -0
  39. package/dist/components/ui/Input.js +10 -0
  40. package/dist/components/ui/alert-dialog.d.ts +18 -0
  41. package/dist/components/ui/alert-dialog.js +41 -0
  42. package/dist/components/ui/avatar.d.ts +11 -0
  43. package/dist/components/ui/avatar.js +22 -0
  44. package/dist/components/ui/breadcrumb.d.ts +10 -0
  45. package/dist/components/ui/breadcrumb.js +36 -0
  46. package/dist/components/ui/card.d.ts +13 -0
  47. package/dist/components/ui/card.js +24 -0
  48. package/dist/components/ui/checkbox.d.ts +3 -0
  49. package/dist/components/ui/checkbox.js +8 -0
  50. package/dist/components/ui/dialog.d.ts +15 -0
  51. package/dist/components/ui/dialog.js +26 -0
  52. package/dist/components/ui/empty.d.ts +13 -0
  53. package/dist/components/ui/empty.js +36 -0
  54. package/dist/components/ui/field.d.ts +24 -0
  55. package/dist/components/ui/field.js +67 -0
  56. package/dist/components/ui/label.d.ts +3 -0
  57. package/dist/components/ui/label.js +6 -0
  58. package/dist/components/ui/menu.d.ts +29 -0
  59. package/dist/components/ui/menu.js +50 -0
  60. package/dist/components/ui/popover.d.ts +9 -0
  61. package/dist/components/ui/popover.js +22 -0
  62. package/dist/components/ui/progress.d.ts +7 -0
  63. package/dist/components/ui/progress.js +19 -0
  64. package/dist/components/ui/select.d.ts +15 -0
  65. package/dist/components/ui/select.js +33 -0
  66. package/dist/components/ui/separator.d.ts +3 -0
  67. package/dist/components/ui/separator.js +7 -0
  68. package/dist/components/ui/sheet.d.ts +14 -0
  69. package/dist/components/ui/sheet.js +36 -0
  70. package/dist/components/ui/sidebar.d.ts +63 -0
  71. package/dist/components/ui/sidebar.js +265 -0
  72. package/dist/components/ui/skeleton.d.ts +3 -0
  73. package/dist/components/ui/skeleton.js +6 -0
  74. package/dist/components/ui/switch.d.ts +5 -0
  75. package/dist/components/ui/switch.js +7 -0
  76. package/dist/components/ui/tabs.d.ts +10 -0
  77. package/dist/components/ui/tabs.js +28 -0
  78. package/dist/components/ui/tooltip.d.ts +6 -0
  79. package/dist/components/ui/tooltip.js +16 -0
  80. package/dist/hooks/use-mobile.d.ts +1 -0
  81. package/dist/hooks/use-mobile.js +15 -0
  82. package/dist/hooks/useApi.d.ts +10 -0
  83. package/dist/hooks/useApi.js +11 -0
  84. package/dist/hooks/useAudit.d.ts +39 -0
  85. package/dist/hooks/useAudit.js +55 -0
  86. package/dist/hooks/useAuth.d.ts +1 -0
  87. package/dist/hooks/useAuth.js +4 -0
  88. package/dist/hooks/useBilling.d.ts +44 -0
  89. package/dist/hooks/useBilling.js +64 -0
  90. package/dist/hooks/useLogin.d.ts +10 -0
  91. package/dist/hooks/useLogin.js +39 -0
  92. package/dist/hooks/useMembers.d.ts +125 -0
  93. package/dist/hooks/useMembers.js +127 -0
  94. package/dist/hooks/useNotificationPrefs.d.ts +20 -0
  95. package/dist/hooks/useNotificationPrefs.js +42 -0
  96. package/dist/hooks/usePasswordReset.d.ts +5 -0
  97. package/dist/hooks/usePasswordReset.js +12 -0
  98. package/dist/hooks/useSEO.d.ts +6 -0
  99. package/dist/hooks/useSEO.js +14 -0
  100. package/dist/hooks/useSessions.d.ts +20 -0
  101. package/dist/hooks/useSessions.js +38 -0
  102. package/dist/hooks/useSiteConfig.d.ts +12 -0
  103. package/dist/hooks/useSiteConfig.js +11 -0
  104. package/dist/hooks/useTokens.d.ts +36 -0
  105. package/dist/hooks/useTokens.js +35 -0
  106. package/dist/hooks/useUsage.d.ts +20 -0
  107. package/dist/hooks/useUsage.js +21 -0
  108. package/dist/hooks/useWorkspace.d.ts +26 -0
  109. package/dist/hooks/useWorkspace.js +32 -0
  110. package/dist/hooks/useZodForm.d.ts +6 -0
  111. package/dist/hooks/useZodForm.js +6 -0
  112. package/dist/hooks/ws.d.ts +4 -0
  113. package/dist/hooks/ws.js +21 -0
  114. package/dist/index.d.ts +124 -0
  115. package/dist/index.js +94 -0
  116. package/dist/layouts/AppShell.d.ts +20 -0
  117. package/dist/layouts/AppShell.js +21 -0
  118. package/dist/layouts/AuthShell.d.ts +7 -0
  119. package/dist/layouts/AuthShell.js +4 -0
  120. package/dist/layouts/SettingsShell.d.ts +19 -0
  121. package/dist/layouts/SettingsShell.js +21 -0
  122. package/dist/lib/api-error.d.ts +1 -0
  123. package/dist/lib/api-error.js +74 -0
  124. package/dist/lib/perm.d.ts +2 -0
  125. package/dist/lib/perm.js +7 -0
  126. package/dist/lib/utils.d.ts +12 -0
  127. package/dist/lib/utils.js +84 -0
  128. package/dist/lib/ws-query-bridge.d.ts +7 -0
  129. package/dist/lib/ws-query-bridge.js +35 -0
  130. package/dist/pages/LandingPage.d.ts +22 -0
  131. package/dist/pages/LandingPage.js +5 -0
  132. package/dist/pages/LoginPage.d.ts +15 -0
  133. package/dist/pages/LoginPage.js +87 -0
  134. package/dist/pages/account/NotificationPreferencesPage.d.ts +13 -0
  135. package/dist/pages/account/NotificationPreferencesPage.js +41 -0
  136. package/dist/pages/account/ProfileSettingsPage.d.ts +4 -0
  137. package/dist/pages/account/ProfileSettingsPage.js +96 -0
  138. package/dist/pages/account/SecuritySettingsPage.d.ts +1 -0
  139. package/dist/pages/account/SecuritySettingsPage.js +134 -0
  140. package/dist/pages/account/TokensPage.d.ts +1 -0
  141. package/dist/pages/account/TokensPage.js +117 -0
  142. package/dist/pages/admin/AdminSettingsPage.d.ts +1 -0
  143. package/dist/pages/admin/AdminSettingsPage.js +210 -0
  144. package/dist/pages/admin/AdminShell.d.ts +7 -0
  145. package/dist/pages/admin/AdminShell.js +79 -0
  146. package/dist/pages/admin/AuditPage.d.ts +1 -0
  147. package/dist/pages/admin/AuditPage.js +164 -0
  148. package/dist/pages/admin/OrdersPage.d.ts +1 -0
  149. package/dist/pages/admin/OrdersPage.js +236 -0
  150. package/dist/pages/admin/OverviewPage.d.ts +1 -0
  151. package/dist/pages/admin/OverviewPage.js +200 -0
  152. package/dist/pages/admin/SSOPage.d.ts +1 -0
  153. package/dist/pages/admin/SSOPage.js +53 -0
  154. package/dist/pages/admin/UserDetailPage.d.ts +1 -0
  155. package/dist/pages/admin/UserDetailPage.js +266 -0
  156. package/dist/pages/admin/UsersPage.d.ts +4 -0
  157. package/dist/pages/admin/UsersPage.js +201 -0
  158. package/dist/pages/admin/WebhookDeliveriesPage.d.ts +1 -0
  159. package/dist/pages/admin/WebhookDeliveriesPage.js +128 -0
  160. package/dist/pages/admin/WorkspaceDetailPage.d.ts +1 -0
  161. package/dist/pages/admin/WorkspaceDetailPage.js +202 -0
  162. package/dist/pages/admin/WorkspacesPage.d.ts +1 -0
  163. package/dist/pages/admin/WorkspacesPage.js +133 -0
  164. package/dist/pages/admin/shared.d.ts +8 -0
  165. package/dist/pages/admin/shared.js +23 -0
  166. package/dist/pages/auth/ForgotPasswordPage.d.ts +6 -0
  167. package/dist/pages/auth/ForgotPasswordPage.js +32 -0
  168. package/dist/pages/auth/InviteAcceptPage.d.ts +5 -0
  169. package/dist/pages/auth/InviteAcceptPage.js +40 -0
  170. package/dist/pages/auth/RegisterPage.d.ts +15 -0
  171. package/dist/pages/auth/RegisterPage.js +80 -0
  172. package/dist/pages/auth/ResetPasswordPage.d.ts +4 -0
  173. package/dist/pages/auth/ResetPasswordPage.js +71 -0
  174. package/dist/pages/auth/VerifyEmailPage.d.ts +1 -0
  175. package/dist/pages/auth/VerifyEmailPage.js +45 -0
  176. package/dist/pages/workspace/BillingPage.d.ts +4 -0
  177. package/dist/pages/workspace/BillingPage.js +183 -0
  178. package/dist/pages/workspace/BillingSuccessPage.d.ts +4 -0
  179. package/dist/pages/workspace/BillingSuccessPage.js +72 -0
  180. package/dist/pages/workspace/MembersPage.d.ts +3 -0
  181. package/dist/pages/workspace/MembersPage.js +262 -0
  182. package/dist/pages/workspace/SchedulesPage.d.ts +4 -0
  183. package/dist/pages/workspace/SchedulesPage.js +177 -0
  184. package/dist/pages/workspace/UsagePage.d.ts +15 -0
  185. package/dist/pages/workspace/UsagePage.js +74 -0
  186. package/dist/pages/workspace/WebhooksPage.d.ts +3 -0
  187. package/dist/pages/workspace/WebhooksPage.js +242 -0
  188. package/dist/pages/workspace/WorkspaceAuditPage.d.ts +3 -0
  189. package/dist/pages/workspace/WorkspaceAuditPage.js +109 -0
  190. package/dist/pages/workspace/WorkspaceGeneralPage.d.ts +4 -0
  191. package/dist/pages/workspace/WorkspaceGeneralPage.js +101 -0
  192. package/dist/provider/PloykitProvider.d.ts +50 -0
  193. package/dist/provider/PloykitProvider.js +240 -0
  194. package/dist/realtime.d.ts +3 -0
  195. package/dist/realtime.js +11 -0
  196. 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,4 @@
1
+ export interface BillingPageProps {
2
+ channel?: string;
3
+ }
4
+ export declare function BillingPage({ channel }?: BillingPageProps): import("react").JSX.Element;
@@ -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,4 @@
1
+ export interface BillingSuccessPageProps {
2
+ redirectTo?: string;
3
+ }
4
+ export declare function BillingSuccessPage({ redirectTo }?: BillingSuccessPageProps): import("react").JSX.Element;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export interface MembersPageProps {
2
+ }
3
+ export declare function MembersPage(_props?: MembersPageProps): import("react").JSX.Element;