dsh-plugin-image-workbench 1.1.5 → 1.1.6
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/LICENSE +21 -21
- package/README.en.md +5 -3
- package/README.md +7 -3
- package/client.js +241 -241
- package/cordis.patch.yml +14 -14
- package/host.js +51 -0
- package/install.mjs +333 -387
- package/package.json +3 -2
- package/scripts/fetch-style-images.mjs +104 -0
- package/scripts/vendor-snapshot.py +154 -154
- package/server/access.py +26 -26
- package/server/app.py +1329 -1329
- package/server/catalog.py +147 -147
- package/server/composer.py +391 -391
- package/server/drafts.py +158 -158
- package/server/history.py +575 -575
- package/server/imaging.py +172 -172
- package/server/pool.py +1335 -1335
- package/server/pool_admin.py +921 -921
- package/server/pool_service.py +1073 -1073
- package/server/requirements.txt +6 -6
- package/server/smoke_check.py +402 -402
- package/server/static/access.html +202 -202
- package/vendor/chatgpt2api/source/.env.example +82 -82
- package/vendor/chatgpt2api/source/.python-version +1 -1
- package/vendor/chatgpt2api/source/LICENSE +21 -21
- package/vendor/chatgpt2api/source/README.md +391 -391
- package/vendor/chatgpt2api/source/VERSION +1 -1
- package/vendor/chatgpt2api/source/api/__init__.py +2 -2
- package/vendor/chatgpt2api/source/api/accounts.py +537 -537
- package/vendor/chatgpt2api/source/api/ai.py +217 -217
- package/vendor/chatgpt2api/source/api/app.py +62 -62
- package/vendor/chatgpt2api/source/api/errors.py +46 -46
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
- package/vendor/chatgpt2api/source/api/support.py +134 -134
- package/vendor/chatgpt2api/source/api/system.py +365 -365
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
- package/vendor/chatgpt2api/source/config.default.json +108 -108
- package/vendor/chatgpt2api/source/main.py +9 -9
- package/vendor/chatgpt2api/source/pyproject.toml +27 -27
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
- package/vendor/chatgpt2api/source/services/config.py +667 -667
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
- package/vendor/chatgpt2api/source/services/image_service.py +376 -376
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
- package/vendor/chatgpt2api/source/services/log_service.py +337 -337
- package/vendor/chatgpt2api/source/services/model_service.py +182 -182
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
- package/vendor/chatgpt2api/source/utils/helper.py +469 -469
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
- package/vendor/chatgpt2api/source/utils/log.py +110 -110
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
- package/vendor/chatgpt2api/source/utils/pow.py +207 -207
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
- package/vendor/chatgpt2api/source/uv.lock +904 -904
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
- package/vendor/chatgpt2api/source/web/components.json +24 -24
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
- package/vendor/chatgpt2api/source/web/package.json +58 -58
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
|
@@ -1,422 +1,422 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { Ban, CheckCircle2, Copy, KeyRound, LoaderCircle, Pencil, Plus, Trash2 } from "lucide-react";
|
|
5
|
-
import { toast } from "sonner";
|
|
6
|
-
|
|
7
|
-
import { Badge } from "@/components/ui/badge";
|
|
8
|
-
import { Button } from "@/components/ui/button";
|
|
9
|
-
import { Card, CardContent } from "@/components/ui/card";
|
|
10
|
-
import {
|
|
11
|
-
Dialog,
|
|
12
|
-
DialogContent,
|
|
13
|
-
DialogDescription,
|
|
14
|
-
DialogFooter,
|
|
15
|
-
DialogHeader,
|
|
16
|
-
DialogTitle,
|
|
17
|
-
} from "@/components/ui/dialog";
|
|
18
|
-
import { Input } from "@/components/ui/input";
|
|
19
|
-
import { createUserKey, deleteUserKey, fetchUserKeys, updateUserKey, type UserKey } from "@/lib/api";
|
|
20
|
-
|
|
21
|
-
function formatDateTime(value?: string | null) {
|
|
22
|
-
if (!value) {
|
|
23
|
-
return "—";
|
|
24
|
-
}
|
|
25
|
-
const date = new Date(value);
|
|
26
|
-
if (Number.isNaN(date.getTime())) {
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
return new Intl.DateTimeFormat("zh-CN", {
|
|
30
|
-
year: "numeric",
|
|
31
|
-
month: "2-digit",
|
|
32
|
-
day: "2-digit",
|
|
33
|
-
hour: "2-digit",
|
|
34
|
-
minute: "2-digit",
|
|
35
|
-
}).format(date);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function UserKeysCard() {
|
|
39
|
-
const didLoadRef = useRef(false);
|
|
40
|
-
const [items, setItems] = useState<UserKey[]>([]);
|
|
41
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
42
|
-
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
43
|
-
const [name, setName] = useState("");
|
|
44
|
-
const [isCreating, setIsCreating] = useState(false);
|
|
45
|
-
const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set());
|
|
46
|
-
const [revealedKey, setRevealedKey] = useState("");
|
|
47
|
-
const [deletingItem, setDeletingItem] = useState<UserKey | null>(null);
|
|
48
|
-
const [editingItem, setEditingItem] = useState<UserKey | null>(null);
|
|
49
|
-
const [editName, setEditName] = useState("");
|
|
50
|
-
const [editKey, setEditKey] = useState("");
|
|
51
|
-
|
|
52
|
-
const load = async () => {
|
|
53
|
-
setIsLoading(true);
|
|
54
|
-
try {
|
|
55
|
-
const data = await fetchUserKeys();
|
|
56
|
-
setItems(data.items);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
toast.error(error instanceof Error ? error.message : "加载用户密钥失败");
|
|
59
|
-
} finally {
|
|
60
|
-
setIsLoading(false);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
if (didLoadRef.current) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
didLoadRef.current = true;
|
|
69
|
-
void load();
|
|
70
|
-
}, []);
|
|
71
|
-
|
|
72
|
-
const handleCreate = async () => {
|
|
73
|
-
setIsCreating(true);
|
|
74
|
-
try {
|
|
75
|
-
const data = await createUserKey(name.trim());
|
|
76
|
-
setItems(data.items);
|
|
77
|
-
setRevealedKey(data.key);
|
|
78
|
-
setName("");
|
|
79
|
-
setIsDialogOpen(false);
|
|
80
|
-
toast.success("用户密钥已创建");
|
|
81
|
-
} catch (error) {
|
|
82
|
-
toast.error(error instanceof Error ? error.message : "创建用户密钥失败");
|
|
83
|
-
} finally {
|
|
84
|
-
setIsCreating(false);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const setItemPending = (id: string, isPending: boolean) => {
|
|
89
|
-
setPendingIds((current) => {
|
|
90
|
-
const next = new Set(current);
|
|
91
|
-
if (isPending) {
|
|
92
|
-
next.add(id);
|
|
93
|
-
} else {
|
|
94
|
-
next.delete(id);
|
|
95
|
-
}
|
|
96
|
-
return next;
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const handleToggle = async (item: UserKey) => {
|
|
101
|
-
setItemPending(item.id, true);
|
|
102
|
-
try {
|
|
103
|
-
const data = await updateUserKey(item.id, { enabled: !item.enabled });
|
|
104
|
-
setItems(data.items);
|
|
105
|
-
toast.success(item.enabled ? "用户密钥已禁用" : "用户密钥已启用");
|
|
106
|
-
} catch (error) {
|
|
107
|
-
toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
|
|
108
|
-
} finally {
|
|
109
|
-
setItemPending(item.id, false);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const handleDelete = async () => {
|
|
114
|
-
if (!deletingItem) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const item = deletingItem;
|
|
118
|
-
setItemPending(item.id, true);
|
|
119
|
-
try {
|
|
120
|
-
const data = await deleteUserKey(item.id);
|
|
121
|
-
setItems(data.items);
|
|
122
|
-
setDeletingItem(null);
|
|
123
|
-
toast.success("用户密钥已删除");
|
|
124
|
-
} catch (error) {
|
|
125
|
-
toast.error(error instanceof Error ? error.message : "删除用户密钥失败");
|
|
126
|
-
} finally {
|
|
127
|
-
setItemPending(item.id, false);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const openEditDialog = (item: UserKey) => {
|
|
132
|
-
setEditingItem(item);
|
|
133
|
-
setEditName(item.name);
|
|
134
|
-
setEditKey("");
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const handleEdit = async () => {
|
|
138
|
-
if (!editingItem) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const item = editingItem;
|
|
142
|
-
const trimmedName = editName.trim();
|
|
143
|
-
const trimmedKey = editKey.trim();
|
|
144
|
-
if (trimmedName === item.name && !trimmedKey) {
|
|
145
|
-
setEditingItem(null);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
setItemPending(item.id, true);
|
|
149
|
-
try {
|
|
150
|
-
const data = await updateUserKey(item.id, {
|
|
151
|
-
...(trimmedName !== item.name ? { name: trimmedName } : {}),
|
|
152
|
-
...(trimmedKey ? { key: trimmedKey } : {}),
|
|
153
|
-
});
|
|
154
|
-
setItems(data.items);
|
|
155
|
-
setEditingItem(null);
|
|
156
|
-
setEditKey("");
|
|
157
|
-
toast.success(trimmedKey ? "用户密钥已更新" : "用户名称已更新");
|
|
158
|
-
} catch (error) {
|
|
159
|
-
toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
|
|
160
|
-
} finally {
|
|
161
|
-
setItemPending(item.id, false);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const handleCopy = async (value: string) => {
|
|
166
|
-
try {
|
|
167
|
-
await navigator.clipboard.writeText(value);
|
|
168
|
-
toast.success("已复制到剪贴板");
|
|
169
|
-
} catch {
|
|
170
|
-
toast.error("复制失败,请手动复制");
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<>
|
|
176
|
-
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
177
|
-
<CardContent className="space-y-6 p-6">
|
|
178
|
-
<div className="flex items-start justify-between gap-4">
|
|
179
|
-
<div className="flex items-center gap-3">
|
|
180
|
-
<div className="flex size-10 items-center justify-center rounded-xl bg-stone-100">
|
|
181
|
-
<KeyRound className="size-5 text-stone-600" />
|
|
182
|
-
</div>
|
|
183
|
-
<div>
|
|
184
|
-
<h2 className="text-lg font-semibold tracking-tight">用户密钥管理</h2>
|
|
185
|
-
<p className="text-sm text-stone-500">为普通用户创建专用密钥;普通用户只能进入画图页,不能查看设置和号池。</p>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
<Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => setIsDialogOpen(true)}>
|
|
189
|
-
<Plus className="size-4" />
|
|
190
|
-
创建用户密钥
|
|
191
|
-
</Button>
|
|
192
|
-
</div>
|
|
193
|
-
|
|
194
|
-
{revealedKey ? (
|
|
195
|
-
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-900">
|
|
196
|
-
<div className="font-medium">新密钥仅展示一次,请立即保存:</div>
|
|
197
|
-
<div className="mt-3 flex flex-col gap-3 rounded-lg border border-emerald-200 bg-white/80 p-3 md:flex-row md:items-center md:justify-between">
|
|
198
|
-
<code className="break-all font-mono text-[13px]">{revealedKey}</code>
|
|
199
|
-
<Button
|
|
200
|
-
type="button"
|
|
201
|
-
variant="outline"
|
|
202
|
-
className="h-9 rounded-xl border-emerald-200 bg-white px-4 text-emerald-700"
|
|
203
|
-
onClick={() => void handleCopy(revealedKey)}
|
|
204
|
-
>
|
|
205
|
-
<Copy className="size-4" />
|
|
206
|
-
复制
|
|
207
|
-
</Button>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
) : null}
|
|
211
|
-
|
|
212
|
-
{isLoading ? (
|
|
213
|
-
<div className="flex items-center justify-center py-10">
|
|
214
|
-
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
215
|
-
</div>
|
|
216
|
-
) : items.length === 0 ? (
|
|
217
|
-
<div className="rounded-xl bg-stone-50 px-6 py-10 text-center text-sm text-stone-500">
|
|
218
|
-
暂无普通用户密钥。点击右上角按钮后即可创建并分发给其他人。
|
|
219
|
-
</div>
|
|
220
|
-
) : (
|
|
221
|
-
<div className="space-y-3">
|
|
222
|
-
{items.map((item) => {
|
|
223
|
-
const isPending = pendingIds.has(item.id);
|
|
224
|
-
return (
|
|
225
|
-
<div key={item.id} className="flex flex-col gap-3 rounded-xl border border-stone-200 bg-white px-4 py-4 md:flex-row md:items-center md:justify-between">
|
|
226
|
-
<div className="min-w-0 space-y-2">
|
|
227
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
228
|
-
<div className="truncate text-sm font-medium text-stone-800">{item.name}</div>
|
|
229
|
-
<Badge variant={item.enabled ? "success" : "secondary"} className="rounded-md">
|
|
230
|
-
{item.enabled ? "已启用" : "已禁用"}
|
|
231
|
-
</Badge>
|
|
232
|
-
</div>
|
|
233
|
-
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500">
|
|
234
|
-
<span>创建时间 {formatDateTime(item.created_at)}</span>
|
|
235
|
-
<span>最近使用 {formatDateTime(item.last_used_at)}</span>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
<div className="flex items-center gap-2">
|
|
240
|
-
<Button
|
|
241
|
-
type="button"
|
|
242
|
-
variant="outline"
|
|
243
|
-
className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
|
|
244
|
-
onClick={() => openEditDialog(item)}
|
|
245
|
-
disabled={isPending}
|
|
246
|
-
>
|
|
247
|
-
{isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
|
|
248
|
-
编辑
|
|
249
|
-
</Button>
|
|
250
|
-
<Button
|
|
251
|
-
type="button"
|
|
252
|
-
variant="outline"
|
|
253
|
-
className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
|
|
254
|
-
onClick={() => void handleToggle(item)}
|
|
255
|
-
disabled={isPending}
|
|
256
|
-
>
|
|
257
|
-
{isPending ? (
|
|
258
|
-
<LoaderCircle className="size-4 animate-spin" />
|
|
259
|
-
) : item.enabled ? (
|
|
260
|
-
<Ban className="size-4" />
|
|
261
|
-
) : (
|
|
262
|
-
<CheckCircle2 className="size-4" />
|
|
263
|
-
)}
|
|
264
|
-
{item.enabled ? "禁用" : "启用"}
|
|
265
|
-
</Button>
|
|
266
|
-
<Button
|
|
267
|
-
type="button"
|
|
268
|
-
variant="outline"
|
|
269
|
-
className="h-9 rounded-xl border-rose-200 bg-white px-4 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
|
|
270
|
-
onClick={() => setDeletingItem(item)}
|
|
271
|
-
disabled={isPending}
|
|
272
|
-
>
|
|
273
|
-
{isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
|
274
|
-
删除
|
|
275
|
-
</Button>
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
);
|
|
279
|
-
})}
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
</CardContent>
|
|
283
|
-
</Card>
|
|
284
|
-
|
|
285
|
-
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
286
|
-
<DialogContent className="rounded-2xl p-6">
|
|
287
|
-
<DialogHeader className="gap-2">
|
|
288
|
-
<DialogTitle>创建用户密钥</DialogTitle>
|
|
289
|
-
<DialogDescription className="text-sm leading-6">
|
|
290
|
-
可选填写一个备注名称,方便区分不同使用者;创建后会生成一条只能查看一次的原始密钥。
|
|
291
|
-
</DialogDescription>
|
|
292
|
-
</DialogHeader>
|
|
293
|
-
<div className="space-y-2">
|
|
294
|
-
<label className="text-sm font-medium text-stone-700">名称(可选)</label>
|
|
295
|
-
<Input
|
|
296
|
-
value={name}
|
|
297
|
-
onChange={(event) => setName(event.target.value)}
|
|
298
|
-
placeholder="例如:设计同学 A、运营临时账号"
|
|
299
|
-
className="h-11 rounded-xl border-stone-200 bg-white"
|
|
300
|
-
/>
|
|
301
|
-
</div>
|
|
302
|
-
<DialogFooter>
|
|
303
|
-
<Button
|
|
304
|
-
type="button"
|
|
305
|
-
variant="secondary"
|
|
306
|
-
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
307
|
-
onClick={() => setIsDialogOpen(false)}
|
|
308
|
-
disabled={isCreating}
|
|
309
|
-
>
|
|
310
|
-
取消
|
|
311
|
-
</Button>
|
|
312
|
-
<Button
|
|
313
|
-
type="button"
|
|
314
|
-
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
315
|
-
onClick={() => void handleCreate()}
|
|
316
|
-
disabled={isCreating}
|
|
317
|
-
>
|
|
318
|
-
{isCreating ? <LoaderCircle className="size-4 animate-spin" /> : <Plus className="size-4" />}
|
|
319
|
-
创建
|
|
320
|
-
</Button>
|
|
321
|
-
</DialogFooter>
|
|
322
|
-
</DialogContent>
|
|
323
|
-
</Dialog>
|
|
324
|
-
|
|
325
|
-
<Dialog open={Boolean(deletingItem)} onOpenChange={(open) => (!open ? setDeletingItem(null) : null)}>
|
|
326
|
-
<DialogContent className="rounded-2xl p-6">
|
|
327
|
-
<DialogHeader className="gap-2">
|
|
328
|
-
<DialogTitle>删除用户密钥</DialogTitle>
|
|
329
|
-
<DialogDescription className="text-sm leading-6">
|
|
330
|
-
确认删除用户密钥「{deletingItem?.name}」吗?删除后该密钥将无法继续调用接口。
|
|
331
|
-
</DialogDescription>
|
|
332
|
-
</DialogHeader>
|
|
333
|
-
<DialogFooter>
|
|
334
|
-
<Button
|
|
335
|
-
type="button"
|
|
336
|
-
variant="secondary"
|
|
337
|
-
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
338
|
-
onClick={() => setDeletingItem(null)}
|
|
339
|
-
disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
|
|
340
|
-
>
|
|
341
|
-
取消
|
|
342
|
-
</Button>
|
|
343
|
-
<Button
|
|
344
|
-
type="button"
|
|
345
|
-
className="h-10 rounded-xl bg-rose-600 px-5 text-white hover:bg-rose-700"
|
|
346
|
-
onClick={() => void handleDelete()}
|
|
347
|
-
disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
|
|
348
|
-
>
|
|
349
|
-
{deletingItem && pendingIds.has(deletingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
|
350
|
-
删除
|
|
351
|
-
</Button>
|
|
352
|
-
</DialogFooter>
|
|
353
|
-
</DialogContent>
|
|
354
|
-
</Dialog>
|
|
355
|
-
|
|
356
|
-
<Dialog
|
|
357
|
-
open={Boolean(editingItem)}
|
|
358
|
-
onOpenChange={(open) => {
|
|
359
|
-
if (!open) {
|
|
360
|
-
setEditingItem(null);
|
|
361
|
-
setEditKey("");
|
|
362
|
-
}
|
|
363
|
-
}}
|
|
364
|
-
>
|
|
365
|
-
<DialogContent className="rounded-2xl p-6">
|
|
366
|
-
<DialogHeader className="gap-2">
|
|
367
|
-
<DialogTitle>编辑用户密钥</DialogTitle>
|
|
368
|
-
<DialogDescription className="text-sm leading-6">
|
|
369
|
-
可以修改备注名称;如需更换专用密钥,直接填写新的原始密钥即可。留空则保持当前密钥不变。
|
|
370
|
-
</DialogDescription>
|
|
371
|
-
</DialogHeader>
|
|
372
|
-
<div className="space-y-4">
|
|
373
|
-
<div className="space-y-2">
|
|
374
|
-
<label className="text-sm font-medium text-stone-700">名称</label>
|
|
375
|
-
<Input
|
|
376
|
-
value={editName}
|
|
377
|
-
onChange={(event) => setEditName(event.target.value)}
|
|
378
|
-
placeholder="例如:设计同学 A、运营临时账号"
|
|
379
|
-
className="h-11 rounded-xl border-stone-200 bg-white"
|
|
380
|
-
/>
|
|
381
|
-
</div>
|
|
382
|
-
<div className="space-y-2">
|
|
383
|
-
<label className="text-sm font-medium text-stone-700">新的专用密钥(可选)</label>
|
|
384
|
-
<Input
|
|
385
|
-
value={editKey}
|
|
386
|
-
onChange={(event) => setEditKey(event.target.value)}
|
|
387
|
-
placeholder="例如:sk-your-custom-user-key"
|
|
388
|
-
className="h-11 rounded-xl border-stone-200 bg-white font-mono"
|
|
389
|
-
/>
|
|
390
|
-
<p className="text-xs leading-5 text-stone-500">
|
|
391
|
-
保存后旧密钥会立即失效,新密钥生效。系统仍只保存哈希,不会回显当前密钥。
|
|
392
|
-
</p>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
<DialogFooter>
|
|
396
|
-
<Button
|
|
397
|
-
type="button"
|
|
398
|
-
variant="secondary"
|
|
399
|
-
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
400
|
-
onClick={() => {
|
|
401
|
-
setEditingItem(null);
|
|
402
|
-
setEditKey("");
|
|
403
|
-
}}
|
|
404
|
-
disabled={editingItem ? pendingIds.has(editingItem.id) : false}
|
|
405
|
-
>
|
|
406
|
-
取消
|
|
407
|
-
</Button>
|
|
408
|
-
<Button
|
|
409
|
-
type="button"
|
|
410
|
-
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
411
|
-
onClick={() => void handleEdit()}
|
|
412
|
-
disabled={editingItem ? pendingIds.has(editingItem.id) : false}
|
|
413
|
-
>
|
|
414
|
-
{editingItem && pendingIds.has(editingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
|
|
415
|
-
保存
|
|
416
|
-
</Button>
|
|
417
|
-
</DialogFooter>
|
|
418
|
-
</DialogContent>
|
|
419
|
-
</Dialog>
|
|
420
|
-
</>
|
|
421
|
-
);
|
|
422
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { Ban, CheckCircle2, Copy, KeyRound, LoaderCircle, Pencil, Plus, Trash2 } from "lucide-react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
} from "@/components/ui/dialog";
|
|
18
|
+
import { Input } from "@/components/ui/input";
|
|
19
|
+
import { createUserKey, deleteUserKey, fetchUserKeys, updateUserKey, type UserKey } from "@/lib/api";
|
|
20
|
+
|
|
21
|
+
function formatDateTime(value?: string | null) {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return "—";
|
|
24
|
+
}
|
|
25
|
+
const date = new Date(value);
|
|
26
|
+
if (Number.isNaN(date.getTime())) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
30
|
+
year: "numeric",
|
|
31
|
+
month: "2-digit",
|
|
32
|
+
day: "2-digit",
|
|
33
|
+
hour: "2-digit",
|
|
34
|
+
minute: "2-digit",
|
|
35
|
+
}).format(date);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function UserKeysCard() {
|
|
39
|
+
const didLoadRef = useRef(false);
|
|
40
|
+
const [items, setItems] = useState<UserKey[]>([]);
|
|
41
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
42
|
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
43
|
+
const [name, setName] = useState("");
|
|
44
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
45
|
+
const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set());
|
|
46
|
+
const [revealedKey, setRevealedKey] = useState("");
|
|
47
|
+
const [deletingItem, setDeletingItem] = useState<UserKey | null>(null);
|
|
48
|
+
const [editingItem, setEditingItem] = useState<UserKey | null>(null);
|
|
49
|
+
const [editName, setEditName] = useState("");
|
|
50
|
+
const [editKey, setEditKey] = useState("");
|
|
51
|
+
|
|
52
|
+
const load = async () => {
|
|
53
|
+
setIsLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
const data = await fetchUserKeys();
|
|
56
|
+
setItems(data.items);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
toast.error(error instanceof Error ? error.message : "加载用户密钥失败");
|
|
59
|
+
} finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (didLoadRef.current) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
didLoadRef.current = true;
|
|
69
|
+
void load();
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const handleCreate = async () => {
|
|
73
|
+
setIsCreating(true);
|
|
74
|
+
try {
|
|
75
|
+
const data = await createUserKey(name.trim());
|
|
76
|
+
setItems(data.items);
|
|
77
|
+
setRevealedKey(data.key);
|
|
78
|
+
setName("");
|
|
79
|
+
setIsDialogOpen(false);
|
|
80
|
+
toast.success("用户密钥已创建");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
toast.error(error instanceof Error ? error.message : "创建用户密钥失败");
|
|
83
|
+
} finally {
|
|
84
|
+
setIsCreating(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const setItemPending = (id: string, isPending: boolean) => {
|
|
89
|
+
setPendingIds((current) => {
|
|
90
|
+
const next = new Set(current);
|
|
91
|
+
if (isPending) {
|
|
92
|
+
next.add(id);
|
|
93
|
+
} else {
|
|
94
|
+
next.delete(id);
|
|
95
|
+
}
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleToggle = async (item: UserKey) => {
|
|
101
|
+
setItemPending(item.id, true);
|
|
102
|
+
try {
|
|
103
|
+
const data = await updateUserKey(item.id, { enabled: !item.enabled });
|
|
104
|
+
setItems(data.items);
|
|
105
|
+
toast.success(item.enabled ? "用户密钥已禁用" : "用户密钥已启用");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
|
|
108
|
+
} finally {
|
|
109
|
+
setItemPending(item.id, false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleDelete = async () => {
|
|
114
|
+
if (!deletingItem) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const item = deletingItem;
|
|
118
|
+
setItemPending(item.id, true);
|
|
119
|
+
try {
|
|
120
|
+
const data = await deleteUserKey(item.id);
|
|
121
|
+
setItems(data.items);
|
|
122
|
+
setDeletingItem(null);
|
|
123
|
+
toast.success("用户密钥已删除");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
toast.error(error instanceof Error ? error.message : "删除用户密钥失败");
|
|
126
|
+
} finally {
|
|
127
|
+
setItemPending(item.id, false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const openEditDialog = (item: UserKey) => {
|
|
132
|
+
setEditingItem(item);
|
|
133
|
+
setEditName(item.name);
|
|
134
|
+
setEditKey("");
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleEdit = async () => {
|
|
138
|
+
if (!editingItem) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const item = editingItem;
|
|
142
|
+
const trimmedName = editName.trim();
|
|
143
|
+
const trimmedKey = editKey.trim();
|
|
144
|
+
if (trimmedName === item.name && !trimmedKey) {
|
|
145
|
+
setEditingItem(null);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setItemPending(item.id, true);
|
|
149
|
+
try {
|
|
150
|
+
const data = await updateUserKey(item.id, {
|
|
151
|
+
...(trimmedName !== item.name ? { name: trimmedName } : {}),
|
|
152
|
+
...(trimmedKey ? { key: trimmedKey } : {}),
|
|
153
|
+
});
|
|
154
|
+
setItems(data.items);
|
|
155
|
+
setEditingItem(null);
|
|
156
|
+
setEditKey("");
|
|
157
|
+
toast.success(trimmedKey ? "用户密钥已更新" : "用户名称已更新");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
toast.error(error instanceof Error ? error.message : "更新用户密钥失败");
|
|
160
|
+
} finally {
|
|
161
|
+
setItemPending(item.id, false);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleCopy = async (value: string) => {
|
|
166
|
+
try {
|
|
167
|
+
await navigator.clipboard.writeText(value);
|
|
168
|
+
toast.success("已复制到剪贴板");
|
|
169
|
+
} catch {
|
|
170
|
+
toast.error("复制失败,请手动复制");
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<Card className="rounded-2xl border-white/80 bg-white/90 shadow-sm">
|
|
177
|
+
<CardContent className="space-y-6 p-6">
|
|
178
|
+
<div className="flex items-start justify-between gap-4">
|
|
179
|
+
<div className="flex items-center gap-3">
|
|
180
|
+
<div className="flex size-10 items-center justify-center rounded-xl bg-stone-100">
|
|
181
|
+
<KeyRound className="size-5 text-stone-600" />
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<h2 className="text-lg font-semibold tracking-tight">用户密钥管理</h2>
|
|
185
|
+
<p className="text-sm text-stone-500">为普通用户创建专用密钥;普通用户只能进入画图页,不能查看设置和号池。</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => setIsDialogOpen(true)}>
|
|
189
|
+
<Plus className="size-4" />
|
|
190
|
+
创建用户密钥
|
|
191
|
+
</Button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{revealedKey ? (
|
|
195
|
+
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-900">
|
|
196
|
+
<div className="font-medium">新密钥仅展示一次,请立即保存:</div>
|
|
197
|
+
<div className="mt-3 flex flex-col gap-3 rounded-lg border border-emerald-200 bg-white/80 p-3 md:flex-row md:items-center md:justify-between">
|
|
198
|
+
<code className="break-all font-mono text-[13px]">{revealedKey}</code>
|
|
199
|
+
<Button
|
|
200
|
+
type="button"
|
|
201
|
+
variant="outline"
|
|
202
|
+
className="h-9 rounded-xl border-emerald-200 bg-white px-4 text-emerald-700"
|
|
203
|
+
onClick={() => void handleCopy(revealedKey)}
|
|
204
|
+
>
|
|
205
|
+
<Copy className="size-4" />
|
|
206
|
+
复制
|
|
207
|
+
</Button>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
) : null}
|
|
211
|
+
|
|
212
|
+
{isLoading ? (
|
|
213
|
+
<div className="flex items-center justify-center py-10">
|
|
214
|
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
|
215
|
+
</div>
|
|
216
|
+
) : items.length === 0 ? (
|
|
217
|
+
<div className="rounded-xl bg-stone-50 px-6 py-10 text-center text-sm text-stone-500">
|
|
218
|
+
暂无普通用户密钥。点击右上角按钮后即可创建并分发给其他人。
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<div className="space-y-3">
|
|
222
|
+
{items.map((item) => {
|
|
223
|
+
const isPending = pendingIds.has(item.id);
|
|
224
|
+
return (
|
|
225
|
+
<div key={item.id} className="flex flex-col gap-3 rounded-xl border border-stone-200 bg-white px-4 py-4 md:flex-row md:items-center md:justify-between">
|
|
226
|
+
<div className="min-w-0 space-y-2">
|
|
227
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
228
|
+
<div className="truncate text-sm font-medium text-stone-800">{item.name}</div>
|
|
229
|
+
<Badge variant={item.enabled ? "success" : "secondary"} className="rounded-md">
|
|
230
|
+
{item.enabled ? "已启用" : "已禁用"}
|
|
231
|
+
</Badge>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500">
|
|
234
|
+
<span>创建时间 {formatDateTime(item.created_at)}</span>
|
|
235
|
+
<span>最近使用 {formatDateTime(item.last_used_at)}</span>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="flex items-center gap-2">
|
|
240
|
+
<Button
|
|
241
|
+
type="button"
|
|
242
|
+
variant="outline"
|
|
243
|
+
className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
|
|
244
|
+
onClick={() => openEditDialog(item)}
|
|
245
|
+
disabled={isPending}
|
|
246
|
+
>
|
|
247
|
+
{isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
|
|
248
|
+
编辑
|
|
249
|
+
</Button>
|
|
250
|
+
<Button
|
|
251
|
+
type="button"
|
|
252
|
+
variant="outline"
|
|
253
|
+
className="h-9 rounded-xl border-stone-200 bg-white px-4 text-stone-700"
|
|
254
|
+
onClick={() => void handleToggle(item)}
|
|
255
|
+
disabled={isPending}
|
|
256
|
+
>
|
|
257
|
+
{isPending ? (
|
|
258
|
+
<LoaderCircle className="size-4 animate-spin" />
|
|
259
|
+
) : item.enabled ? (
|
|
260
|
+
<Ban className="size-4" />
|
|
261
|
+
) : (
|
|
262
|
+
<CheckCircle2 className="size-4" />
|
|
263
|
+
)}
|
|
264
|
+
{item.enabled ? "禁用" : "启用"}
|
|
265
|
+
</Button>
|
|
266
|
+
<Button
|
|
267
|
+
type="button"
|
|
268
|
+
variant="outline"
|
|
269
|
+
className="h-9 rounded-xl border-rose-200 bg-white px-4 text-rose-600 hover:bg-rose-50 hover:text-rose-700"
|
|
270
|
+
onClick={() => setDeletingItem(item)}
|
|
271
|
+
disabled={isPending}
|
|
272
|
+
>
|
|
273
|
+
{isPending ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
|
274
|
+
删除
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</CardContent>
|
|
283
|
+
</Card>
|
|
284
|
+
|
|
285
|
+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
286
|
+
<DialogContent className="rounded-2xl p-6">
|
|
287
|
+
<DialogHeader className="gap-2">
|
|
288
|
+
<DialogTitle>创建用户密钥</DialogTitle>
|
|
289
|
+
<DialogDescription className="text-sm leading-6">
|
|
290
|
+
可选填写一个备注名称,方便区分不同使用者;创建后会生成一条只能查看一次的原始密钥。
|
|
291
|
+
</DialogDescription>
|
|
292
|
+
</DialogHeader>
|
|
293
|
+
<div className="space-y-2">
|
|
294
|
+
<label className="text-sm font-medium text-stone-700">名称(可选)</label>
|
|
295
|
+
<Input
|
|
296
|
+
value={name}
|
|
297
|
+
onChange={(event) => setName(event.target.value)}
|
|
298
|
+
placeholder="例如:设计同学 A、运营临时账号"
|
|
299
|
+
className="h-11 rounded-xl border-stone-200 bg-white"
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
<DialogFooter>
|
|
303
|
+
<Button
|
|
304
|
+
type="button"
|
|
305
|
+
variant="secondary"
|
|
306
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
307
|
+
onClick={() => setIsDialogOpen(false)}
|
|
308
|
+
disabled={isCreating}
|
|
309
|
+
>
|
|
310
|
+
取消
|
|
311
|
+
</Button>
|
|
312
|
+
<Button
|
|
313
|
+
type="button"
|
|
314
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
315
|
+
onClick={() => void handleCreate()}
|
|
316
|
+
disabled={isCreating}
|
|
317
|
+
>
|
|
318
|
+
{isCreating ? <LoaderCircle className="size-4 animate-spin" /> : <Plus className="size-4" />}
|
|
319
|
+
创建
|
|
320
|
+
</Button>
|
|
321
|
+
</DialogFooter>
|
|
322
|
+
</DialogContent>
|
|
323
|
+
</Dialog>
|
|
324
|
+
|
|
325
|
+
<Dialog open={Boolean(deletingItem)} onOpenChange={(open) => (!open ? setDeletingItem(null) : null)}>
|
|
326
|
+
<DialogContent className="rounded-2xl p-6">
|
|
327
|
+
<DialogHeader className="gap-2">
|
|
328
|
+
<DialogTitle>删除用户密钥</DialogTitle>
|
|
329
|
+
<DialogDescription className="text-sm leading-6">
|
|
330
|
+
确认删除用户密钥「{deletingItem?.name}」吗?删除后该密钥将无法继续调用接口。
|
|
331
|
+
</DialogDescription>
|
|
332
|
+
</DialogHeader>
|
|
333
|
+
<DialogFooter>
|
|
334
|
+
<Button
|
|
335
|
+
type="button"
|
|
336
|
+
variant="secondary"
|
|
337
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
338
|
+
onClick={() => setDeletingItem(null)}
|
|
339
|
+
disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
|
|
340
|
+
>
|
|
341
|
+
取消
|
|
342
|
+
</Button>
|
|
343
|
+
<Button
|
|
344
|
+
type="button"
|
|
345
|
+
className="h-10 rounded-xl bg-rose-600 px-5 text-white hover:bg-rose-700"
|
|
346
|
+
onClick={() => void handleDelete()}
|
|
347
|
+
disabled={deletingItem ? pendingIds.has(deletingItem.id) : false}
|
|
348
|
+
>
|
|
349
|
+
{deletingItem && pendingIds.has(deletingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />}
|
|
350
|
+
删除
|
|
351
|
+
</Button>
|
|
352
|
+
</DialogFooter>
|
|
353
|
+
</DialogContent>
|
|
354
|
+
</Dialog>
|
|
355
|
+
|
|
356
|
+
<Dialog
|
|
357
|
+
open={Boolean(editingItem)}
|
|
358
|
+
onOpenChange={(open) => {
|
|
359
|
+
if (!open) {
|
|
360
|
+
setEditingItem(null);
|
|
361
|
+
setEditKey("");
|
|
362
|
+
}
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
<DialogContent className="rounded-2xl p-6">
|
|
366
|
+
<DialogHeader className="gap-2">
|
|
367
|
+
<DialogTitle>编辑用户密钥</DialogTitle>
|
|
368
|
+
<DialogDescription className="text-sm leading-6">
|
|
369
|
+
可以修改备注名称;如需更换专用密钥,直接填写新的原始密钥即可。留空则保持当前密钥不变。
|
|
370
|
+
</DialogDescription>
|
|
371
|
+
</DialogHeader>
|
|
372
|
+
<div className="space-y-4">
|
|
373
|
+
<div className="space-y-2">
|
|
374
|
+
<label className="text-sm font-medium text-stone-700">名称</label>
|
|
375
|
+
<Input
|
|
376
|
+
value={editName}
|
|
377
|
+
onChange={(event) => setEditName(event.target.value)}
|
|
378
|
+
placeholder="例如:设计同学 A、运营临时账号"
|
|
379
|
+
className="h-11 rounded-xl border-stone-200 bg-white"
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
<div className="space-y-2">
|
|
383
|
+
<label className="text-sm font-medium text-stone-700">新的专用密钥(可选)</label>
|
|
384
|
+
<Input
|
|
385
|
+
value={editKey}
|
|
386
|
+
onChange={(event) => setEditKey(event.target.value)}
|
|
387
|
+
placeholder="例如:sk-your-custom-user-key"
|
|
388
|
+
className="h-11 rounded-xl border-stone-200 bg-white font-mono"
|
|
389
|
+
/>
|
|
390
|
+
<p className="text-xs leading-5 text-stone-500">
|
|
391
|
+
保存后旧密钥会立即失效,新密钥生效。系统仍只保存哈希,不会回显当前密钥。
|
|
392
|
+
</p>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<DialogFooter>
|
|
396
|
+
<Button
|
|
397
|
+
type="button"
|
|
398
|
+
variant="secondary"
|
|
399
|
+
className="h-10 rounded-xl bg-stone-100 px-5 text-stone-700 hover:bg-stone-200"
|
|
400
|
+
onClick={() => {
|
|
401
|
+
setEditingItem(null);
|
|
402
|
+
setEditKey("");
|
|
403
|
+
}}
|
|
404
|
+
disabled={editingItem ? pendingIds.has(editingItem.id) : false}
|
|
405
|
+
>
|
|
406
|
+
取消
|
|
407
|
+
</Button>
|
|
408
|
+
<Button
|
|
409
|
+
type="button"
|
|
410
|
+
className="h-10 rounded-xl bg-stone-950 px-5 text-white hover:bg-stone-800"
|
|
411
|
+
onClick={() => void handleEdit()}
|
|
412
|
+
disabled={editingItem ? pendingIds.has(editingItem.id) : false}
|
|
413
|
+
>
|
|
414
|
+
{editingItem && pendingIds.has(editingItem.id) ? <LoaderCircle className="size-4 animate-spin" /> : <Pencil className="size-4" />}
|
|
415
|
+
保存
|
|
416
|
+
</Button>
|
|
417
|
+
</DialogFooter>
|
|
418
|
+
</DialogContent>
|
|
419
|
+
</Dialog>
|
|
420
|
+
</>
|
|
421
|
+
);
|
|
422
|
+
}
|