feat: migrate BrokerPositionTable, PositionTicker and TableSkeleton

This commit is contained in:
Sergey Krylov 2026-06-21 18:41:53 +03:00
parent 9a94f4e3a9
commit da656330b3
3 changed files with 149 additions and 142 deletions

View File

@ -1,4 +1,4 @@
import { SkeletonBlock } from '@/shared/ui/SkeletonBlock';
import { Skeleton } from '@moex-vibe/design-system';
const tdStyle = {
borderBottom: '1px solid #eeeeee',
@ -15,7 +15,7 @@ export function TableSkeleton({ rows = 5, columns }: { rows?: number; columns: C
<tr key={i}>
{columns.map((col, j) => (
<td key={j} style={tdStyle}>
<SkeletonBlock height={12} width={col.width} />
<Skeleton height={12} width={col.width} shape="text" />
</td>
))}
</tr>

View File

@ -1,44 +1,36 @@
import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api/responses';
import { TableSkeleton } from '@/shared/ui/TableSkeleton';
import { Box } from '@mui/material';
import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system';
import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters';
import { PositionTicker } from './PositionTicker';
const tableStyle = {
const tableSx = {
width: '100%',
borderCollapse: 'collapse',
fontSize: 14,
} satisfies React.CSSProperties;
} as const;
const thStyle = {
borderBottom: '1px solid #e0e0e0',
color: 'var(--color-text-secondary)',
const thSx = {
borderBottom: '1px solid',
borderColor: 'divider',
color: 'text.secondary',
fontWeight: 600,
padding: '10px 8px',
} satisfies React.CSSProperties;
p: 1,
textAlign: 'left',
} as const;
const tdStyle = {
borderBottom: '1px solid #eeeeee',
padding: '10px 8px',
const tdSx = {
borderBottom: '1px solid',
borderColor: 'divider',
p: 1,
verticalAlign: 'top',
} satisfies React.CSSProperties;
textAlign: 'right',
} as const;
const pagButtonStyle = {
padding: '6px 14px',
borderRadius: 6,
border: '1px solid #e0e0e0',
background: 'var(--color-surface)',
color: 'var(--color-text)',
fontSize: 14,
fontWeight: 600,
cursor: 'pointer',
lineHeight: 1.4,
} satisfies React.CSSProperties;
const pagButtonDisabledStyle = {
...pagButtonStyle,
opacity: 0.35,
cursor: 'not-allowed',
} satisfies React.CSSProperties;
const tdSxLeft = {
...tdSx,
textAlign: 'left',
} as const;
function formatQuantity(value: number | null | undefined) {
return value == null ? '-' : value.toLocaleString('ru-RU');
@ -69,127 +61,127 @@ export function BrokerPositionTable({
return (
<section aria-labelledby={`broker-${title.toLowerCase()}-heading`}>
<div
style={{
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 12,
gap: 1.5,
justifyContent: 'space-between',
marginBottom: 10,
mb: 1.5,
}}
>
<h2 id={`broker-${title.toLowerCase()}-heading`} style={{ fontSize: 20, margin: 0 }}>
<Heading level={2} id={`broker-${title.toLowerCase()}-heading`}>
{title}
</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button
type="button"
</Heading>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="secondary"
size="small"
aria-label="Предыдущая страница"
onClick={onPrevious}
disabled={!canGoBack || isFetching}
style={canGoBack && !isFetching ? pagButtonStyle : pagButtonDisabledStyle}
>
{isFetching ? (
<span
className="loading-spinner"
style={{ width: 14, height: 14, display: 'block' }}
/>
) : (
'←'
)}
</button>
<span
style={{
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '←'}
</Button>
<Box
sx={{
minWidth: 20,
textAlign: 'center',
color: 'var(--color-text-secondary)',
color: 'text.secondary',
fontSize: 14,
fontWeight: 600,
}}
>
{pageNumber}
</span>
<button
type="button"
</Box>
<Button
variant="secondary"
size="small"
aria-label="Следующая страница"
onClick={onNext}
disabled={!canGoForward || isFetching}
style={canGoForward && !isFetching ? pagButtonStyle : pagButtonDisabledStyle}
>
{isFetching ? (
<span
className="loading-spinner"
style={{ width: 14, height: 14, display: 'block' }}
/>
) : (
'→'
)}
</button>
</div>
</div>
{isFetching ? <Skeleton shape="circular" width={14} height={14} /> : '→'}
</Button>
</Box>
</Box>
{isLoading ? (
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
<table style={tableStyle}>
<thead>
<tr>
<th align="left" style={thStyle}>
<Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<Box component="table" sx={tableSx}>
<Box component="thead">
<Box component="tr">
<Box component="th" sx={thSx} align="left">
Тикер
</th>
<th align="left" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="left">
Название
</th>
<th align="right" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="right">
Количество
</th>
<th align="right" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="right">
Цена
</th>
<th align="right" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="right">
Стоимость
</th>
</tr>
</thead>
<TableSkeleton
rows={4}
columns={[
{ width: '30%' },
{ width: '50%' },
{ width: '20%' },
{ width: '25%' },
{ width: '25%' },
]}
/>
</table>
</div>
</Box>
</Box>
</Box>
<Box component="tbody">
{[1, 2, 3, 4].map((row) => (
<Box component="tr" key={row}>
<Box component="td" sx={tdSxLeft}>
<Skeleton height={12} width="30%" shape="text" />
</Box>
<Box component="td" sx={tdSxLeft}>
<Skeleton height={12} width="50%" shape="text" />
</Box>
<Box component="td" sx={tdSx}>
<Skeleton height={12} width="20%" shape="text" />
</Box>
<Box component="td" sx={tdSx}>
<Skeleton height={12} width="25%" shape="text" />
</Box>
<Box component="td" sx={tdSx}>
<Skeleton height={12} width="25%" shape="text" />
</Box>
</Box>
))}
</Box>
</Box>
</Box>
) : positions.length === 0 && !isFetching ? (
<p style={{ color: 'var(--color-text-secondary)' }}>{emptyMessage}</p>
<Text component="p" tone="secondary">
{emptyMessage}
</Text>
) : (
<div className="table-container">
<div style={{ overflowX: 'auto', background: 'var(--color-surface)' }}>
<table aria-label={`Брокерские позиции: ${title}`} style={tableStyle}>
<thead>
<tr>
<th align="left" style={thStyle}>
<Box sx={{ position: 'relative' }}>
<Box sx={{ overflowX: 'auto', bgcolor: 'surface.default' }}>
<Box component="table" sx={tableSx} aria-label={`Брокерские позиции: ${title}`}>
<Box component="thead">
<Box component="tr">
<Box component="th" sx={thSx} align="left">
Тикер
</th>
<th align="left" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="left">
Название
</th>
<th align="right" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="right">
Количество
</th>
<th align="right" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="right">
Цена
</th>
<th align="right" style={thStyle}>
</Box>
<Box component="th" sx={thSx} align="right">
Стоимость
</th>
</tr>
</thead>
<tbody>
</Box>
</Box>
</Box>
<Box component="tbody">
{positions.map((position) => (
<tr
<Box
component="tr"
key={
position.positionUid ||
position.instrumentUid ||
@ -197,37 +189,45 @@ export function BrokerPositionTable({
position.figi
}
>
<td style={tdStyle}>
<Box component="td" sx={tdSxLeft}>
<PositionTicker position={position} />
</td>
<td style={tdStyle}>
<span style={{ color: 'var(--color-text-secondary)' }}>
{position.name || '-'}
</span>
</td>
<td align="right" style={tdStyle}>
</Box>
<Box component="td" sx={tdSxLeft}>
<Text tone="secondary">{position.name || '-'}</Text>
</Box>
<Box component="td" sx={tdSx}>
{formatQuantity(position.quantity)}
</td>
<td align="right" style={tdStyle}>
</Box>
<Box component="td" sx={tdSx}>
{formatMoney(position.currentPrice)}
</td>
<td align="right" style={tdStyle}>
</Box>
<Box component="td" sx={tdSx}>
{formatMoney(position.currentValue)}
</td>
</tr>
</Box>
</Box>
))}
</tbody>
</table>
</div>
</Box>
</Box>
</Box>
{isFetching && (
<div className="table-loading-overlay">
<div className="loading-spinner" />
<span style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>
<Box
sx={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
bgcolor: 'rgba(255,255,255,0.7)',
}}
>
<Box className="loading-spinner" />
<Text variant="caption" tone="secondary">
Загрузка страницы {pageNumber}
</span>
</div>
</Text>
</Box>
)}
</div>
</Box>
)}
</section>
);

View File

@ -1,4 +1,5 @@
import { Link } from 'react-router-dom';
import { Box } from '@mui/material';
import type { BrokerPosition } from '@/shared/api/responses';
import { getBrokerInstrumentPath } from '@/entities/broker-position';
@ -11,12 +12,18 @@ export function PositionTicker({ position }: { position: BrokerPosition }) {
});
if (!path || label === '-') {
return <strong>{label}</strong>;
return (
<Box component="span" sx={{ fontWeight: 700 }}>
{label}
</Box>
);
}
return (
<Link to={path} style={{ fontWeight: 700 }}>
{label}
<Link to={path}>
<Box component="span" sx={{ fontWeight: 700 }}>
{label}
</Box>
</Link>
);
}