@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.
- package/README.md +41 -3
- package/dist/components/ConfirmDialog.d.ts +13 -0
- package/dist/components/ConfirmDialog.js +29 -0
- package/dist/components/CronInput.d.ts +15 -0
- package/dist/components/CronInput.js +112 -0
- package/dist/components/DataTable.d.ts +22 -0
- package/dist/components/DataTable.js +13 -0
- package/dist/components/ErrorBoundary.d.ts +16 -0
- package/dist/components/ErrorBoundary.js +24 -0
- package/dist/components/FormField.d.ts +10 -0
- package/dist/components/FormField.js +22 -0
- package/dist/components/ImpersonationBanner.d.ts +3 -0
- package/dist/components/ImpersonationBanner.js +10 -0
- package/dist/components/NotificationBell.d.ts +3 -0
- package/dist/components/NotificationBell.js +47 -0
- package/dist/components/OnboardingChecklist.d.ts +12 -0
- package/dist/components/OnboardingChecklist.js +43 -0
- package/dist/components/Page.d.ts +17 -0
- package/dist/components/Page.js +16 -0
- package/dist/components/SecretModal.d.ts +16 -0
- package/dist/components/SecretModal.js +45 -0
- package/dist/components/SettingsCard.d.ts +10 -0
- package/dist/components/SettingsCard.js +6 -0
- package/dist/components/SiteBanner.d.ts +6 -0
- package/dist/components/SiteBanner.js +11 -0
- package/dist/components/SiteBannerLayer.d.ts +4 -0
- package/dist/components/SiteBannerLayer.js +9 -0
- package/dist/components/UserMenu.d.ts +13 -0
- package/dist/components/UserMenu.js +30 -0
- package/dist/components/WorkspaceSwitcher.d.ts +1 -0
- package/dist/components/WorkspaceSwitcher.js +53 -0
- package/dist/components/toast.d.ts +7 -0
- package/dist/components/toast.js +11 -0
- package/dist/components/ui/Badge.d.ts +13 -0
- package/dist/components/ui/Badge.js +21 -0
- package/dist/components/ui/Button.d.ts +10 -0
- package/dist/components/ui/Button.js +33 -0
- package/dist/components/ui/Input.d.ts +3 -0
- package/dist/components/ui/Input.js +10 -0
- package/dist/components/ui/alert-dialog.d.ts +18 -0
- package/dist/components/ui/alert-dialog.js +41 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +22 -0
- package/dist/components/ui/breadcrumb.d.ts +10 -0
- package/dist/components/ui/breadcrumb.js +36 -0
- package/dist/components/ui/card.d.ts +13 -0
- package/dist/components/ui/card.js +24 -0
- package/dist/components/ui/checkbox.d.ts +3 -0
- package/dist/components/ui/checkbox.js +8 -0
- package/dist/components/ui/dialog.d.ts +15 -0
- package/dist/components/ui/dialog.js +26 -0
- package/dist/components/ui/empty.d.ts +13 -0
- package/dist/components/ui/empty.js +36 -0
- package/dist/components/ui/field.d.ts +24 -0
- package/dist/components/ui/field.js +67 -0
- package/dist/components/ui/label.d.ts +3 -0
- package/dist/components/ui/label.js +6 -0
- package/dist/components/ui/menu.d.ts +29 -0
- package/dist/components/ui/menu.js +50 -0
- package/dist/components/ui/popover.d.ts +9 -0
- package/dist/components/ui/popover.js +22 -0
- package/dist/components/ui/progress.d.ts +7 -0
- package/dist/components/ui/progress.js +19 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +33 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/separator.js +7 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +36 -0
- package/dist/components/ui/sidebar.d.ts +63 -0
- package/dist/components/ui/sidebar.js +265 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.js +6 -0
- package/dist/components/ui/switch.d.ts +5 -0
- package/dist/components/ui/switch.js +7 -0
- package/dist/components/ui/tabs.d.ts +10 -0
- package/dist/components/ui/tabs.js +28 -0
- package/dist/components/ui/tooltip.d.ts +6 -0
- package/dist/components/ui/tooltip.js +16 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/hooks/useApi.d.ts +10 -0
- package/dist/hooks/useApi.js +11 -0
- package/dist/hooks/useAudit.d.ts +39 -0
- package/dist/hooks/useAudit.js +55 -0
- package/dist/hooks/useAuth.d.ts +1 -0
- package/dist/hooks/useAuth.js +4 -0
- package/dist/hooks/useBilling.d.ts +44 -0
- package/dist/hooks/useBilling.js +64 -0
- package/dist/hooks/useLogin.d.ts +10 -0
- package/dist/hooks/useLogin.js +39 -0
- package/dist/hooks/useMembers.d.ts +125 -0
- package/dist/hooks/useMembers.js +127 -0
- package/dist/hooks/useNotificationPrefs.d.ts +20 -0
- package/dist/hooks/useNotificationPrefs.js +42 -0
- package/dist/hooks/usePasswordReset.d.ts +5 -0
- package/dist/hooks/usePasswordReset.js +12 -0
- package/dist/hooks/useSEO.d.ts +6 -0
- package/dist/hooks/useSEO.js +14 -0
- package/dist/hooks/useSessions.d.ts +20 -0
- package/dist/hooks/useSessions.js +38 -0
- package/dist/hooks/useSiteConfig.d.ts +12 -0
- package/dist/hooks/useSiteConfig.js +11 -0
- package/dist/hooks/useTokens.d.ts +36 -0
- package/dist/hooks/useTokens.js +35 -0
- package/dist/hooks/useUsage.d.ts +20 -0
- package/dist/hooks/useUsage.js +21 -0
- package/dist/hooks/useWorkspace.d.ts +26 -0
- package/dist/hooks/useWorkspace.js +32 -0
- package/dist/hooks/useZodForm.d.ts +6 -0
- package/dist/hooks/useZodForm.js +6 -0
- package/dist/hooks/ws.d.ts +4 -0
- package/dist/hooks/ws.js +21 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +94 -0
- package/dist/layouts/AppShell.d.ts +20 -0
- package/dist/layouts/AppShell.js +21 -0
- package/dist/layouts/AuthShell.d.ts +7 -0
- package/dist/layouts/AuthShell.js +4 -0
- package/dist/layouts/SettingsShell.d.ts +19 -0
- package/dist/layouts/SettingsShell.js +21 -0
- package/dist/lib/api-error.d.ts +1 -0
- package/dist/lib/api-error.js +74 -0
- package/dist/lib/perm.d.ts +2 -0
- package/dist/lib/perm.js +7 -0
- package/dist/lib/utils.d.ts +12 -0
- package/dist/lib/utils.js +84 -0
- package/dist/lib/ws-query-bridge.d.ts +7 -0
- package/dist/lib/ws-query-bridge.js +35 -0
- package/dist/pages/LandingPage.d.ts +22 -0
- package/dist/pages/LandingPage.js +5 -0
- package/dist/pages/LoginPage.d.ts +15 -0
- package/dist/pages/LoginPage.js +87 -0
- package/dist/pages/account/NotificationPreferencesPage.d.ts +13 -0
- package/dist/pages/account/NotificationPreferencesPage.js +41 -0
- package/dist/pages/account/ProfileSettingsPage.d.ts +4 -0
- package/dist/pages/account/ProfileSettingsPage.js +96 -0
- package/dist/pages/account/SecuritySettingsPage.d.ts +1 -0
- package/dist/pages/account/SecuritySettingsPage.js +134 -0
- package/dist/pages/account/TokensPage.d.ts +1 -0
- package/dist/pages/account/TokensPage.js +117 -0
- package/dist/pages/admin/AdminSettingsPage.d.ts +1 -0
- package/dist/pages/admin/AdminSettingsPage.js +210 -0
- package/dist/pages/admin/AdminShell.d.ts +7 -0
- package/dist/pages/admin/AdminShell.js +79 -0
- package/dist/pages/admin/AuditPage.d.ts +1 -0
- package/dist/pages/admin/AuditPage.js +164 -0
- package/dist/pages/admin/OrdersPage.d.ts +1 -0
- package/dist/pages/admin/OrdersPage.js +236 -0
- package/dist/pages/admin/OverviewPage.d.ts +1 -0
- package/dist/pages/admin/OverviewPage.js +200 -0
- package/dist/pages/admin/SSOPage.d.ts +1 -0
- package/dist/pages/admin/SSOPage.js +53 -0
- package/dist/pages/admin/UserDetailPage.d.ts +1 -0
- package/dist/pages/admin/UserDetailPage.js +266 -0
- package/dist/pages/admin/UsersPage.d.ts +4 -0
- package/dist/pages/admin/UsersPage.js +201 -0
- package/dist/pages/admin/WebhookDeliveriesPage.d.ts +1 -0
- package/dist/pages/admin/WebhookDeliveriesPage.js +128 -0
- package/dist/pages/admin/WorkspaceDetailPage.d.ts +1 -0
- package/dist/pages/admin/WorkspaceDetailPage.js +202 -0
- package/dist/pages/admin/WorkspacesPage.d.ts +1 -0
- package/dist/pages/admin/WorkspacesPage.js +133 -0
- package/dist/pages/admin/shared.d.ts +8 -0
- package/dist/pages/admin/shared.js +23 -0
- package/dist/pages/auth/ForgotPasswordPage.d.ts +6 -0
- package/dist/pages/auth/ForgotPasswordPage.js +32 -0
- package/dist/pages/auth/InviteAcceptPage.d.ts +5 -0
- package/dist/pages/auth/InviteAcceptPage.js +40 -0
- package/dist/pages/auth/RegisterPage.d.ts +15 -0
- package/dist/pages/auth/RegisterPage.js +80 -0
- package/dist/pages/auth/ResetPasswordPage.d.ts +4 -0
- package/dist/pages/auth/ResetPasswordPage.js +71 -0
- package/dist/pages/auth/VerifyEmailPage.d.ts +1 -0
- package/dist/pages/auth/VerifyEmailPage.js +45 -0
- package/dist/pages/workspace/BillingPage.d.ts +4 -0
- package/dist/pages/workspace/BillingPage.js +183 -0
- package/dist/pages/workspace/BillingSuccessPage.d.ts +4 -0
- package/dist/pages/workspace/BillingSuccessPage.js +72 -0
- package/dist/pages/workspace/MembersPage.d.ts +3 -0
- package/dist/pages/workspace/MembersPage.js +262 -0
- package/dist/pages/workspace/SchedulesPage.d.ts +4 -0
- package/dist/pages/workspace/SchedulesPage.js +177 -0
- package/dist/pages/workspace/UsagePage.d.ts +15 -0
- package/dist/pages/workspace/UsagePage.js +74 -0
- package/dist/pages/workspace/WebhooksPage.d.ts +3 -0
- package/dist/pages/workspace/WebhooksPage.js +242 -0
- package/dist/pages/workspace/WorkspaceAuditPage.d.ts +3 -0
- package/dist/pages/workspace/WorkspaceAuditPage.js +109 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.d.ts +4 -0
- package/dist/pages/workspace/WorkspaceGeneralPage.js +101 -0
- package/dist/provider/PloykitProvider.d.ts +50 -0
- package/dist/provider/PloykitProvider.js +240 -0
- package/dist/realtime.d.ts +3 -0
- package/dist/realtime.js +11 -0
- package/package.json +63 -6
|
@@ -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;
|