From 0b95ca36f1ca4e7310e4cb4eed5acea4a4ed86aa Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 23 Sep 2025 01:40:14 +0800 Subject: [PATCH] Initial project setup with Next.js accounts manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 10 + FEATURES.md | 120 ++++ app/globals.css | 124 +++- app/layout.tsx | 2 + app/page.tsx | 102 +-- components.json | 22 + components/accounts-manager.tsx | 181 +++++ components/accounts/account-details.tsx | 246 +++++++ components/accounts/account-filters.tsx | 78 +++ components/accounts/account-table.tsx | 241 +++++++ components/accounts/account-upload.tsx | 299 ++++++++ components/accounts/batch-operations.tsx | 200 ++++++ components/accounts/index.ts | 7 + components/accounts/stats-cards.tsx | 93 +++ components/shared/index.ts | 4 + components/shared/owner-selector.tsx | 45 ++ components/shared/platform-selector.tsx | 45 ++ components/shared/stats-select.tsx | 80 +++ components/shared/status-selector.tsx | 45 ++ components/ui/badge.tsx | 46 ++ components/ui/button.tsx | 58 ++ components/ui/card.tsx | 92 +++ components/ui/checkbox.tsx | 32 + components/ui/dialog.tsx | 143 ++++ components/ui/input.tsx | 21 + components/ui/select.tsx | 185 +++++ components/ui/sonner.tsx | 25 + components/ui/table.tsx | 116 ++++ components/ui/tabs.tsx | 66 ++ components/ui/textarea.tsx | 18 + lib/api.ts | 73 ++ lib/hooks/use-accounts.ts | 265 +++++++ lib/hooks/use-stats-data.ts | 59 ++ lib/types.ts | 101 +++ lib/utils.ts | 6 + package.json | 20 +- pnpm-lock.yaml | 849 +++++++++++++++++++++++ 37 files changed, 4004 insertions(+), 115 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 FEATURES.md create mode 100644 components.json create mode 100644 components/accounts-manager.tsx create mode 100644 components/accounts/account-details.tsx create mode 100644 components/accounts/account-filters.tsx create mode 100644 components/accounts/account-table.tsx create mode 100644 components/accounts/account-upload.tsx create mode 100644 components/accounts/batch-operations.tsx create mode 100644 components/accounts/index.ts create mode 100644 components/accounts/stats-cards.tsx create mode 100644 components/shared/index.ts create mode 100644 components/shared/owner-selector.tsx create mode 100644 components/shared/platform-selector.tsx create mode 100644 components/shared/stats-select.tsx create mode 100644 components/shared/status-selector.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 lib/api.ts create mode 100644 lib/hooks/use-accounts.ts create mode 100644 lib/hooks/use-stats-data.ts create mode 100644 lib/types.ts create mode 100644 lib/utils.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..392a9c5 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm dlx:*)", + "Bash(pnpm dev:*)", + "Bash(mkdir:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..3a0c1fa --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,120 @@ +# 账户管理系统 - 更新说明 + +## 🚀 新增功能 + +### 1. 账户上传功能 📤 + +#### ---- 分割格式(推荐) +``` +user1@gmail.com----password123----cookie=abc123----token=xyz789 +user2@gmail.com----{"password":"456789","token":"fb_token"} +user3@twitter.com----oauth_token----oauth_secret----user_data +``` + +#### 默认格式(一行一个) +``` +user1@gmail.com +user2@facebook.com +simple_username +my_account_id +``` + +**格式说明:** +- **---- 分割格式**: `customId----纯数据内容` + - customId: 唯一标识符(第一个----之前) + - 数据内容: 后面所有内容都作为纯数据(可包含多个----) +- **默认格式**: 整行既作为customId也作为data +- **平台和状态**: 在界面上方外部设置,应用到所有上传的账户 + +**特点:** +- 支持 `----` 分割格式:只在第一个 `----` 处分割,后面的所有内容都作为数据 +- 支持默认格式:一行一个ID,整行既作为customId也作为data +- 第一个字段为唯一ID(customId) +- 数据部分可以包含任意格式和多个 `----` 分隔符 +- 平台和状态统一在外部设置 + +#### 其他支持格式 +- **制表符分隔**:`platform customId data status` +- **逗号分隔**:`platform,customId,data,status` +- **JSON格式**:`{"platform":"google","customId":"user@gmail.com","data":"..."}` + +### 2. 智能数据源集成 🧠 + +- **所有者ID**:从后端数据自动获取现有所有者列表,支持下拉选择或手动输入 +- **平台选择**:从后端数据自动获取现有平台列表,支持下拉选择或手动输入 ✨ +- **状态选择**:从后端数据自动获取现有状态类型,支持下拉选择或手动输入 +- **外部统一设置**:平台和状态在界面外部统一设置,应用到所有账户 ✨ +- **灵活输入**:所有选择器都支持手动输入自定义值 + +### 3. 界面优化 ✨ + +- **三列布局**:所有者ID、平台、状态并排显示 +- **响应式布局**:修复超出背景框问题,使用更大的对话框 +- **滚动支持**:内容区域支持滚动,避免内容被截断 +- **纯数据输入**:输入框内只需要填写纯粹的账户数据 + +### 4. 组件架构 🏗️ + +``` +components/accounts/ +├── stats-cards.tsx # 统计卡片 +├── account-filters.tsx # 筛选器 +├── batch-operations.tsx # 批量操作 +├── account-upload.tsx # 账户上传 ✨ +├── account-table.tsx # 账户表格 +├── account-details.tsx # 账户详情/编辑 +└── index.ts # 导出文件 +``` + +## 🎯 使用方法 + +### 上传账户 +1. 点击页面右上角的"上传账户"按钮 +2. 选择或输入所有者ID +3. 设置平台(应用到所有账户) +4. 设置默认状态(应用到所有账户) +5. 选择上传方式: + - **批量上传**:粘贴格式化的账户数据 + - **单个添加**:手动填写单个账户信息 + +### ---- 分割格式示例 +``` +john@gmail.com----password123----cookie_data----session_token +jane.doe@facebook.com----{"token":"fb_token_12345","user_id":"987654321","refresh_token":"refresh123"} +@twitter_user----oauth_token_value----oauth_secret_value----additional_data----more_info +simple@example.com----plaintext_password_data +complex@example.com----key1=value1----key2=value2----key3=value3----extra_field=data +``` + +### 默认格式示例 +``` +john@gmail.com +jane.doe@facebook.com +@twitter_user +simple@example.com +my_username_123 +account_id_456 +``` + +**解析规则:** +- **---- 分割格式**:只在第一个 `----` 处分割 + - 第一部分:唯一ID + - 第二部分:所有剩余内容作为纯数据(保持原格式) +- **默认格式**:整行作为唯一ID,同时也作为data +- 平台和状态从外部设置应用 + +## 🔧 技术特性 + +- **类型安全**:完整的TypeScript类型定义 +- **组件分离**:每个功能独立组件,易于维护 +- **状态管理**:使用自定义Hook管理状态 +- **数据验证**:完善的数据格式验证和错误处理 +- **用户体验**:加载状态、错误提示、确认对话框 + +## 📱 界面特色 + +- **简洁大气**:使用shadcn/ui组件库,现代化设计 +- **单页面操作**:所有功能集中在一个页面,无需跳转 +- **批量操作**:支持批量选择、删除、更新 +- **智能筛选**:多维度筛选条件 +- **详细信息**:完整的账户详情查看和编辑功能 \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index a2dc41e..dc98be7 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,122 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..2497c1a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { Toaster } from "@/components/ui/sonner"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -28,6 +29,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > {children} + ); diff --git a/app/page.tsx b/app/page.tsx index 21b686d..6b5ead2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,103 +1,11 @@ -import Image from "next/image"; +"use client"; + +import { AccountsManager } from "@/components/accounts-manager"; export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- - -
- +
+
); } diff --git a/components.json b/components.json new file mode 100644 index 0000000..b7b9791 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/accounts-manager.tsx b/components/accounts-manager.tsx new file mode 100644 index 0000000..4cafab4 --- /dev/null +++ b/components/accounts-manager.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Account } from '@/lib/types'; +import { useAccounts } from '@/lib/hooks/use-accounts'; + +// 组件导入 +import { StatsCards } from './accounts/stats-cards'; +import { AccountFilters } from './accounts/account-filters'; +import { BatchOperations } from './accounts/batch-operations'; +import { AccountUpload } from './accounts/account-upload'; +import { AccountTable } from './accounts/account-table'; +import { AccountDetails } from './accounts/account-details'; + +export function AccountsManager() { + const { + accounts, + stats, + loading, + selectedIds, + pagination, + filters, + fetchAccounts, + fetchStats, + handleFilterChange, + handlePageChange, + handlePageSizeChange, + handleSelectAll, + handleSelectOne, + handleBatchDelete, + handleBatchUpdate, + handleUploadAccounts, + setSelectedIds + } = useAccounts(); + + // 账户详情状态 + const [detailsDialog, setDetailsDialog] = useState(false); + const [detailsMode, setDetailsMode] = useState<'view' | 'edit'>('view'); + const [selectedAccount, setSelectedAccount] = useState(null); + + // 处理查看详情 + const handleViewAccount = (account: Account) => { + setSelectedAccount(account); + setDetailsMode('view'); + setDetailsDialog(true); + }; + + // 处理编辑账户 + const handleEditAccount = (account: Account) => { + setSelectedAccount(account); + setDetailsMode('edit'); + setDetailsDialog(true); + }; + + // 处理删除单个账户 + const handleDeleteAccount = async (account: Account) => { + if (!confirm('确认删除此账户?')) return; + + try { + // 设置选中的账户ID + setSelectedIds([account.id]); + // 执行删除 + await handleBatchDelete(); + } catch (error) { + console.error('Failed to delete account:', error); + } + }; + + // 处理保存编辑 + const handleSaveAccount = async (account: Account) => { + try { + console.log('开始保存账户:', account); + + // 构建payload,将null/undefined转换为空字符串 + const payload: Partial> = {}; + + if (account.status !== null && account.status !== undefined) { + payload.status = account.status; + } + if (account.ownerId !== null && account.ownerId !== undefined) { + payload.ownerId = account.ownerId; + } + if (account.notes !== null && account.notes !== undefined) { + payload.notes = account.notes || ''; // 将null转换为空字符串 + } + if (account.platform !== null && account.platform !== undefined) { + payload.platform = account.platform; + } + + console.log('更新payload:', payload); + console.log('账户ID:', account.id); + + if (Object.keys(payload).length === 0) { + console.log('没有有效的字段需要更新'); + return; + } + + // 直接传递账户ID数组,避免状态竞态条件 + await handleBatchUpdate(payload, [account.id]); + + console.log('账户保存完成'); + } catch (error) { + console.error('Failed to save account:', error); + } + }; + + return ( +
+
+ {/* 页面标题 */} +
+
+

账户管理系统

+

轻量化账户管理平台

+
+ +
+ + {/* 统计卡片 */} +
+ +
+ + {/* 账户管理区域 */} + + + 账户列表 + 管理和查看所有账户信息 + + + {/* 筛选条件 */} + + + {/* 批量操作 */} + selectedIds.includes(account.id))} + stats={stats} + onBatchUpdate={handleBatchUpdate} + onBatchDelete={handleBatchDelete} + /> + + {/* 账户表格 - 移除高度限制 */} +
+ +
+
+
+
+ + {/* 账户详情对话框 */} + +
+ ); +} \ No newline at end of file diff --git a/components/accounts/account-details.tsx b/components/accounts/account-details.tsx new file mode 100644 index 0000000..2fecc18 --- /dev/null +++ b/components/accounts/account-details.tsx @@ -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; + onDelete?: (account: Account) => Promise; +} + +export function AccountDetails({ + account, + open, + mode, + stats, + onOpenChange, + onSave, + onDelete +}: AccountDetailsProps) { + const [editData, setEditData] = useState>({}); + 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 可用; + case 'locked': + return 已锁定; + case 'banned': + return 已封禁; + default: + return {status}; + } + }; + + const formatData = (data: string) => { + try { + const parsed = JSON.parse(data); + return JSON.stringify(parsed, null, 2); + } catch { + return data; + } + }; + + if (!account) return null; + + return ( + + + + + {isEditing ? '编辑账户' : '账户详情'} + + + 账户ID: {account.id} + + + +
+
+
+ + {isEditing ? ( + setEditData({...editData, platform: value})} + stats={stats} + placeholder="选择平台" + inputPlaceholder="或直接输入平台名称" + /> + ) : ( +
{account.platform}
+ )} +
+ +
+ + {isEditing ? ( + setEditData({...editData, customId: e.target.value})} + /> + ) : ( +
{account.customId}
+ )} +
+
+ +
+
+ + {isEditing ? ( + setEditData({...editData, ownerId: value})} + stats={stats} + placeholder="选择所有者" + inputPlaceholder="或直接输入所有者ID" + /> + ) : ( +
{account.ownerId}
+ )} +
+ +
+ + {isEditing ? ( + setEditData({...editData, status: value})} + stats={stats} + placeholder="选择状态" + inputPlaceholder="或直接输入状态" + /> + ) : ( +
{getStatusBadge(account.status)}
+ )} +
+
+ +
+ + {isEditing ? ( +