@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,236 @@
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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
5
+ import { useConfirm } from '../../components/ConfirmDialog';
6
+ import { DataTable } from '../../components/DataTable';
7
+ import { PageEmpty, PageError, PageHeader } from '../../components/Page';
8
+ import { SettingsCard } from '../../components/SettingsCard';
9
+ import { toast } from '../../components/toast';
10
+ import { Badge, StatusBadge } from '../../components/ui/Badge';
11
+ import { Button } from '../../components/ui/Button';
12
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../components/ui/select';
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger, } from '../../components/ui/tabs';
14
+ import { useApi } from '../../hooks/useApi';
15
+ import { formatDateTime, shortID, statusLabel } from '../../lib/utils';
16
+ import { ADMIN_PAGE_SIZE, adminError, adminListQuery, formatCents, } from './shared';
17
+ const ORDER_STATUSES = ['pending', 'paid', 'failed', 'canceled', 'refunded'];
18
+ function orderStatusLabel(s) {
19
+ if (s === 'refunded')
20
+ return '已退款';
21
+ return statusLabel(s);
22
+ }
23
+ const PROCESS_STATUS_LABEL = {
24
+ processed: '已处理',
25
+ pending: '待处理',
26
+ error: '处理出错',
27
+ };
28
+ export function OrdersPage() {
29
+ const api = useApi();
30
+ const qc = useQueryClient();
31
+ const confirm = useConfirm();
32
+ const [searchParams, setSearchParams] = useSearchParams();
33
+ const statusParam = searchParams.get('status') ?? '';
34
+ const status = ORDER_STATUSES.includes(statusParam) ? statusParam : '';
35
+ const orderPage = Math.max(1, Number.parseInt(searchParams.get('page') ?? '1', 10) || 1);
36
+ const setOrderFilter = (nextStatus, nextPage = 1) => {
37
+ const p = new URLSearchParams(searchParams);
38
+ if (nextStatus)
39
+ p.set('status', nextStatus);
40
+ else
41
+ p.delete('status');
42
+ if (nextPage > 1)
43
+ p.set('page', String(nextPage));
44
+ else
45
+ p.delete('page');
46
+ setSearchParams(p, { replace: true });
47
+ };
48
+ const [eventPage, setEventPage] = useState(1);
49
+ const ordersQ = useQuery({
50
+ queryKey: ['admin', 'orders', orderPage, status],
51
+ queryFn: () => api.get(`/api/admin/orders${adminListQuery(orderPage, ADMIN_PAGE_SIZE, { status })}`),
52
+ });
53
+ const eventsQ = useQuery({
54
+ queryKey: ['admin', 'payment-events', eventPage],
55
+ queryFn: () => api.get(`/api/admin/payment-events${adminListQuery(eventPage, ADMIN_PAGE_SIZE)}`),
56
+ });
57
+ const channelsQ = useQuery({
58
+ queryKey: ['admin', 'billing-config'],
59
+ queryFn: () => api.get('/api/admin/billing/config'),
60
+ });
61
+ const [testingChannel, setTestingChannel] = useState('');
62
+ const testConnM = useMutation({
63
+ mutationFn: (channel) => api.post('/api/admin/billing/test-connection', { channel }),
64
+ onMutate: (channel) => setTestingChannel(channel),
65
+ onSettled: () => setTestingChannel(''),
66
+ onSuccess: (_d, channel) => toast.success(`${channel} 连接正常`),
67
+ onError: (e, channel) => toast.error(adminError(e, `${channel} 连接测试失败`)),
68
+ });
69
+ const cancelM = useMutation({
70
+ mutationFn: (o) => api.delete(`/api/admin/orders/${o.id}`),
71
+ onSuccess: (_d, o) => {
72
+ toast.success(`订单 ${shortID(o.id)} 已取消`);
73
+ void qc.invalidateQueries({ queryKey: ['admin', 'orders'] });
74
+ },
75
+ onError: (e) => toast.error(adminError(e, '订单取消失败')),
76
+ });
77
+ const markPaidM = useMutation({
78
+ mutationFn: (o) => api.post(`/api/admin/billing/orders/${o.id}/mark-paid`),
79
+ onSuccess: (_d, o) => {
80
+ toast.success(`订单 ${shortID(o.id)} 已核销(manual 标记已付)`);
81
+ void qc.invalidateQueries({ queryKey: ['admin', 'orders'] });
82
+ },
83
+ onError: (e) => toast.error(adminError(e, '标记已付失败')),
84
+ });
85
+ const expiryM = useMutation({
86
+ mutationFn: () => api.post('/api/admin/billing/run-expiry'),
87
+ onSuccess: (d) => toast.success(`本轮到期降级完成:降级 ${d?.expired ?? 0} 个工作区`),
88
+ onError: (e) => toast.error(adminError(e, '到期降级触发失败')),
89
+ });
90
+ const overageM = useMutation({
91
+ mutationFn: () => api.post('/api/admin/billing/run-overage'),
92
+ onSuccess: (d) => toast.success(`本轮超额出账完成:新建 ${d?.orders_created ?? 0} 笔订单`),
93
+ onError: (e) => toast.error(adminError(e, '超额出账触发失败')),
94
+ });
95
+ const onCancelOrder = async (o) => {
96
+ const ok = await confirm({
97
+ title: '取消订单',
98
+ description: `将取消 ${o.workspace_name} 的 ${formatCents(o.amount_cents, o.currency)} 订单(${shortID(o.id)})。仅 pending 订单可取消。`,
99
+ confirmText: '取消订单',
100
+ danger: true,
101
+ });
102
+ if (ok)
103
+ cancelM.mutate(o);
104
+ };
105
+ const onMarkPaid = async (o) => {
106
+ const ok = await confirm({
107
+ title: '标记已付(manual 核销)',
108
+ description: `确认已线下收到 ${o.workspace_name} 的 ${formatCents(o.amount_cents, o.currency)}(订单 ${shortID(o.id)},渠道 manual)?标记后 pending→paid 并发 billing.order_marked_paid 事件。`,
109
+ confirmText: '标记已付',
110
+ danger: true,
111
+ });
112
+ if (ok)
113
+ markPaidM.mutate(o);
114
+ };
115
+ const onRunExpiry = async () => {
116
+ const ok = await confirm({
117
+ title: '触发一轮到期降级',
118
+ description: '将立即扫描全部工作区,把订阅已到期的工作区降级为免费套餐——命中的工作区即刻失去付费能力(用户可重新购买恢复)。扫描幂等,可重复执行;操作计入平台审计日志。',
119
+ confirmText: '执行降级扫描',
120
+ danger: true,
121
+ });
122
+ if (ok)
123
+ expiryM.mutate();
124
+ };
125
+ const onRunOverage = async () => {
126
+ const ok = await confirm({
127
+ title: '触发一轮超额出账',
128
+ description: '将立即为全部超额工作区按当前用量创建计量账单订单(真实出账,用户会看到新的待支付订单)。操作幂等,可重复执行;结果计入平台审计日志。',
129
+ confirmText: '执行出账',
130
+ danger: true,
131
+ });
132
+ if (ok)
133
+ overageM.mutate();
134
+ };
135
+ const orders = ordersQ.data?.items ?? [];
136
+ const ordersTotal = ordersQ.data?.total ?? 0;
137
+ const pastEndOrders = !ordersQ.isPending && orders.length === 0 && orderPage > 1;
138
+ const orderColumns = [
139
+ {
140
+ key: 'workspace',
141
+ header: '工作区',
142
+ render: (o) => (_jsxs("div", { className: "min-w-0 max-w-[16rem]", children: [_jsx("p", { className: "truncate font-medium", children: o.workspace_name || '—' }), _jsx("p", { className: "truncate font-mono text-xs text-muted-foreground", title: o.workspace_id, children: shortID(o.workspace_id) })] })),
143
+ },
144
+ {
145
+ key: 'plan',
146
+ header: '套餐 / 周期',
147
+ className: 'w-32',
148
+ render: (o) => (_jsxs("div", { className: "min-w-0 max-w-[12rem]", children: [_jsx("p", { className: "truncate", children: o.plan_code }), _jsx("p", { className: "truncate text-xs text-muted-foreground", children: o.interval })] })),
149
+ },
150
+ {
151
+ key: 'amount_cents',
152
+ header: '金额',
153
+ className: 'w-28',
154
+ render: (o) => (_jsx("span", { className: "whitespace-nowrap font-medium tabular-nums", children: formatCents(o.amount_cents, o.currency) })),
155
+ },
156
+ {
157
+ key: 'channel',
158
+ header: '渠道',
159
+ className: 'w-24',
160
+ render: (o) => _jsx("span", { className: "text-muted-foreground", children: o.channel }),
161
+ },
162
+ {
163
+ key: 'status',
164
+ header: '状态',
165
+ className: 'w-24',
166
+ render: (o) => _jsx(StatusBadge, { status: o.status }),
167
+ },
168
+ {
169
+ key: 'created_at',
170
+ header: '创建时间',
171
+ className: 'w-44',
172
+ render: (o) => (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", children: formatDateTime(o.created_at) })),
173
+ },
174
+ {
175
+ key: 'actions',
176
+ header: '操作',
177
+ className: 'w-36',
178
+ render: (o) => (_jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [o.status === 'pending' && o.channel === 'manual' && (_jsx(Button, { variant: "outline", size: "sm", disabled: markPaidM.isPending, title: "\u7EBF\u4E0B\u6536\u6B3E\u786E\u8BA4\u540E\u6838\u9500\uFF1Apending\u2192paid\uFF0C\u5E76\u53D1 billing.order_marked_paid \u4E8B\u4EF6", onClick: () => void onMarkPaid(o), children: "\u6807\u8BB0\u5DF2\u4ED8" })), _jsx(Button, { variant: "outline", size: "sm", disabled: o.status !== 'pending' || cancelM.isPending, title: o.status !== 'pending' ? '仅 pending 订单可取消' : undefined, onClick: () => void onCancelOrder(o), children: "\u53D6\u6D88" })] })),
179
+ },
180
+ ];
181
+ const events = eventsQ.data?.items ?? [];
182
+ const eventsTotal = eventsQ.data?.total ?? 0;
183
+ const pastEndEvents = !eventsQ.isPending && events.length === 0 && eventPage > 1;
184
+ const eventColumns = [
185
+ {
186
+ key: 'channel',
187
+ header: '渠道 / 事件',
188
+ render: (e) => (_jsxs("div", { className: "min-w-0 max-w-[16rem]", children: [_jsx("p", { className: "truncate", children: e.channel }), _jsx("p", { className: "truncate text-xs text-muted-foreground", children: _jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: e.event_type }) })] })),
189
+ },
190
+ {
191
+ key: 'channel_event_id',
192
+ header: '渠道事件 ID',
193
+ className: 'w-40',
194
+ render: (e) => (_jsx("span", { className: "truncate font-mono text-xs text-muted-foreground", title: e.channel_event_id, children: shortID(e.channel_event_id) })),
195
+ },
196
+ {
197
+ key: 'order_id',
198
+ header: '关联订单',
199
+ className: 'w-28',
200
+ render: (e) => e.order_id ? (_jsx("span", { className: "font-mono text-xs", title: e.order_id, children: shortID(e.order_id) })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" })),
201
+ },
202
+ {
203
+ key: 'processed',
204
+ header: '处理状态',
205
+ className: 'w-32',
206
+ render: (e) => (_jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [e.processed ? (_jsx(Badge, { variant: "outline", children: "\u5DF2\u5904\u7406" })) : (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: "\u672A\u5904\u7406" })), e.process_status !== 'processed' && (_jsx("span", { className: "truncate text-xs text-muted-foreground", title: e.process_error ?? '', children: PROCESS_STATUS_LABEL[e.process_status] ?? e.process_status }))] })),
207
+ },
208
+ {
209
+ key: 'created_at',
210
+ header: '时间',
211
+ className: 'w-44',
212
+ render: (e) => (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", children: formatDateTime(e.created_at) })),
213
+ },
214
+ ];
215
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u8BA2\u5355", description: "\u8DE8\u5DE5\u4F5C\u533A\u8BA2\u5355\u3001\u652F\u4ED8\u4E8B\u4EF6\u5E42\u7B49\u5C42\u4E0E\u8BA1\u8D39\u8FD0\u7EF4\u624B\u52A8\u89E6\u53D1", actions: _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: expiryM.isPending, onClick: () => void onRunExpiry(), children: expiryM.isPending ? '执行中…' : '触发到期降级' }), _jsx(Button, { variant: "outline", size: "sm", disabled: overageM.isPending, onClick: () => void onRunOverage(), children: overageM.isPending ? '执行中…' : '触发超额出账' })] }) }), _jsx(SettingsCard, { title: "\u652F\u4ED8\u6E20\u9053", description: "\u6E20\u9053\u51ED\u636E\u914D\u7F6E\u72B6\u6001\u4E0E\u8FDE\u901A\u6027\uFF08env \u662F\u5BC6\u94A5\u771F\u6E90\uFF0C\u6B64\u5904\u53EA\u8BFB\u53EF\u89C1\u6027\uFF1B\u5BC6\u94A5 DB \u5316\u5F85\u7B2C\u4E8C\u6E20\u9053\u51FA\u73B0\u518D\u505A\uFF09", children: channelsQ.isError ? (_jsx(PageError, { message: adminError(channelsQ.error, '渠道配置加载失败'), onRetry: () => void channelsQ.refetch() })) : (_jsxs("div", { className: "space-y-2", children: [channelsQ.isPending && (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u52A0\u8F7D\u6E20\u9053\u914D\u7F6E\u4E2D\u2026" })), (channelsQ.data?.channels ?? []).map((ch) => (_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 rounded-lg border px-4 py-3 text-sm", children: [_jsx("span", { className: "min-w-20 font-medium", children: ch.name }), ch.configured ? (_jsx(Badge, { children: "\u5DF2\u914D\u7F6E" })) : (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: "\u672A\u914D\u7F6E" })), _jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", title: "\u5BC6\u94A5\u63A9\u7801\uFF08\u539F\u59CB\u5BC6\u94A5\u4E0D\u53EF\u89C1\uFF09", children: ch.key_masked }), ch.webhook_configured ? (_jsx(Badge, { variant: "outline", children: "webhook \u5DF2\u914D\u7F6E" })) : (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: "webhook \u672A\u914D\u7F6E" })), _jsx("span", { className: "flex-1" }), _jsx(Button, { variant: "outline", size: "sm", disabled: !ch.configured ||
216
+ ch.name === 'manual' ||
217
+ testConnM.isPending, title: !ch.configured
218
+ ? '渠道未配置密钥,无法测试'
219
+ : ch.name === 'manual'
220
+ ? 'manual 为线下收款渠道,无外部服务可测试连接'
221
+ : '向渠道发起一次只读连通性请求', onClick: () => testConnM.mutate(ch.name), children: testingChannel === ch.name ? '测试中…' : '测试连接' })] }, ch.name))), !channelsQ.isPending &&
222
+ (channelsQ.data?.channels ?? []).length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u6682\u65E0\u6E20\u9053" }))] })) }), _jsxs(Tabs, { defaultValue: "orders", children: [_jsxs(TabsList, { className: "mb-4", children: [_jsx(TabsTrigger, { value: "orders", children: "\u8BA2\u5355" }), _jsx(TabsTrigger, { value: "payment-events", children: "\u652F\u4ED8\u4E8B\u4EF6" })] }), _jsxs(TabsContent, { value: "orders", children: [_jsx("div", { className: "mb-4 flex flex-wrap items-center gap-2", children: _jsxs(Select, { value: status || 'all', onValueChange: (v) => {
223
+ const next = typeof v === 'string' && v !== 'all' ? v : '';
224
+ setOrderFilter(next, 1);
225
+ }, children: [_jsx(SelectTrigger, { size: "sm", className: "w-44", "aria-label": "\u6309\u8BA2\u5355\u72B6\u6001\u8FC7\u6EE4", children: _jsx(SelectValue, { children: status ? orderStatusLabel(status) : '全部状态' }) }), _jsxs(SelectContent, { align: "start", children: [_jsx(SelectItem, { value: "all", children: "\u5168\u90E8\u72B6\u6001" }), ORDER_STATUSES.map((s) => (_jsx(SelectItem, { value: s, children: orderStatusLabel(s) }, s)))] })] }) }), ordersQ.isError ? (_jsx(PageError, { message: `${adminError(ordersQ.error, '订单列表加载失败')};重试仍失败时,请稍后再来或联系平台管理员`, onRetry: () => void ordersQ.refetch() })) : (_jsx(DataTable, { columns: orderColumns, rows: orders, rowKey: (o) => o.id, loading: ordersQ.isPending, pagination: {
226
+ page: orderPage,
227
+ pageSize: ADMIN_PAGE_SIZE,
228
+ total: ordersTotal,
229
+ onPageChange: (p) => setOrderFilter(status, p),
230
+ }, empty: pastEndOrders ? (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(PageEmpty, { label: "\u6CA1\u6709\u66F4\u591A\u8BA2\u5355\u4E86", hint: "\u5F53\u524D\u9875\u6570\u636E\u53EF\u80FD\u5DF2\u88AB\u5220\u9664" }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setOrderFilter(status, 1), children: "\u8FD4\u56DE\u7B2C\u4E00\u9875" })] })) : status ? (_jsx(PageEmpty, { label: "\u6CA1\u6709\u8BE5\u72B6\u6001\u7684\u8BA2\u5355", hint: "\u6362\u4E2A\u72B6\u6001\u6216\u6E05\u9664\u8FC7\u6EE4" })) : (_jsx(PageEmpty, { label: "\u6682\u65E0\u8BA2\u5355", hint: "\u5DE5\u4F5C\u533A\u5B8C\u6210\u8D2D\u4E70\u540E\u8BA2\u5355\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\uFF08\u542B\u7EBF\u4E0B manual \u5355\uFF09" })) }))] }), _jsx(TabsContent, { value: "payment-events", children: eventsQ.isError ? (_jsx(PageError, { message: adminError(eventsQ.error, '支付事件加载失败'), onRetry: () => void eventsQ.refetch() })) : (_jsx(DataTable, { columns: eventColumns, rows: events, rowKey: (e) => e.id, loading: eventsQ.isPending, pagination: {
231
+ page: eventPage,
232
+ pageSize: ADMIN_PAGE_SIZE,
233
+ total: eventsTotal,
234
+ onPageChange: setEventPage,
235
+ }, empty: pastEndEvents ? (_jsxs("div", { className: "flex flex-col items-center gap-3 py-10 text-sm text-muted-foreground", children: [_jsx("p", { children: "\u6CA1\u6709\u66F4\u591A\u652F\u4ED8\u4E8B\u4EF6\u4E86" }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setEventPage(1), children: "\u8FD4\u56DE\u7B2C\u4E00\u9875" })] })) : (_jsx(PageEmpty, { label: "\u6682\u65E0\u652F\u4ED8\u4E8B\u4EF6", hint: "\u6E20\u9053\u56DE\u8C03\u5230\u8FBE\u540E\u5728\u6B64\u53EF\u89C1\uFF08\u542B\u672A\u6D88\u8D39\u4E8B\u4EF6\uFF09" })) })) })] })] }));
236
+ }
@@ -0,0 +1 @@
1
+ export declare function OverviewPage(): import("react").JSX.Element;
@@ -0,0 +1,200 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { useLocation, useSearchParams } from 'react-router';
4
+ import { Copy, Megaphone } from 'lucide-react';
5
+ import { useMutation, useQuery } from '@tanstack/react-query';
6
+ import { DataTable } from '../../components/DataTable';
7
+ import { FormField } from '../../components/FormField';
8
+ import { PageEmpty, PageError, PageHeader } from '../../components/Page';
9
+ import { toast } from '../../components/toast';
10
+ import { Badge } from '../../components/ui/Badge';
11
+ import { Button } from '../../components/ui/Button';
12
+ import { Card, CardAction, CardDescription, CardHeader, CardTitle, } from '../../components/ui/card';
13
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../components/ui/dialog';
14
+ import { Input } from '../../components/ui/Input';
15
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../components/ui/select';
16
+ import { useApi } from '../../hooks/useApi';
17
+ import { apiErrorMessage } from '../../lib/api-error';
18
+ import { formatDateTime } from '../../lib/utils';
19
+ import { adminError } from './shared';
20
+ const DAY_OPTIONS = [7, 14, 30, 90];
21
+ const RECENT_LIMIT = 20;
22
+ const STATS_CALIBER = {
23
+ 总用户: '全部注册用户(含已封禁)',
24
+ 活跃用户: '状态为「生效中」的用户数(非登录活跃度)',
25
+ 工作区: '全部工作区总数',
26
+ 付费工作区: '当前套餐不是免费套餐的工作区数',
27
+ '近 7 天新增用户': '最近 7 天内注册的用户数',
28
+ '近 30 天新增用户': '最近 30 天内注册的用户数',
29
+ };
30
+ function EventTypeCode({ type }) {
31
+ return (_jsx("code", { className: "block max-w-[16rem] truncate rounded bg-muted px-1.5 py-0.5 font-mono text-xs", title: type, children: type }));
32
+ }
33
+ const analyticsColumns = [
34
+ {
35
+ key: 'event_type',
36
+ header: '事件类型',
37
+ render: (r) => _jsx(EventTypeCode, { type: r.event_type }),
38
+ },
39
+ {
40
+ key: 'count',
41
+ header: '次数',
42
+ className: 'w-24 text-right',
43
+ render: (r) => (_jsx("span", { className: "font-medium tabular-nums", children: r.count.toLocaleString() })),
44
+ },
45
+ {
46
+ key: 'last_at',
47
+ header: '最近发生',
48
+ className: 'w-44',
49
+ render: (r) => (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", children: formatDateTime(r.last_at) })),
50
+ },
51
+ ];
52
+ export function OverviewPage() {
53
+ const api = useApi();
54
+ const { hash } = useLocation();
55
+ const [searchParams, setSearchParams] = useSearchParams();
56
+ const daysParam = Number(searchParams.get('days'));
57
+ const [days, setDays] = useState(DAY_OPTIONS.includes(daysParam) ? daysParam : 7);
58
+ const [recentType, setRecentType] = useState(searchParams.get('type') ?? '');
59
+ const updateParams = (next) => {
60
+ const p = new URLSearchParams(searchParams);
61
+ if (next.days !== undefined) {
62
+ if (next.days === 7)
63
+ p.delete('days');
64
+ else
65
+ p.set('days', String(next.days));
66
+ }
67
+ if (next.type !== undefined) {
68
+ if (next.type === '')
69
+ p.delete('type');
70
+ else
71
+ p.set('type', next.type);
72
+ }
73
+ setSearchParams(p, { replace: true });
74
+ };
75
+ const [announceOpen, setAnnounceOpen] = useState(false);
76
+ const [target, setTarget] = useState('all');
77
+ const [targetUser, setTargetUser] = useState('');
78
+ const [title, setTitle] = useState('');
79
+ const [body, setBody] = useState('');
80
+ const [link, setLink] = useState('');
81
+ const notifyM = useMutation({
82
+ mutationFn: () => api.post('/api/admin/notify', {
83
+ target,
84
+ user_id: target === 'user' ? targetUser.trim() : undefined,
85
+ title: title.trim(),
86
+ body: body.trim(),
87
+ link: link.trim() || undefined,
88
+ }),
89
+ onSuccess: (r) => {
90
+ if (typeof r?.sent === 'number') {
91
+ const failedSuffix = typeof r.failed === 'number' && r.failed > 0
92
+ ? `,失败 ${r.failed}`
93
+ : '';
94
+ toast.success(`公告已发送:成功 ${r.sent}${failedSuffix}`);
95
+ }
96
+ else if (target === 'user') {
97
+ toast.success('公告已发送给指定用户');
98
+ }
99
+ else {
100
+ toast.success('公告已全员发送,逐人明细可在「审计」页查看');
101
+ }
102
+ setAnnounceOpen(false);
103
+ setTitle('');
104
+ setBody('');
105
+ setLink('');
106
+ setTargetUser('');
107
+ },
108
+ onError: (e) => toast.error(adminError(e, '公告发送失败')),
109
+ });
110
+ const statsQ = useQuery({
111
+ queryKey: ['admin', 'stats'],
112
+ queryFn: () => api.get('/api/admin/stats'),
113
+ });
114
+ const analyticsQ = useQuery({
115
+ queryKey: ['admin', 'analytics', days],
116
+ queryFn: () => api.get(`/api/admin/analytics?days=${days}`),
117
+ });
118
+ const [knownTypes, setKnownTypes] = useState([]);
119
+ useEffect(() => {
120
+ const types = (analyticsQ.data ?? []).map((r) => r.event_type);
121
+ if (types.length === 0)
122
+ return;
123
+ setKnownTypes((prev) => {
124
+ const merged = Array.from(new Set([...prev, ...types])).sort();
125
+ return merged.length === prev.length ? prev : merged;
126
+ });
127
+ }, [analyticsQ.data]);
128
+ const recentQ = useQuery({
129
+ queryKey: ['admin', 'analytics-recent', recentType],
130
+ queryFn: () => api.get(`/api/admin/analytics/recent?limit=${RECENT_LIMIT}${recentType ? `&type=${encodeURIComponent(recentType)}` : ''}`),
131
+ });
132
+ const recentColumns = [
133
+ {
134
+ key: 'event_type',
135
+ header: '事件类型',
136
+ render: (r) => _jsx(EventTypeCode, { type: r.event_type }),
137
+ },
138
+ {
139
+ key: 'entity',
140
+ header: '对象',
141
+ render: (r) => r.entity_type || r.entity_id ? (_jsx("span", { className: "block max-w-[16rem] truncate font-mono text-xs text-muted-foreground", title: `${r.entity_type ?? ''} ${r.entity_id ?? ''}`, children: [r.entity_type, r.entity_id].filter(Boolean).join(' ') })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" })),
142
+ },
143
+ {
144
+ key: 'workspace_id',
145
+ header: '工作区',
146
+ className: 'w-32',
147
+ render: (r) => r.workspace_id ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "font-mono text-xs text-muted-foreground", title: r.workspace_id, children: r.workspace_id.slice(0, 8) }), _jsx(Button, { variant: "ghost", size: "sm", className: "size-6 p-0 text-muted-foreground", "aria-label": `复制工作区 ID ${r.workspace_id}`, title: `复制完整 ID:${r.workspace_id}`, onClick: () => {
148
+ const full = r.workspace_id ?? '';
149
+ if (typeof navigator.clipboard?.writeText === 'function') {
150
+ void navigator.clipboard
151
+ .writeText(full)
152
+ .then(() => toast.success('工作区 ID 已复制'))
153
+ .catch(() => toast.error(`复制失败,完整 ID:${full}`));
154
+ }
155
+ else {
156
+ toast.info(`当前环境不支持自动复制,完整 ID:${full}`);
157
+ }
158
+ }, children: _jsx(Copy, { size: 12, "aria-hidden": "true" }) })] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u5E73\u53F0" })),
159
+ },
160
+ {
161
+ key: 'created_at',
162
+ header: '时间',
163
+ className: 'w-44',
164
+ render: (r) => (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", children: formatDateTime(r.created_at) })),
165
+ },
166
+ ];
167
+ useEffect(() => {
168
+ if (hash === '#analytics') {
169
+ document
170
+ .getElementById('admin-analytics')
171
+ ?.scrollIntoView({ block: 'start' });
172
+ }
173
+ }, [hash]);
174
+ const stats = statsQ.data
175
+ ? [
176
+ ['总用户', statsQ.data.total_users],
177
+ ['活跃用户', statsQ.data.active_users],
178
+ ['工作区', statsQ.data.total_workspaces],
179
+ ['付费工作区', statsQ.data.paid_workspaces],
180
+ ['近 7 天新增用户', statsQ.data.new_users_7d],
181
+ ['近 30 天新增用户', statsQ.data.new_users_30d],
182
+ ]
183
+ : [];
184
+ return (_jsxs("div", { className: "space-y-6", children: [_jsx(PageHeader, { title: "\u6982\u89C8", description: "\u5E73\u53F0\u5168\u5C40\u6307\u6807\u4E0E\u4E8B\u4EF6\u57CB\u70B9\u6C47\u603B", actions: _jsxs(Button, { onClick: () => setAnnounceOpen(true), children: [_jsx(Megaphone, {}), " \u53D1\u5E03\u516C\u544A"] }) }), statsQ.isPending && (_jsx("p", { className: "text-sm text-muted-foreground", children: "\u52A0\u8F7D\u6307\u6807\u4E2D\u2026" })), statsQ.isError && (_jsx(PageError, { message: adminError(statsQ.error, '指标加载失败'), onRetry: () => void statsQ.refetch() })), statsQ.isSuccess && (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6", children: stats.map(([label, value]) => (_jsx(Card, { size: "sm", title: STATS_CALIBER[label], children: _jsxs(CardHeader, { children: [_jsx(CardDescription, { children: label }), _jsx(CardTitle, { className: "text-2xl font-semibold tabular-nums", children: value.toLocaleString() })] }) }, label))) })), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: _jsx("h2", { className: "font-medium", children: "\u6700\u8FD1\u4E8B\u4EF6" }) }), _jsx(CardDescription, { children: "\u6D3B\u52A8\u6D41\uFF1A\u8FD1\u7AEF\u57CB\u70B9\u4E8B\u4EF6\uFF0C\u6309\u65F6\u95F4\u5012\u5E8F" }), _jsx(CardAction, { children: _jsxs(Select, { value: recentType || 'all', onValueChange: (v) => {
185
+ const next = typeof v === 'string' && v !== 'all' ? v : '';
186
+ setRecentType(next);
187
+ updateParams({ type: next });
188
+ }, children: [_jsx(SelectTrigger, { size: "sm", "aria-label": "\u6309\u4E8B\u4EF6\u7C7B\u578B\u8FC7\u6EE4\u6D3B\u52A8\u6D41", children: _jsx(SelectValue, { children: recentType || '全部类型' }) }), _jsxs(SelectContent, { align: "end", children: [_jsx(SelectItem, { value: "all", children: "\u5168\u90E8\u7C7B\u578B" }), knownTypes.map((t) => (_jsx(SelectItem, { value: t, children: t }, t)))] })] }) })] }), _jsx("div", { className: "px-6", children: recentQ.isError ? (_jsxs("p", { className: "flex items-center gap-2 py-6 text-sm text-muted-foreground", children: ["\u6D3B\u52A8\u6D41\u52A0\u8F7D\u5931\u8D25\uFF08", apiErrorMessage(recentQ.error, '未知错误'), "\uFF09", _jsx("button", { className: "underline", onClick: () => void recentQ.refetch(), children: "\u91CD\u8BD5" })] })) : (_jsx(DataTable, { columns: recentColumns, rows: recentQ.data ?? [], rowKey: (r) => String(r.id), loading: recentQ.isPending, empty: recentType ? (_jsx(PageEmpty, { label: `近端无 ${recentType} 事件`, hint: "\u6362\u4E2A\u7C7B\u578B\u6216\u6E05\u9664\u8FC7\u6EE4" })) : (_jsx(PageEmpty, { label: "\u6682\u65E0\u57CB\u70B9\u4E8B\u4EF6", hint: "\u4E1A\u52A1\u6D41\u91CF\u5230\u8FBE\u540E\u5728\u6B64\u5B9E\u65F6\u53EF\u89C1\uFF08analytics \u7BA1\u7EBF\u4EA7\u7269\uFF09" })) })) })] }), _jsx("section", { id: "admin-analytics", className: "scroll-mt-4", children: _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: _jsx("h2", { className: "font-medium", children: "\u4E8B\u4EF6\u57CB\u70B9\u6C47\u603B" }) }), _jsx(CardDescription, { children: "\u6309\u4E8B\u4EF6\u7C7B\u578B\u805A\u5408\u7684\u51FA\u73B0\u6B21\u6570\u4E0E\u6700\u8FD1\u53D1\u751F\u65F6\u95F4" }), _jsx(CardAction, { children: _jsxs(Select, { value: days, onValueChange: (v) => {
189
+ if (typeof v === 'number') {
190
+ setDays(v);
191
+ updateParams({ days: v });
192
+ }
193
+ }, children: [_jsx(SelectTrigger, { size: "sm", "aria-label": "\u7EDF\u8BA1\u65F6\u95F4\u8303\u56F4", children: _jsxs(SelectValue, { children: ["\u8FD1 ", days, " \u5929"] }) }), _jsx(SelectContent, { align: "end", children: DAY_OPTIONS.map((d) => (_jsxs(SelectItem, { value: d, children: ["\u8FD1 ", d, " \u5929"] }, d))) })] }) })] }), _jsx("div", { className: "px-6", children: analyticsQ.isError ? (_jsxs("p", { className: "flex items-center gap-2 py-6 text-sm text-muted-foreground", children: ["\u57CB\u70B9\u6C47\u603B\u52A0\u8F7D\u5931\u8D25\uFF08", apiErrorMessage(analyticsQ.error, '未知错误'), "\uFF09", _jsx("button", { className: "underline", onClick: () => void analyticsQ.refetch(), children: "\u91CD\u8BD5" })] })) : (_jsx(DataTable, { columns: analyticsColumns, rows: analyticsQ.data ?? [], rowKey: (r) => r.event_type, loading: analyticsQ.isPending, empty: _jsx(PageEmpty, { label: "\u6682\u65E0\u57CB\u70B9\u4E8B\u4EF6", hint: "\u8FD1\u671F\u6CA1\u6709\u4E8B\u4EF6\u5199\u5165\uFF0C\u6216\u57CB\u70B9\u7BA1\u7EBF\u672A\u63A5\u7EBF" }) })) })] }) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [_jsx(Badge, { variant: "outline", className: "mr-1", children: "\u63D0\u793A" }), "\u7BA1\u7406\u53F0\u6570\u636E\u4E3A\u5E73\u53F0\u7EA7\u89C6\u56FE\uFF1B\u7528\u6237/\u5DE5\u4F5C\u533A\u7684\u5177\u4F53\u64CD\u4F5C\u89C1\u5DE6\u4FA7\u5BFC\u822A\u3002"] }), _jsx(Dialog, { open: announceOpen, onOpenChange: setAnnounceOpen, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "\u53D1\u5E03\u516C\u544A" }), _jsx(DialogDescription, { children: "\u4EE5\u7AD9\u5185\u901A\u77E5\u53D1\u9001\u3002\u5168\u5458\u6A21\u5F0F\u5355\u6B21\u4E0A\u9650 1000 \u4EBA\uFF1B\u540C\u6807\u9898\u540C\u65E5\u53EA\u53D1\u4E00\u6B21\uFF08\u91CD\u590D \u53D1\u9001\u81EA\u52A8\u8DF3\u8FC7\uFF09\u3002" })] }), _jsxs("div", { className: "space-y-4", children: [_jsx(FormField, { label: "\u6536\u4EF6\u8303\u56F4", htmlFor: "announce-target", children: _jsxs(Select, { value: target, onValueChange: (v) => {
194
+ if (v === 'user' || v === 'all')
195
+ setTarget(v);
196
+ }, children: [_jsx(SelectTrigger, { id: "announce-target", "aria-label": "\u6536\u4EF6\u8303\u56F4", children: _jsx(SelectValue, { children: target === 'all' ? '全部活跃用户' : '指定用户' }) }), _jsxs(SelectContent, { align: "start", children: [_jsx(SelectItem, { value: "all", children: "\u5168\u90E8\u6D3B\u8DC3\u7528\u6237" }), _jsx(SelectItem, { value: "user", children: "\u6307\u5B9A\u7528\u6237" })] })] }) }), target === 'user' && (_jsx(FormField, { label: "\u7528\u6237 ID\uFF08\u5FC5\u586B\uFF09", htmlFor: "announce-user", children: _jsx(Input, { id: "announce-user", className: "font-mono", placeholder: "\u8F93\u5165\u76EE\u6807\u7528\u6237\u7684\u7528\u6237 ID", value: targetUser, onChange: (e) => setTargetUser(e.target.value) }) })), _jsx(FormField, { label: "\u6807\u9898\uFF08\u5FC5\u586B\uFF09", htmlFor: "announce-title", children: _jsx(Input, { id: "announce-title", placeholder: "\u5982\uFF1A\u4ECA\u665A 22:00 \u4F8B\u884C\u7EF4\u62A4", value: title, onChange: (e) => setTitle(e.target.value) }) }), _jsx(FormField, { label: "\u6B63\u6587\uFF08\u5FC5\u586B\uFF09", htmlFor: "announce-body", children: _jsx("textarea", { id: "announce-body", className: "flex min-h-24 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", placeholder: "\u516C\u544A\u5185\u5BB9\uFF08\u652F\u6301\u591A\u884C\uFF09", value: body, onChange: (e) => setBody(e.target.value) }) }), _jsx(FormField, { label: "\u8DF3\u8F6C\u94FE\u63A5\uFF08\u53EF\u9009\uFF09", htmlFor: "announce-link", children: _jsx(Input, { id: "announce-link", placeholder: "https://\u2026 \u6216\u7AD9\u5185\u9875\u9762\u8DEF\u5F84\uFF1B\u586B\u5199\u540E\u901A\u77E5\u53EF\u70B9\u51FB\u8DF3\u8F6C", value: link, onChange: (e) => setLink(e.target.value) }) })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setAnnounceOpen(false), children: "\u53D6\u6D88" }), _jsx(Button, { disabled: notifyM.isPending ||
197
+ !title.trim() ||
198
+ !body.trim() ||
199
+ (target === 'user' && !targetUser.trim()), onClick: () => notifyM.mutate(), children: notifyM.isPending ? '发送中…' : '发送' })] })] }) })] }));
200
+ }
@@ -0,0 +1 @@
1
+ export declare function SSOPage(): import("react").JSX.Element;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Link } from 'react-router';
4
+ import { useQuery } from '@tanstack/react-query';
5
+ import { DataTable } from '../../components/DataTable';
6
+ import { PageEmpty, PageError, PageHeader } from '../../components/Page';
7
+ import { Badge } from '../../components/ui/Badge';
8
+ import { Button } from '../../components/ui/Button';
9
+ import { useApi } from '../../hooks/useApi';
10
+ import { formatDateTime } from '../../lib/utils';
11
+ import { ADMIN_PAGE_SIZE, adminError } from './shared';
12
+ export function SSOPage() {
13
+ const api = useApi();
14
+ const listQ = useQuery({
15
+ queryKey: ['admin', 'sso'],
16
+ queryFn: () => api.get('/api/admin/sso'),
17
+ });
18
+ const [page, setPage] = useState(1);
19
+ const all = listQ.data ?? [];
20
+ const total = all.length;
21
+ const rows = all.slice((page - 1) * ADMIN_PAGE_SIZE, page * ADMIN_PAGE_SIZE);
22
+ const pastEnd = !listQ.isPending && rows.length === 0 && page > 1;
23
+ const columns = [
24
+ {
25
+ key: 'workspace',
26
+ header: '工作区',
27
+ render: (s) => (_jsxs("div", { className: "min-w-0 max-w-[16rem]", children: [_jsx("p", { className: "truncate font-medium", children: _jsx(Link, { to: `/admin/workspaces/${s.workspace_id}`, className: "transition-colors hover:text-primary hover:underline", title: "\u524D\u5F80\u8BE5\u5DE5\u4F5C\u533A\u7684\u7BA1\u7406\u8BE6\u60C5\u9875", children: s.workspace_name || '—' }) }), _jsx("p", { className: "truncate font-mono text-xs text-muted-foreground", children: s.workspace_slug })] })),
28
+ },
29
+ {
30
+ key: 'issuer_url',
31
+ header: 'Issuer',
32
+ render: (s) => (_jsxs("div", { className: "min-w-0 max-w-[24rem]", children: [_jsx("p", { className: "truncate font-mono text-xs", title: s.issuer_url, children: s.issuer_url }), _jsxs("p", { className: "truncate font-mono text-xs text-muted-foreground", title: s.client_id, children: ["client_id: ", s.client_id] })] })),
33
+ },
34
+ {
35
+ key: 'secret',
36
+ header: 'client_secret',
37
+ className: 'w-36',
38
+ render: (s) => s.secret_sealed ? (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: "\u5DF2\u52A0\u5BC6" })) : (_jsxs("div", { className: "min-w-0", children: [_jsx(Badge, { variant: "destructive", children: "\u660E\u6587" }), _jsx("p", { className: "mt-1 max-w-[12rem] truncate text-xs text-destructive", title: "\u5B58\u91CF\u660E\u6587\uFF08\u8FC1\u79FB 016 \u65F6\u4EE3\u5199\u5165\uFF09\uFF1A\u90E8\u7F72\u914D\u7F6E PLOYKIT_SEAL_KEY \u540E\uFF0C\u5230\u8BE5\u5DE5\u4F5C\u533A\u81EA\u52A9\u9762\u91CD\u65B0\u4FDD\u5B58\u4E00\u6B21\u5373\u53EF\u52A0\u5BC6", children: "\u9700\u91CD\u65B0\u4FDD\u5B58\u4EE5\u52A0\u5BC6" })] })),
39
+ },
40
+ {
41
+ key: 'created_at',
42
+ header: '配置 / 更新时间',
43
+ className: 'w-44',
44
+ render: (s) => (_jsxs("div", { className: "whitespace-nowrap text-muted-foreground", children: [_jsx("p", { children: formatDateTime(s.created_at) }), s.updated_at !== s.created_at && (_jsxs("p", { className: "text-xs", children: ["\u66F4\u65B0 ", formatDateTime(s.updated_at)] }))] })),
45
+ },
46
+ ];
47
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "SSO", description: "\u8DE8\u5DE5\u4F5C\u533A\u8054\u90A6 SSO\uFF08OIDC\uFF09\u6982\u89C8\u2014\u2014\u53EA\u8BFB\uFF1B\u5BC6\u94A5\u5BC6\u5C01\u72B6\u6001\u4E0E\u914D\u7F6E\u5F52\u5C5E\u4E00\u89C8" }), _jsx("p", { className: "mb-4 max-w-3xl text-sm text-muted-foreground", children: "\u6BCF\u5DE5\u4F5C\u533A\u81F3\u591A\u4E00\u6761\u8054\u90A6 IdP \u914D\u7F6E\uFF08\u4F01\u4E1A SSO \u767B\u5F55\u5165\u53E3\uFF09\u3002\u914D\u7F6E\u7684\u589E\u5220\u6539\u5728\u5404\u5DE5\u4F5C\u533A \u81EA\u52A9\u9762\u5B8C\u6210\uFF08\u5DE5\u4F5C\u533A\u8BBE\u7F6E \u2192 \u8054\u90A6 SSO\uFF09\uFF0C\u7BA1\u7406\u53F0\u4E0D\u4EE3\u4E3A\u64CD\u4F5C\uFF08\u70B9\u5DE5\u4F5C\u533A\u540D\u53EF\u76F4\u8FBE\u5176\u7BA1\u7406\u8BE6\u60C5\u9875\uFF09\uFF1Bclient_secret \u6C38\u4E0D\u56DE\u4F20\uFF0C\u6B64\u5904\u53EA\u5C55\u793A\u5176\u5BC6\u5C01\u72B6\u6001\uFF08sealed:v1 \u52A0\u5BC6 / \u5B58\u91CF\u660E\u6587\u8B66\u544A\uFF09\u3002" }), listQ.isError ? (_jsx(PageError, { message: adminError(listQ.error, 'SSO 概览加载失败'), onRetry: () => void listQ.refetch() })) : (_jsx(DataTable, { columns: columns, rows: rows, rowKey: (s) => s.workspace_id, loading: listQ.isPending, pagination: {
48
+ page,
49
+ pageSize: ADMIN_PAGE_SIZE,
50
+ total,
51
+ onPageChange: setPage,
52
+ }, empty: pastEnd ? (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(PageEmpty, { label: "\u6CA1\u6709\u66F4\u591A\u914D\u7F6E\u4E86", hint: "\u5F53\u524D\u9875\u6570\u636E\u53EF\u80FD\u5DF2\u88AB\u5220\u9664" }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setPage(1), children: "\u8FD4\u56DE\u7B2C\u4E00\u9875" })] })) : (_jsx(PageEmpty, { label: "\u65E0\u5DE5\u4F5C\u533A\u914D\u7F6E\u8054\u90A6 SSO", hint: "\u5DE5\u4F5C\u533A\u5728\u81EA\u52A9\u9762\u914D\u7F6E\u4F01\u4E1A IdP \u540E\u4F1A\u5728\u6B64\u51FA\u73B0" })) }))] }));
53
+ }
@@ -0,0 +1 @@
1
+ export declare function UserDetailPage(): import("react").JSX.Element;