Skip to content

Commit 37625f4

Browse files
committed
feat: ✨ API密钥新增显示全部
#77
1 parent 7763f8a commit 37625f4

2 files changed

Lines changed: 169 additions & 88 deletions

File tree

web/src/components/modules/setting/APIKey.tsx

Lines changed: 145 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22

33
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
44
import { useTranslations } from 'next-intl';
5-
import { KeyRound, Plus, Loader, Copy, Trash2, Check, X, Info, CalendarDays, Pencil } from 'lucide-react';
5+
import { KeyRound, Plus, Loader, Copy, Trash2, Check, X, Info, CalendarDays, Pencil, Maximize2 } from 'lucide-react';
66
import { motion, AnimatePresence } from 'motion/react';
77
import { Input } from '@/components/ui/input';
88
import { Calendar } from '@/components/ui/calendar';
99
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
1010
import { Switch } from '@/components/ui/switch';
1111
import { Badge } from '@/components/ui/badge';
12+
import {
13+
MorphingDialog,
14+
MorphingDialogContainer,
15+
MorphingDialogContent,
16+
MorphingDialogTrigger,
17+
useMorphingDialog,
18+
} from '@/components/ui/morphing-dialog';
1219
import {
1320
useAPIKeyList,
1421
useCreateAPIKey,
@@ -255,7 +262,7 @@ function APIKeyForm({ apiKey, isPending, submitLabel, onSubmit, onClose }: APIKe
255262
disabled={isPending}
256263
aria-pressed={neverExpire}
257264
className={cn(
258-
'h-9 px-3 rounded-xl border text-sm transition-colors',
265+
'h-9 px-3 rounded-xl border text-sm transition-colors whitespace-nowrap shrink-0',
259266
neverExpire
260267
? 'bg-primary text-primary-foreground border-primary/30'
261268
: 'border-border bg-muted/20 text-foreground hover:bg-muted/30',
@@ -336,88 +343,39 @@ function APIKeyForm({ apiKey, isPending, submitLabel, onSubmit, onClose }: APIKe
336343
);
337344
}
338345

339-
function APIKeyAddOverlay({
340-
layoutId,
341-
onClose,
342-
}: {
343-
layoutId: string;
344-
onClose: () => void;
345-
}) {
346-
const t = useTranslations('setting');
347-
const createAPIKey = useCreateAPIKey();
348-
349-
const handleSubmit = useCallback((data: Omit<APIKey, 'id' | 'api_key'>) => {
350-
createAPIKey.mutate(data, {
351-
onSuccess: () => {
352-
toast.success(t('apiKey.toast.createSuccess'));
353-
onClose();
354-
},
355-
onError: (error) => {
356-
const msg = (error as unknown as ApiError)?.message;
357-
toast.error(t('apiKey.toast.createError'), { description: msg });
358-
},
359-
});
360-
}, [createAPIKey, onClose, t]);
361-
362-
return (
363-
<motion.div
364-
layoutId={layoutId}
365-
className="absolute inset-x-0 top-0 z-20 bg-card p-5 rounded-3xl border border-border custom-shadow"
366-
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
367-
>
368-
<APIKeyForm
369-
isPending={createAPIKey.isPending}
370-
submitLabel={t('apiKey.form.create')}
371-
onSubmit={handleSubmit}
372-
onClose={onClose}
373-
/>
374-
</motion.div>
375-
);
376-
}
377-
378-
function APIKeyEditOverlay({
346+
function APIKeyFormOverlay({
379347
layoutId,
380348
apiKey,
349+
isPending,
350+
submitLabel,
351+
onSubmit,
381352
onClose,
382353
}: {
383354
layoutId: string;
384-
apiKey: APIKey;
355+
apiKey?: APIKey;
356+
isPending: boolean;
357+
submitLabel: string;
358+
onSubmit: (data: Omit<APIKey, 'id' | 'api_key'>) => void;
385359
onClose: () => void;
386360
}) {
387-
const t = useTranslations('setting');
388-
const updateAPIKey = useUpdateAPIKey();
389-
390-
const handleSubmit = useCallback((data: Omit<APIKey, 'id' | 'api_key'>) => {
391-
updateAPIKey.mutate({ id: apiKey.id, ...data }, {
392-
onSuccess: () => {
393-
toast.success(t('apiKey.toast.updateSuccess'));
394-
onClose();
395-
},
396-
onError: (error) => {
397-
const msg = (error as unknown as ApiError)?.message;
398-
toast.error(t('apiKey.toast.updateError'), { description: msg });
399-
},
400-
});
401-
}, [updateAPIKey, apiKey.id, onClose, t]);
402-
403361
return (
404362
<motion.div
405363
layoutId={layoutId}
406-
className="absolute inset-x-0 top-0 z-20 bg-card p-5 rounded-3xl border border-border custom-shadow"
364+
className="absolute left-1/2 top-1/2 z-20 w-[min(420px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 bg-card p-5 rounded-3xl border border-border custom-shadow max-h-[80vh] overflow-auto"
407365
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
408366
>
409367
<APIKeyForm
410368
apiKey={apiKey}
411-
isPending={updateAPIKey.isPending}
412-
submitLabel={t('apiKey.form.save')}
413-
onSubmit={handleSubmit}
369+
isPending={isPending}
370+
submitLabel={submitLabel}
371+
onSubmit={onSubmit}
414372
onClose={onClose}
415373
/>
416374
</motion.div>
417375
);
418376
}
419377

420-
function APIKeyStatsOverlay({
378+
function APIKeyStatsCard({
421379
layoutId,
422380
apiKey,
423381
onClose,
@@ -433,7 +391,7 @@ function APIKeyStatsOverlay({
433391
return (
434392
<motion.div
435393
layoutId={layoutId}
436-
className="absolute inset-x-0 top-0 z-30 flex flex-col bg-card p-5 rounded-3xl border border-border custom-shadow"
394+
className="absolute left-1/2 top-1/2 z-30 w-[min(320px,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 flex flex-col bg-card p-5 rounded-3xl border border-border custom-shadow max-h-[80vh] overflow-auto"
437395
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
438396
>
439397
<div className="flex items-center justify-between gap-2 mb-3">
@@ -501,10 +459,11 @@ function APIKeyStatsOverlay({
501459
);
502460
}
503461

504-
function KeyItem({
462+
function APIKeyKeyItem({
505463
apiKey,
506464
statsLayoutId,
507465
editLayoutId,
466+
deleteLayoutId,
508467
onViewStats,
509468
onEdit,
510469
onDelete,
@@ -513,6 +472,7 @@ function KeyItem({
513472
apiKey: APIKey;
514473
statsLayoutId: string;
515474
editLayoutId: string;
475+
deleteLayoutId: string;
516476
onViewStats: () => void;
517477
onEdit: () => void;
518478
onDelete: () => void;
@@ -613,7 +573,7 @@ function KeyItem({
613573

614574
{!confirmDelete && (
615575
<motion.button
616-
layoutId={`delete-btn-${apiKey.id}`}
576+
layoutId={deleteLayoutId}
617577
onClick={() => setConfirmDelete(true)}
618578
className="flex h-8 w-8 items-center justify-center rounded-lg bg-destructive/10 text-destructive transition-colors hover:bg-destructive hover:text-destructive-foreground"
619579
>
@@ -625,7 +585,7 @@ function KeyItem({
625585
<AnimatePresence>
626586
{confirmDelete && (
627587
<motion.div
628-
layoutId={`delete-btn-${apiKey.id}`}
588+
layoutId={deleteLayoutId}
629589
className="absolute inset-0 flex items-center justify-center gap-2 bg-destructive p-3 rounded-xl"
630590
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
631591
>
@@ -650,16 +610,31 @@ function KeyItem({
650610
);
651611
}
652612

653-
export function SettingAPIKey() {
613+
function APIKeyPanelBase({
614+
idPrefix,
615+
containerClassName,
616+
listClassName,
617+
renderHeaderExtra,
618+
}: {
619+
idPrefix: string;
620+
containerClassName: string;
621+
listClassName: string;
622+
renderHeaderExtra?: (ctx: {
623+
disabled: boolean;
624+
onCloseAllOverlays: () => void;
625+
}) => React.ReactNode;
626+
}) {
654627
const t = useTranslations('setting');
655628
const { data: apiKeys, isLoading: apiKeysLoading, error: apiKeysError } = useAPIKeyList();
656629
const createAPIKey = useCreateAPIKey();
630+
const updateAPIKey = useUpdateAPIKey();
657631
const deleteAPIKey = useDeleteAPIKey();
658632

659633
const instanceId = useId();
660-
const addLayoutId = `add-btn-apikey-${instanceId}`;
661-
const statsPrefix = `apikey-stats-${instanceId}`;
662-
const editPrefix = `apikey-edit-${instanceId}`;
634+
const addLayoutId = `add-btn-${idPrefix}-${instanceId}`;
635+
const statsPrefix = `${idPrefix}-stats-${instanceId}`;
636+
const editPrefix = `${idPrefix}-edit-${instanceId}`;
637+
const deletePrefix = `${idPrefix}-delete-`;
663638

664639
const [isAdding, setIsAdding] = useState(false);
665640
const [viewingStats, setViewingStats] = useState<{ apiKey: APIKey; layoutId: string } | null>(null);
@@ -691,37 +666,71 @@ export function SettingAPIKey() {
691666
setEditingKey(null);
692667
}, []);
693668

669+
const disabledHeaderActions = createAPIKey.isPending || isAdding || !!viewingStats || !!editingKey;
670+
671+
const handleCreate = useCallback((data: Omit<APIKey, 'id' | 'api_key'>) => {
672+
createAPIKey.mutate(data, {
673+
onSuccess: () => {
674+
toast.success(t('apiKey.toast.createSuccess'));
675+
setIsAdding(false);
676+
},
677+
onError: (error) => {
678+
const msg = (error as unknown as ApiError)?.message;
679+
toast.error(t('apiKey.toast.createError'), { description: msg });
680+
},
681+
});
682+
}, [createAPIKey, t]);
683+
684+
const handleUpdate = useCallback((apiKey: APIKey, data: Omit<APIKey, 'id' | 'api_key'>) => {
685+
updateAPIKey.mutate({ id: apiKey.id, ...data }, {
686+
onSuccess: () => {
687+
toast.success(t('apiKey.toast.updateSuccess'));
688+
setEditingKey(null);
689+
},
690+
onError: (error) => {
691+
const msg = (error as unknown as ApiError)?.message;
692+
toast.error(t('apiKey.toast.updateError'), { description: msg });
693+
},
694+
});
695+
}, [t, updateAPIKey]);
696+
694697
return (
695-
<div className="rounded-3xl border border-border bg-card p-6 custom-shadow space-y-5 relative">
698+
<div className={containerClassName}>
696699
<div className="flex items-center justify-between gap-3">
697700
<h2 className="text-lg font-bold text-card-foreground flex items-center gap-2">
698701
<KeyRound className="h-5 w-5" />
699702
{t('apiKey.title')}
700703
</h2>
701-
<motion.button
702-
layoutId={addLayoutId}
703-
type="button"
704-
onClick={() => setIsAdding(true)}
705-
disabled={createAPIKey.isPending || isAdding || !!viewingStats || !!editingKey}
706-
className="h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted disabled:opacity-50"
707-
title={t('apiKey.add') ?? '添加'}
708-
>
709-
<Plus className="h-4 w-4" />
710-
</motion.button>
704+
<div className="flex items-center gap-2">
705+
<motion.button
706+
layoutId={addLayoutId}
707+
type="button"
708+
onClick={() => setIsAdding(true)}
709+
disabled={disabledHeaderActions}
710+
className="h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted disabled:opacity-50"
711+
title={t('apiKey.add')}
712+
>
713+
<Plus className="h-4 w-4" />
714+
</motion.button>
715+
{renderHeaderExtra?.({ disabled: disabledHeaderActions, onCloseAllOverlays: closeAllOverlays })}
716+
</div>
711717
</div>
712718

713719
<AnimatePresence>
714720
{isAdding && (
715-
<APIKeyAddOverlay
721+
<APIKeyFormOverlay
716722
layoutId={addLayoutId}
723+
isPending={createAPIKey.isPending}
724+
submitLabel={t('apiKey.form.create')}
725+
onSubmit={handleCreate}
717726
onClose={() => setIsAdding(false)}
718727
/>
719728
)}
720729
</AnimatePresence>
721730

722731
<AnimatePresence>
723732
{viewingStats && (
724-
<APIKeyStatsOverlay
733+
<APIKeyStatsCard
725734
layoutId={viewingStats.layoutId}
726735
apiKey={viewingStats.apiKey}
727736
onClose={() => setViewingStats(null)}
@@ -731,15 +740,18 @@ export function SettingAPIKey() {
731740

732741
<AnimatePresence>
733742
{editingKey && (
734-
<APIKeyEditOverlay
743+
<APIKeyFormOverlay
735744
layoutId={editingKey.layoutId}
736745
apiKey={editingKey.apiKey}
746+
isPending={updateAPIKey.isPending}
747+
submitLabel={t('apiKey.form.save')}
748+
onSubmit={(data) => handleUpdate(editingKey.apiKey, data)}
737749
onClose={() => setEditingKey(null)}
738750
/>
739751
)}
740752
</AnimatePresence>
741753

742-
<div className="space-y-2 h-32 overflow-y-auto">
754+
<div className={listClassName}>
743755
{apiKeysLoading ? (
744756
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
745757
<Loader className="h-4 w-4 animate-spin" />
@@ -757,12 +769,14 @@ export function SettingAPIKey() {
757769
{sortedApiKeys.map((apiKey) => {
758770
const statsLayoutId = `${statsPrefix}-${apiKey.id}`;
759771
const editLayoutId = `${editPrefix}-${apiKey.id}`;
772+
const deleteLayoutId = `${deletePrefix}${apiKey.id}`;
760773
return (
761-
<KeyItem
774+
<APIKeyKeyItem
762775
key={apiKey.id}
763776
apiKey={apiKey}
764777
statsLayoutId={statsLayoutId}
765778
editLayoutId={editLayoutId}
779+
deleteLayoutId={deleteLayoutId}
766780
onViewStats={() => {
767781
closeAllOverlays();
768782
setViewingStats({ apiKey, layoutId: statsLayoutId });
@@ -782,3 +796,46 @@ export function SettingAPIKey() {
782796
</div>
783797
);
784798
}
799+
800+
function APIKeyDialogPanel() {
801+
const { setIsOpen } = useMorphingDialog();
802+
return (
803+
<APIKeyPanelBase
804+
idPrefix="apikey-dialog"
805+
containerClassName="rounded-3xl border border-border bg-card p-6 space-y-5 relative w-screen max-w-full md:max-w-xl"
806+
listClassName="space-y-2 max-h-[70vh] overflow-y-auto"
807+
renderHeaderExtra={() => (
808+
<button
809+
type="button"
810+
onClick={() => setIsOpen(false)}
811+
className="h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted"
812+
title="Close"
813+
>
814+
<X className="h-4 w-4" />
815+
</button>
816+
)}
817+
/>
818+
);
819+
}
820+
821+
export function SettingAPIKey() {
822+
return (
823+
<APIKeyPanelBase
824+
idPrefix="apikey"
825+
containerClassName="rounded-3xl border border-border bg-card p-6 custom-shadow space-y-5 relative"
826+
listClassName="space-y-2 h-32 overflow-y-auto"
827+
renderHeaderExtra={() => (
828+
<MorphingDialog>
829+
<MorphingDialogTrigger className="h-9 w-9 flex items-center justify-center rounded-lg bg-muted/60 text-muted-foreground transition-colors hover:bg-muted">
830+
<Maximize2 className="h-4 w-4" />
831+
</MorphingDialogTrigger>
832+
<MorphingDialogContainer>
833+
<MorphingDialogContent className="relative">
834+
<APIKeyDialogPanel />
835+
</MorphingDialogContent>
836+
</MorphingDialogContainer>
837+
</MorphingDialog>
838+
)}
839+
/>
840+
);
841+
}

0 commit comments

Comments
 (0)