From 2889a16e8d28c0cb78cf988386772994300e91a8 Mon Sep 17 00:00:00 2001 From: CsJsss <764527108@qq.com> Date: Wed, 3 Jun 2026 22:11:59 +0800 Subject: [PATCH] feat: support market price (csqaq) for trade records --- frontend/src/pages/CompletedTradesPage.tsx | 165 +++++++++++++++++---- frontend/wailsjs/go/models.ts | 14 ++ pkg/orm/interfaces.go | 4 +- pkg/orm/trade.go | 34 +++-- pkg/service/market/service.go | 36 ++--- pkg/service/service.go | 2 + pkg/service/trade/service.go | 153 ++++++++++++++++--- 7 files changed, 322 insertions(+), 86 deletions(-) diff --git a/frontend/src/pages/CompletedTradesPage.tsx b/frontend/src/pages/CompletedTradesPage.tsx index cf9c791..e1781e1 100644 --- a/frontend/src/pages/CompletedTradesPage.tsx +++ b/frontend/src/pages/CompletedTradesPage.tsx @@ -40,11 +40,14 @@ import PnlSummaryCards from '../components/PnlSummaryCards'; import ReceiptIcon from '@mui/icons-material/Receipt'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import PageSearchBar from '../components/PageSearchBar'; +import Tooltip from '@mui/material/Tooltip'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { useCompletedTrades } from '../hooks/useCompletedTrades'; import { useCompletedTradesSummary } from '../hooks/useCompletedTradesSummary'; import { useUnmatchedSells } from '../hooks/useUnmatchedSells'; import { useUIStore } from '../store/uiStore'; import { formatCNY, plHexColor } from '../lib/format'; +import { BrowserOpenURL } from '../../wailsjs/runtime/runtime'; import type { model, trade } from '../lib/wails'; declare module '@tanstack/react-table' { @@ -58,6 +61,9 @@ type TabKey = 'completed' | 'unmatched'; interface GroupedTrade { itemName: string; + exterior: string; + csqaqGoodsId?: number; + marketHashName?: string; count: number; trades: trade.CompletedTradeView[]; totalBuyPrice: number; @@ -65,10 +71,14 @@ interface GroupedTrade { totalGrossPl: number; totalFee: number; totalNetPl: number; + marketPrice?: number; + marketTotal?: number; + postTradePl?: number; } interface GroupedUnmatchedSell { itemName: string; + exterior: string; count: number; sells: model.TradeRecord[]; totalSellPrice: number; @@ -215,16 +225,32 @@ const groupedColumns: ColumnDef[] = [ }, { accessorKey: 'itemName', - header: 'Item Name', - cell: (info) => ( - - {info.getValue() as string} - + header: '物品名称', + cell: ({ row }) => ( + + + {row.original.itemName} + {row.original.exterior ? ` (${row.original.exterior})` : ''} + + {row.original.csqaqGoodsId ? ( + + { + e.stopPropagation(); + BrowserOpenURL(`https://www.csqaq.com/goods/${row.original.csqaqGoodsId}`); + }} + > + + + + ) : null} + ), }, { accessorKey: 'count', - header: 'Trades', + header: '交易数', meta: { align: 'right' }, cell: (info) => ( @@ -234,7 +260,7 @@ const groupedColumns: ColumnDef[] = [ }, { accessorKey: 'totalBuyPrice', - header: 'Total Buy', + header: '买入总额', meta: { align: 'right' }, cell: (info) => ( @@ -244,7 +270,7 @@ const groupedColumns: ColumnDef[] = [ }, { accessorKey: 'totalSellPrice', - header: 'Total Sell', + header: '卖出总额', meta: { align: 'right' }, cell: (info) => ( @@ -252,9 +278,38 @@ const groupedColumns: ColumnDef[] = [ ), }, + { + id: 'marketTotal', + header: '市场总价', + meta: { align: 'right' }, + cell: ({ row }) => ( + + {row.original.marketTotal != null ? formatCNY(row.original.marketTotal) : '--'} + + ), + }, + { + id: 'postTradePl', + header: '交易后盈亏', + meta: { align: 'right' }, + cell: ({ row }) => { + if (row.original.postTradePl == null) + return ( + + -- + + ); + const v = row.original.postTradePl; + return ( + + {formatCNY(v)} + + ); + }, + }, { accessorKey: 'totalGrossPl', - header: 'Gross P/L', + header: '毛利', meta: { align: 'right' }, cell: (info) => { const v = info.getValue() as number; @@ -267,7 +322,7 @@ const groupedColumns: ColumnDef[] = [ }, { accessorKey: 'totalFee', - header: 'Fees', + header: '手续费', meta: { align: 'right' }, cell: (info) => ( @@ -277,7 +332,7 @@ const groupedColumns: ColumnDef[] = [ }, { accessorKey: 'totalNetPl', - header: 'Net P/L', + header: '净利润', meta: { align: 'right' }, cell: (info) => { const v = info.getValue() as number; @@ -308,16 +363,17 @@ const unmatchedGroupedColumns: ColumnDef[] = [ }, { accessorKey: 'itemName', - header: 'Item Name', - cell: (info) => ( + header: '物品名称', + cell: ({ row }) => ( - {info.getValue() as string} + {row.original.itemName} + {row.original.exterior ? ` (${row.original.exterior})` : ''} ), }, { accessorKey: 'count', - header: 'Sells', + header: '卖出数', meta: { align: 'right' }, cell: (info) => ( @@ -327,7 +383,7 @@ const unmatchedGroupedColumns: ColumnDef[] = [ }, { accessorKey: 'totalSellPrice', - header: 'Total Sell', + header: '卖出总额', meta: { align: 'right' }, cell: (info) => ( @@ -337,7 +393,7 @@ const unmatchedGroupedColumns: ColumnDef[] = [ }, { accessorKey: 'totalFee', - header: 'Fees', + header: '手续费', meta: { align: 'right' }, cell: (info) => ( @@ -469,6 +525,8 @@ function CompletedTradesContent({ ['count', '交易数', true], ['totalBuy', '买入总额', true], ['totalSell', '卖出总额', true], + ['marketTotal', '市场总价', true], + ['postTradePl', '交易后盈亏', true], ['grossPl', '毛利', true], ['fees', '手续费', true], ['netPl', '净利润', true], @@ -491,13 +549,14 @@ function CompletedTradesContent({ {groups.map((group) => { - const expanded = expandedNames.has(group.itemName); + const groupKey = `${group.itemName}|${group.exterior}`; + const expanded = expandedNames.has(groupKey); return ( - + toggle(group.itemName)} + onClick={() => toggle(groupKey)} > @@ -509,9 +568,27 @@ function CompletedTradesContent({ - - {group.itemName} - + + + {group.itemName} + {group.exterior ? ` (${group.exterior})` : ''} + + {group.csqaqGoodsId ? ( + + { + e.stopPropagation(); + BrowserOpenURL( + `https://www.csqaq.com/goods/${group.csqaqGoodsId}`, + ); + }} + > + + + + ) : null} + @@ -528,6 +605,30 @@ function CompletedTradesContent({ {formatCNY(group.totalSellPrice)} + + + {group.marketTotal != null ? formatCNY(group.marketTotal) : '--'} + + + + {group.postTradePl != null ? ( + + {formatCNY(group.postTradePl)} + + ) : ( + + -- + + )} + (); for (const s of sells) { const name = s.itemName ?? 'Unknown'; - const arr = map.get(name); + const exterior = s.exterior ?? ''; + const key = `${name}|${exterior}`; + const arr = map.get(key); if (arr) arr.push(s); - else map.set(name, [s]); + else map.set(key, [s]); } - return Array.from(map, ([itemName, sells]) => { + return Array.from(map, ([key, sells]) => { let totalSellPrice = 0; let totalFee = 0; for (const s of sells) { totalSellPrice += s.totalPrice; totalFee += s.fee; } - return { itemName, count: sells.length, sells, totalSellPrice, totalFee }; + const [itemName, exterior] = key.split('|', 2); + return { itemName, exterior, count: sells.length, sells, totalSellPrice, totalFee }; }).sort((a, b) => a.itemName.localeCompare(b.itemName)); }, [sells]); @@ -797,7 +901,7 @@ function UnmatchedSellsContent({ getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getCoreRowModel: getCoreRowModel(), - getRowId: (row) => row.itemName, + getRowId: (row) => `${row.itemName}|${row.exterior}`, }); const totalSells = sells.length; @@ -927,7 +1031,8 @@ function UnmatchedSellsContent({ {table.getRowModel().rows.map((groupRow) => { - const expanded = expandedNames.has(groupRow.original.itemName); + const groupKey = `${groupRow.original.itemName}|${groupRow.original.exterior}`; + const expanded = expandedNames.has(groupKey); return ( (t.palette.mode === 'dark' ? '#111114' : '#f8fafc'), cursor: 'pointer', }} - onClick={() => toggle(groupRow.original.itemName)} + onClick={() => toggle(groupKey)} > {groupRow.getVisibleCells().map((cell) => ( 0 { + avgSellPrice := g.TotalSellPrice / g.TotalQuantity + ptpl := (mp - avgSellPrice) * g.TotalQuantity + g.PostTradePl = &ptpl + } + } +} + func (svc *service) ListCompletedTrades(accountID uint) ([]CompletedTradeView, error) { sells, err := svc.orm.FindSellsWithMatchedBuy(accountID) if err != nil {