更新描述
This commit is contained in:
parent
f6436db200
commit
6f5bc03b88
401
README.md
401
README.md
@ -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:
|
||||

|
||||

|
||||

|
||||

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