Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 145 additions & 34 deletions frontend/src/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,31 @@ import InventoryIcon from '@mui/icons-material/Inventory';
import ReceiptIcon from '@mui/icons-material/Receipt';
import RedeemIcon from '@mui/icons-material/Redeem';
import PaymentsIcon from '@mui/icons-material/Payments';
import ShowChartIcon from '@mui/icons-material/ShowChart';
import DashboardIcon from '@mui/icons-material/Dashboard';
import ErrorBanner from '../components/ErrorBanner';
import EmptyState from '../components/EmptyState';
import { useDashboard } from '../hooks/useDashboard';
import { useMonthlyBreakdown } from '../hooks/useMonthlyBreakdown';
import { useInventory } from '../hooks/useInventory';
import { useUIStore } from '../store/uiStore';
import { formatCNY, plColor, plHexColor } from '../lib/format';
import { priceSourceLabel } from '../lib/constants';
import ShowChartIcon from '@mui/icons-material/ShowChart';
import ReactEChartsCore from 'echarts-for-react/lib/core';
import * as echarts from 'echarts/core';
import { BarChart, LineChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import { BarChart, LineChart, PieChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([BarChart, LineChart, GridComponent, TooltipComponent, CanvasRenderer]);
echarts.use([
BarChart,
LineChart,
PieChart,
GridComponent,
TooltipComponent,
LegendComponent,
CanvasRenderer,
]);

const POS_COLOR = '#22c55e';
const NEG_COLOR = '#ef4444';
Expand Down Expand Up @@ -93,6 +102,86 @@ export default function DashboardPage() {
const currentYear = new Date().getFullYear();
const { data: monthly = [] } = useMonthlyBreakdown(selectedAccountId, currentYear);

const { data: chartData } = useInventory(null, {
page: 1,
pageSize: 200,
weaponType: '',
sortBy: 'itemName',
sortDir: 'asc',
});

const marketChartOption = useMemo(() => {
const allGroups = chartData?.groups ?? [];
if (allGroups.length === 0) return null;

const typeMap = new Map<string, number>();
let grandTotal = 0;
for (const g of allGroups) {
if (g.marketPrice == null) continue;
const wt = g.weaponType || '其他';
const mv = g.marketPrice * g.totalQuantity;
typeMap.set(wt, (typeMap.get(wt) ?? 0) + mv);
grandTotal += mv;
}
if (grandTotal === 0) return null;

const types = Array.from(typeMap.keys());
const colors = [
'#f97316',
'#14b8a6',
'#3b82f6',
'#a855f7',
'#eab308',
'#ec4899',
'#06b6d4',
'#84cc16',
'#f43f5e',
'#8b5cf6',
'#22d3ee',
'#f59e0b',
];

return {
totalCents: grandTotal,
option: {
color: colors,
backgroundColor: 'transparent',
tooltip: {
trigger: 'item' as const,
backgroundColor: '#18181b',
borderColor: '#27272a',
textStyle: { color: '#d4d4d8', fontSize: 13, fontFamily: "'Geist Variable', sans-serif" },
formatter: (p: { name: string; value: number; percent: number }) =>
`${p.name}<br/><b>¥${p.value.toLocaleString()}</b> ${p.percent}%`,
},
series: [
{
type: 'pie',
radius: ['55%', '78%'],
center: ['50%', '50%'],
selectedMode: 'single',
selectedOffset: 4,
itemStyle: { borderColor: '#09090b', borderWidth: 2, borderRadius: 2 },
data: types.map((t) => ({ name: t, value: typeMap.get(t)! / 100 })),
label: {
show: true,
position: 'outside' as const,
formatter: '{b} {d}%',
fontFamily: "'Geist Variable', sans-serif",
fontSize: 12,
color: '#a1a1aa',
},
labelLine: { lineStyle: { color: '#3f3f46' } },
emphasis: {
label: { show: true, fontSize: 14, fontWeight: 600 },
scaleSize: 6,
},
},
],
},
};
}, [chartData?.groups]);

const netWorth = data
? data.totalAvailableBalance +
data.totalFrozenBalance +
Expand Down Expand Up @@ -308,39 +397,61 @@ export default function DashboardPage() {
</Grid>
</Grid>

{/* Cost + market value row */}
{/* Cost / Market Value / P&L + Chart row */}
<Grid container spacing={2} mt={2}>
<Grid item xs={4}>
<StatCard
label="持仓成本"
value={formatCNY(data.inventoryCost)}
icon={<PaymentsIcon fontSize="small" color="action" />}
/>
</Grid>
<Grid item xs={4}>
<StatCard
label={`持仓市值(${priceSourceLabel[data.priceSource] ?? data.priceSource})`}
value={formatCNY(data.inventoryMarketValue)}
icon={<ShowChartIcon fontSize="small" color="action" />}
/>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<StatCard
label="持仓成本"
value={formatCNY(data.inventoryCost)}
icon={<PaymentsIcon fontSize="small" color="action" />}
/>
<StatCard
label={`持仓市值(${priceSourceLabel[data.priceSource] ?? data.priceSource})`}
value={formatCNY(data.inventoryMarketValue)}
icon={<ShowChartIcon fontSize="small" color="action" />}
/>
<StatCard
label="未实现盈亏"
value={formatCNY(data.inventoryMarketValue - data.inventoryCost)}
color={plColor(data.inventoryMarketValue - data.inventoryCost)}
accentLeft={
data.inventoryMarketValue - data.inventoryCost >= 0 ? '#22c55e' : '#ef4444'
}
icon={
<TrendingUpIcon
fontSize="small"
sx={{
color: plHexColor(data.inventoryMarketValue - data.inventoryCost),
}}
/>
}
/>
</Box>
</Grid>
<Grid item xs={4}>
<StatCard
label="未实现盈亏"
value={formatCNY(data.inventoryMarketValue - data.inventoryCost)}
color={plColor(data.inventoryMarketValue - data.inventoryCost)}
accentLeft={
data.inventoryMarketValue - data.inventoryCost >= 0 ? '#22c55e' : '#ef4444'
}
icon={
<TrendingUpIcon
fontSize="small"
sx={{
color: plHexColor(data.inventoryMarketValue - data.inventoryCost),
}}
/>
}
/>
<Grid item xs={8}>
{marketChartOption && (
<Card sx={{ borderRadius: '10px', p: 2, height: '100%' }}>
<Typography
variant="overline"
color="text.disabled"
sx={{ letterSpacing: '0.08em' }}
>
持仓市值分布
</Typography>
<Typography variant="h6" fontWeight={600} mt={0.5}>
{formatCNY(marketChartOption.totalCents)}
</Typography>
<Box sx={{ height: 220, mt: 1 }}>
<ReactEChartsCore
echarts={echarts}
option={marketChartOption.option}
style={{ height: '100%', width: '100%' }}
notMerge
/>
</Box>
</Card>
)}
</Grid>
</Grid>
</>
Expand Down
27 changes: 26 additions & 1 deletion frontend/src/pages/InventoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import Tooltip from '@mui/material/Tooltip';
import type { model } from '../lib/wails';

declare module '@tanstack/react-table' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta<TData, TValue> {
Expand Down Expand Up @@ -133,6 +132,25 @@ const groupedColumns: ColumnDef<GroupRowData>[] = [
</Typography>
),
},
{
id: 'marketTotal',
header: '市场总价',
meta: { align: 'right' },
cell: ({ row }) => {
const { marketPrice, totalQuantity } = row.original;
if (marketPrice == null)
return (
<Typography variant="body2" color="text.secondary" className="mono-num">
--
</Typography>
);
return (
<Typography variant="body2" color="text.secondary" className="mono-num">
{formatCNY(marketPrice * totalQuantity)}
</Typography>
);
},
},
{
id: 'priceUpdatedAt',
header: '行情时间',
Expand Down Expand Up @@ -444,6 +462,13 @@ export default function InventoryPage() {
{group.marketPrice != null ? formatCNY(group.marketPrice) : '--'}
</Typography>
</TableCell>
<TableCell sx={{ py: 1 }} align="right">
<Typography variant="body2" color="text.secondary" className="mono-num">
{group.marketPrice != null
? formatCNY(group.marketPrice * group.totalQuantity)
: '--'}
</Typography>
</TableCell>
<TableCell sx={{ py: 1 }} align="right">
<Typography variant="caption" color="text.disabled">
{group.marketPriceUpdatedAt
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/PnLPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export default function PnLPage() {
},
series: [
{
name: 'Net P/L',
name: '月度净盈亏',
type: 'bar',
data: barValues,
barWidth: '55%',
Expand All @@ -139,7 +139,7 @@ export default function PnLPage() {
},
},
{
name: 'Cumulative P/L',
name: '累计盈亏',
type: 'line',
data: cumulative,
smooth: true,
Expand Down
7 changes: 7 additions & 0 deletions pkg/service/inventory/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var appSortFields = map[string]bool{
"totalBuyPrice": true,
"avgBuyPrice": true,
"marketPrice": true,
"marketTotal": true,
"unrealizedPl": true,
"plPercent": true,
}
Expand Down Expand Up @@ -276,6 +277,8 @@ func (s *service) sortGroups(groups []InventoryGroup, sortBy, sortDir string) {
less = a.TotalQuantity < b.TotalQuantity
case "marketPrice":
less = ptrValue(a.MarketPrice) < ptrValue(b.MarketPrice)
case "marketTotal":
less = marketTotal(a) < marketTotal(b)
case "unrealizedPl":
less = ptrValue(a.UnrealizedPl) < ptrValue(b.UnrealizedPl)
case "plPercent":
Expand Down Expand Up @@ -344,3 +347,7 @@ func plPercent(g InventoryGroup) float64 {
}
return float64(*g.MarketPrice-g.AvgBuyPrice) / float64(g.AvgBuyPrice)
}

func marketTotal(g InventoryGroup) int64 {
return ptrValue(g.MarketPrice) * g.TotalQuantity
}
Loading