@ploykit/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +41 -3
  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 +63 -6
@@ -0,0 +1,262 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useSearchParams } from 'react-router';
4
+ import { z } from 'zod';
5
+ import { UserPlus, Link2 } from 'lucide-react';
6
+ import { PageHeader, PageLoading, PageError, PageEmpty, } from '../../components/Page';
7
+ import { DataTable } from '../../components/DataTable';
8
+ import { FormField } from '../../components/FormField';
9
+ import { SecretModal, ImpactConfirmation } from '../../components/SecretModal';
10
+ import { useConfirm } from '../../components/ConfirmDialog';
11
+ import { toast } from '../../components/toast';
12
+ import { Button } from '../../components/ui/Button';
13
+ import { Input } from '../../components/ui/Input';
14
+ import { Badge, StatusBadge } from '../../components/ui/Badge';
15
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
16
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../components/ui/select';
17
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '../../components/ui/dialog';
18
+ import { useZodForm } from '../../hooks/useZodForm';
19
+ import { useMembers, useMemberMutations, ASSIGNABLE_ROLES, LIST_PAGE_SIZE, } from '../../hooks/useMembers';
20
+ import { formatDateTime } from '../../lib/utils';
21
+ import { apiErrorMessage } from '../../lib/api-error';
22
+ import { roleSatisfies } from '../../lib/perm';
23
+ const inviteSchema = z.object({
24
+ email: z.string().min(1, '请输入邮箱').email('邮箱格式不正确'),
25
+ role: z.enum(['admin', 'member']),
26
+ });
27
+ const shareLinkSchema = z.object({
28
+ role: z.enum(['admin', 'member']),
29
+ max_uses: z.number().int('须为整数').min(1, '至少 1 次').max(10000),
30
+ ttl_hours: z.number().int('须为整数').min(1, '至少 1 小时').max(24 * 30),
31
+ });
32
+ const ROLE_LABEL = { owner: '所有者', admin: '管理员', member: '成员' };
33
+ const MEMBER_TABS = ['members', 'invitations', 'share-links'];
34
+ function pageFromParams(v) {
35
+ const n = Number.parseInt(v ?? '', 10);
36
+ return Number.isInteger(n) && n >= 1 ? n : 1;
37
+ }
38
+ export function MembersPage(_props = {}) {
39
+ const [searchParams, setSearchParams] = useSearchParams();
40
+ const tabParam = searchParams.get('tab');
41
+ const tab = tabParam !== null && MEMBER_TABS.includes(tabParam)
42
+ ? tabParam
43
+ : 'members';
44
+ const memberPage = pageFromParams(searchParams.get('mpage'));
45
+ const invitePage = pageFromParams(searchParams.get('ipage'));
46
+ const linkPage = pageFromParams(searchParams.get('lpage'));
47
+ const setTab = (t) => {
48
+ setSearchParams((prev) => {
49
+ const next = new URLSearchParams(prev);
50
+ next.set('tab', t);
51
+ return next;
52
+ });
53
+ };
54
+ const setPageParam = (key, page) => {
55
+ setSearchParams((prev) => {
56
+ const next = new URLSearchParams(prev);
57
+ next.set(key, String(page));
58
+ return next;
59
+ }, { replace: true });
60
+ };
61
+ const { wsId, myRole, members, membersTotal, invitations, invitationsTotal, shareLinks, shareLinksTotal, loading, membersError, invitationsError, shareLinksError, refetchAll, } = useMembers({ members: memberPage, invitations: invitePage, shareLinks: linkPage });
62
+ const mut = useMemberMutations();
63
+ const confirm = useConfirm();
64
+ const [inviteOpen, setInviteOpen] = useState(false);
65
+ const [shareOpen, setShareOpen] = useState(false);
66
+ const [secret, setSecret] = useState(null);
67
+ const [transferTarget, setTransferTarget] = useState(null);
68
+ const inviteForm = useZodForm({
69
+ schema: inviteSchema,
70
+ defaultValues: { email: '', role: 'member' },
71
+ });
72
+ const linkForm = useZodForm({
73
+ schema: shareLinkSchema,
74
+ defaultValues: { role: 'member', max_uses: 10, ttl_hours: 72 },
75
+ });
76
+ if (!wsId) {
77
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u6210\u5458", description: "\u7BA1\u7406\u5F53\u524D\u5DE5\u4F5C\u533A\u7684\u6210\u5458\u3001\u9080\u8BF7\u4E0E\u5206\u4EAB\u94FE\u63A5" }), _jsx(PageEmpty, { label: "\u8BF7\u5148\u521B\u5EFA\u5E76\u9009\u62E9\u4E00\u4E2A\u5DE5\u4F5C\u533A" })] }));
78
+ }
79
+ if (loading)
80
+ return _jsx(PageLoading, { label: "\u52A0\u8F7D\u6210\u5458\u6570\u636E\u2026" });
81
+ if (membersError) {
82
+ return (_jsx(PageError, { message: apiErrorMessage(membersError, '成员数据加载失败'), onRetry: () => refetchAll() }));
83
+ }
84
+ const canManage = roleSatisfies(myRole ?? undefined, 'admin');
85
+ const canTransfer = myRole === 'owner';
86
+ const onRoleChange = async (m, role) => {
87
+ if (role === m.role)
88
+ return;
89
+ try {
90
+ await mut.updateRole.mutateAsync({ userId: m.user_id, role });
91
+ toast.success(`已将 ${m.email} 的角色改为 ${ROLE_LABEL[role] ?? role}`);
92
+ }
93
+ catch (e) {
94
+ toast.error(apiErrorMessage(e, '角色变更失败'));
95
+ }
96
+ };
97
+ const onRemove = async (m) => {
98
+ if (!(await confirm({
99
+ title: '移除成员',
100
+ description: `确定将 ${m.email} 移出本工作区?其将立即失去访问权限。`,
101
+ confirmText: '移除', danger: true,
102
+ })))
103
+ return;
104
+ try {
105
+ await mut.removeMember.mutateAsync(m.user_id);
106
+ toast.success('成员已移除');
107
+ }
108
+ catch (e) {
109
+ toast.error(apiErrorMessage(e, '移除失败') + (isConflict(e) ? '(请先转移或删除工作区)' : ''));
110
+ }
111
+ };
112
+ const onRevokeInvite = async (inv) => {
113
+ if (!(await confirm({
114
+ title: '撤销邀请',
115
+ description: `撤销发给 ${inv.email} 的邀请?对方将无法再接受。`,
116
+ confirmText: '撤销', danger: true,
117
+ })))
118
+ return;
119
+ try {
120
+ await mut.revokeInvitation.mutateAsync(inv.id);
121
+ toast.success('邀请已撤销');
122
+ }
123
+ catch (e) {
124
+ toast.error(apiErrorMessage(e, '撤销失败'));
125
+ }
126
+ };
127
+ const onRevokeLink = async (link) => {
128
+ if (!(await confirm({
129
+ title: '撤销分享链接',
130
+ description: `撤销前缀为 ${link.code_prefix}… 的分享链接?已兑换的成员不受影响。`,
131
+ confirmText: '撤销', danger: true,
132
+ })))
133
+ return;
134
+ try {
135
+ await mut.revokeShareLink.mutateAsync(link.id);
136
+ toast.success('分享链接已撤销');
137
+ }
138
+ catch (e) {
139
+ toast.error(apiErrorMessage(e, '撤销失败'));
140
+ }
141
+ };
142
+ const onTransfer = async () => {
143
+ const m = transferTarget;
144
+ if (!m)
145
+ return;
146
+ try {
147
+ await mut.transferOwnership.mutateAsync(m.user_id);
148
+ toast.success(`所有权已转让给 ${m.email}(你已降为成员)`);
149
+ setTransferTarget(null);
150
+ }
151
+ catch (e) {
152
+ toast.error(apiErrorMessage(e, '所有权转让失败'));
153
+ }
154
+ };
155
+ const submitInvite = inviteForm.handleSubmit(async (values) => {
156
+ try {
157
+ await mut.invite.mutateAsync(values);
158
+ toast.success(`邀请已发送至 ${values.email}`);
159
+ inviteForm.reset();
160
+ setInviteOpen(false);
161
+ }
162
+ catch (e) {
163
+ toast.error(apiErrorMessage(e, '邀请发送失败'));
164
+ }
165
+ });
166
+ const submitLink = linkForm.handleSubmit(async (values) => {
167
+ try {
168
+ const res = await mut.createShareLink.mutateAsync(values);
169
+ toast.success('分享链接已创建');
170
+ setSecret(res.code);
171
+ linkForm.reset();
172
+ setShareOpen(false);
173
+ }
174
+ catch (e) {
175
+ toast.error(apiErrorMessage(e, '创建分享链接失败'));
176
+ }
177
+ });
178
+ const memberColumns = [
179
+ {
180
+ key: 'email',
181
+ header: '邮箱',
182
+ className: 'w-[28%]',
183
+ render: (m) => (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate font-medium text-foreground", children: m.email }), m.display_name && _jsx("p", { className: "truncate text-xs text-muted-foreground", children: m.display_name })] })),
184
+ },
185
+ {
186
+ key: 'role',
187
+ header: '角色',
188
+ className: 'w-[20%]',
189
+ render: (m) => m.role === 'owner' ? (_jsx(Badge, { variant: "outline", children: ROLE_LABEL.owner })) : canManage ? (_jsxs(Select, { value: m.role, onValueChange: (v) => typeof v === 'string' && onRoleChange(m, v), children: [_jsx(SelectTrigger, { size: "sm", "aria-label": `变更 ${m.email} 的角色`, children: _jsx(SelectValue, { children: ROLE_LABEL[m.role] ?? m.role }) }), _jsx(SelectContent, { children: ASSIGNABLE_ROLES.map((r) => (_jsx(SelectItem, { value: r, children: ROLE_LABEL[r] }, r))) })] })) : (_jsx(Badge, { variant: "secondary", children: ROLE_LABEL[m.role] ?? m.role })),
190
+ },
191
+ {
192
+ key: 'created_at',
193
+ header: '加入时间',
194
+ className: 'w-[22%]',
195
+ render: (m) => _jsx("span", { className: "text-muted-foreground", children: formatDateTime(m.created_at) }),
196
+ },
197
+ {
198
+ key: 'actions',
199
+ header: '操作',
200
+ className: 'w-[22%] text-right',
201
+ render: (m) => (_jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [canTransfer && m.role !== 'owner' && (_jsx(Button, { size: "sm", variant: "outline", disabled: mut.transferOwnership.isPending, title: "\u76EE\u6807\u5347\u4E3A owner\uFF0C\u4F60\u964D\u4E3A member\uFF08\u5355\u4E8B\u52A1\u539F\u5B50\uFF09", onClick: () => setTransferTarget(m), children: "\u8F6C\u8BA9\u6240\u6709\u6743" })), m.role !== 'owner' && canManage ? (_jsx(Button, { size: "sm", variant: "destructive", disabled: mut.removeMember.isPending, onClick: () => onRemove(m), children: "\u79FB\u9664" })) : null] })),
202
+ },
203
+ ];
204
+ const invitationColumns = [
205
+ { key: 'email', header: '邮箱', render: (i) => _jsx("span", { className: "font-medium", children: i.email }) },
206
+ { key: 'role', header: '角色', render: (i) => _jsx(Badge, { variant: "secondary", children: ROLE_LABEL[i.role] ?? i.role }) },
207
+ { key: 'status', header: '状态', render: (i) => _jsx(StatusBadge, { status: i.status }) },
208
+ {
209
+ key: 'expires_at', header: '过期时间',
210
+ render: (i) => _jsx("span", { className: "text-muted-foreground", children: formatDateTime(i.expires_at) }),
211
+ },
212
+ {
213
+ key: 'actions', header: '操作', className: 'text-right',
214
+ render: (i) => i.status === 'pending' && canManage ? (_jsx(Button, { size: "sm", variant: "outline", disabled: mut.revokeInvitation.isPending, onClick: () => onRevokeInvite(i), children: "\u64A4\u9500" })) : null,
215
+ },
216
+ ];
217
+ const shareLinkColumns = [
218
+ {
219
+ key: 'code_prefix', header: '兑换码前缀',
220
+ render: (l) => _jsxs("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: [l.code_prefix, "\u2026"] }),
221
+ },
222
+ { key: 'role', header: '授予角色', render: (l) => _jsx(Badge, { variant: "secondary", children: ROLE_LABEL[l.role] ?? l.role }) },
223
+ {
224
+ key: 'uses', header: '兑换 / 上限',
225
+ render: (l) => (_jsxs("span", { className: "tabular-nums", children: [l.uses, " / ", l.max_uses === -1 ? '不限' : l.max_uses] })),
226
+ },
227
+ {
228
+ key: 'expires_at', header: '过期时间',
229
+ render: (l) => _jsx("span", { className: "text-muted-foreground", children: formatDateTime(l.expires_at) }),
230
+ },
231
+ {
232
+ key: 'actions', header: '操作', className: 'text-right',
233
+ render: (l) => canManage ? (_jsx(Button, { size: "sm", variant: "outline", disabled: mut.revokeShareLink.isPending, onClick: () => onRevokeLink(l), children: "\u64A4\u9500" })) : null,
234
+ },
235
+ ];
236
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u6210\u5458", description: "\u7BA1\u7406\u5F53\u524D\u5DE5\u4F5C\u533A\u7684\u6210\u5458\u3001\u9080\u8BF7\u4E0E\u5206\u4EAB\u94FE\u63A5", actions: canManage ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", onClick: () => setShareOpen(true), children: [_jsx(Link2, {}), " \u521B\u5EFA\u5206\u4EAB\u94FE\u63A5"] }), _jsxs(Button, { onClick: () => setInviteOpen(true), children: [_jsx(UserPlus, {}), " \u9080\u8BF7\u6210\u5458"] })] })) : undefined }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "members", children: ["\u6210\u5458\uFF08", membersTotal, "\uFF09"] }), _jsxs(TabsTrigger, { value: "invitations", children: ["\u9080\u8BF7\uFF08", invitationsTotal, "\uFF09"] }), _jsxs(TabsTrigger, { value: "share-links", children: ["\u5206\u4EAB\u94FE\u63A5\uFF08", shareLinksTotal, "\uFF09"] })] }), _jsx(TabsContent, { value: "members", className: "mt-4", children: _jsx(DataTable, { columns: memberColumns, rows: members, rowKey: (m) => m.user_id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u6210\u5458" }), pagination: {
237
+ page: memberPage,
238
+ pageSize: LIST_PAGE_SIZE,
239
+ total: membersTotal,
240
+ onPageChange: (p) => setPageParam('mpage', p),
241
+ } }) }), _jsx(TabsContent, { value: "invitations", className: "mt-4", children: invitationsError ? (_jsx(PageError, { message: apiErrorMessage(invitationsError, '邀请列表加载失败'), onRetry: () => refetchAll() })) : (_jsx(DataTable, { columns: invitationColumns, rows: invitations, rowKey: (i) => i.id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u9080\u8BF7", hint: "\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u9080\u8BF7\u6210\u5458\u201D\u53D1\u9001\u65B0\u9080\u8BF7" }), pagination: {
242
+ page: invitePage,
243
+ pageSize: LIST_PAGE_SIZE,
244
+ total: invitationsTotal,
245
+ onPageChange: (p) => setPageParam('ipage', p),
246
+ } })) }), _jsx(TabsContent, { value: "share-links", className: "mt-4", children: shareLinksError ? (_jsx(PageError, { message: apiErrorMessage(shareLinksError, '分享链接加载失败'), onRetry: () => refetchAll() })) : (_jsx(DataTable, { columns: shareLinkColumns, rows: shareLinks, rowKey: (l) => l.id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u5206\u4EAB\u94FE\u63A5", hint: "\u5206\u4EAB\u94FE\u63A5\u53EF\u8BA9\u7528\u6237\u81EA\u52A9\u52A0\u5165\u5DE5\u4F5C\u533A\uFF1B\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u521B\u5EFA\u5206\u4EAB\u94FE\u63A5\u201D\u751F\u6210\u5151\u6362\u7801" }), pagination: {
247
+ page: linkPage,
248
+ pageSize: LIST_PAGE_SIZE,
249
+ total: shareLinksTotal,
250
+ onPageChange: (p) => setPageParam('lpage', p),
251
+ } })) })] }), _jsx(Dialog, { open: inviteOpen, onOpenChange: setInviteOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "\u9080\u8BF7\u6210\u5458" }), _jsx(DialogDescription, { children: "\u5411\u5BF9\u65B9\u90AE\u7BB1\u53D1\u9001\u52A0\u5165\u672C\u5DE5\u4F5C\u533A\u7684\u9080\u8BF7\u3002" })] }), _jsxs("form", { className: "space-y-4", onSubmit: submitInvite, noValidate: true, children: [_jsx(FormField, { label: "\u90AE\u7BB1", error: inviteForm.formState.errors.email?.message, htmlFor: "invite-email", children: _jsx(Input, { id: "invite-email", type: "email", autoComplete: "email", placeholder: "name@example.com", ...inviteForm.register('email') }) }), _jsx(FormField, { label: "\u89D2\u8272", error: inviteForm.formState.errors.role?.message, children: _jsxs(Select, { value: inviteForm.watch('role'), onValueChange: (v) => typeof v === 'string' && inviteForm.setValue('role', v), children: [_jsx(SelectTrigger, { className: "w-full", "aria-label": "\u9080\u8BF7\u89D2\u8272", children: _jsx(SelectValue, { children: ROLE_LABEL[inviteForm.watch('role')] }) }), _jsx(SelectContent, { children: ASSIGNABLE_ROLES.map((r) => (_jsx(SelectItem, { value: r, children: ROLE_LABEL[r] }, r))) })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setInviteOpen(false), children: "\u53D6\u6D88" }), _jsx(Button, { type: "submit", disabled: mut.invite.isPending, children: mut.invite.isPending ? '发送中…' : '发送邀请' })] })] })] }) }), _jsx(Dialog, { open: shareOpen, onOpenChange: setShareOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "\u521B\u5EFA\u5206\u4EAB\u94FE\u63A5" }), _jsx(DialogDescription, { children: "\u751F\u6210\u53EF\u5206\u4EAB\u7684\u5151\u6362\u7801\uFF1B\u6301\u6709\u8005\u53EF\u6309\u6240\u9009\u89D2\u8272\u81EA\u52A9\u52A0\u5165\u5DE5\u4F5C\u533A\u3002\u5B8C\u6574\u5151\u6362\u7801\u4EC5\u521B\u5EFA\u65F6\u5C55\u793A\u4E00\u6B21\u3002" })] }), _jsxs("form", { className: "space-y-4", onSubmit: submitLink, children: [_jsx(FormField, { label: "\u6388\u4E88\u89D2\u8272", error: linkForm.formState.errors.role?.message, children: _jsxs(Select, { value: linkForm.watch('role'), onValueChange: (v) => typeof v === 'string' && linkForm.setValue('role', v), children: [_jsx(SelectTrigger, { className: "w-full", "aria-label": "\u5206\u4EAB\u94FE\u63A5\u89D2\u8272", children: _jsx(SelectValue, { children: ROLE_LABEL[linkForm.watch('role')] }) }), _jsx(SelectContent, { children: ASSIGNABLE_ROLES.map((r) => (_jsx(SelectItem, { value: r, children: ROLE_LABEL[r] }, r))) })] }) }), _jsx(FormField, { label: "\u6700\u5927\u5151\u6362\u6B21\u6570", error: linkForm.formState.errors.max_uses?.message, htmlFor: "link-max", children: _jsx(Input, { id: "link-max", type: "number", min: 1, ...linkForm.register('max_uses', { valueAsNumber: true }) }) }), _jsx(FormField, { label: "\u6709\u6548\u671F\uFF08\u5C0F\u65F6\uFF09", error: linkForm.formState.errors.ttl_hours?.message, htmlFor: "link-ttl", children: _jsx(Input, { id: "link-ttl", type: "number", min: 1, ...linkForm.register('ttl_hours', { valueAsNumber: true }) }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setShareOpen(false), children: "\u53D6\u6D88" }), _jsx(Button, { type: "submit", disabled: mut.createShareLink.isPending, children: mut.createShareLink.isPending ? '创建中…' : '创建' })] })] })] }) }), _jsx(SecretModal, { open: secret !== null, onOpenChange: (o) => !o && setSecret(null), secretName: "\u5206\u4EAB\u94FE\u63A5\u5151\u6362\u7801", secretValue: secret ?? '' }), transferTarget && (_jsx(ImpactConfirmation, { open: true, onOpenChange: (o) => {
252
+ if (!o)
253
+ setTransferTarget(null);
254
+ }, onConfirm: () => void onTransfer(), title: `转让所有权给 ${transferTarget.email}`, confirmText: "\u786E\u8BA4\u8F6C\u8BA9", danger: true, impacts: [
255
+ `${transferTarget.email} 将成为新 owner(获得工作区最高权与计费控制权)`,
256
+ '你将降为 member(普通成员权限;转让单事务原子,失败不产生中间态)',
257
+ '目标须是本工作区活跃成员且账号未禁用;钩子拦截/非法目标返回 409',
258
+ ] }))] }));
259
+ }
260
+ function isConflict(e) {
261
+ return typeof e === 'object' && e !== null && 'status' in e && e.status === 409;
262
+ }
@@ -0,0 +1,4 @@
1
+ export declare const MISFIRE_LABEL: Record<string, string>;
2
+ export interface SchedulesPageProps {
3
+ }
4
+ export declare function SchedulesPage(_props?: SchedulesPageProps): import("react").JSX.Element;
@@ -0,0 +1,177 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { Plus } from 'lucide-react';
5
+ import { z } from 'zod';
6
+ import { PageHeader, PageLoading, PageError, PageEmpty, } from '../../components/Page';
7
+ import { DataTable } from '../../components/DataTable';
8
+ import { FormField } from '../../components/FormField';
9
+ import { CronInput, describeCron } from '../../components/CronInput';
10
+ import { toast } from '../../components/toast';
11
+ import { Button } from '../../components/ui/Button';
12
+ import { Input } from '../../components/ui/Input';
13
+ import { Switch } from '../../components/ui/switch';
14
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '../../components/ui/dialog';
15
+ import { useApi } from '../../hooks/useApi';
16
+ import { useWorkspace } from '../../hooks/useWorkspace';
17
+ import { useZodForm } from '../../hooks/useZodForm';
18
+ import { useConfirm } from '../../components/ConfirmDialog';
19
+ import { browserTzLabel, formatDateTimeTz } from '../../lib/utils';
20
+ import { apiErrorMessage } from '../../lib/api-error';
21
+ export const MISFIRE_LABEL = {
22
+ skip: '错过跳过',
23
+ once: '错过补发',
24
+ };
25
+ function localTimezone() {
26
+ try {
27
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
28
+ }
29
+ catch {
30
+ return 'UTC';
31
+ }
32
+ }
33
+ const createSchema = z.object({
34
+ kind: z.string().trim().min(1, '请填写触发标识(kind),如 task.cleanup'),
35
+ cron_expr: z.string().trim().regex(/^\S+\s+\S+\s+\S+\s+\S+\s+\S+$/, '表达式须为五字段:分 时 日 月 周'),
36
+ timezone: z.string().min(1, '请选择时区'),
37
+ misfire: z.enum(['skip', 'once']),
38
+ });
39
+ export function SchedulesPage(_props = {}) {
40
+ const { current } = useWorkspace();
41
+ const api = useApi();
42
+ const wsId = current?.id ?? '';
43
+ const base = '/api/schedules';
44
+ const [createOpen, setCreateOpen] = useState(false);
45
+ const tzLabel = browserTzLabel();
46
+ const plansQ = useQuery({
47
+ queryKey: ['schedules', wsId],
48
+ queryFn: () => api.get(base),
49
+ enabled: !!wsId,
50
+ });
51
+ const qc = useQueryClient();
52
+ const invalidate = () => qc.invalidateQueries({ queryKey: ['schedules', wsId] });
53
+ const onError = (e, fallback) => toast.error(apiErrorMessage(e, fallback));
54
+ const toggle = useMutation({
55
+ mutationFn: (p) => api.patch(`${base}/${p.id}`, { enabled: !p.enabled }),
56
+ onSuccess: async () => { toast.success('状态已更新'); await invalidate(); },
57
+ onError: (e) => onError(e, '操作失败'),
58
+ });
59
+ const create = useMutation({
60
+ mutationFn: (values) => api.post(base, values),
61
+ onSuccess: async () => { toast.success('计划已创建'); await invalidate(); },
62
+ onError: (e) => onError(e, '创建失败'),
63
+ });
64
+ const [editing, setEditing] = useState(null);
65
+ const updateM = useMutation({
66
+ mutationFn: (v) => api.patch(`${base}/${v.plan.id}`, v.body),
67
+ onSuccess: async (_d, v) => {
68
+ toast.success(`计划 ${v.plan.kind} 已更新`);
69
+ setEditing(null);
70
+ await invalidate();
71
+ },
72
+ onError: (e) => onError(e, '更新失败'),
73
+ });
74
+ const confirm = useConfirm();
75
+ const removeM = useMutation({
76
+ mutationFn: (p) => api.delete(`${base}/${p.id}`),
77
+ onSuccess: async (_d, p) => { toast.success(`计划 ${p.kind} 已删除`); await invalidate(); },
78
+ onError: (e) => onError(e, '删除失败'),
79
+ });
80
+ const onRemove = async (p) => {
81
+ const ok = await confirm({
82
+ title: '删除调度计划',
83
+ description: `确定删除计划 ${p.kind}(${describeCron(p.cron_expr)},计划时区 ${p.timezone})吗?` +
84
+ `下次触发 ${formatDateTimeTz(p.next_fire_at)}。删除后该计划不再触发,操作不可恢复。`,
85
+ confirmText: '删除',
86
+ danger: true,
87
+ });
88
+ if (ok)
89
+ removeM.mutate(p);
90
+ };
91
+ const form = useZodForm({
92
+ schema: createSchema,
93
+ defaultValues: { kind: '', cron_expr: '0 9 * * *', timezone: localTimezone(), misfire: 'skip' },
94
+ });
95
+ const cronValue = { cron: form.watch('cron_expr') ?? '', timezone: form.watch('timezone') ?? 'UTC' };
96
+ const setCron = (v) => {
97
+ form.setValue('cron_expr', v.cron, { shouldValidate: true });
98
+ form.setValue('timezone', v.timezone, { shouldValidate: true });
99
+ };
100
+ const submitCreate = form.handleSubmit((values) => {
101
+ create.mutate(values, {
102
+ onSuccess: () => {
103
+ form.reset();
104
+ setCreateOpen(false);
105
+ },
106
+ });
107
+ });
108
+ const editForm = useZodForm({
109
+ schema: createSchema,
110
+ defaultValues: { kind: '', cron_expr: '', timezone: 'UTC', misfire: 'skip' },
111
+ values: editing
112
+ ? {
113
+ kind: editing.kind,
114
+ cron_expr: editing.cron_expr,
115
+ timezone: editing.timezone,
116
+ misfire: editing.misfire === 'once' ? 'once' : 'skip',
117
+ }
118
+ : undefined,
119
+ });
120
+ const editCronValue = { cron: editForm.watch('cron_expr') ?? '', timezone: editForm.watch('timezone') ?? 'UTC' };
121
+ const setEditCron = (v) => {
122
+ editForm.setValue('cron_expr', v.cron, { shouldValidate: true });
123
+ editForm.setValue('timezone', v.timezone, { shouldValidate: true });
124
+ };
125
+ const submitEdit = editForm.handleSubmit((values) => {
126
+ if (!editing)
127
+ return;
128
+ const body = {};
129
+ if (values.cron_expr !== editing.cron_expr)
130
+ body.cron_expr = values.cron_expr;
131
+ if (values.timezone !== editing.timezone)
132
+ body.timezone = values.timezone;
133
+ if (values.misfire !== editing.misfire)
134
+ body.misfire = values.misfire;
135
+ if (Object.keys(body).length === 0) {
136
+ setEditing(null);
137
+ return;
138
+ }
139
+ updateM.mutate({ plan: editing, body });
140
+ });
141
+ const columns = [
142
+ {
143
+ key: 'kind', header: '触发',
144
+ render: (p) => (_jsxs("div", { className: "min-w-0", children: [_jsx("code", { className: "block max-w-[20rem] break-all rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: p.kind }), _jsx("p", { className: "mt-1 max-w-[20rem] break-words text-xs text-muted-foreground", children: describeCron(p.cron_expr) })] })),
145
+ },
146
+ {
147
+ key: 'cron', header: '表达式 / 时区',
148
+ render: (p) => (_jsxs("div", { className: "min-w-0", children: [_jsx("code", { className: "block max-w-[20rem] break-all font-mono text-xs", children: p.cron_expr }), _jsx("p", { className: "max-w-[20rem] break-words text-xs text-muted-foreground", children: p.timezone })] })),
149
+ },
150
+ {
151
+ key: 'misfire', header: '错过策略',
152
+ render: (p) => (_jsx("span", { className: "rounded border bg-muted px-1.5 py-0.5 text-xs", children: MISFIRE_LABEL[p.misfire] ?? p.misfire })),
153
+ },
154
+ {
155
+ key: 'next_fire_at', header: '下次触发',
156
+ render: (p) => (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", title: `计划时区 ${p.timezone};此处按浏览器本地时区(${tzLabel})显示`, children: formatDateTimeTz(p.next_fire_at) })),
157
+ },
158
+ {
159
+ key: 'last_fired_at', header: '最近触发',
160
+ render: (p) => p.last_fired_at
161
+ ? (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", title: `计划时区 ${p.timezone};此处按浏览器本地时区(${tzLabel})显示`, children: formatDateTimeTz(p.last_fired_at) }))
162
+ : _jsx("span", { className: "text-muted-foreground", children: "\u2014" }),
163
+ },
164
+ {
165
+ key: 'enabled', header: '启用',
166
+ render: (p) => (_jsx(Switch, { checked: p.enabled, disabled: toggle.isPending, onCheckedChange: () => toggle.mutate(p), "aria-label": `${p.enabled ? '停用' : '启用'}计划 ${p.kind}` })),
167
+ },
168
+ {
169
+ key: 'actions', header: '', className: 'text-right',
170
+ render: (p) => (_jsxs("div", { className: "flex justify-end gap-1", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setEditing(p), children: "\u7F16\u8F91" }), _jsx(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", disabled: removeM.isPending, onClick: () => void onRemove(p), children: "\u5220\u9664" })] })),
171
+ },
172
+ ];
173
+ if (!wsId) {
174
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u8C03\u5EA6", description: "\u7528\u6237\u81EA\u5B9A\u4E49\u5B9A\u65F6\u89E6\u53D1\uFF08cron + \u65F6\u533A\uFF09" }), _jsx(PageEmpty, { label: "\u8BF7\u5148\u521B\u5EFA\u5E76\u9009\u62E9\u4E00\u4E2A\u5DE5\u4F5C\u533A" })] }));
175
+ }
176
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u8C03\u5EA6", description: `用户自定义定时触发(工作区:${current?.name ?? wsId})`, actions: _jsxs(Button, { onClick: () => setCreateOpen(true), children: [_jsx(Plus, {}), " \u521B\u5EFA\u8BA1\u5212"] }) }), plansQ.isError ? (_jsx(PageError, { message: apiErrorMessage(plansQ.error, '计划列表加载失败'), onRetry: () => plansQ.refetch() })) : plansQ.isPending ? (_jsx(PageLoading, { label: "\u52A0\u8F7D\u8BA1\u5212\u2026" })) : (_jsx(DataTable, { columns: columns, rows: plansQ.data?.items ?? [], rowKey: (p) => p.id, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u8C03\u5EA6\u8BA1\u5212", hint: "\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u521B\u5EFA\u8BA1\u5212\u201D\u8BBE\u7F6E\u7B2C\u4E00\u4E2A\u5B9A\u65F6\u89E6\u53D1" }) })), _jsx(Dialog, { open: createOpen, onOpenChange: setCreateOpen, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "\u521B\u5EFA\u8C03\u5EA6\u8BA1\u5212" }), _jsx(DialogDescription, { children: "\u4E94\u5B57\u6BB5 cron \u8868\u8FBE\u5F0F + IANA \u65F6\u533A\uFF1B\u9519\u8FC7\u7B56\u7565\"\u8DF3\u8FC7\"\u7B49\u4E0B\u4E00\u4E2A\u5468\u671F\uFF0C \"\u8865\u53D1\"\u4F1A\u5148\u8865\u6700\u8FD1\u4E00\u6B21\u3002\u9884\u89C8\u7531\u540E\u7AEF\u8BA1\u7B97\uFF0C\u4E0E\u5B9E\u9645\u89E6\u53D1\u4E00\u81F4\u3002" })] }), _jsxs("form", { className: "space-y-4", onSubmit: submitCreate, children: [_jsx(FormField, { label: "\u89E6\u53D1\u6807\u8BC6\uFF08kind\uFF09", error: form.formState.errors.kind?.message, htmlFor: "sched-kind", children: _jsx(Input, { id: "sched-kind", placeholder: "task.cleanup", ...form.register('kind') }) }), _jsx(FormField, { label: "\u8C03\u5EA6\u89C4\u5219", error: form.formState.errors.cron_expr?.message ?? form.formState.errors.timezone?.message, children: _jsx(CronInput, { idPrefix: "sched", value: cronValue, onChange: setCron }) }), _jsx(FormField, { label: "\u9519\u8FC7\u7B56\u7565", error: form.formState.errors.misfire?.message, htmlFor: "sched-misfire", children: _jsxs("select", { id: "sched-misfire", className: "h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", ...form.register('misfire'), children: [_jsx("option", { value: "skip", children: "\u9519\u8FC7\u8DF3\u8FC7\uFF08\u9ED8\u8BA4\uFF09" }), _jsx("option", { value: "once", children: "\u9519\u8FC7\u8865\u53D1\u4E00\u6B21" })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setCreateOpen(false), children: "\u53D6\u6D88" }), _jsx(Button, { type: "submit", disabled: create.isPending, children: create.isPending ? '创建中…' : '创建计划' })] })] })] }) }), _jsx(Dialog, { open: editing !== null, onOpenChange: (o) => !o && setEditing(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "\u7F16\u8F91\u8C03\u5EA6\u8BA1\u5212" }), _jsxs(DialogDescription, { children: ["\u4FEE\u6539 ", editing?.kind, " \u7684\u8C03\u5EA6\u89C4\u5219\uFF1B\u53EA\u63D0\u4EA4\u6539\u52A8\u7684\u5B57\u6BB5\uFF0C\u4E0B\u6B21\u89E6\u53D1\u65F6\u95F4\u6309\u65B0\u89C4\u5219\u91CD\u65B0\u8BA1\u7B97\u3002"] })] }), _jsxs("form", { className: "space-y-4", onSubmit: submitEdit, children: [_jsx(FormField, { label: "\u89E6\u53D1\u6807\u8BC6\uFF08kind\uFF0C\u4E0D\u53EF\u4FEE\u6539\uFF09", htmlFor: "sched-edit-kind", children: _jsx(Input, { id: "sched-edit-kind", value: editing?.kind ?? '', readOnly: true, disabled: true }) }), _jsx(FormField, { label: "\u8C03\u5EA6\u89C4\u5219", error: editForm.formState.errors.cron_expr?.message ?? editForm.formState.errors.timezone?.message, children: _jsx(CronInput, { idPrefix: "sched-edit", value: editCronValue, onChange: setEditCron }) }), _jsx(FormField, { label: "\u9519\u8FC7\u7B56\u7565", error: editForm.formState.errors.misfire?.message, htmlFor: "sched-edit-misfire", children: _jsxs("select", { id: "sched-edit-misfire", className: "h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", ...editForm.register('misfire'), children: [_jsx("option", { value: "skip", children: "\u9519\u8FC7\u8DF3\u8FC7\uFF08\u9ED8\u8BA4\uFF09" }), _jsx("option", { value: "once", children: "\u9519\u8FC7\u8865\u53D1\u4E00\u6B21" })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setEditing(null), children: "\u53D6\u6D88" }), _jsx(Button, { type: "submit", disabled: updateM.isPending, children: updateM.isPending ? '保存中…' : '保存修改' })] })] })] }) })] }));
177
+ }
@@ -0,0 +1,15 @@
1
+ import { type PKUsageItem } from '../../hooks/useUsage';
2
+ interface ReservationDetail {
3
+ id: string;
4
+ amount: number;
5
+ status: string;
6
+ expires_at: string;
7
+ }
8
+ export type UsageItem = PKUsageItem & {
9
+ reserved?: number;
10
+ reservations?: ReservationDetail[];
11
+ };
12
+ export interface UsagePageProps {
13
+ }
14
+ export declare function UsagePage(_props?: UsagePageProps): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Link } from 'react-router';
3
+ import { PageHeader, PageLoading, PageError, PageEmpty } from '../../components/Page';
4
+ import { Badge } from '../../components/ui/Badge';
5
+ import { Card, CardHeader, CardTitle, CardAction, CardContent } from '../../components/ui/card';
6
+ import { Progress, ProgressLabel, ProgressValue, } from '../../components/ui/progress';
7
+ import { DataTable } from '../../components/DataTable';
8
+ import { useUsage } from '../../hooks/useUsage';
9
+ import { apiErrorMessage } from '../../lib/api-error';
10
+ const NEAR_FULL_PCT = 80;
11
+ const DIM_LABEL = {
12
+ tasks_monthly: '任务数(本月)',
13
+ workspaces: '工作区数',
14
+ seats: '成员席位',
15
+ webhooks: 'Webhook 订阅数',
16
+ };
17
+ function dimLabel(key) {
18
+ return DIM_LABEL[key] ?? key;
19
+ }
20
+ function fmtNum(n) {
21
+ return n.toLocaleString('zh-CN');
22
+ }
23
+ const RESERVATION_STATUS_LABEL = {
24
+ reserved: '预留中',
25
+ settled: '已结算',
26
+ released: '已释放',
27
+ expired: '已过期',
28
+ };
29
+ function reservationStatusLabel(status) {
30
+ return RESERVATION_STATUS_LABEL[status] ?? status;
31
+ }
32
+ function formatExpires(iso) {
33
+ return iso.slice(0, 16).replace('T', ' ');
34
+ }
35
+ function SegmentedProgress({ item, reserved, total }) {
36
+ const base = Math.max(total, 1);
37
+ const usedPct = Math.min(100, Math.round(((item.used + item.granted) / base) * 100));
38
+ const reservedPct = Math.min(100 - usedPct, Math.round((reserved / base) * 100));
39
+ const now = item.used + item.granted + reserved;
40
+ return (_jsx("div", { "data-slot": "progress", role: "progressbar", "aria-label": `${dimLabel(item.key)} 用量`, "aria-valuemin": 0, "aria-valuemax": Math.max(total, now), "aria-valuenow": now, "aria-valuetext": `已用 ${fmtNum(item.used)}${item.granted > 0 ? `(另获赠 ${fmtNum(item.granted)})` : ''},预留中 ${fmtNum(reserved)},上限 ${fmtNum(total)}`, className: "flex flex-wrap gap-3", children: _jsxs("div", { className: "relative h-1.5 w-full overflow-hidden rounded-full bg-muted", children: [_jsx("div", { "data-testid": "progress-indicator-used", "data-slot": "progress-indicator-used", className: "absolute inset-y-0 left-0 bg-primary transition-all", style: { width: `${usedPct}%` } }), _jsx("div", { "data-testid": "progress-indicator-reserved", "data-slot": "progress-indicator-reserved", className: "absolute inset-y-0 bg-primary/40 transition-all", style: { width: `${reservedPct}%`, left: `${usedPct}%` } })] }) }));
41
+ }
42
+ const RESERVATION_COLUMNS = [
43
+ { key: 'amount', header: '数量' },
44
+ { key: 'status', header: '状态', render: (r) => reservationStatusLabel(r.status) },
45
+ { key: 'expires_at', header: '到期时间', render: (r) => formatExpires(r.expires_at) },
46
+ ];
47
+ function UsageCard({ item }) {
48
+ const unlimited = item.limit < 0;
49
+ const reserved = item.reserved ?? 0;
50
+ const hasReserved = reserved > 0;
51
+ const total = unlimited ? Math.max(item.used + reserved, 1) : item.limit;
52
+ const pct = unlimited
53
+ ? Math.min(100, Math.round(((item.used + reserved) / Math.max(total, 10)) * 100))
54
+ : Math.min(100, Math.round(((item.used + item.granted + reserved) / Math.max(total, 1)) * 100));
55
+ const committed = item.used + item.granted + reserved;
56
+ const full = !unlimited && committed >= item.limit;
57
+ const nearFull = !unlimited && !full && pct >= NEAR_FULL_PCT;
58
+ const valueText = `${fmtNum(item.used)}${item.granted > 0 ? ` (+${fmtNum(item.granted)})` : ''} / ${unlimited ? '∞' : fmtNum(total)}`;
59
+ return (_jsxs(Card, { size: "sm", children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { title: DIM_LABEL[item.key] ? item.key : undefined, children: dimLabel(item.key) }), unlimited && _jsx(CardAction, { children: _jsx(Badge, { variant: "secondary", children: "\u4E0D\u9650\u91CF" }) }), !unlimited && full && _jsx(CardAction, { children: _jsx(Badge, { variant: "destructive", children: "\u5DF2\u7528\u5C3D" }) })] }), _jsxs(CardContent, { children: [hasReserved ? (_jsxs(_Fragment, { children: [_jsx(SegmentedProgress, { item: item, reserved: reserved, total: total }), _jsxs("p", { className: "mt-1.5 text-sm text-muted-foreground", children: ["\u5DF2\u7528 ", _jsxs("span", { className: "font-medium text-foreground tabular-nums", children: [fmtNum(item.used), item.granted > 0 ? ` (+${fmtNum(item.granted)})` : ''] }), ' · 预留中 ', _jsx("span", { className: "font-medium text-foreground tabular-nums", children: fmtNum(reserved) }), ` / ${unlimited ? '∞' : fmtNum(total)}`] })] })) : (_jsxs(Progress, { value: pct, children: [_jsx(ProgressLabel, { children: dimLabel(item.key) }), _jsx(ProgressValue, { children: () => valueText })] })), !unlimited && (full || nearFull) && (_jsxs("p", { className: "mt-2 text-xs text-muted-foreground", children: [full ? `该维度配额已用尽${hasReserved ? '(含预留中)' : ''},可升级套餐提高上限。` : '该维度配额即将用尽。', _jsx(Link, { to: "/settings/workspace/billing", className: "ml-1 inline-flex items-center py-1.5 -my-1.5 text-primary underline-offset-2 hover:underline", children: "\u5347\u7EA7\u5957\u9910" })] })), hasReserved && (item.reservations?.length ?? 0) > 0 && (_jsxs("details", { className: "mt-3 group", "data-testid": "reservation-details", children: [_jsxs("summary", { className: "cursor-pointer select-none text-xs text-muted-foreground hover:text-foreground", children: ["\u9884\u7559\u660E\u7EC6\uFF08", item.reservations.length, " \u7B14\uFF09"] }), _jsx("div", { className: "mt-2", children: _jsx(DataTable, { columns: RESERVATION_COLUMNS, rows: item.reservations, rowKey: (r) => r.id }) })] }))] })] }));
60
+ }
61
+ export function UsagePage(_props = {}) {
62
+ const { wsId, usage, loading, error, refetch } = useUsage();
63
+ if (!wsId) {
64
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u7528\u91CF" }), _jsx(PageEmpty, { label: "\u8BF7\u5148\u521B\u5EFA\u5E76\u9009\u62E9\u4E00\u4E2A\u5DE5\u4F5C\u533A" })] }));
65
+ }
66
+ if (loading)
67
+ return _jsx(PageLoading, { label: "\u52A0\u8F7D\u7528\u91CF\u6570\u636E\u2026" });
68
+ if (error) {
69
+ return _jsx(PageError, { message: apiErrorMessage(error, '用量数据加载失败'), onRetry: () => refetch() });
70
+ }
71
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u7528\u91CF", description: usage
72
+ ? `当前套餐 ${usage.plan_code} · 计费周期 ${usage.period}(周期内累计,超限将按套餐规则限制或计费)`
73
+ : undefined }), usage && usage.items.length > 0 ? (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: usage.items.map((item) => (_jsx(UsageCard, { item: item }, item.key))) })) : (_jsx(PageEmpty, { label: "\u5F53\u524D\u5957\u9910\u6CA1\u6709\u8BA1\u91CF\u7EF4\u5EA6" }))] }));
74
+ }
@@ -0,0 +1,3 @@
1
+ export interface WebhooksPageProps {
2
+ }
3
+ export declare function WebhooksPage(_props?: WebhooksPageProps): import("react").JSX.Element;