@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,210 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { FormField } from '../../components/FormField';
5
+ import { PageError, PageHeader } from '../../components/Page';
6
+ import { toast } from '../../components/toast';
7
+ import { Badge } from '../../components/ui/Badge';
8
+ import { Button } from '../../components/ui/Button';
9
+ import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from '../../components/ui/card';
10
+ import { Input } from '../../components/ui/Input';
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../components/ui/select';
12
+ import { Switch } from '../../components/ui/switch';
13
+ import { useApi } from '../../hooks/useApi';
14
+ import { SiteBanner } from '../../components/SiteBanner';
15
+ import { SITE_CONFIG_QUERY_KEY } from '../../hooks/useSiteConfig';
16
+ import { adminError } from './shared';
17
+ const ERROR_TOAST_MS = 8_000;
18
+ function parseRateLimit(raw) {
19
+ const s = raw.trim();
20
+ if (s === '') {
21
+ return { ok: false, msg: '不能留空:填写 0 表示关闭限流' };
22
+ }
23
+ const n = Number(s);
24
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0 || n > 100000) {
25
+ return { ok: false, msg: '必须是 0..100000 的整数(0 = 关闭限流)' };
26
+ }
27
+ return { ok: true, v: s };
28
+ }
29
+ async function putKeys(api, entries) {
30
+ const results = [];
31
+ for (const e of entries) {
32
+ try {
33
+ await api.put(`/api/admin/settings/${e.key}`, { value: e.value });
34
+ results.push({ key: e.key, label: e.label, ok: true });
35
+ }
36
+ catch (err) {
37
+ results.push({ key: e.key, label: e.label, ok: false, err });
38
+ }
39
+ }
40
+ return results;
41
+ }
42
+ export function AdminSettingsPage() {
43
+ const api = useApi();
44
+ const qc = useQueryClient();
45
+ const settingsQ = useQuery({
46
+ queryKey: ['admin', 'settings'],
47
+ queryFn: () => api.get('/api/admin/settings'),
48
+ });
49
+ const [bannerText, setBannerText] = useState('');
50
+ const [bannerKind, setBannerKind] = useState('info');
51
+ const [maintenance, setMaintenance] = useState(false);
52
+ const [allowSignup, setAllowSignup] = useState(true);
53
+ const [domains, setDomains] = useState('');
54
+ const [rlPerUser, setRlPerUser] = useState('0');
55
+ const [rlPerIP, setRlPerIP] = useState('30');
56
+ const [rlAllowlist, setRlAllowlist] = useState('');
57
+ const [rlError, setRlError] = useState(null);
58
+ const [hydrated, setHydrated] = useState(false);
59
+ const eff = settingsQ.data?.effective;
60
+ useEffect(() => {
61
+ if (!eff || hydrated)
62
+ return;
63
+ setBannerText(eff.banner_text ?? '');
64
+ setBannerKind(eff.banner_kind === 'warn' ? 'warn' : 'info');
65
+ setMaintenance(eff.maintenance_mode === '1');
66
+ setAllowSignup(eff.allow_signup !== '0');
67
+ setDomains(eff.allowed_domains ?? '');
68
+ setRlPerUser(eff.rate_limit_per_user_per_min ?? '0');
69
+ setRlPerIP(eff.rate_limit_per_ip_per_min ?? '30');
70
+ setRlAllowlist(eff.rate_limit_allowlist ?? '');
71
+ setHydrated(true);
72
+ }, [eff, hydrated]);
73
+ const afterSave = () => {
74
+ qc.invalidateQueries({ queryKey: ['admin', 'settings'] });
75
+ qc.invalidateQueries({ queryKey: SITE_CONFIG_QUERY_KEY });
76
+ };
77
+ const rollbackToServer = async () => {
78
+ await qc.invalidateQueries({ queryKey: ['admin', 'settings'] });
79
+ setHydrated(false);
80
+ };
81
+ const reportKeySaves = (results, successMsg, what) => {
82
+ const okCount = results.filter((r) => r.ok).length;
83
+ const failed = results.filter((r) => !r.ok);
84
+ afterSave();
85
+ if (failed.length === 0) {
86
+ toast.success(successMsg);
87
+ return;
88
+ }
89
+ const failedLabel = failed.map((f) => f.label).join('、');
90
+ const reason = adminError(failed[0].err, '保存失败');
91
+ if (okCount > 0) {
92
+ toast.error(`${what}部分保存:成功 ${okCount}/${results.length} 项已生效;失败项(${failedLabel})原因:${reason}`, { duration: ERROR_TOAST_MS });
93
+ }
94
+ else {
95
+ toast.error(`${what}保存失败(${failedLabel}):${reason}`, {
96
+ duration: ERROR_TOAST_MS,
97
+ });
98
+ }
99
+ void rollbackToServer();
100
+ };
101
+ const saveM = useMutation({
102
+ mutationFn: (p) => api.put(`/api/admin/settings/${p.key}`, { value: p.value }),
103
+ onSuccess: (_r, p) => {
104
+ toast.success(`${p.label}已保存`);
105
+ afterSave();
106
+ },
107
+ onError: (e, p) => {
108
+ toast.error(adminError(e, `${p.label}保存失败`), {
109
+ duration: ERROR_TOAST_MS,
110
+ });
111
+ void rollbackToServer();
112
+ },
113
+ });
114
+ const bannerM = useMutation({
115
+ mutationFn: () => putKeys(api, [
116
+ { key: 'banner_text', label: '公告文本', value: bannerText },
117
+ { key: 'banner_kind', label: '公告样式', value: bannerKind },
118
+ ]),
119
+ onSuccess: (results) => reportKeySaves(results, '公告已保存,全站顶条即时生效', '公告'),
120
+ });
121
+ const domainsM = useMutation({
122
+ mutationFn: () => api.put('/api/admin/settings/allowed_domains', { value: domains.trim() }),
123
+ onSuccess: () => {
124
+ toast.success('域名白名单已保存');
125
+ afterSave();
126
+ },
127
+ onError: (e) => {
128
+ toast.error(adminError(e, '域名白名单保存失败'), {
129
+ duration: ERROR_TOAST_MS,
130
+ });
131
+ void rollbackToServer();
132
+ },
133
+ });
134
+ const rateLimitM = useMutation({
135
+ mutationFn: async () => {
136
+ const perUser = parseRateLimit(rlPerUser);
137
+ const perIP = parseRateLimit(rlPerIP);
138
+ if (!perUser.ok)
139
+ throw new Error(`每用户限流:${perUser.msg}`);
140
+ if (!perIP.ok)
141
+ throw new Error(`每 IP 限流:${perIP.msg}`);
142
+ setRlError(null);
143
+ return putKeys(api, [
144
+ {
145
+ key: 'rate_limit_per_user_per_min',
146
+ label: '每用户限流',
147
+ value: perUser.v,
148
+ },
149
+ { key: 'rate_limit_per_ip_per_min', label: '每 IP 限流', value: perIP.v },
150
+ { key: 'rate_limit_allowlist', label: '豁免名单', value: rlAllowlist.trim() },
151
+ ]);
152
+ },
153
+ onSuccess: (results) => reportKeySaves(results, '限流配置已保存(重启后生效)', '限流配置'),
154
+ onError: (e) => {
155
+ const msg = e instanceof Error ? e.message : '限流配置保存失败';
156
+ setRlError(msg);
157
+ toast.error(msg, { duration: ERROR_TOAST_MS });
158
+ },
159
+ });
160
+ const [testTo, setTestTo] = useState('');
161
+ const [testMsg, setTestMsg] = useState(null);
162
+ const testEmailM = useMutation({
163
+ mutationFn: () => api.post('/api/admin/settings/test-email', {
164
+ to: testTo.trim(),
165
+ }),
166
+ onSuccess: (r) => {
167
+ setTestMsg(r?.mode === 'dev'
168
+ ? `开发通道(emaildev):${r.hint ?? '邮件内容已打日志,请到后端日志查看'}`
169
+ : '测试邮件已发出,请到收件箱(含垃圾箱)确认');
170
+ },
171
+ onError: (e) => setTestMsg(`发送失败:${adminError(e, '未知错误')}`),
172
+ });
173
+ if (settingsQ.isPending) {
174
+ return _jsx("p", { className: "text-sm text-muted-foreground", children: "\u52A0\u8F7D\u8BBE\u7F6E\u4E2D\u2026" });
175
+ }
176
+ if (settingsQ.isError) {
177
+ return (_jsx(PageError, { message: adminError(settingsQ.error, '设置加载失败'), onRetry: () => void settingsQ.refetch() }));
178
+ }
179
+ const desc = settingsQ.data?.descriptions ?? {};
180
+ return (_jsxs("div", { className: "space-y-6", children: [_jsx(PageHeader, { title: "\u7CFB\u7EDF", description: "\u7AD9\u70B9\u516C\u544A\u3001\u7EF4\u62A4\u6A21\u5F0F\u3001\u6CE8\u518C\u7BA1\u63A7\u3001API \u9650\u6D41\u4E0E\u90AE\u4EF6\u901A\u9053\u2014\u2014\u4FDD\u5B58\u540E\u5168\u7AD9\u5373\u65F6\u751F\u6548\uFF08API \u9650\u6D41\u9664\u5916\uFF1A\u91CD\u542F\u540E\u751F\u6548\uFF09" }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardDescription, { children: "\u7AD9\u70B9\u516C\u544A" }), _jsx(CardTitle, { children: "\u9876\u90E8\u516C\u544A\u6A2A\u6761" }), _jsx(CardAction, { children: _jsx(Badge, { variant: bannerText.trim() ? 'secondary' : 'outline', children: bannerText.trim() ? '展示中' : '未展示' }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: desc.banner_text }), _jsxs("div", { className: "rounded-lg border border-dashed", children: [_jsx(SiteBanner, { text: bannerText, kind: bannerKind }), !bannerText.trim() && (_jsx("p", { className: "px-4 py-2 text-xs text-muted-foreground", children: "\u9884\u89C8\uFF1A\u516C\u544A\u4E3A\u7A7A\u65F6\u4E0D\u6E32\u67D3\u6A2A\u6761" }))] }), _jsx(FormField, { label: "\u516C\u544A\u6587\u672C", htmlFor: "banner-text", children: _jsx("textarea", { id: "banner-text", className: "flex min-h-16 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: "\u5982\uFF1A\u4ECA\u665A 22:00\u201323:00 \u4F8B\u884C\u7EF4\u62A4\uFF0C\u671F\u95F4\u53EF\u80FD\u77ED\u6682\u4E0D\u53EF\u7528", maxLength: 500, value: bannerText, onChange: (e) => setBannerText(e.target.value) }) }), _jsx(FormField, { label: "\u6837\u5F0F", htmlFor: "banner-kind", children: _jsxs(Select, { value: bannerKind, onValueChange: (v) => setBannerKind(v === 'warn' ? 'warn' : 'info'), children: [_jsx(SelectTrigger, { id: "banner-kind", size: "sm", "aria-label": "\u516C\u544A\u6837\u5F0F", className: "w-40", children: _jsx(SelectValue, { children: bannerKind === 'warn' ? '警示(橙红)' : '信息(蓝)' }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "info", children: "\u4FE1\u606F\uFF08\u84DD\uFF09" }), _jsx(SelectItem, { value: "warn", children: "\u8B66\u793A\uFF08\u6A59\u7EA2\uFF09" })] })] }) }), _jsx("div", { children: _jsx(Button, { size: "sm", disabled: bannerM.isPending || !hydrated, onClick: () => bannerM.mutate(), children: bannerM.isPending ? '保存中…' : '保存公告' }) })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardDescription, { children: "\u7EF4\u62A4\u6A21\u5F0F" }), _jsx(CardTitle, { children: "\u5168\u7AD9\"\u7EF4\u62A4\u4E2D\"\u63D0\u793A\u6761" }), _jsx(CardAction, { children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: maintenance ? 'destructive' : 'outline', children: maintenance ? '维护中' : '正常运行' }), _jsx(Switch, { checked: maintenance, onCheckedChange: (v) => {
181
+ if (saveM.isPending)
182
+ return;
183
+ setMaintenance(v);
184
+ saveM.mutate({
185
+ key: 'maintenance_mode',
186
+ value: v ? '1' : '0',
187
+ label: '维护模式',
188
+ });
189
+ }, "aria-label": "\u7EF4\u62A4\u6A21\u5F0F\u5F00\u5173" })] }) })] }), _jsx(CardContent, { children: _jsx("p", { className: "text-sm text-muted-foreground", children: desc.maintenance_mode }) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardDescription, { children: "\u6CE8\u518C\u7BA1\u63A7" }), _jsx(CardTitle, { children: "\u6CE8\u518C\u5F00\u5173\u4E0E\u57DF\u540D\u767D\u540D\u5355" }), _jsx(CardAction, { children: _jsx(Switch, { checked: allowSignup, onCheckedChange: (v) => {
190
+ if (saveM.isPending)
191
+ return;
192
+ setAllowSignup(v);
193
+ saveM.mutate({
194
+ key: 'allow_signup',
195
+ value: v ? '1' : '0',
196
+ label: '注册开关',
197
+ });
198
+ }, "aria-label": "\u5F00\u653E\u6CE8\u518C\u5F00\u5173" }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: desc.allow_signup }), _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Badge, { variant: allowSignup ? 'secondary' : 'destructive', children: allowSignup ? '开放注册' : '邀请制' }), !allowSignup && (_jsx("span", { className: "text-muted-foreground", children: "\u6CE8\u518C\u9875\u4F1A\u663E\u793A\"\u5F53\u524D\u4E3A\u9080\u8BF7\u5236\u6CE8\u518C\"\u63D0\u793A\uFF08\u4E0D\u963B\u65AD\u63D0\u4EA4\uFF0C\u670D\u52A1\u7AEF\u624D\u662F\u8FB9\u754C\uFF09" }))] }), _jsx(FormField, { label: "\u57DF\u540D\u767D\u540D\u5355\uFF08\u9017\u53F7\u5206\u9694\uFF0C\u7A7A = \u4E0D\u9650\uFF09", htmlFor: "allowed-domains", children: _jsx(Input, { id: "allowed-domains", placeholder: "\u5982\uFF1Acorp.com, edu.cn", value: domains, onChange: (e) => setDomains(e.target.value) }) }), _jsx("div", { children: _jsx(Button, { size: "sm", variant: "outline", disabled: domainsM.isPending || !hydrated, onClick: () => domainsM.mutate(), children: domainsM.isPending ? '保存中…' : '保存白名单' }) })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardDescription, { children: "API \u9650\u6D41" }), _jsx(CardTitle, { children: "\u6BCF\u7528\u6237 / \u6BCF IP \u9650\u6D41\u4E0E\u8C41\u514D\u540D\u5355" }), _jsx(CardAction, { children: _jsx(Badge, { variant: rlPerUser !== '0' || rlPerIP !== '0' ? 'secondary' : 'outline', children: rlPerUser !== '0' || rlPerIP !== '0' ? '启用中' : '按缺省' }) })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: [desc.rate_limit_per_user_per_min, ' ', "\u4FEE\u6539\u540E\u9700\u91CD\u542F\u751F\u6548\uFF08\u542F\u52A8\u671F\u9759\u6001\u6CE8\u5165\uFF0C\u4E0D\u505A\u70ED\u91CD\u8F7D\uFF09\u3002"] }), _jsxs("div", { className: "grid max-w-3xl gap-4 sm:grid-cols-2", children: [_jsx(FormField, { label: "\u6BCF\u7528\u6237\u8BF7\u6C42/\u5206\uFF080 = \u5173\u95ED\uFF09", htmlFor: "rl-per-user", error: rlError?.startsWith('每用户') ? rlError : undefined, children: _jsx(Input, { id: "rl-per-user", type: "text", inputMode: "numeric", placeholder: "\u5982 600\uFF1B0 = \u5173\u95ED", value: rlPerUser, onChange: (e) => {
199
+ setRlPerUser(e.target.value);
200
+ if (rlError?.startsWith('每用户'))
201
+ setRlError(null);
202
+ }, "aria-invalid": rlError?.startsWith('每用户') ? true : undefined }) }), _jsx(FormField, { label: "\u6BCF IP \u8BF7\u6C42/\u5206\uFF08\u8BA4\u8BC1\u8DEF\u5F84\uFF1B0 = \u5173\u95ED\uFF09", htmlFor: "rl-per-ip", error: rlError?.startsWith('每 IP') ? rlError : undefined, children: _jsx(Input, { id: "rl-per-ip", type: "text", inputMode: "numeric", placeholder: "\u7F3A\u7701 30\uFF1B0 = \u5173\u95ED", value: rlPerIP, onChange: (e) => {
203
+ setRlPerIP(e.target.value);
204
+ if (rlError?.startsWith('每 IP'))
205
+ setRlError(null);
206
+ }, "aria-invalid": rlError?.startsWith('每 IP') ? true : undefined }) })] }), _jsx(FormField, { label: "\u8C41\u514D\u540D\u5355\uFF08\u9017\u53F7\u5206\u9694 IP \u6216 CIDR\uFF0C\u7A7A = \u65E0\u8C41\u514D\uFF09", htmlFor: "rl-allowlist", children: _jsx(Input, { id: "rl-allowlist", className: "font-mono", placeholder: "\u5982 10.0.0.5, 192.168.0.0/16", value: rlAllowlist, onChange: (e) => setRlAllowlist(e.target.value) }) }), _jsx("div", { children: _jsx(Button, { size: "sm", variant: "outline", disabled: rateLimitM.isPending || !hydrated, onClick: () => rateLimitM.mutate(), children: rateLimitM.isPending ? '保存中…' : '保存限流配置' }) })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardDescription, { children: "\u90AE\u4EF6\u901A\u9053" }), _jsx(CardTitle, { children: "\u53D1\u9001\u6D4B\u8BD5\u90AE\u4EF6" })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "\u9A8C\u8BC1\u51FA\u7AD9\u90AE\u4EF6\u901A\u9053\u8FDE\u901A\u6027\u3002\u5B9E\u9645\u884C\u4E3A\u53D6\u51B3\u4E8E\u90E8\u7F72\u65F6\u7684\u90AE\u4EF6\u901A\u9053\uFF1A\u5F00\u53D1\u901A\u9053\u53EA\u6253\u65E5\u5FD7\u3001 \u4E0D\u771F\u53D1\u4FE1\uFF1B\u63A5\u5165 SMTP \u540E\u6B64\u6309\u94AE\u53D1\u9001\u771F\u5B9E\u90AE\u4EF6\u3002" }), _jsxs("div", { className: "flex max-w-md gap-2", children: [_jsx(Input, { type: "email", placeholder: "\u6536\u4EF6\u90AE\u7BB1\uFF0C\u5982 ops@example.com", value: testTo, onChange: (e) => setTestTo(e.target.value), "aria-label": "\u6D4B\u8BD5\u90AE\u4EF6\u6536\u4EF6\u90AE\u7BB1" }), _jsx(Button, { size: "sm", className: "shrink-0", disabled: testEmailM.isPending || !testTo.trim(), onClick: () => {
207
+ setTestMsg(null);
208
+ testEmailM.mutate();
209
+ }, children: testEmailM.isPending ? '发送中…' : '发送' })] }), testMsg && (_jsx("p", { role: "status", className: "rounded-md border bg-muted px-3 py-2 text-sm text-muted-foreground", children: testMsg }))] })] })] }));
210
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface AdminShellProps {
3
+ children: ReactNode;
4
+ productPath?: string;
5
+ loginPath?: string;
6
+ }
7
+ export declare function AdminShell({ children, productPath, loginPath, }: AdminShellProps): import("react").JSX.Element;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Link, Navigate, useLocation } from 'react-router';
3
+ import { Activity, ArrowLeft, Building2, KeyRound, LayoutDashboard, Receipt, ScrollText, Settings, ShieldCheck, Users, Webhook, } from 'lucide-react';
4
+ import { buttonVariants } from '../../components/ui/Button';
5
+ import { UserMenu } from '../../components/UserMenu';
6
+ import { useAuth } from '../../hooks/useAuth';
7
+ import { cn } from '../../lib/utils';
8
+ const NAV = [
9
+ {
10
+ to: '/admin/overview',
11
+ label: '概览',
12
+ icon: _jsx(LayoutDashboard, { size: 16, "aria-hidden": "true" }),
13
+ },
14
+ {
15
+ to: '/admin/users',
16
+ label: '用户',
17
+ icon: _jsx(Users, { size: 16, "aria-hidden": "true" }),
18
+ },
19
+ {
20
+ to: '/admin/workspaces',
21
+ label: '工作区',
22
+ icon: _jsx(Building2, { size: 16, "aria-hidden": "true" }),
23
+ },
24
+ {
25
+ to: '/admin/orders',
26
+ label: '订单',
27
+ icon: _jsx(Receipt, { size: 16, "aria-hidden": "true" }),
28
+ },
29
+ {
30
+ to: '/admin/webhook-deliveries',
31
+ label: 'Webhooks',
32
+ icon: _jsx(Webhook, { size: 16, "aria-hidden": "true" }),
33
+ },
34
+ {
35
+ to: '/admin/sso',
36
+ label: 'SSO',
37
+ icon: _jsx(KeyRound, { size: 16, "aria-hidden": "true" }),
38
+ },
39
+ {
40
+ to: '/admin/audit',
41
+ label: '审计',
42
+ icon: _jsx(ScrollText, { size: 16, "aria-hidden": "true" }),
43
+ },
44
+ {
45
+ to: '/admin/overview#analytics',
46
+ label: '分析',
47
+ icon: _jsx(Activity, { size: 16, "aria-hidden": "true" }),
48
+ hash: true,
49
+ },
50
+ {
51
+ to: '/admin/settings',
52
+ label: '系统',
53
+ icon: _jsx(Settings, { size: 16, "aria-hidden": "true" }),
54
+ },
55
+ ];
56
+ function isActive(pathname, hash, item) {
57
+ if (item.hash)
58
+ return pathname === '/admin/overview' && hash === '#analytics';
59
+ if (item.to === '/admin/overview')
60
+ return pathname === '/admin/overview' && hash !== '#analytics';
61
+ return pathname === item.to || pathname.startsWith(item.to + '/');
62
+ }
63
+ function renderNav(pathname, hash, className) {
64
+ return NAV.map((item) => {
65
+ const active = isActive(pathname, hash, item);
66
+ return (_jsxs(Link, { to: item.to, "aria-current": active ? 'page' : undefined, className: cn(buttonVariants({ variant: 'ghost' }), className, active && 'bg-muted'), children: [item.icon, item.label] }, item.label));
67
+ });
68
+ }
69
+ export function AdminShell({ children, productPath = '/app', loginPath = '/login', }) {
70
+ const { user, loading } = useAuth();
71
+ const { pathname, hash, search } = useLocation();
72
+ if (loading) {
73
+ return (_jsx("div", { className: "flex min-h-svh items-center justify-center text-sm text-muted-foreground", children: "\u52A0\u8F7D\u4E2D\u2026" }));
74
+ }
75
+ if (!user) {
76
+ return (_jsx(Navigate, { to: loginPath, state: { from: `${pathname}${search}` }, replace: true }));
77
+ }
78
+ return (_jsxs("div", { className: "flex h-svh flex-col bg-background text-foreground", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center justify-between gap-3 border-b px-4 lg:px-6", children: [_jsxs(Link, { to: "/admin/overview", className: "flex items-center gap-2 text-sm font-semibold text-foreground", children: [_jsx("span", { className: "flex size-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary", children: _jsx(ShieldCheck, { size: 16, "aria-hidden": "true" }) }), "\u7BA1\u7406\u63A7\u5236\u53F0"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Link, { to: productPath, className: cn(buttonVariants({ variant: 'ghost', size: 'sm' }), 'text-muted-foreground'), children: [_jsx(ArrowLeft, { size: 15, "aria-hidden": "true" }), " \u8FD4\u56DE\u4EA7\u54C1"] }), _jsx(UserMenu, {})] })] }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsx("nav", { "aria-label": "\u7BA1\u7406\u63A7\u5236\u53F0\u5BFC\u822A", className: "flex gap-1 overflow-x-auto border-b px-3 py-2 md:hidden", children: renderNav(pathname, hash, 'shrink-0 justify-start whitespace-nowrap px-3') }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx("aside", { className: "hidden w-56 shrink-0 border-r md:block", children: _jsx("nav", { "aria-label": "\u7BA1\u7406\u63A7\u5236\u53F0\u5BFC\u822A", className: "space-y-1 p-3", children: renderNav(pathname, hash, 'w-full justify-start') }) }), _jsx("main", { className: "min-w-0 flex-1 overflow-y-auto", children: _jsx("div", { className: "mx-auto w-full max-w-6xl p-4 lg:p-6", children: children }) })] })] })] }));
79
+ }
@@ -0,0 +1 @@
1
+ export declare function AuditPage(): import("react").JSX.Element;
@@ -0,0 +1,164 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { useSearchParams } from 'react-router';
4
+ import { Download, Search } from 'lucide-react';
5
+ import { useQuery } from '@tanstack/react-query';
6
+ import { apiFetch } from '@ploykit/client';
7
+ import { DataTable } from '../../components/DataTable';
8
+ import { PageEmpty, PageError, PageHeader } from '../../components/Page';
9
+ import { toast } from '../../components/toast';
10
+ import { Button } from '../../components/ui/Button';
11
+ import { Input } from '../../components/ui/Input';
12
+ import { useApi } from '../../hooks/useApi';
13
+ import { apiErrorMessage } from '../../lib/api-error';
14
+ import { shortID, formatDateTime } from '../../lib/utils';
15
+ import { ADMIN_PAGE_SIZE, adminError } from './shared';
16
+ const emptyDraft = {
17
+ workspace: '',
18
+ action: '',
19
+ actor: '',
20
+ actorId: '',
21
+ from: '',
22
+ to: '',
23
+ };
24
+ function draftFromParams(p) {
25
+ return {
26
+ workspace: p.get('workspace') ?? '',
27
+ action: p.get('action') ?? '',
28
+ actor: p.get('actor') ?? '',
29
+ actorId: p.get('actor_id') ?? '',
30
+ from: p.get('from') ?? '',
31
+ to: p.get('to') ?? '',
32
+ };
33
+ }
34
+ function paramsFromDraft(d, page) {
35
+ const p = new URLSearchParams();
36
+ if (d.workspace.trim())
37
+ p.set('workspace', d.workspace.trim());
38
+ if (d.action.trim())
39
+ p.set('action', d.action.trim());
40
+ if (d.actor.trim())
41
+ p.set('actor', d.actor.trim());
42
+ if (d.actorId.trim())
43
+ p.set('actor_id', d.actorId.trim());
44
+ if (d.from)
45
+ p.set('from', d.from);
46
+ if (d.to)
47
+ p.set('to', d.to);
48
+ if (page > 1)
49
+ p.set('page', String(page));
50
+ return p;
51
+ }
52
+ function auditQueryString(d, limit, offset, withPaging) {
53
+ const p = new URLSearchParams();
54
+ if (d.workspace.trim())
55
+ p.set('workspace', d.workspace.trim());
56
+ if (d.action.trim())
57
+ p.set('action', d.action.trim());
58
+ if (d.actor.trim())
59
+ p.set('actor', d.actor.trim());
60
+ if (d.actorId.trim())
61
+ p.set('actor_id', d.actorId.trim());
62
+ if (d.from)
63
+ p.set('from', `${d.from}T00:00:00Z`);
64
+ if (d.to) {
65
+ const t = new Date(`${d.to}T00:00:00Z`);
66
+ t.setUTCDate(t.getUTCDate() + 1);
67
+ p.set('to', t.toISOString());
68
+ }
69
+ if (withPaging) {
70
+ p.set('limit', String(limit));
71
+ p.set('offset', String(offset));
72
+ }
73
+ const qs = p.toString();
74
+ return qs ? `?${qs}` : '';
75
+ }
76
+ export function AuditPage() {
77
+ const api = useApi();
78
+ const [searchParams, setSearchParams] = useSearchParams();
79
+ const [draft, setDraft] = useState(() => draftFromParams(searchParams));
80
+ useEffect(() => {
81
+ setDraft(draftFromParams(searchParams));
82
+ }, [searchParams]);
83
+ const applied = useMemo(() => draftFromParams(searchParams), [searchParams]);
84
+ const page = Math.max(1, Number.parseInt(searchParams.get('page') ?? '1', 10) || 1);
85
+ const [exporting, setExporting] = useState(false);
86
+ const offset = (page - 1) * ADMIN_PAGE_SIZE;
87
+ const auditQ = useQuery({
88
+ queryKey: [
89
+ 'admin',
90
+ 'audit',
91
+ auditQueryString(applied, ADMIN_PAGE_SIZE, offset, true),
92
+ ],
93
+ queryFn: () => api.get(`/api/admin/audit${auditQueryString(applied, ADMIN_PAGE_SIZE, offset, true)}`),
94
+ });
95
+ const onExportCsv = async () => {
96
+ setExporting(true);
97
+ try {
98
+ const blob = await apiFetch(`/api/admin/audit/export.csv${auditQueryString(applied, 0, 0, false)}`, { responseType: 'blob' });
99
+ const url = URL.createObjectURL(blob);
100
+ const a = document.createElement('a');
101
+ a.href = url;
102
+ a.download = `audit-admin-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.csv`;
103
+ a.click();
104
+ URL.revokeObjectURL(url);
105
+ toast.success('CSV 导出已开始下载');
106
+ }
107
+ catch (e) {
108
+ toast.error(apiErrorMessage(e, '导出失败'));
109
+ }
110
+ finally {
111
+ setExporting(false);
112
+ }
113
+ };
114
+ const apply = () => {
115
+ setSearchParams(paramsFromDraft(draft, 1));
116
+ };
117
+ const items = auditQ.data?.items ?? [];
118
+ const total = auditQ.data?.total ?? 0;
119
+ const pastEnd = !auditQ.isPending && items.length === 0 && page > 1;
120
+ const columns = [
121
+ {
122
+ key: 'created_at',
123
+ header: '时间',
124
+ className: 'w-44',
125
+ render: (e) => (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", children: formatDateTime(e.created_at) })),
126
+ },
127
+ {
128
+ key: 'workspace',
129
+ header: '工作区',
130
+ className: 'w-28',
131
+ render: (e) => e.workspace_id ? (_jsx("span", { className: "font-mono text-xs", title: e.workspace_id, children: shortID(e.workspace_id) })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u5E73\u53F0\u7EA7" })),
132
+ },
133
+ {
134
+ key: 'actor',
135
+ header: '操作者',
136
+ className: 'w-[22%]',
137
+ render: (e) => {
138
+ const snap = (e.actor_snapshot ?? {});
139
+ const email = typeof snap.email === 'string' ? snap.email : '';
140
+ return (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate", children: email || e.actor_id || '—' }), _jsx("p", { className: "text-xs text-muted-foreground", children: e.actor_type })] }));
141
+ },
142
+ },
143
+ {
144
+ key: 'action',
145
+ header: '动作',
146
+ className: 'w-[20%]',
147
+ render: (e) => (_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: e.action })),
148
+ },
149
+ {
150
+ key: 'resource',
151
+ header: '资源',
152
+ render: (e) => (_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate", children: e.resource_type }), e.resource_id && (_jsx("p", { className: "truncate font-mono text-xs text-muted-foreground", children: e.resource_id }))] })),
153
+ },
154
+ ];
155
+ return (_jsxs("div", { children: [_jsx(PageHeader, { title: "\u5BA1\u8BA1", description: "\u5E73\u53F0\u7EA7\u5BA1\u8BA1\u65E5\u5FD7\uFF08\u8DE8\u5DE5\u4F5C\u533A\uFF1B\u6309\u65F6\u95F4\u5012\u5E8F\uFF0C\u4FDD\u7559\u6700\u8FD1\u4E8B\u4EF6\uFF09" }), _jsxs("div", { className: "mb-4 flex flex-wrap items-end gap-2", children: [_jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u5DE5\u4F5C\u533A ID" }), _jsx(Input, { className: "w-56 font-mono", placeholder: "workspace uuid", value: draft.workspace, onChange: (e) => setDraft({ ...draft, workspace: e.target.value }) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u52A8\u4F5C" }), _jsx(Input, { className: "w-44", placeholder: "\u5982 workspace.rename", value: draft.action, onChange: (e) => setDraft({ ...draft, action: e.target.value }) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u64CD\u4F5C\u8005\u90AE\u7BB1" }), _jsx(Input, { className: "w-48", placeholder: "\u90AE\u7BB1\u524D\u7F00\uFF0C\u5982 alice@", value: draft.actor, onChange: (e) => setDraft({ ...draft, actor: e.target.value }) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u64CD\u4F5C\u8005 ID" }), _jsx(Input, { className: "w-52 font-mono", placeholder: "actor uuid", value: draft.actorId, onChange: (e) => setDraft({ ...draft, actorId: e.target.value }) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u8D77\u59CB\u65E5\u671F" }), _jsx(Input, { className: "w-40", type: "date", value: draft.from, onChange: (e) => setDraft({ ...draft, from: e.target.value }) })] }), _jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7ED3\u675F\u65E5\u671F" }), _jsx(Input, { className: "w-40", type: "date", value: draft.to, onChange: (e) => setDraft({ ...draft, to: e.target.value }) })] }), _jsxs(Button, { onClick: apply, children: [_jsx(Search, {}), " \u67E5\u8BE2"] }), _jsx(Button, { variant: "outline", onClick: () => {
156
+ setDraft(emptyDraft);
157
+ setSearchParams(paramsFromDraft(emptyDraft, 1));
158
+ }, children: "\u91CD\u7F6E" }), _jsxs(Button, { variant: "outline", disabled: exporting, onClick: onExportCsv, children: [_jsx(Download, {}), " ", exporting ? '导出中…' : '导出 CSV'] })] }), auditQ.isError ? (_jsx(PageError, { message: adminError(auditQ.error, '审计日志加载失败'), onRetry: () => void auditQ.refetch() })) : (_jsx(DataTable, { columns: columns, rows: items, rowKey: (e) => e.id, loading: auditQ.isPending, pagination: {
159
+ page,
160
+ pageSize: ADMIN_PAGE_SIZE,
161
+ total,
162
+ onPageChange: (p) => setSearchParams(paramsFromDraft(applied, p)),
163
+ }, empty: pastEnd ? (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(PageEmpty, { label: "\u6CA1\u6709\u66F4\u591A\u5BA1\u8BA1\u8BB0\u5F55\u4E86", hint: "\u5F53\u524D\u9875\u6570\u636E\u53EF\u80FD\u5DF2\u88AB\u8F6E\u8F6C\u5220\u9664" }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setSearchParams(paramsFromDraft(applied, 1)), children: "\u8FD4\u56DE\u7B2C\u4E00\u9875" })] })) : (_jsx(PageEmpty, { label: "\u6682\u65E0\u5BA1\u8BA1\u8BB0\u5F55", hint: "\u8C03\u6574\u8FC7\u6EE4\u6761\u4EF6\u6216\u89E6\u53D1\u4E00\u4E9B\u64CD\u4F5C\u540E\u518D\u6765" })) }))] }));
164
+ }
@@ -0,0 +1 @@
1
+ export declare function OrdersPage(): import("react").JSX.Element;