v1.0.1 (2026-04-12)
- 🔧 账户名称优化:改为港股账户、美股账户、A股账户、加密货币账户 - ✨ 成交总额自动计算:根据数量 × 价格自动计算 - 🗑️ 删除持仓功能:支持通过卖出全部来删除持仓 - 🎨 Select 组件显示优化:修复了下拉框显示问题 - 📝 代码注释中文化
This commit is contained in:
parent
6f5bc03b88
commit
6ceb4a8e9b
10
README.md
10
README.md
@ -206,7 +206,7 @@ HTTPS_PROXY="http://192.168.48.171:7893"
|
|||||||
1. 点击右上角「**记录交易**」按钮
|
1. 点击右上角「**记录交易**」按钮
|
||||||
2. 选择账户和交易类型
|
2. 选择账户和交易类型
|
||||||
3. 输入证券代码(支持搜索)
|
3. 输入证券代码(支持搜索)
|
||||||
4. 填写数量、价格、金额
|
4. 填写数量、价格(成交总额自动计算)
|
||||||
5. 点击「**确认记录**」
|
5. 点击「**确认记录**」
|
||||||
6. 在确认对话框中核实信息,点击「**确认**」
|
6. 在确认对话框中核实信息,点击「**确认**」
|
||||||
|
|
||||||
@ -382,6 +382,14 @@ MIT License - 详见 [LICENSE](LICENSE) 文件
|
|||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.1 (2026-04-12)
|
||||||
|
|
||||||
|
- 🔧 账户名称优化:改为港股账户、美股账户、A股账户、加密货币账户
|
||||||
|
- ✨ 成交总额自动计算:根据数量 × 价格自动计算
|
||||||
|
- 🗑️ 删除持仓功能:支持通过卖出全部来删除持仓
|
||||||
|
- 🎨 Select 组件显示优化:修复了下拉框显示问题
|
||||||
|
- 📝 代码注释中文化
|
||||||
|
|
||||||
### v1.0.0 (2026-04-12)
|
### v1.0.0 (2026-04-12)
|
||||||
|
|
||||||
- ✨ 初始版本发布
|
- ✨ 初始版本发布
|
||||||
|
|||||||
@ -15,10 +15,10 @@ async function main() {
|
|||||||
|
|
||||||
// 创建各市场的账户
|
// 创建各市场的账户
|
||||||
const accounts = [
|
const accounts = [
|
||||||
{ name: '富途港股', marketType: MarketType.HK, baseCurrency: 'HKD' },
|
{ name: '港股账户', marketType: MarketType.HK, baseCurrency: 'HKD' },
|
||||||
{ name: '老虎美股', marketType: MarketType.US, baseCurrency: 'USD' },
|
{ name: '美股账户', marketType: MarketType.US, baseCurrency: 'USD' },
|
||||||
{ name: 'A股通', marketType: MarketType.CN, baseCurrency: 'CNY' },
|
{ name: 'A股账户', marketType: MarketType.CN, baseCurrency: 'CNY' },
|
||||||
{ name: '币安', marketType: MarketType.CRYPTO, baseCurrency: 'USDT' },
|
{ name: '加密货币账户', marketType: MarketType.CRYPTO, baseCurrency: 'USDT' },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const acc of accounts) {
|
for (const acc of accounts) {
|
||||||
|
|||||||
245
src/app/page.tsx
245
src/app/page.tsx
@ -17,7 +17,7 @@ import {
|
|||||||
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,
|
Download, Upload, Trash2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
fetchAccounts, fetchTransactions, fetchPositions,
|
fetchAccounts, fetchTransactions, fetchPositions,
|
||||||
@ -86,10 +86,12 @@ export default function Dashboard() {
|
|||||||
const [securities, setSecurities] = useState<Security[]>([])
|
const [securities, setSecurities] = useState<Security[]>([])
|
||||||
const [analytics, setAnalytics] = useState<{ prices: Record<string, any>; positions: PositionAnalytics[]; summary: AnalyticsSummary } | null>(null)
|
const [analytics, setAnalytics] = useState<{ prices: Record<string, any>; positions: PositionAnalytics[]; summary: AnalyticsSummary } | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [selectedAccount, setSelectedAccount] = useState<string>('')
|
const [selectedAccountId, setSelectedAccountId] = useState<string>('')
|
||||||
const [showTxDialog, setShowTxDialog] = useState(false)
|
const [showTxDialog, setShowTxDialog] = useState(false)
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
||||||
const [showImportDialog, setShowImportDialog] = useState(false)
|
const [showImportDialog, setShowImportDialog] = useState(false)
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||||
|
const [positionToDelete, setPositionToDelete] = useState<PositionAnalytics | null>(null)
|
||||||
const [symbolSearch, setSymbolSearch] = useState('')
|
const [symbolSearch, setSymbolSearch] = useState('')
|
||||||
const [filteredSecurities, setFilteredSecurities] = useState<Security[]>([])
|
const [filteredSecurities, setFilteredSecurities] = useState<Security[]>([])
|
||||||
const [importFile, setImportFile] = useState<File | null>(null)
|
const [importFile, setImportFile] = useState<File | null>(null)
|
||||||
@ -101,13 +103,16 @@ export default function Dashboard() {
|
|||||||
symbol: '',
|
symbol: '',
|
||||||
quantity: '',
|
quantity: '',
|
||||||
price: '',
|
price: '',
|
||||||
amount: '',
|
amount: '', // 仅用于出金/入金
|
||||||
fee: '0',
|
fee: '0',
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
notes: '',
|
notes: '',
|
||||||
executedAt: new Date().toISOString().slice(0, 16),
|
executedAt: new Date().toISOString().slice(0, 16),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取当前选中的账户对象
|
||||||
|
const selectedAccount = accounts.find(a => a.id === selectedAccountId)
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -128,15 +133,15 @@ export default function Dashboard() {
|
|||||||
setAnalytics(analyticsData)
|
setAnalytics(analyticsData)
|
||||||
|
|
||||||
// 默认选中第一个账户
|
// 默认选中第一个账户
|
||||||
if (accountsData.length > 0 && !selectedAccount) {
|
if (accountsData.length > 0 && !selectedAccountId) {
|
||||||
setSelectedAccount(accountsData[0].id)
|
setSelectedAccountId(accountsData[0].id)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('加载数据失败')
|
toast.error('加载数据失败')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [selectedAccount])
|
}, [selectedAccountId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
@ -169,22 +174,30 @@ export default function Dashboard() {
|
|||||||
setFilteredSecurities([])
|
setFilteredSecurities([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 当数量或价格变化时,自动计算成交总额
|
||||||
|
const calculatedAmount = txForm.quantity && txForm.price
|
||||||
|
? (parseFloat(txForm.quantity) * parseFloat(txForm.price)).toFixed(2)
|
||||||
|
: ''
|
||||||
|
|
||||||
// 提交交易记录
|
// 提交交易记录
|
||||||
const handleSubmitTx = async () => {
|
const handleSubmitTx = async () => {
|
||||||
try {
|
try {
|
||||||
const accountId = selectedAccount || accounts[0]?.id
|
if (!selectedAccountId) {
|
||||||
if (!accountId) {
|
|
||||||
toast.error('请先选择账户')
|
toast.error('请先选择账户')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const amount = txForm.quantity && txForm.price
|
||||||
|
? (parseFloat(txForm.quantity) * parseFloat(txForm.price)).toFixed(2)
|
||||||
|
: txForm.amount
|
||||||
|
|
||||||
await createTransaction({
|
await createTransaction({
|
||||||
accountId,
|
accountId: selectedAccountId,
|
||||||
type: txForm.type,
|
type: txForm.type,
|
||||||
symbol: txForm.symbol || undefined,
|
symbol: txForm.symbol || undefined,
|
||||||
quantity: txForm.quantity ? parseFloat(txForm.quantity) : undefined,
|
quantity: txForm.quantity ? parseFloat(txForm.quantity) : undefined,
|
||||||
price: txForm.price ? parseFloat(txForm.price) : undefined,
|
price: txForm.price ? parseFloat(txForm.price) : undefined,
|
||||||
amount: parseFloat(txForm.amount),
|
amount: parseFloat(amount || '0'),
|
||||||
fee: parseFloat(txForm.fee) || 0,
|
fee: parseFloat(txForm.fee) || 0,
|
||||||
currency: txForm.currency,
|
currency: txForm.currency,
|
||||||
notes: txForm.notes || undefined,
|
notes: txForm.notes || undefined,
|
||||||
@ -200,9 +213,48 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除持仓
|
||||||
|
const handleDeletePosition = async () => {
|
||||||
|
if (!positionToDelete) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 通过卖出全部来删除持仓
|
||||||
|
const position = positions.find(p => p.symbol === positionToDelete.symbol)
|
||||||
|
if (!position) {
|
||||||
|
toast.error('未找到持仓记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createTransaction({
|
||||||
|
accountId: selectedAccountId || position.accountId,
|
||||||
|
type: 'SELL',
|
||||||
|
symbol: positionToDelete.symbol,
|
||||||
|
quantity: parseFloat(positionToDelete.quantity.toString()),
|
||||||
|
price: parseFloat(positionToDelete.currentPrice.toString()),
|
||||||
|
amount: parseFloat((positionToDelete.quantity * positionToDelete.currentPrice).toFixed(2)),
|
||||||
|
fee: 0,
|
||||||
|
currency: positionToDelete.currency,
|
||||||
|
notes: '删除持仓',
|
||||||
|
executedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success('持仓已删除')
|
||||||
|
setShowDeleteDialog(false)
|
||||||
|
setPositionToDelete(null)
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('删除持仓失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开删除确认对话框
|
||||||
|
const openDeleteDialog = (pos: PositionAnalytics) => {
|
||||||
|
setPositionToDelete(pos)
|
||||||
|
setShowDeleteDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
// 重置交易表单
|
// 重置交易表单
|
||||||
const resetTxForm = () => {
|
const resetTxForm = () => {
|
||||||
const account = accounts.find(a => a.id === selectedAccount)
|
|
||||||
setTxForm({
|
setTxForm({
|
||||||
type: 'BUY',
|
type: 'BUY',
|
||||||
symbol: '',
|
symbol: '',
|
||||||
@ -210,7 +262,7 @@ export default function Dashboard() {
|
|||||||
price: '',
|
price: '',
|
||||||
amount: '',
|
amount: '',
|
||||||
fee: '0',
|
fee: '0',
|
||||||
currency: account?.baseCurrency || 'USD',
|
currency: selectedAccount?.baseCurrency || 'USD',
|
||||||
notes: '',
|
notes: '',
|
||||||
executedAt: new Date().toISOString().slice(0, 16),
|
executedAt: new Date().toISOString().slice(0, 16),
|
||||||
})
|
})
|
||||||
@ -224,18 +276,19 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
// 获取确认对话框显示信息
|
// 获取确认对话框显示信息
|
||||||
const getConfirmInfo = () => {
|
const getConfirmInfo = () => {
|
||||||
const account = accounts.find(a => a.id === selectedAccount)
|
|
||||||
const typeLabel = transactionTypeLabels[txForm.type]
|
const typeLabel = transactionTypeLabels[txForm.type]
|
||||||
const symbolLabel = txForm.symbol ? `${txForm.symbol} ` : ''
|
const symbolLabel = txForm.symbol ? `${txForm.symbol} ` : ''
|
||||||
const qtyLabel = txForm.quantity ? `${txForm.quantity}股 ` : ''
|
const qtyLabel = txForm.quantity ? `${txForm.quantity}股 ` : ''
|
||||||
const priceLabel = txForm.price ? `@ ${formatCurrency(parseFloat(txForm.price), txForm.currency)}` : ''
|
const priceLabel = txForm.price ? `@ ${formatCurrency(parseFloat(txForm.price), txForm.currency)}` : ''
|
||||||
const totalLabel = txForm.amount ? `合计 ${formatCurrency(parseFloat(txForm.amount), txForm.currency)}` : ''
|
const total = txForm.quantity && txForm.price
|
||||||
|
? (parseFloat(txForm.quantity) * parseFloat(txForm.price)).toFixed(2)
|
||||||
|
: txForm.amount
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account: account?.name || '未选择',
|
account: selectedAccount?.name || '未选择',
|
||||||
market: account ? marketLabels[account.marketType] : '',
|
market: selectedAccount ? marketLabels[selectedAccount.marketType] : '',
|
||||||
action: `${typeLabel} ${symbolLabel}${qtyLabel}${priceLabel}`,
|
action: `${typeLabel} ${symbolLabel}${qtyLabel}${priceLabel}`,
|
||||||
total: totalLabel,
|
total: total ? `合计 ${formatCurrency(parseFloat(total), txForm.currency)}` : '',
|
||||||
fee: txForm.fee && parseFloat(txForm.fee) > 0 ? `手续费: ${formatCurrency(parseFloat(txForm.fee), txForm.currency)}` : '',
|
fee: txForm.fee && parseFloat(txForm.fee) > 0 ? `手续费: ${formatCurrency(parseFloat(txForm.fee), txForm.currency)}` : '',
|
||||||
time: new Date(txForm.executedAt).toLocaleString('zh-CN'),
|
time: new Date(txForm.executedAt).toLocaleString('zh-CN'),
|
||||||
}
|
}
|
||||||
@ -290,7 +343,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
// 执行批量导入
|
// 执行批量导入
|
||||||
const handleExecuteImport = async () => {
|
const handleExecuteImport = async () => {
|
||||||
if (!selectedAccount) {
|
if (!selectedAccountId) {
|
||||||
toast.error('请先选择导入目标账户')
|
toast.error('请先选择导入目标账户')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -317,7 +370,7 @@ export default function Dashboard() {
|
|||||||
notes: tx.notes || null,
|
notes: tx.notes || null,
|
||||||
executedAt: tx.executedAt,
|
executedAt: tx.executedAt,
|
||||||
})),
|
})),
|
||||||
accountId: selectedAccount,
|
accountId: selectedAccountId,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -360,9 +413,16 @@ export default function Dashboard() {
|
|||||||
</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={selectedAccountId} onValueChange={(v) => v && setSelectedAccountId(v)}>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[140px]">
|
||||||
<SelectValue placeholder="选择账户" />
|
<SelectValue placeholder="选择账户">
|
||||||
|
{selectedAccount ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{marketIcons[selectedAccount.marketType]}
|
||||||
|
<span>{selectedAccount.name}</span>
|
||||||
|
</div>
|
||||||
|
) : '选择账户'}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{accounts.map((acc) => (
|
{accounts.map((acc) => (
|
||||||
@ -538,6 +598,7 @@ export default function Dashboard() {
|
|||||||
<TableHead className="text-right">当前价</TableHead>
|
<TableHead className="text-right">当前价</TableHead>
|
||||||
<TableHead className="text-right">市值</TableHead>
|
<TableHead className="text-right">市值</TableHead>
|
||||||
<TableHead className="text-right">盈亏</TableHead>
|
<TableHead className="text-right">盈亏</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -579,10 +640,15 @@ export default function Dashboard() {
|
|||||||
<span className="text-xs">({formatPercent(pos.pnlPercent)})</span>
|
<span className="text-xs">({formatPercent(pos.pnlPercent)})</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => openDeleteDialog(pos)}>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)) : (
|
)) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||||
暂无持仓记录
|
暂无持仓记录
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -750,10 +816,12 @@ export default function Dashboard() {
|
|||||||
<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>账户</Label>
|
||||||
<Select value={selectedAccount} onValueChange={(v) => v && setSelectedAccount(v)}>
|
<Select value={selectedAccountId} onValueChange={(v) => v && setSelectedAccountId(v)}>
|
||||||
<SelectTrigger id="account">
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="选择账户" />
|
<SelectValue>
|
||||||
|
{selectedAccount ? selectedAccount.name : '选择账户'}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{accounts.map((acc) => (
|
{accounts.map((acc) => (
|
||||||
@ -766,9 +834,9 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
{/* 交易类型选择 */}
|
{/* 交易类型选择 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="type">交易类型</Label>
|
<Label>交易类型</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 })}>
|
||||||
<SelectTrigger id="type">
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -785,11 +853,10 @@ export default function Dashboard() {
|
|||||||
{/* 证券代码搜索(买入/卖出/分红时显示) */}
|
{/* 证券代码搜索(买入/卖出/分红时显示) */}
|
||||||
{['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>证券代码</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
id="symbol"
|
|
||||||
className="pl-9"
|
className="pl-9"
|
||||||
placeholder="搜索证券代码或名称"
|
placeholder="搜索证券代码或名称"
|
||||||
value={symbolSearch || txForm.symbol}
|
value={symbolSearch || txForm.symbol}
|
||||||
@ -826,9 +893,8 @@ export default function Dashboard() {
|
|||||||
{['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">
|
||||||
<Label htmlFor="quantity">数量</Label>
|
<Label>数量</Label>
|
||||||
<Input
|
<Input
|
||||||
id="quantity"
|
|
||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
@ -837,9 +903,8 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="price">价格</Label>
|
<Label>价格</Label>
|
||||||
<Input
|
<Input
|
||||||
id="price"
|
|
||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
@ -850,26 +915,41 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 金额输入 */}
|
{/* 成交总额(自动计算) */}
|
||||||
<div className="space-y-2">
|
{['BUY', 'SELL', 'DIVIDEND'].includes(txForm.type) && (
|
||||||
<Label htmlFor="amount">
|
<div className="space-y-2">
|
||||||
{['DEPOSIT', 'WITHDRAW'].includes(txForm.type) ? '金额' : '成交总额'}
|
<Label>成交总额</Label>
|
||||||
</Label>
|
<div className="p-3 bg-muted rounded-lg">
|
||||||
<Input
|
<span className="text-2xl font-bold">
|
||||||
id="amount"
|
{calculatedAmount ? formatCurrency(parseFloat(calculatedAmount), txForm.currency) : '—'}
|
||||||
type="number"
|
</span>
|
||||||
step="any"
|
{txForm.quantity && txForm.price && (
|
||||||
placeholder="0.00"
|
<span className="text-sm text-muted-foreground ml-2">
|
||||||
value={txForm.amount}
|
({txForm.quantity} × {formatCurrency(parseFloat(txForm.price), txForm.currency)})
|
||||||
onChange={(e) => setTxForm({ ...txForm, amount: e.target.value })}
|
</span>
|
||||||
/>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 入金/出金金额输入 */}
|
||||||
|
{['DEPOSIT', 'WITHDRAW'].includes(txForm.type) && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>金额</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
placeholder="0.00"
|
||||||
|
value={txForm.amount}
|
||||||
|
onChange={(e) => setTxForm({ ...txForm, amount: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 手续费输入 */}
|
{/* 手续费输入 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="fee">手续费</Label>
|
<Label>手续费</Label>
|
||||||
<Input
|
<Input
|
||||||
id="fee"
|
|
||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
@ -880,9 +960,8 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* 交易时间 */}
|
{/* 交易时间 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="executedAt">交易时间</Label>
|
<Label>交易时间</Label>
|
||||||
<Input
|
<Input
|
||||||
id="executedAt"
|
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={txForm.executedAt}
|
value={txForm.executedAt}
|
||||||
onChange={(e) => setTxForm({ ...txForm, executedAt: e.target.value })}
|
onChange={(e) => setTxForm({ ...txForm, executedAt: e.target.value })}
|
||||||
@ -891,9 +970,8 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* 备注 */}
|
{/* 备注 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="notes">备注</Label>
|
<Label>备注</Label>
|
||||||
<Input
|
<Input
|
||||||
id="notes"
|
|
||||||
placeholder="可选备注"
|
placeholder="可选备注"
|
||||||
value={txForm.notes}
|
value={txForm.notes}
|
||||||
onChange={(e) => setTxForm({ ...txForm, notes: e.target.value })}
|
onChange={(e) => setTxForm({ ...txForm, notes: e.target.value })}
|
||||||
@ -902,11 +980,15 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* 提交按钮 */}
|
{/* 提交按钮 */}
|
||||||
<Button className="w-full" onClick={() => {
|
<Button className="w-full" onClick={() => {
|
||||||
if (!selectedAccount) {
|
if (!selectedAccountId) {
|
||||||
toast.error('请先选择账户')
|
toast.error('请先选择账户')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!txForm.amount) {
|
if (['BUY', 'SELL', 'DIVIDEND'].includes(txForm.type) && (!txForm.quantity || !txForm.price)) {
|
||||||
|
toast.error('请输入数量和价格')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (['DEPOSIT', 'WITHDRAW'].includes(txForm.type) && !txForm.amount) {
|
||||||
toast.error('请输入金额')
|
toast.error('请输入金额')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -967,6 +1049,52 @@ export default function Dashboard() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 删除持仓确认对话框 */}
|
||||||
|
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
<DialogContent className="sm:max-w-sm">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>确认删除持仓</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{positionToDelete && (
|
||||||
|
<div className="py-4 space-y-3">
|
||||||
|
<div className="text-center py-2">
|
||||||
|
<div className="text-lg font-bold">{positionToDelete.symbol}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{positionToDelete.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/50 p-3 rounded-lg space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">数量</span>
|
||||||
|
<span className="font-mono">{positionToDelete.quantity.toFixed(positionToDelete.isCrypto ? 6 : 0)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">当前价</span>
|
||||||
|
<span className="font-mono">{formatCurrency(positionToDelete.currentPrice, positionToDelete.currency)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">市值</span>
|
||||||
|
<span className="font-mono font-bold">{formatCurrency(positionToDelete.marketValue, positionToDelete.currency)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-destructive text-center">
|
||||||
|
确定要删除此持仓吗?这将自动卖出全部持仓
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DialogFooter className="gap-2 sm:gap-0">
|
||||||
|
<Button variant="outline" onClick={() => {
|
||||||
|
setShowDeleteDialog(false)
|
||||||
|
setPositionToDelete(null)
|
||||||
|
}}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeletePosition}>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* 导入对话框 */}
|
{/* 导入对话框 */}
|
||||||
<Dialog open={showImportDialog} onOpenChange={setShowImportDialog}>
|
<Dialog open={showImportDialog} onOpenChange={setShowImportDialog}>
|
||||||
<DialogContent className="sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
@ -987,9 +1115,8 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* 文件选择 */}
|
{/* 文件选择 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="importFile">选择 CSV 文件</Label>
|
<Label>选择 CSV 文件</Label>
|
||||||
<Input
|
<Input
|
||||||
id="importFile"
|
|
||||||
type="file"
|
type="file"
|
||||||
accept=".csv"
|
accept=".csv"
|
||||||
onChange={handleFileImport}
|
onChange={handleFileImport}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user