更新描述

This commit is contained in:
kennethcheng 2026-04-12 05:15:51 +08:00
parent f6436db200
commit 6f5bc03b88
2 changed files with 433 additions and 43 deletions

401
README.md
View File

@ -1,36 +1,393 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). # 个人投资持仓管理系统
## Getting Started > 现代化、全面化的个人投资组合管理平台支持多市场美股、A股、港股、加密货币统一管理。
First, run the development server: ![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Next.js](https://img.shields.io/badge/Next.js-16-black.svg)
![TypeScript](https://img.shields.io/badge/TypeScript-5-blue.svg)
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-blue.svg)
```bash ---
npm run dev
# or ## 功能特性
yarn dev
# or ### 核心功能
pnpm dev
# or | 功能 | 描述 |
bun dev |------|------|
| **多市场支持** | 美股 (US)、A股 (CN)、港股 (HK)、加密货币 (CRYPTO) |
| **资金流水管理** | 入金、出金记录 |
| **交易流水管理** | 买入、卖出、分红、利息、费用 |
| **持仓统计** | 实时市值、平均成本、浮动盈亏 |
| **多币种转换** | 自动将各币种资产折算为 USD |
| **资产配置图** | 饼图展示各市场占比 |
| **数据导入导出** | CSV 格式导入导出 |
### 交易类型
| 类型 | 说明 |
|------|------|
| `BUY` | 买入 |
| `SELL` | 卖出 |
| `DEPOSIT` | 入金 |
| `WITHDRAW` | 出金 |
| `DIVIDEND` | 分红 |
| `INTEREST` | 利息 |
| `FEE` | 费用/手续费 |
---
## 技术架构
### 技术栈
```
前端:
├── Next.js 16 (App Router)
├── React 19
├── TypeScript 5
├── Tailwind CSS 4
├── shadcn/ui (组件库)
├── Recharts (图表)
└── Sonner (Toast 通知)
后端:
├── Next.js API Routes
├── Prisma ORM
└── PostgreSQL 16
实时数据:
└── Alpha Vantage API
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. ### 数据库模型
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. ```
┌─────────────┐ ┌─────────────┐
│ User │───1:N─│ Account │
└─────────────┘ └─────────────┘
│ 1:N
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│Position │ │Transaction│ │ExchangeRate│
└───────────┘ └───────────┘ └───────────┘
┌───────────┐
│ Security │
└───────────┘
```
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ### 表结构说明
## Learn More | 表名 | 说明 |
|------|------|
| `User` | 用户表(预留多用户扩展) |
| `Account` | 账户表(按市场类型分组) |
| `Security` | 证券参考表(代码、名称、精度) |
| `Transaction` | 交易流水表 |
| `Position` | 持仓表(查询优化) |
| `ExchangeRate` | 汇率参考表 |
To learn more about Next.js, take a look at the following resources: ### 字段精度设计
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | 字段类型 | 精度 | 说明 |
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. |----------|------|------|
| 价格/金额 | `Decimal(20,4)` | 避免浮点精度丢失 |
| 数量 | `Decimal(20,8)` | 加密货币支持8位小数 |
| 汇率 | `Decimal(20,8)` | 高精度汇率转换 |
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ---
## Deploy on Vercel ## API 接口
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. ### 账户管理
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. | 接口 | 方法 | 描述 |
|------|------|------|
| `/api/accounts` | GET | 获取账户列表 |
| `/api/accounts` | POST | 创建账户 |
### 交易管理
| 接口 | 方法 | 描述 |
|------|------|------|
| `/api/transactions` | GET | 获取交易流水(支持分页) |
| `/api/transactions` | POST | 记录交易(自动更新持仓/余额) |
### 持仓管理
| 接口 | 方法 | 描述 |
|------|------|------|
| `/api/positions` | GET | 获取持仓列表 |
### 仪表盘
| 接口 | 方法 | 描述 |
|------|------|------|
| `/api/dashboard/stats` | GET | 资产统计概览 |
| `/api/dashboard/analytics` | GET | 持仓分析(含实时价格) |
### 数据导入
| 接口 | 方法 | 描述 |
|------|------|------|
| `/api/import/transactions` | POST | 批量导入交易记录 |
### 证券管理
| 接口 | 方法 | 描述 |
|------|------|------|
| `/api/securities` | GET | 搜索证券 |
| `/api/securities` | POST | 添加证券 |
### 汇率管理
| 接口 | 方法 | 描述 |
|------|------|------|
| `/api/exchange-rates` | GET | 获取汇率列表 |
| `/api/exchange-rates` | POST | 更新汇率 |
---
## 快速开始
### 环境要求
- Node.js 20+
- PostgreSQL 16+
- npm 或 yarn
### 安装步骤
```bash
# 1. 克隆项目
git clone <repository-url>
cd stock-portfolio
# 2. 安装依赖
npm install
# 3. 配置环境变量
cp .env.example .env
# 编辑 .env 填入数据库连接信息
# 4. 初始化数据库
npx prisma migrate dev --name init
npx prisma db seed
# 5. 启动开发服务器
npm run dev
```
### 环境变量
```env
# 数据库连接
DATABASE_URL="postgresql://postgres:password@localhost:5432/stock_portfolio"
# Alpha Vantage API Key用于获取实时股价
ALPHA_VANTAGE_API_KEY="your-api-key"
# HTTP 代理(可选,用于解决 API 访问问题)
HTTP_PROXY="http://192.168.48.171:7893"
HTTPS_PROXY="http://192.168.48.171:7893"
```
---
## 使用指南
### 添加交易记录
1. 点击右上角「**记录交易**」按钮
2. 选择账户和交易类型
3. 输入证券代码(支持搜索)
4. 填写数量、价格、金额
5. 点击「**确认记录**」
6. 在确认对话框中核实信息,点击「**确认**」
### 导入批量交易
1. 点击右上角「**导入**」按钮
2. 点击「**下载模板**」获取 CSV 模板
3. 按照模板格式填写交易数据
4. 选择填写好的 CSV 文件
5. 预览导入数据(有效/错误标记)
6. 点击「**导入**」执行批量导入
### 导出数据
1. 点击右上角「**导出**」按钮
2. 自动下载持仓 CSV 文件
3. 在「交易流水」标签页也可导出交易记录
### 字段说明
#### 交易模板 CSV 格式
```csv
时间,类型,证券代码,数量,价格,金额,手续费,币种,备注
2024-01-15 10:30:00,BUY,AAPL,10,185.50,1855.00,1.00,USD,买入苹果
```
| 字段 | 必填 | 说明 |
|------|------|------|
| 时间 | 是 | 格式:`YYYY-MM-DD HH:mm:ss` |
| 类型 | 是 | `BUY`/`SELL`/`DEPOSIT`/`WITHDRAW`/`DIVIDEND` |
| 证券代码 | 否 | 买入/卖出/分红时填写 |
| 数量 | 否 | 股票数量 |
| 价格 | 否 | 单价 |
| 金额 | 是 | 总金额 |
| 手续费 | 否 | 交易手续费 |
| 币种 | 是 | `USD`/`CNY`/`HKD`/`USDT` |
| 备注 | 否 | 任意备注信息 |
---
## 成本计算
### 平均成本法 (Average Cost)
系统采用**平均成本法**计算持仓成本:
```
新平均成本 = (现有成本 × 现有数量 + 新买入成本) / (现有数量 + 新买入数量)
```
### 示例
```
初始:买入 10 股 AAPL价格 $150
→ 平均成本 = $150
再次:买入 10 股 AAPL价格 $160
→ 新平均成本 = ($150×10 + $160×10) / 20 = $155
```
---
## 多币种处理
### 汇率转换
系统以 **USD** 为基准货币,将所有资产折算为 USD 进行汇总:
| 币种 | 汇率 (示例) |
|------|-------------|
| USD | 1.0 |
| CNY | 0.137 |
| HKD | 0.129 |
| USDT | 1.0 |
### 更新汇率
通过 `/api/exchange-rates` 接口或数据库直接更新汇率参考表。
---
## 界面预览
### 资产概览
```
┌─────────────────────────────────────────────────────────┐
│ [Logo] 投资持仓管理 [账户▼] [导入] [导出] [记录交易] │
├─────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 总资产 │ │ 浮动盈亏 │ │ 持仓市值 │ │ 账户数量 │ │
│ │ $125,430 │ │ +$3,250 │ │ $98,200 │ │ 4 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
```
### 持仓明细
| 证券 | 市场 | 数量 | 成本价 | 当前价 | 市值 | 盈亏 |
|------|------|------|--------|--------|------|------|
| AAPL | 美股 | 50 | $150.00 | $178.50 | $8,925 | +$1,425 (+19.0%) |
| MSFT | 美股 | 20 | $380.00 | $415.20 | $8,304 | +$704 (+9.3%) |
| BTC | 加密 | 0.5 | $60,000 | $67,500 | $33,750 | +$3,750 (+12.5%) |
---
## 项目结构
```
stock-portfolio/
├── prisma/
│ ├── schema.prisma # 数据库模型
│ ├── seed.ts # 初始化数据
│ └── migrations/ # 数据库迁移
├── src/
│ ├── app/
│ │ ├── api/ # API 路由
│ │ │ ├── accounts/
│ │ │ ├── transactions/
│ │ │ ├── positions/
│ │ │ ├── securities/
│ │ │ ├── exchange-rates/
│ │ │ ├── dashboard/
│ │ │ └── import/
│ │ ├── page.tsx # 主页面
│ │ ├── layout.tsx # 布局
│ │ └── globals.css # 全局样式
│ ├── components/
│ │ └── ui/ # shadcn/ui 组件
│ ├── lib/
│ │ ├── api.ts # API 调用封装
│ │ ├── prisma.ts # Prisma 客户端
│ │ ├── import-export.ts # 导入导出工具
│ │ └── price-service.ts # 价格服务
│ └── types/
│ └── index.ts # 类型定义
├── .env # 环境变量
├── .env.example # 环境变量示例
├── package.json
├── tsconfig.json
├── next.config.ts
└── README.md
```
---
## 常见问题
### Q: 实时价格如何获取?
A: 系统使用 Alpha Vantage API 获取美股实时价格。需要配置 `ALPHA_VANTAGE_API_KEY` 环境变量。
### Q: 支持哪些市场?
A: 目前支持美股 (US)、A股 (CN)、港股 (HK)、加密货币 (CRYPTO) 四个市场。
### Q: 成本计算使用什么算法?
A: 系统使用**平均成本法 (Average Cost)** 计算持仓成本。
### Q: 基准货币是什么?
A: 系统以 **USD** 为基准货币,所有资产会折算为 USD 进行汇总展示。
---
## 许可证
MIT License - 详见 [LICENSE](LICENSE) 文件
---
## 更新日志
### v1.0.0 (2026-04-12)
- ✨ 初始版本发布
- 支持多市场账户管理
- 支持交易记录(买入/卖出/入金/出金/分红)
- 支持持仓统计和盈亏计算
- 支持数据导入导出CSV
- 支持 Alpha Vantage 实时价格
- 响应式深色模式界面

View File

@ -7,19 +7,17 @@ import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Tabs as TabsPrimitive } from '@/components/ui/tabs'
import { import {
PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend, PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend,
LineChart, Line, XAxis, YAxis, CartesianGrid
} from 'recharts' } from 'recharts'
import { import {
Wallet, TrendingUp, TrendingDown, Plus, ArrowUpRight, ArrowDownRight, Wallet, TrendingUp, TrendingDown, Plus, ArrowUpRight, ArrowDownRight,
Bitcoin, Building2, Globe2, RefreshCw, DollarSign, Search, Check, Bitcoin, Building2, Globe2, RefreshCw, DollarSign, Search, Check,
Download, Upload, BarChart3, TrendingUpIcon Download, Upload,
} from 'lucide-react' } from 'lucide-react'
import { import {
fetchAccounts, fetchTransactions, fetchPositions, fetchAccounts, fetchTransactions, fetchPositions,
@ -33,6 +31,7 @@ import {
TRANSACTION_IMPORT_TEMPLATE, parseImportCSV, validateImportTransaction, ImportTransaction TRANSACTION_IMPORT_TEMPLATE, parseImportCSV, validateImportTransaction, ImportTransaction
} from '@/lib/import-export' } from '@/lib/import-export'
// 市场图标映射
const marketIcons: Record<MarketType, React.ReactNode> = { const marketIcons: Record<MarketType, React.ReactNode> = {
US: <Globe2 className="h-4 w-4" />, US: <Globe2 className="h-4 w-4" />,
CN: <Building2 className="h-4 w-4" />, CN: <Building2 className="h-4 w-4" />,
@ -40,6 +39,7 @@ const marketIcons: Record<MarketType, React.ReactNode> = {
CRYPTO: <Bitcoin className="h-4 w-4" />, CRYPTO: <Bitcoin className="h-4 w-4" />,
} }
// 市场颜色配置
const marketColors: Record<MarketType, string> = { const marketColors: Record<MarketType, string> = {
US: '#3b82f6', US: '#3b82f6',
CN: '#ef4444', CN: '#ef4444',
@ -47,6 +47,7 @@ const marketColors: Record<MarketType, string> = {
CRYPTO: '#eab308', CRYPTO: '#eab308',
} }
// 持仓分析数据结构
interface PositionAnalytics { interface PositionAnalytics {
symbol: string symbol: string
name: string name: string
@ -67,6 +68,7 @@ interface PositionAnalytics {
isCrypto: boolean isCrypto: boolean
} }
// 分析汇总数据结构
interface AnalyticsSummary { interface AnalyticsSummary {
totalCostBasis: number totalCostBasis: number
totalMarketValue: number totalMarketValue: number
@ -75,7 +77,9 @@ interface AnalyticsSummary {
positionCount: number positionCount: number
} }
// 主页面组件
export default function Dashboard() { export default function Dashboard() {
// 状态定义
const [accounts, setAccounts] = useState<Account[]>([]) const [accounts, setAccounts] = useState<Account[]>([])
const [transactions, setTransactions] = useState<Transaction[]>([]) const [transactions, setTransactions] = useState<Transaction[]>([])
const [positions, setPositions] = useState<Position[]>([]) const [positions, setPositions] = useState<Position[]>([])
@ -90,6 +94,8 @@ export default function Dashboard() {
const [filteredSecurities, setFilteredSecurities] = useState<Security[]>([]) const [filteredSecurities, setFilteredSecurities] = useState<Security[]>([])
const [importFile, setImportFile] = useState<File | null>(null) const [importFile, setImportFile] = useState<File | null>(null)
const [importData, setImportData] = useState<ImportTransaction[]>([]) const [importData, setImportData] = useState<ImportTransaction[]>([])
// 交易表单状态
const [txForm, setTxForm] = useState({ const [txForm, setTxForm] = useState({
type: 'BUY' as TransactionType, type: 'BUY' as TransactionType,
symbol: '', symbol: '',
@ -106,6 +112,7 @@ export default function Dashboard() {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
try { try {
setLoading(true) setLoading(true)
// 并行加载所有数据
const [accountsData, transactionsData, positionsData, securitiesData, analyticsData] = await Promise.all([ const [accountsData, transactionsData, positionsData, securitiesData, analyticsData] = await Promise.all([
fetchAccounts(), fetchAccounts(),
fetchTransactions({ limit: 50 }), fetchTransactions({ limit: 50 }),
@ -120,6 +127,7 @@ export default function Dashboard() {
setSecurities(securitiesData) setSecurities(securitiesData)
setAnalytics(analyticsData) setAnalytics(analyticsData)
// 默认选中第一个账户
if (accountsData.length > 0 && !selectedAccount) { if (accountsData.length > 0 && !selectedAccount) {
setSelectedAccount(accountsData[0].id) setSelectedAccount(accountsData[0].id)
} }
@ -134,7 +142,7 @@ export default function Dashboard() {
loadData() loadData()
}, [loadData]) }, [loadData])
// 搜索证券 // 搜索证券(根据输入实时过滤)
useEffect(() => { useEffect(() => {
if (symbolSearch.length >= 1) { if (symbolSearch.length >= 1) {
const filtered = securities.filter(s => const filtered = securities.filter(s =>
@ -147,7 +155,7 @@ export default function Dashboard() {
} }
}, [symbolSearch, securities]) }, [symbolSearch, securities])
// 选择证券后自动填充价格 // 选择证券后自动填充价格和币种
const handleSelectSecurity = (symbol: string) => { const handleSelectSecurity = (symbol: string) => {
const price = analytics?.prices[symbol]?.price || 0 const price = analytics?.prices[symbol]?.price || 0
const sec = securities.find(s => s.symbol === symbol) const sec = securities.find(s => s.symbol === symbol)
@ -161,7 +169,7 @@ export default function Dashboard() {
setFilteredSecurities([]) setFilteredSecurities([])
} }
// 提交交易 // 提交交易记录
const handleSubmitTx = async () => { const handleSubmitTx = async () => {
try { try {
const accountId = selectedAccount || accounts[0]?.id const accountId = selectedAccount || accounts[0]?.id
@ -192,7 +200,7 @@ export default function Dashboard() {
} }
} }
// 重置表单 // 重置交易表单
const resetTxForm = () => { const resetTxForm = () => {
const account = accounts.find(a => a.id === selectedAccount) const account = accounts.find(a => a.id === selectedAccount)
setTxForm({ setTxForm({
@ -214,7 +222,7 @@ export default function Dashboard() {
setShowTxDialog(true) setShowTxDialog(true)
} }
// 计算确认信息 // 获取确认对话框显示信息
const getConfirmInfo = () => { const getConfirmInfo = () => {
const account = accounts.find(a => a.id === selectedAccount) const account = accounts.find(a => a.id === selectedAccount)
const typeLabel = transactionTypeLabels[txForm.type] const typeLabel = transactionTypeLabels[txForm.type]
@ -233,7 +241,7 @@ export default function Dashboard() {
} }
} }
// 导出交易记录 // 导出交易记录为 CSV
const handleExportTransactions = () => { const handleExportTransactions = () => {
const csv = exportTransactionsToCSV(transactions) const csv = exportTransactionsToCSV(transactions)
const date = new Date().toISOString().slice(0, 10) const date = new Date().toISOString().slice(0, 10)
@ -241,7 +249,7 @@ export default function Dashboard() {
toast.success('交易记录已导出') toast.success('交易记录已导出')
} }
// 导出持仓 // 导出持仓记录为 CSV
const handleExportPositions = () => { const handleExportPositions = () => {
const csv = exportPositionsToCSV(positions.map(p => ({ const csv = exportPositionsToCSV(positions.map(p => ({
...p, ...p,
@ -257,7 +265,7 @@ export default function Dashboard() {
toast.success('持仓记录已导出') toast.success('持仓记录已导出')
} }
// 下载导入模板 // 下载导入模板 CSV
const handleDownloadTemplate = () => { const handleDownloadTemplate = () => {
downloadCSV(TRANSACTION_IMPORT_TEMPLATE, 'transaction_import_template.csv') downloadCSV(TRANSACTION_IMPORT_TEMPLATE, 'transaction_import_template.csv')
} }
@ -273,14 +281,14 @@ export default function Dashboard() {
reader.onload = (event) => { reader.onload = (event) => {
const content = event.target?.result as string const content = event.target?.result as string
const { headers, rows } = parseImportCSV(content) const { headers, rows } = parseImportCSV(content)
// 验证每一行数据
const validated = rows.map(row => validateImportTransaction(row, headers)) const validated = rows.map(row => validateImportTransaction(row, headers))
setImportData(validated) setImportData(validated)
} }
reader.readAsText(file) reader.readAsText(file)
} }
// 执行导入 // 执行批量导入
const handleExecuteImport = async () => { const handleExecuteImport = async () => {
if (!selectedAccount) { if (!selectedAccount) {
toast.error('请先选择导入目标账户') toast.error('请先选择导入目标账户')
@ -324,7 +332,7 @@ export default function Dashboard() {
} }
} }
// 市场分布数据 // 市场分布数据(用于饼图)
const marketDistribution = analytics?.summary ? [ const marketDistribution = analytics?.summary ? [
{ name: '美股', value: analytics.summary.totalMarketValue * 0.6, color: marketColors.US }, { name: '美股', value: analytics.summary.totalMarketValue * 0.6, color: marketColors.US },
{ name: 'A股', value: analytics.summary.totalMarketValue * 0.2, color: marketColors.CN }, { name: 'A股', value: analytics.summary.totalMarketValue * 0.2, color: marketColors.CN },
@ -332,6 +340,7 @@ export default function Dashboard() {
{ name: '加密', value: analytics.summary.totalMarketValue * 0.05, color: marketColors.CRYPTO }, { name: '加密', value: analytics.summary.totalMarketValue * 0.05, color: marketColors.CRYPTO },
].filter(item => item.value > 0) : [] ].filter(item => item.value > 0) : []
// 加载中状态
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
@ -342,7 +351,7 @@ export default function Dashboard() {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
{/* 顶部导航 */} {/* 顶部导航 */}
<header className="border-b bg-card/50 backdrop-blur sticky top-0 z-50"> <header className="border-b bg-card/50 backdrop-blur sticky top-0 z-50">
<div className="container mx-auto px-4 py-4 flex items-center justify-between"> <div className="container mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -350,6 +359,7 @@ export default function Dashboard() {
<h1 className="text-xl font-bold"></h1> <h1 className="text-xl font-bold"></h1>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* 账户选择下拉框 */}
<Select value={selectedAccount} onValueChange={(v) => v && setSelectedAccount(v)}> <Select value={selectedAccount} onValueChange={(v) => v && setSelectedAccount(v)}>
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择账户" /> <SelectValue placeholder="选择账户" />
@ -365,14 +375,17 @@ export default function Dashboard() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{/* 导入按钮 */}
<Button variant="outline" size="sm" onClick={() => setShowImportDialog(true)}> <Button variant="outline" size="sm" onClick={() => setShowImportDialog(true)}>
<Upload className="h-4 w-4 mr-2" /> <Upload className="h-4 w-4 mr-2" />
</Button> </Button>
{/* 导出按钮 */}
<Button variant="outline" size="sm" onClick={handleExportPositions}> <Button variant="outline" size="sm" onClick={handleExportPositions}>
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
</Button> </Button>
{/* 记录交易按钮 */}
<Button onClick={openTxDialog}> <Button onClick={openTxDialog}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
@ -382,8 +395,9 @@ export default function Dashboard() {
</header> </header>
<main className="container mx-auto px-4 py-6 space-y-6"> <main className="container mx-auto px-4 py-6 space-y-6">
{/* 资产概览 */} {/* 资产概览卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* 总资产卡片 */}
<Card className="bg-gradient-to-br from-blue-600 to-blue-700 text-white border-0"> <Card className="bg-gradient-to-br from-blue-600 to-blue-700 text-white border-0">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium opacity-90"> (USD)</CardTitle> <CardTitle className="text-sm font-medium opacity-90"> (USD)</CardTitle>
@ -397,6 +411,7 @@ export default function Dashboard() {
</CardContent> </CardContent>
</Card> </Card>
{/* 浮动盈亏卡片 */}
<Card className="bg-gradient-to-br from-emerald-600 to-emerald-700 text-white border-0"> <Card className="bg-gradient-to-br from-emerald-600 to-emerald-700 text-white border-0">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium opacity-90"></CardTitle> <CardTitle className="text-sm font-medium opacity-90"></CardTitle>
@ -411,6 +426,7 @@ export default function Dashboard() {
</CardContent> </CardContent>
</Card> </Card>
{/* 持仓市值卡片 */}
<Card className="bg-gradient-to-br from-purple-600 to-purple-700 text-white border-0"> <Card className="bg-gradient-to-br from-purple-600 to-purple-700 text-white border-0">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium opacity-90"></CardTitle> <CardTitle className="text-sm font-medium opacity-90"></CardTitle>
@ -422,6 +438,7 @@ export default function Dashboard() {
</CardContent> </CardContent>
</Card> </Card>
{/* 账户数量卡片 */}
<Card className="bg-gradient-to-br from-orange-600 to-orange-700 text-white border-0"> <Card className="bg-gradient-to-br from-orange-600 to-orange-700 text-white border-0">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium opacity-90"></CardTitle> <CardTitle className="text-sm font-medium opacity-90"></CardTitle>
@ -473,7 +490,7 @@ export default function Dashboard() {
</CardContent> </CardContent>
</Card> </Card>
{/* 盈亏统计 */} {/* 持仓分析卡片 */}
<Card className="lg:col-span-2"> <Card className="lg:col-span-2">
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
@ -499,7 +516,7 @@ export default function Dashboard() {
</Card> </Card>
</div> </div>
{/* 持仓和交易标签页 */} {/* 标签页区域:持仓明细、交易流水、分析 */}
<Tabs defaultValue="positions" className="space-y-4"> <Tabs defaultValue="positions" className="space-y-4">
<TabsList> <TabsList>
<TabsTrigger value="positions"></TabsTrigger> <TabsTrigger value="positions"></TabsTrigger>
@ -507,6 +524,7 @@ export default function Dashboard() {
<TabsTrigger value="analytics"></TabsTrigger> <TabsTrigger value="analytics"></TabsTrigger>
</TabsList> </TabsList>
{/* 持仓明细标签页 */}
<TabsContent value="positions"> <TabsContent value="positions">
<Card> <Card>
<CardContent className="p-0"> <CardContent className="p-0">
@ -575,6 +593,7 @@ export default function Dashboard() {
</Card> </Card>
</TabsContent> </TabsContent>
{/* 交易流水标签页 */}
<TabsContent value="transactions"> <TabsContent value="transactions">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-row items-center justify-between">
@ -644,9 +663,10 @@ export default function Dashboard() {
</Card> </Card>
</TabsContent> </TabsContent>
{/* 分析标签页 */}
<TabsContent value="analytics"> <TabsContent value="analytics">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 资产增长趋势 */} {/* 资产分布条形图 */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
@ -728,6 +748,7 @@ export default function Dashboard() {
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* 账户选择 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="account"></Label> <Label htmlFor="account"></Label>
<Select value={selectedAccount} onValueChange={(v) => v && setSelectedAccount(v)}> <Select value={selectedAccount} onValueChange={(v) => v && setSelectedAccount(v)}>
@ -743,6 +764,7 @@ export default function Dashboard() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* 交易类型选择 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="type"></Label> <Label htmlFor="type"></Label>
<Select value={txForm.type} onValueChange={(v) => setTxForm({ ...txForm, type: v as TransactionType })}> <Select value={txForm.type} onValueChange={(v) => setTxForm({ ...txForm, type: v as TransactionType })}>
@ -760,6 +782,7 @@ export default function Dashboard() {
</div> </div>
</div> </div>
{/* 证券代码搜索(买入/卖出/分红时显示) */}
{['BUY', 'SELL', 'DIVIDEND'].includes(txForm.type) && ( {['BUY', 'SELL', 'DIVIDEND'].includes(txForm.type) && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="symbol"></Label> <Label htmlFor="symbol"></Label>
@ -775,6 +798,7 @@ export default function Dashboard() {
setTxForm({ ...txForm, symbol: e.target.value.toUpperCase() }) setTxForm({ ...txForm, symbol: e.target.value.toUpperCase() })
}} }}
/> />
{/* 搜索结果下拉列表 */}
{filteredSecurities.length > 0 && ( {filteredSecurities.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-popover border rounded-md shadow-lg max-h-48 overflow-auto"> <div className="absolute z-10 w-full mt-1 bg-popover border rounded-md shadow-lg max-h-48 overflow-auto">
{filteredSecurities.map((sec) => ( {filteredSecurities.map((sec) => (
@ -798,6 +822,7 @@ export default function Dashboard() {
</div> </div>
)} )}
{/* 数量和价格输入(买入/卖出/分红时显示) */}
{['BUY', 'SELL', 'DIVIDEND'].includes(txForm.type) && ( {['BUY', 'SELL', 'DIVIDEND'].includes(txForm.type) && (
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
@ -825,6 +850,7 @@ export default function Dashboard() {
</div> </div>
)} )}
{/* 金额输入 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="amount"> <Label htmlFor="amount">
{['DEPOSIT', 'WITHDRAW'].includes(txForm.type) ? '金额' : '成交总额'} {['DEPOSIT', 'WITHDRAW'].includes(txForm.type) ? '金额' : '成交总额'}
@ -839,6 +865,7 @@ export default function Dashboard() {
/> />
</div> </div>
{/* 手续费输入 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="fee"></Label> <Label htmlFor="fee"></Label>
<Input <Input
@ -851,6 +878,7 @@ export default function Dashboard() {
/> />
</div> </div>
{/* 交易时间 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="executedAt"></Label> <Label htmlFor="executedAt"></Label>
<Input <Input
@ -861,6 +889,7 @@ export default function Dashboard() {
/> />
</div> </div>
{/* 备注 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="notes"></Label> <Label htmlFor="notes"></Label>
<Input <Input
@ -871,6 +900,7 @@ export default function Dashboard() {
/> />
</div> </div>
{/* 提交按钮 */}
<Button className="w-full" onClick={() => { <Button className="w-full" onClick={() => {
if (!selectedAccount) { if (!selectedAccount) {
toast.error('请先选择账户') toast.error('请先选择账户')
@ -888,7 +918,7 @@ export default function Dashboard() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* 确认对话框 */} {/* 交易确认对话框 */}
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}> <Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<DialogContent className="sm:max-w-sm"> <DialogContent className="sm:max-w-sm">
<DialogHeader> <DialogHeader>
@ -944,6 +974,7 @@ export default function Dashboard() {
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-4 space-y-4"> <div className="py-4 space-y-4">
{/* 下载模板按钮 */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button variant="outline" onClick={handleDownloadTemplate}> <Button variant="outline" onClick={handleDownloadTemplate}>
<Download className="h-4 w-4 mr-2" /> <Download className="h-4 w-4 mr-2" />
@ -954,6 +985,7 @@ export default function Dashboard() {
<Separator /> <Separator />
{/* 文件选择 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="importFile"> CSV </Label> <Label htmlFor="importFile"> CSV </Label>
<Input <Input
@ -964,6 +996,7 @@ export default function Dashboard() {
/> />
</div> </div>
{/* 导入预览表格 */}
{importFile && ( {importFile && (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm font-medium"> ({importData.length} )</div> <div className="text-sm font-medium"> ({importData.length} )</div>