From 6ceb4a8e9bf971d84f97b7e0d5f2ff77853d33d8 Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sun, 12 Apr 2026 05:37:59 +0800 Subject: [PATCH] v1.0.1 (2026-04-12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🔧 账户名称优化:改为港股账户、美股账户、A股账户、加密货币账户 - ✨ 成交总额自动计算:根据数量 × 价格自动计算 - 🗑️ 删除持仓功能:支持通过卖出全部来删除持仓 - 🎨 Select 组件显示优化:修复了下拉框显示问题 - 📝 代码注释中文化 --- README.md | 10 +- prisma/seed.ts | 8 +- src/app/page.tsx | 245 +++++++++++++++++++++++++++++++++++------------ 3 files changed, 199 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index a8cd940..06e5ec1 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ HTTPS_PROXY="http://192.168.48.171:7893" 1. 点击右上角「**记录交易**」按钮 2. 选择账户和交易类型 3. 输入证券代码(支持搜索) -4. 填写数量、价格、金额 +4. 填写数量、价格(成交总额自动计算) 5. 点击「**确认记录**」 6. 在确认对话框中核实信息,点击「**确认**」 @@ -382,6 +382,14 @@ MIT License - 详见 [LICENSE](LICENSE) 文件 ## 更新日志 +### v1.0.1 (2026-04-12) + +- 🔧 账户名称优化:改为港股账户、美股账户、A股账户、加密货币账户 +- ✨ 成交总额自动计算:根据数量 × 价格自动计算 +- 🗑️ 删除持仓功能:支持通过卖出全部来删除持仓 +- 🎨 Select 组件显示优化:修复了下拉框显示问题 +- 📝 代码注释中文化 + ### v1.0.0 (2026-04-12) - ✨ 初始版本发布 diff --git a/prisma/seed.ts b/prisma/seed.ts index 31cd61c..17c8cc2 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -15,10 +15,10 @@ async function main() { // 创建各市场的账户 const accounts = [ - { name: '富途港股', marketType: MarketType.HK, baseCurrency: 'HKD' }, - { name: '老虎美股', marketType: MarketType.US, baseCurrency: 'USD' }, - { name: 'A股通', marketType: MarketType.CN, baseCurrency: 'CNY' }, - { name: '币安', marketType: MarketType.CRYPTO, baseCurrency: 'USDT' }, + { name: '港股账户', marketType: MarketType.HK, baseCurrency: 'HKD' }, + { name: '美股账户', marketType: MarketType.US, baseCurrency: 'USD' }, + { name: 'A股账户', marketType: MarketType.CN, baseCurrency: 'CNY' }, + { name: '加密货币账户', marketType: MarketType.CRYPTO, baseCurrency: 'USDT' }, ] for (const acc of accounts) { diff --git a/src/app/page.tsx b/src/app/page.tsx index 7bd5bc6..0a11afa 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -17,7 +17,7 @@ import { import { Wallet, TrendingUp, TrendingDown, Plus, ArrowUpRight, ArrowDownRight, Bitcoin, Building2, Globe2, RefreshCw, DollarSign, Search, Check, - Download, Upload, + Download, Upload, Trash2, } from 'lucide-react' import { fetchAccounts, fetchTransactions, fetchPositions, @@ -86,10 +86,12 @@ export default function Dashboard() { const [securities, setSecurities] = useState([]) const [analytics, setAnalytics] = useState<{ prices: Record; positions: PositionAnalytics[]; summary: AnalyticsSummary } | null>(null) const [loading, setLoading] = useState(true) - const [selectedAccount, setSelectedAccount] = useState('') + const [selectedAccountId, setSelectedAccountId] = useState('') const [showTxDialog, setShowTxDialog] = useState(false) const [showConfirmDialog, setShowConfirmDialog] = useState(false) const [showImportDialog, setShowImportDialog] = useState(false) + const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const [positionToDelete, setPositionToDelete] = useState(null) const [symbolSearch, setSymbolSearch] = useState('') const [filteredSecurities, setFilteredSecurities] = useState([]) const [importFile, setImportFile] = useState(null) @@ -101,13 +103,16 @@ export default function Dashboard() { symbol: '', quantity: '', price: '', - amount: '', + amount: '', // 仅用于出金/入金 fee: '0', currency: 'USD', notes: '', executedAt: new Date().toISOString().slice(0, 16), }) + // 获取当前选中的账户对象 + const selectedAccount = accounts.find(a => a.id === selectedAccountId) + // 加载数据 const loadData = useCallback(async () => { try { @@ -128,15 +133,15 @@ export default function Dashboard() { setAnalytics(analyticsData) // 默认选中第一个账户 - if (accountsData.length > 0 && !selectedAccount) { - setSelectedAccount(accountsData[0].id) + if (accountsData.length > 0 && !selectedAccountId) { + setSelectedAccountId(accountsData[0].id) } } catch (error) { toast.error('加载数据失败') } finally { setLoading(false) } - }, [selectedAccount]) + }, [selectedAccountId]) useEffect(() => { loadData() @@ -169,22 +174,30 @@ export default function Dashboard() { setFilteredSecurities([]) } + // 当数量或价格变化时,自动计算成交总额 + const calculatedAmount = txForm.quantity && txForm.price + ? (parseFloat(txForm.quantity) * parseFloat(txForm.price)).toFixed(2) + : '' + // 提交交易记录 const handleSubmitTx = async () => { try { - const accountId = selectedAccount || accounts[0]?.id - if (!accountId) { + if (!selectedAccountId) { toast.error('请先选择账户') return } + const amount = txForm.quantity && txForm.price + ? (parseFloat(txForm.quantity) * parseFloat(txForm.price)).toFixed(2) + : txForm.amount + await createTransaction({ - accountId, + accountId: selectedAccountId, type: txForm.type, symbol: txForm.symbol || undefined, quantity: txForm.quantity ? parseFloat(txForm.quantity) : undefined, price: txForm.price ? parseFloat(txForm.price) : undefined, - amount: parseFloat(txForm.amount), + amount: parseFloat(amount || '0'), fee: parseFloat(txForm.fee) || 0, currency: txForm.currency, 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 account = accounts.find(a => a.id === selectedAccount) setTxForm({ type: 'BUY', symbol: '', @@ -210,7 +262,7 @@ export default function Dashboard() { price: '', amount: '', fee: '0', - currency: account?.baseCurrency || 'USD', + currency: selectedAccount?.baseCurrency || 'USD', notes: '', executedAt: new Date().toISOString().slice(0, 16), }) @@ -224,18 +276,19 @@ export default function Dashboard() { // 获取确认对话框显示信息 const getConfirmInfo = () => { - const account = accounts.find(a => a.id === selectedAccount) const typeLabel = transactionTypeLabels[txForm.type] const symbolLabel = txForm.symbol ? `${txForm.symbol} ` : '' const qtyLabel = txForm.quantity ? `${txForm.quantity}股 ` : '' 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 { - account: account?.name || '未选择', - market: account ? marketLabels[account.marketType] : '', + account: selectedAccount?.name || '未选择', + market: selectedAccount ? marketLabels[selectedAccount.marketType] : '', 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)}` : '', time: new Date(txForm.executedAt).toLocaleString('zh-CN'), } @@ -290,7 +343,7 @@ export default function Dashboard() { // 执行批量导入 const handleExecuteImport = async () => { - if (!selectedAccount) { + if (!selectedAccountId) { toast.error('请先选择导入目标账户') return } @@ -317,7 +370,7 @@ export default function Dashboard() { notes: tx.notes || null, executedAt: tx.executedAt, })), - accountId: selectedAccount, + accountId: selectedAccountId, }), }) @@ -360,9 +413,16 @@ export default function Dashboard() {
{/* 账户选择下拉框 */} - v && setSelectedAccountId(v)}> + + + {selectedAccount ? ( +
+ {marketIcons[selectedAccount.marketType]} + {selectedAccount.name} +
+ ) : '选择账户'} +
{accounts.map((acc) => ( @@ -538,6 +598,7 @@ export default function Dashboard() { 当前价 市值 盈亏 + @@ -579,10 +640,15 @@ export default function Dashboard() { ({formatPercent(pos.pnlPercent)})
+ + + )) : ( - + 暂无持仓记录 @@ -750,10 +816,12 @@ export default function Dashboard() {
{/* 账户选择 */}
- - v && setSelectedAccountId(v)}> + + + {selectedAccount ? selectedAccount.name : '选择账户'} + {accounts.map((acc) => ( @@ -766,9 +834,9 @@ export default function Dashboard() {
{/* 交易类型选择 */}
- +
- +
- + )} - {/* 金额输入 */} -
- - setTxForm({ ...txForm, amount: e.target.value })} - /> -
+ {/* 成交总额(自动计算) */} + {['BUY', 'SELL', 'DIVIDEND'].includes(txForm.type) && ( +
+ +
+ + {calculatedAmount ? formatCurrency(parseFloat(calculatedAmount), txForm.currency) : '—'} + + {txForm.quantity && txForm.price && ( + + ({txForm.quantity} × {formatCurrency(parseFloat(txForm.price), txForm.currency)}) + + )} +
+
+ )} + + {/* 入金/出金金额输入 */} + {['DEPOSIT', 'WITHDRAW'].includes(txForm.type) && ( +
+ + setTxForm({ ...txForm, amount: e.target.value })} + /> +
+ )} {/* 手续费输入 */}
- + - + setTxForm({ ...txForm, executedAt: e.target.value })} @@ -891,9 +970,8 @@ export default function Dashboard() { {/* 备注 */}
- + setTxForm({ ...txForm, notes: e.target.value })} @@ -902,11 +980,15 @@ export default function Dashboard() { {/* 提交按钮 */} + + + + + {/* 导入对话框 */} @@ -987,9 +1115,8 @@ export default function Dashboard() { {/* 文件选择 */}
- +