Initial project setup with Next.js accounts manager
- Set up Next.js project structure - Added UI components and styling - Configured package dependencies - Added feature documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
246
components/accounts/account-details.tsx
Normal file
246
components/accounts/account-details.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Account, StatsOverview } from '@/lib/types';
|
||||
import { PlatformSelector, OwnerSelector, StatusSelector } from '@/components/shared';
|
||||
|
||||
interface AccountDetailsProps {
|
||||
account: Account | null;
|
||||
open: boolean;
|
||||
mode: 'view' | 'edit';
|
||||
stats: StatsOverview | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave?: (account: Account) => Promise<void>;
|
||||
onDelete?: (account: Account) => Promise<void>;
|
||||
}
|
||||
|
||||
export function AccountDetails({
|
||||
account,
|
||||
open,
|
||||
mode,
|
||||
stats,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
onDelete
|
||||
}: AccountDetailsProps) {
|
||||
const [editData, setEditData] = useState<Partial<Account>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isEditing = mode === 'edit';
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!account || !onSave) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const updatedAccount = { ...account, ...editData };
|
||||
await onSave(updatedAccount);
|
||||
onOpenChange(false);
|
||||
setEditData({});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!account || !onDelete) return;
|
||||
|
||||
if (!confirm('确认删除此账户?')) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await onDelete(account);
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'available':
|
||||
return <Badge variant="default" className="bg-green-500">可用</Badge>;
|
||||
case 'locked':
|
||||
return <Badge variant="secondary">已锁定</Badge>;
|
||||
case 'banned':
|
||||
return <Badge variant="destructive">已封禁</Badge>;
|
||||
default:
|
||||
return <Badge variant="outline">{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const formatData = (data: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditing ? '编辑账户' : '账户详情'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
账户ID: {account.id}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">平台</label>
|
||||
{isEditing ? (
|
||||
<PlatformSelector
|
||||
value={editData.platform ?? account.platform}
|
||||
onValueChange={(value) => setEditData({...editData, platform: value})}
|
||||
stats={stats}
|
||||
placeholder="选择平台"
|
||||
inputPlaceholder="或直接输入平台名称"
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 bg-muted rounded">{account.platform}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">自定义ID</label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={editData.customId ?? account.customId}
|
||||
onChange={(e) => setEditData({...editData, customId: e.target.value})}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 bg-muted rounded font-mono">{account.customId}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">所有者ID</label>
|
||||
{isEditing ? (
|
||||
<OwnerSelector
|
||||
value={editData.ownerId ?? account.ownerId}
|
||||
onValueChange={(value) => setEditData({...editData, ownerId: value})}
|
||||
stats={stats}
|
||||
placeholder="选择所有者"
|
||||
inputPlaceholder="或直接输入所有者ID"
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 bg-muted rounded">{account.ownerId}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">状态</label>
|
||||
{isEditing ? (
|
||||
<StatusSelector
|
||||
value={editData.status ?? account.status}
|
||||
onValueChange={(value) => setEditData({...editData, status: value})}
|
||||
stats={stats}
|
||||
placeholder="选择状态"
|
||||
inputPlaceholder="或直接输入状态"
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2">{getStatusBadge(account.status)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">账户数据</label>
|
||||
{isEditing ? (
|
||||
<Textarea
|
||||
value={editData.data ?? account.data}
|
||||
onChange={(e) => setEditData({...editData, data: e.target.value})}
|
||||
className="min-h-[120px] font-mono text-sm"
|
||||
/>
|
||||
) : (
|
||||
<pre className="p-3 bg-muted rounded text-sm font-mono whitespace-pre-wrap break-all max-h-[200px] overflow-y-auto">
|
||||
{formatData(account.data)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">备注</label>
|
||||
{isEditing ? (
|
||||
<Textarea
|
||||
value={editData.notes ?? account.notes ?? ''}
|
||||
onChange={(e) => setEditData({...editData, notes: e.target.value})}
|
||||
placeholder="输入备注..."
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 bg-muted rounded min-h-[40px]">
|
||||
{account.notes || '无备注'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isEditing && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<label className="font-medium">创建时间</label>
|
||||
<div>{new Date(account.createdAt).toLocaleString('zh-CN')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="font-medium">更新时间</label>
|
||||
<div>{new Date(account.updatedAt).toLocaleString('zh-CN')}</div>
|
||||
</div>
|
||||
{account.lockedAt && (
|
||||
<div className="col-span-full md:col-span-2">
|
||||
<label className="font-medium">锁定时间</label>
|
||||
<div>{new Date(account.lockedAt).toLocaleString('zh-CN')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={loading}
|
||||
>
|
||||
{isEditing ? '取消' : '关闭'}
|
||||
</Button>
|
||||
|
||||
{isEditing ? (
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="space-x-2">
|
||||
{onDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '删除中...' : '删除'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user