From be04efb7126d97059db981370753eddfe2093515 Mon Sep 17 00:00:00 2001 From: CZ Date: Tue, 20 Feb 2024 11:09:40 +0800 Subject: [PATCH 1/2] refactor(gui): Responsible DataView (TableView + DataView) --- ui/web/src/modules/Interactive/DataView.tsx | 11 + ui/web/src/modules/Interactive/ListView.tsx | 38 ++++ ui/web/src/modules/Interactive/TableView.tsx | 53 +++++ ui/web/src/modules/Interactive/index.ts | 3 + ui/web/src/modules/Terminals/TerminalList.tsx | 207 ++++++------------ 5 files changed, 178 insertions(+), 134 deletions(-) create mode 100644 ui/web/src/modules/Interactive/DataView.tsx create mode 100644 ui/web/src/modules/Interactive/ListView.tsx create mode 100644 ui/web/src/modules/Interactive/TableView.tsx diff --git a/ui/web/src/modules/Interactive/DataView.tsx b/ui/web/src/modules/Interactive/DataView.tsx new file mode 100644 index 000000000..bcdfb74ec --- /dev/null +++ b/ui/web/src/modules/Interactive/DataView.tsx @@ -0,0 +1,11 @@ +import { Table } from '@tanstack/react-table'; +import { ListView } from './ListView'; +import { TableView } from './TableView'; + +export function DataView(props: { table: Table }) { + if (window.outerWidth >= 1080) { + return ; + } + + return ; +} diff --git a/ui/web/src/modules/Interactive/ListView.tsx b/ui/web/src/modules/Interactive/ListView.tsx new file mode 100644 index 000000000..2ed5dcb79 --- /dev/null +++ b/ui/web/src/modules/Interactive/ListView.tsx @@ -0,0 +1,38 @@ +import { Descriptions, List, Space } from '@douyinfe/semi-ui'; +import { Table, flexRender } from '@tanstack/react-table'; + +export function ListView(props: { table: Table }) { + const { table } = props; + return ( + +
{table.getRowModel().rows.length} Items
+ + {table.getRowModel().rows.map((row) => ( + + + {table.getHeaderGroups().map((headerGroup) => + headerGroup.headers.map((header, idx) => { + return ( + + {row + .getVisibleCells() + .filter((cell) => cell.column.id === header.column.id) + .map((cell) => flexRender(cell.column.columnDef.cell, cell.getContext()))} + + ); + }), + )} + + + ))} + +
+ ); +} diff --git a/ui/web/src/modules/Interactive/TableView.tsx b/ui/web/src/modules/Interactive/TableView.tsx new file mode 100644 index 000000000..5a72d6b5a --- /dev/null +++ b/ui/web/src/modules/Interactive/TableView.tsx @@ -0,0 +1,53 @@ +import { Space } from '@douyinfe/semi-ui'; +import { Table, flexRender } from '@tanstack/react-table'; + +export function TableView(props: { table: Table }) { + const { table } = props; + return ( + +
{table.getRowModel().rows.length} Items
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((header) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.footer, header.getContext())} +
+
+ ); +} diff --git a/ui/web/src/modules/Interactive/index.ts b/ui/web/src/modules/Interactive/index.ts index 8b166a86e..52fd8a31a 100644 --- a/ui/web/src/modules/Interactive/index.ts +++ b/ui/web/src/modules/Interactive/index.ts @@ -1 +1,4 @@ export * from './Button'; +export * from './DataView'; +export * from './ListView'; +export * from './TableView'; diff --git a/ui/web/src/modules/Terminals/TerminalList.tsx b/ui/web/src/modules/Terminals/TerminalList.tsx index 739bdeb7f..ba7026f66 100644 --- a/ui/web/src/modules/Terminals/TerminalList.tsx +++ b/ui/web/src/modules/Terminals/TerminalList.tsx @@ -1,13 +1,14 @@ -import { List, Space, Typography } from '@douyinfe/semi-ui'; -import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { Typography } from '@douyinfe/semi-ui'; +import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { formatTime } from '@yuants/data-model'; import { ITerminalInfo } from '@yuants/protocol'; import { formatDuration, intervalToDuration } from 'date-fns'; import { useObservableState } from 'observable-hooks'; +import { useMemo } from 'react'; import { of, shareReplay, switchMap } from 'rxjs'; -import { Button } from '../Interactive'; +import { Button, DataView } from '../Interactive'; import { registerPage } from '../Pages'; -import { TerminalListItem, terminate } from './TerminalListItem'; +import { terminate } from './TerminalListItem'; import { terminal$ } from './create-connection'; export const terminalList$ = terminal$.pipe( @@ -15,139 +16,77 @@ export const terminalList$ = terminal$.pipe( shareReplay(1), ); -const columnHelper = createColumnHelper(); - -const columns = [ - columnHelper.accessor('terminal_id', { - header: () => '终端 ID', - cell: (info) => {info.getValue()}, - }), - columnHelper.accessor('name', { - header: () => '终端名', - }), - columnHelper.accessor('updated_at', { - header: () => '最近更新时间', - cell: (info) => formatTime(info.getValue() || NaN), - }), - columnHelper.accessor('start_timestamp_in_ms', { - header: () => '启动时间', - cell: (info) => formatTime(info.getValue() || NaN), - }), - columnHelper.accessor( - (x) => formatDuration(intervalToDuration({ start: x.start_timestamp_in_ms!, end: Date.now() })), - { - id: 'start_time', - header: () => '启动时长', - }, - ), - columnHelper.accessor((x) => Object.values(x.serviceInfo || {}).length, { - id: 'serviceLength', - header: () => '提供服务数', - }), - columnHelper.accessor((x) => x.channelIdSchemas?.length, { - id: 'channelIdSchemaLength', - header: () => '提供频道数', - }), - columnHelper.accessor((x) => Object.keys(x.subscriptions || {}).length, { - id: 'subscribeTerminalLength', - header: () => `订阅终端数`, - }), - columnHelper.accessor( - (x) => Object.values(x.subscriptions || {}).reduce((acc, cur) => acc + cur.length, 0), - { - id: 'subscribeChannelLength', - header: () => '订阅频道数', - }, - ), - columnHelper.accessor((x) => 0, { - id: 'actions', - header: () => '操作', - cell: (x) => { - const term = x.row.original; - - return ( - - ); - }, - }), -]; +registerPage('TerminalList', () => { + const terminals = useObservableState(terminalList$, []); -const TerminalListDesktop = (props: { terminals: ITerminalInfo[] }) => { - const table = useReactTable({ columns, data: props.terminals, getCoreRowModel: getCoreRowModel() }); + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); - return ( - -
{table.getRowModel().rows.length} Items
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - - - {table.getFooterGroups().map((footerGroup) => ( - - {footerGroup.headers.map((header) => ( - - ))} - - ))} - -
- {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
- {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.footer, header.getContext())} -
-
- ); -}; + const columns = [ + columnHelper.accessor('terminal_id', { + header: () => '终端 ID', + cell: (info) => {info.getValue()}, + }), + columnHelper.accessor('name', { + header: () => '终端名', + }), + columnHelper.accessor('updated_at', { + header: () => '最近更新时间', + cell: (info) => formatTime(info.getValue() || NaN), + }), + columnHelper.accessor('start_timestamp_in_ms', { + header: () => '启动时间', + cell: (info) => formatTime(info.getValue() || NaN), + }), + columnHelper.accessor( + (x) => formatDuration(intervalToDuration({ start: x.start_timestamp_in_ms!, end: Date.now() })), + { + id: 'start_time', + header: () => '启动时长', + }, + ), + columnHelper.accessor((x) => Object.values(x.serviceInfo || {}).length, { + id: 'serviceLength', + header: () => '提供服务数', + }), + columnHelper.accessor((x) => x.channelIdSchemas?.length, { + id: 'channelIdSchemaLength', + header: () => '提供频道数', + }), + columnHelper.accessor((x) => Object.keys(x.subscriptions || {}).length, { + id: 'subscribeTerminalLength', + header: () => `订阅终端数`, + }), + columnHelper.accessor( + (x) => Object.values(x.subscriptions || {}).reduce((acc, cur) => acc + cur.length, 0), + { + id: 'subscribeChannelLength', + header: () => '订阅频道数', + }, + ), + columnHelper.accessor((x) => 0, { + id: 'actions', + header: () => '操作', + cell: (x) => { + const term = x.row.original; -registerPage('TerminalList', () => { - const terminals = useObservableState(terminalList$, []); + return ( + + ); + }, + }), + ]; + return columns; + }, []); - if (window.outerWidth >= 1080) { - return ; - } + const table = useReactTable({ columns, data: terminals, getCoreRowModel: getCoreRowModel() }); - return ( - - - 终端数量: {terminals.length} - - - {terminals.map((term) => ( - - ))} - - - ); + return ; }); From 45f896052650b927913741c3f35acb86f9b3b63d Mon Sep 17 00:00:00 2001 From: CZ Date: Tue, 20 Feb 2024 12:06:01 +0800 Subject: [PATCH 2/2] refactor(gui): apply DataView for AccountList and ProductList --- .../src/modules/AccountInfo/AccountList.tsx | 127 ++++++++++++++-- ui/web/src/modules/Products/ProductList.tsx | 136 +++++++++--------- 2 files changed, 187 insertions(+), 76 deletions(-) diff --git a/ui/web/src/modules/AccountInfo/AccountList.tsx b/ui/web/src/modules/AccountInfo/AccountList.tsx index fbd8dacb4..263fcc543 100644 --- a/ui/web/src/modules/AccountInfo/AccountList.tsx +++ b/ui/web/src/modules/AccountInfo/AccountList.tsx @@ -1,17 +1,124 @@ -import { List } from '@douyinfe/semi-ui'; +import { Space, Spin, Typography } from '@douyinfe/semi-ui'; +import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { formatTime } from '@yuants/data-model'; import { useObservableState } from 'observable-hooks'; +import { useMemo } from 'react'; +import { executeCommand } from '../CommandCenter'; +import { Button, DataView } from '../Interactive'; import { registerPage } from '../Pages'; -import { AccountInfoItem } from './AccountInfoItem'; -import { accountIds$ } from './model'; +import { accountIds$, useAccountInfo } from './model'; registerPage('AccountList', () => { const accountIds = useObservableState(accountIds$, []); - return ( - - {accountIds.map((accountId) => ( - - ))} - - ); + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); + return [ + columnHelper.accessor((x) => x, { + id: 'account_id', + header: () => '账户 ID', + cell: (x) => {x.renderValue()}, + }), + columnHelper.accessor((x) => x, { + id: 'currency', + header: () => '货币', + cell: (x) => { + const account_id = x.row.original; + const accountInfo$ = useMemo(() => useAccountInfo(account_id), [account_id]); + const accountInfo = useObservableState(accountInfo$); + + return accountInfo?.money.currency; + }, + }), + columnHelper.accessor((x) => x, { + id: 'equity', + header: () => '净值', + cell: (x) => { + const account_id = x.row.original; + const accountInfo$ = useMemo(() => useAccountInfo(account_id), [account_id]); + const accountInfo = useObservableState(accountInfo$); + + return accountInfo?.money.equity; + }, + }), + columnHelper.accessor((x) => x, { + id: 'balance', + header: () => '余额', + cell: (x) => { + const account_id = x.row.original; + const accountInfo$ = useMemo(() => useAccountInfo(account_id), [account_id]); + const accountInfo = useObservableState(accountInfo$); + + return accountInfo?.money.balance; + }, + }), + columnHelper.accessor((x) => x, { + id: 'profit', + header: () => '盈亏', + cell: (x) => { + const account_id = x.row.original; + const accountInfo$ = useMemo(() => useAccountInfo(account_id), [account_id]); + const accountInfo = useObservableState(accountInfo$); + + return accountInfo?.money.profit; + }, + }), + columnHelper.accessor((x) => x, { + id: 'used-margin-ratio', + header: () => '保证金使用率', + cell: (x) => { + const account_id = x.row.original; + const accountInfo$ = useMemo(() => useAccountInfo(account_id), [account_id]); + const accountInfo = useObservableState(accountInfo$); + + if (!accountInfo) return null; + + const value = (accountInfo.money.used / accountInfo.money.equity) * 100; + if (Number.isNaN(value)) return 'N/A'; + + return value.toFixed(2) + '%'; + }, + }), + columnHelper.accessor((x) => x, { + id: 'time', + header: () => '更新时间', + cell: (x) => { + const account_id = x.row.original; + const accountInfo$ = useMemo(() => useAccountInfo(account_id), [account_id]); + const accountInfo = useObservableState(accountInfo$); + + if (!accountInfo) return ; + + const updated_at = accountInfo.updated_at || (accountInfo.timestamp_in_us ?? NaN) / 1000; + const timeLag = Date.now() - updated_at; + + return ( + + {formatTime(updated_at)} + {timeLag > 60_000 && ( + + 信息更新于 {formatTime(accountInfo.timestamp_in_us / 1000)},已经{' '} + {(timeLag / 1000).toFixed(0)} 秒未更新,可能已经失去响应 + + )} + + ); + }, + }), + columnHelper.accessor((x) => x, { + id: 'actions', + header: () => '操作', + cell: (x) => { + const account_id = x.row.original; + return ; + }, + }), + ]; + }, []); + + const data = accountIds; + + const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() }); + + return ; }); diff --git a/ui/web/src/modules/Products/ProductList.tsx b/ui/web/src/modules/Products/ProductList.tsx index 29f5f36ee..8f9e123cb 100644 --- a/ui/web/src/modules/Products/ProductList.tsx +++ b/ui/web/src/modules/Products/ProductList.tsx @@ -1,13 +1,15 @@ import { IconCopyAdd, IconDelete, IconEdit, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; -import { Button, Modal, Space, Table, Toast } from '@douyinfe/semi-ui'; +import { Button, Modal, Space, Toast } from '@douyinfe/semi-ui'; import { StockMarket } from '@icon-park/react'; +import { createColumnHelper, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { IProduct } from '@yuants/protocol'; import { useObservable, useObservableState } from 'observable-hooks'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EMPTY, combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; import { executeCommand } from '../CommandCenter'; import Form, { showForm } from '../Form'; +import { DataView } from '../Interactive'; import { registerPage } from '../Pages'; import { terminal$ } from '../Terminals'; @@ -53,6 +55,71 @@ registerPage('ProductList', () => { const products = useObservableState(products$); + const columns = useMemo(() => { + const columnHelper = createColumnHelper(); + + return [ + columnHelper.accessor('datasource_id', { + header: () => '数据源ID', + }), + columnHelper.accessor('product_id', { + header: () => '品种ID', + }), + columnHelper.accessor('name', { header: () => '品种名称' }), + columnHelper.accessor('quote_currency', { header: () => '计价货币' }), + columnHelper.accessor('base_currency', { header: () => '基准货币' }), + columnHelper.accessor((x) => `${x.value_scale || ''} ${x.value_scale_unit || ''}`, { + id: 'value_scale', + header: () => '价值尺度', + }), + columnHelper.accessor('volume_step', { header: () => '成交量粒度' }), + columnHelper.accessor('price_step', { header: () => '报价粒度' }), + columnHelper.accessor('margin_rate', { header: () => '保证金率' }), + columnHelper.accessor('spread', { header: () => '点差' }), + columnHelper.accessor((x) => 0, { + id: 'actions', + header: () => '操作', + cell: (x) => { + const item = x.row.original; + return ( + + + + + + ); + }, + }), + ]; + }, []); + + const data = useMemo(() => products?.map((x) => x.origin) ?? [], [products]); + + const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + }); + const [isModalVisible, setModalVisible] = useState(false); const [formData, setFormData] = useState({} as IProduct); @@ -86,71 +153,8 @@ registerPage('ProductList', () => { 刷新 - record.origin.datasource_id }, - { title: '品种 ID', render: (_, record) => record.origin.product_id }, - { title: '品种名称', render: (_, record) => record.origin.name }, - { title: '计价货币', render: (_, record) => record.origin.quote_currency }, - { title: '基准货币', render: (_, record) => record.origin.base_currency }, - { - title: '价值尺度', - render: (_, record) => `${record.origin.value_scale} ${record.origin.value_scale_unit || ''}`, - }, - { - title: '成交量粒度', - render: (_, record) => record.origin.volume_step, - }, - { - title: '报价粒度', - render: (_, record) => record.origin.price_step, - }, - { - title: '保证金率', - render: (_, record) => record.origin.margin_rate, - }, - { - title: '点差', - render: (_, record) => record.origin.spread, - }, - { - title: '允许做空', - render: (_, record) => (record.origin.allow_short ? '是' : '否'), - }, - { - title: '操作', - render: (_, record) => ( - - - - - - ), - }, - ]} - >
{