feat: enhance table layout and interaction across various components
Deploy production / deploy (push) Successful in 53s
Deploy production / deploy (push) Successful in 53s
- Introduced a new `WorkspaceTable` component for consistent table styling and behavior. - Updated `BacktestPage`, `DatasetPage`, and `DataPreparationPage` to utilize the new `WorkspaceTable`. - Implemented compact row heights and ellipsis for long text in table cells. - Added validation for online field queries to ensure at least one filter is provided. - Enhanced error handling and user feedback for invalid input scenarios. - Improved pagination controls with dynamic page size options. - Documented table display specifications in `ui-tables.md`. - Added tests to verify table layout, interaction, and error handling.
This commit is contained in:
@@ -35,6 +35,29 @@ import type {
|
||||
import "./style.css";
|
||||
import { SourceDetails, sourceLabel } from "../research/SourceDetails";
|
||||
|
||||
function BacktestStatusTag({ status }: { status: string }) {
|
||||
const color = ["completed", "complete", "saved"].includes(status)
|
||||
? "green"
|
||||
: ["failed", "collection_failed"].includes(status)
|
||||
? "red"
|
||||
: ["completed_with_errors", "needs_review", "unknown"].includes(status)
|
||||
? "orange"
|
||||
: [
|
||||
"running",
|
||||
"submitting",
|
||||
"submitted",
|
||||
"collecting",
|
||||
"stopping",
|
||||
].includes(status)
|
||||
? "blue"
|
||||
: "grey";
|
||||
return (
|
||||
<Tag color={color} size="small">
|
||||
<span title={labels[status] || status}>{labels[status] || status}</span>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export function BacktestPage({
|
||||
version = 0,
|
||||
active,
|
||||
@@ -395,7 +418,7 @@ export function BacktestPage({
|
||||
{
|
||||
title: "状态",
|
||||
width: 160,
|
||||
render: (_, r) => labels[r!.status] || r!.status,
|
||||
render: (_, r) => <BacktestStatusTag status={r!.status} />,
|
||||
},
|
||||
{
|
||||
title: "已保存 / 总数",
|
||||
@@ -895,17 +918,23 @@ export function BacktestPage({
|
||||
{
|
||||
title: "平台",
|
||||
width: 120,
|
||||
render: (_, i) => labels[i!.platform_status],
|
||||
render: (_, i) => (
|
||||
<BacktestStatusTag status={i!.platform_status} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "收集",
|
||||
width: 100,
|
||||
render: (_, i) => labels[i!.collection_status],
|
||||
render: (_, i) => (
|
||||
<BacktestStatusTag status={i!.collection_status} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "保存",
|
||||
width: 100,
|
||||
render: (_, i) => labels[i!.persistence_status],
|
||||
render: (_, i) => (
|
||||
<BacktestStatusTag status={i!.persistence_status} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Sharpe",
|
||||
|
||||
@@ -99,3 +99,8 @@
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.backtest-page .semi-table-row-cell .semi-tag,
|
||||
.backtest-sheet .semi-table-row-cell .semi-tag {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Table } from "@douyinfe/semi-ui-19";
|
||||
import type { TableProps } from "@douyinfe/semi-ui-19/lib/es/table/interface";
|
||||
import "./workspace-table.css";
|
||||
|
||||
/** Shared compact table boundary. Business pages retain selection, sorting and paging state. */
|
||||
export function WorkspaceTable<T extends Record<string, unknown>>({
|
||||
columns = [],
|
||||
className = "",
|
||||
fill = false,
|
||||
...props
|
||||
}: TableProps<T> & { fill?: boolean }) {
|
||||
return (
|
||||
<Table<T>
|
||||
{...props}
|
||||
size="small"
|
||||
pagination={false}
|
||||
className={`workspace-table ${fill ? "workspace-table-fill" : ""} ${className}`}
|
||||
columns={columns.map((column) => ({
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
...column,
|
||||
render: (value, record, index, options) => (
|
||||
<div
|
||||
className="workspace-cell"
|
||||
title={typeof value === "string" ? value : undefined}
|
||||
>
|
||||
{column.render
|
||||
? column.render(value, record, index, options)
|
||||
: (value ?? "未提供")}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/* Canonical table metrics; see docs/ui-tables.md. Values match Alpha management. */
|
||||
.workspace-table {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
--workspace-row-height: 40px;
|
||||
}
|
||||
.workspace-table table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.workspace-table .semi-table-row-cell,
|
||||
.workspace-table .semi-table-row-head {
|
||||
box-sizing: border-box;
|
||||
height: var(--workspace-row-height);
|
||||
padding: 8px 16px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.workspace-cell {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-height: 23px;
|
||||
line-height: 23px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
}
|
||||
.workspace-cell > .semi-button {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.workspace-cell .semi-button {
|
||||
height: 23px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.workspace-cell .semi-button-content {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.workspace-cell-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 23px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.workspace-cell-actions > .semi-button {
|
||||
padding: 0 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.workspace-cell .semi-tag {
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.workspace-table-fill,
|
||||
.workspace-table-fill > .semi-spin,
|
||||
.workspace-table-fill .semi-spin-children,
|
||||
.workspace-table-fill .semi-table-small,
|
||||
.workspace-table-fill .semi-table-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.workspace-table-fill .semi-table-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.workspace-table-fill .semi-table-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.workspace-tabs {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.workspace-tabs > .semi-tabs-bar {
|
||||
background: var(--semi-color-fill-0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.workspace-tabs > .semi-tabs-content {
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.workspace-tabs .semi-tabs-pane-active,
|
||||
.workspace-tabs .semi-tabs-pane-active > .semi-tabs-pane-motion-overlay {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.workspace-table-footer {
|
||||
min-height: 44px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.workspace-table-footer > .semi-page {
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.workspace-table-footer {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WorkspaceTable as Table } from "../components/WorkspaceTable";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Banner,
|
||||
@@ -5,9 +6,9 @@ import {
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Table,
|
||||
TextArea,
|
||||
Toast,
|
||||
Tag,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
import {
|
||||
api,
|
||||
@@ -78,6 +79,7 @@ export function DatasetPage({
|
||||
const [sort, setSort] = useState("name"),
|
||||
[direction, setDirection] = useState("asc"),
|
||||
[page, setPage] = useState(1),
|
||||
[pageSize, setPageSize] = useState(25),
|
||||
[rev, setRev] = useState(0);
|
||||
const [data, setData] = useState<Result>({
|
||||
items: [],
|
||||
@@ -129,7 +131,7 @@ export function DatasetPage({
|
||||
const c = new AbortController();
|
||||
setLoading(true);
|
||||
api<Result>(
|
||||
`/catalog/datasets?${queryString({ ...scope, q, category, subcategory, sort, direction, offset: (page - 1) * 25 })}`,
|
||||
`/catalog/datasets?${queryString({ ...scope, q, category, subcategory, sort, direction, limit: pageSize, offset: (page - 1) * pageSize })}`,
|
||||
{ signal: c.signal },
|
||||
)
|
||||
.then(setData)
|
||||
@@ -149,6 +151,7 @@ export function DatasetPage({
|
||||
sort,
|
||||
direction,
|
||||
page,
|
||||
pageSize,
|
||||
version,
|
||||
rev,
|
||||
]);
|
||||
@@ -304,18 +307,21 @@ export function DatasetPage({
|
||||
<Button onClick={onChat}>AI 助手</Button>
|
||||
</div>
|
||||
<Table<Entry>
|
||||
fill
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={data.items}
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
scroll={{ x: 1050, y: "100%" }}
|
||||
scroll={{ x: 1180, y: "100%" }}
|
||||
columns={[
|
||||
{
|
||||
title: "数据集",
|
||||
width: 280,
|
||||
render: (_, r) => (
|
||||
<Button
|
||||
theme="borderless"
|
||||
title={r.name || r.id}
|
||||
onClick={() => void work(() => view(r))}
|
||||
>
|
||||
{r.name || r.id}
|
||||
@@ -330,14 +336,17 @@ export function DatasetPage({
|
||||
{ title: "价值评分", dataIndex: "value_score", render: displayValue },
|
||||
{
|
||||
title: "同步状态",
|
||||
render: (_, r) =>
|
||||
r.collection_version ? "已同步字段" : "待同步字段",
|
||||
render: (_, r) => (
|
||||
<Tag color={r.collection_version ? "green" : "grey"}>
|
||||
{r.collection_version ? "已同步字段" : "待同步字段"}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 240,
|
||||
render: (_, r) => (
|
||||
<div className="preparation-tools">
|
||||
<div className="workspace-cell-actions">
|
||||
<Button
|
||||
theme="borderless"
|
||||
disabled={busy}
|
||||
@@ -365,7 +374,7 @@ export function DatasetPage({
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="preparation-tools">
|
||||
<div className="table-pagination workspace-table-footer">
|
||||
<span>
|
||||
{data.total} 个数据集 ·{" "}
|
||||
{formatTime(data.synced_at, account?.timezone)}
|
||||
@@ -373,7 +382,13 @@ export function DatasetPage({
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
total={data.total}
|
||||
pageSize={25}
|
||||
pageSize={pageSize}
|
||||
showSizeChanger
|
||||
pageSizeOpts={[25, 50, 100]}
|
||||
onPageSizeChange={(size) => {
|
||||
setPageSize(size);
|
||||
setPage(1);
|
||||
}}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WorkspaceTable as Table } from "../components/WorkspaceTable";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Banner,
|
||||
@@ -5,7 +6,6 @@ import {
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Table,
|
||||
TextArea,
|
||||
Toast,
|
||||
Tabs,
|
||||
@@ -34,6 +34,7 @@ export function DataPreparationPage({ active }: { active: boolean }) {
|
||||
}),
|
||||
[q, setQ] = useState(""),
|
||||
[page, setPage] = useState(1),
|
||||
[pageSize, setPageSize] = useState(25),
|
||||
[rev, setRev] = useState(0);
|
||||
const [selected, setSelected] = useState<Record<string, Preparation>>({}),
|
||||
[scope, setScope] = useState(defaultScope),
|
||||
@@ -60,18 +61,18 @@ export function DataPreparationPage({ active }: { active: boolean }) {
|
||||
if (!active) return;
|
||||
const c = new AbortController();
|
||||
api<Page<Preparation>>(
|
||||
`/data-preparations?${queryString({ q, offset: (page - 1) * 25, scope_key: scopeFilter ? scopeKey(scope) : undefined })}`,
|
||||
`/data-preparations?${queryString({ q, limit: pageSize, offset: (page - 1) * pageSize, scope_key: scopeFilter ? scopeKey(scope) : undefined })}`,
|
||||
{ signal: c.signal },
|
||||
)
|
||||
.then((r) => {
|
||||
setData(r);
|
||||
setPage((p) => Math.min(p, Math.max(1, Math.ceil(r.total / 25))));
|
||||
setPage((p) => Math.min(p, Math.max(1, Math.ceil(r.total / pageSize))));
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!c.signal.aborted) setError(e.message);
|
||||
});
|
||||
return () => c.abort();
|
||||
}, [active, q, page, rev, scopeFilter, scopeKey(scope)]);
|
||||
}, [active, q, page, pageSize, rev, scopeFilter, scopeKey(scope)]);
|
||||
useEffect(() => {
|
||||
if (!detail) return;
|
||||
const c = new AbortController();
|
||||
@@ -169,11 +170,12 @@ export function DataPreparationPage({ active }: { active: boolean }) {
|
||||
/>
|
||||
)}
|
||||
<Table<Preparation>
|
||||
fill
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={data.items}
|
||||
pagination={false}
|
||||
scroll={{ x: 1000, y: "100%" }}
|
||||
scroll={{ x: 1140, y: "100%" }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: Object.keys(selected),
|
||||
onChange: (keys) =>
|
||||
@@ -189,20 +191,32 @@ export function DataPreparationPage({ active }: { active: boolean }) {
|
||||
columns={[
|
||||
{
|
||||
title: "集合名称",
|
||||
width: 260,
|
||||
render: (_, r) => (
|
||||
<Button theme="borderless" onClick={() => open(r)}>
|
||||
<Button theme="borderless" title={r.name} onClick={() => open(r)}>
|
||||
{r.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{ title: "范围", render: (_, r) => scopeLabel(r.scope) },
|
||||
{ title: "字段数", dataIndex: "field_count" },
|
||||
{ title: "数据集数", dataIndex: "dataset_count" },
|
||||
{ title: "更新时间", render: (_, r) => formatTime(r.updated_at) },
|
||||
{
|
||||
title: "范围",
|
||||
width: 240,
|
||||
render: (_, r) => (
|
||||
<span title={scopeLabel(r.scope)}>{scopeLabel(r.scope)}</span>
|
||||
),
|
||||
},
|
||||
{ title: "字段数", dataIndex: "field_count", width: 100 },
|
||||
{ title: "数据集数", dataIndex: "dataset_count", width: 100 },
|
||||
{
|
||||
title: "更新时间",
|
||||
width: 180,
|
||||
render: (_, r) => formatTime(r.updated_at),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 200,
|
||||
render: (_, r) => (
|
||||
<div className="preparation-tools">
|
||||
<div className="workspace-cell-actions">
|
||||
<Button theme="borderless" onClick={() => open(r)}>
|
||||
查看
|
||||
</Button>
|
||||
@@ -233,12 +247,21 @@ export function DataPreparationPage({ active }: { active: boolean }) {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
total={data.total}
|
||||
pageSize={25}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
<footer className="table-pagination workspace-table-footer">
|
||||
<span>共 {data.total} 个集合</span>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
total={data.total}
|
||||
pageSize={pageSize}
|
||||
showSizeChanger
|
||||
pageSizeOpts={[25, 50, 100]}
|
||||
onPageSizeChange={(size) => {
|
||||
setPageSize(size);
|
||||
setPage(1);
|
||||
}}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</footer>
|
||||
<Modal
|
||||
visible={creating && active}
|
||||
title="新建数据准备集合"
|
||||
@@ -363,7 +386,13 @@ export function DataPreparationPage({ active }: { active: boolean }) {
|
||||
onCancel={() => setAdding(false)}
|
||||
>
|
||||
{error && <Banner type="danger" description={error} />}
|
||||
<Tabs activeKey={tab} onChange={setTab} tabPaneMotion={false}>
|
||||
<Tabs
|
||||
className="workspace-tabs"
|
||||
type="card"
|
||||
activeKey={tab}
|
||||
onChange={setTab}
|
||||
tabPaneMotion={false}
|
||||
>
|
||||
{(["local", "worldquant"] as const).map((source) => (
|
||||
<TabPane
|
||||
key={source}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { WorkspaceTable as Table } from "../components/WorkspaceTable";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Table,
|
||||
} from "@douyinfe/semi-ui-19";
|
||||
import { Banner, Button, Input, Modal, Pagination } from "@douyinfe/semi-ui-19";
|
||||
import { api, queryString } from "../api";
|
||||
import { FieldTable } from "./FieldTable";
|
||||
import {
|
||||
@@ -134,6 +128,7 @@ export function DataPreparationPicker({
|
||||
</div>
|
||||
<Table<Preparation>
|
||||
rowKey="id"
|
||||
scroll={{ x: 900, y: 320 }}
|
||||
dataSource={data.items}
|
||||
size="small"
|
||||
pagination={false}
|
||||
@@ -160,9 +155,11 @@ export function DataPreparationPicker({
|
||||
columns={[
|
||||
{
|
||||
title: "集合名称",
|
||||
width: 260,
|
||||
render: (_, r) => (
|
||||
<Button
|
||||
theme="borderless"
|
||||
title={r.name}
|
||||
onClick={() => {
|
||||
setPreview(r);
|
||||
setFieldPage(1);
|
||||
@@ -172,7 +169,7 @@ export function DataPreparationPicker({
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{ title: "范围", render: (_, r) => scopeLabel(r.scope) },
|
||||
{ title: "范围", width: 280, render: (_, r) => scopeLabel(r.scope) },
|
||||
{ title: "字段数", dataIndex: "field_count" },
|
||||
{ title: "数据集数", dataIndex: "dataset_count" },
|
||||
...(!multiple
|
||||
|
||||
@@ -114,7 +114,8 @@ export function FieldBrowser({
|
||||
}) {
|
||||
const [scope, setScope] = useState<Scope>(fixedScope ?? defaultScope),
|
||||
[draft, setDraft] = useState<Record<string, string>>({}),
|
||||
[filters, setFilters] = useState<Record<string, string>>({});
|
||||
[filters, setFilters] = useState<Record<string, string>>({}),
|
||||
[queriedScope, setQueriedScope] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1),
|
||||
[size, setSize] = useState(25),
|
||||
[refresh, setRefresh] = useState(0),
|
||||
@@ -131,13 +132,30 @@ export function FieldBrowser({
|
||||
[adding, setAdding] = useState(false);
|
||||
const actual = fixedScope ?? scope,
|
||||
key = scopeKey(actual);
|
||||
const hasCriteria = Object.entries(draft).some(
|
||||
([k, v]) =>
|
||||
[
|
||||
"q",
|
||||
"dataset_id",
|
||||
"field_type",
|
||||
"coverage_min",
|
||||
"coverage_max",
|
||||
"user_count_min",
|
||||
"user_count_max",
|
||||
"alpha_count_min",
|
||||
"alpha_count_max",
|
||||
].includes(k) && v.trim() !== "",
|
||||
);
|
||||
const canRead = source === "local" || queriedScope === key;
|
||||
useEffect(() => {
|
||||
setQueriedScope(null);
|
||||
setLoading(false);
|
||||
setSelected({});
|
||||
setData({ items: [], total: 0, limit: 25, offset: 0 });
|
||||
setPage(1);
|
||||
}, [key]);
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
if (!active || !canRead) return;
|
||||
const c = new AbortController();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
@@ -163,7 +181,7 @@ export function FieldBrowser({
|
||||
if (!c.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => c.abort();
|
||||
}, [active, source, key, filters, page, size, refresh, revision]);
|
||||
}, [active, source, key, filters, page, size, refresh, revision, canRead]);
|
||||
const update = (k: string, v: string) =>
|
||||
setDraft((old) => ({ ...old, [k]: v }));
|
||||
return (
|
||||
@@ -202,7 +220,9 @@ export function FieldBrowser({
|
||||
onChange={(v) => update("field_type", String(v ?? ""))}
|
||||
/>
|
||||
<Button
|
||||
disabled={source === "worldquant" && (!hasCriteria || loading)}
|
||||
onClick={() => {
|
||||
setQueriedScope(key);
|
||||
setFilters({
|
||||
...draft,
|
||||
synced_from: draft.synced_from
|
||||
@@ -223,6 +243,12 @@ export function FieldBrowser({
|
||||
onClick={() => {
|
||||
setDraft({});
|
||||
setFilters({});
|
||||
setQueriedScope(null);
|
||||
setSelected({});
|
||||
setLoading(false);
|
||||
setError("");
|
||||
if (source === "worldquant")
|
||||
setData({ items: [], total: 0, limit: size, offset: 0 });
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
@@ -312,6 +338,11 @@ export function FieldBrowser({
|
||||
</div>
|
||||
)}
|
||||
{error && <Banner type="danger" description={error} />}
|
||||
{source === "worldquant" && !canRead && (
|
||||
<p className="preparation-query-hint">
|
||||
请输入关键词、数据集或其他筛选条件,然后点击查询。
|
||||
</p>
|
||||
)}
|
||||
<div className="preparation-tools">
|
||||
<Button
|
||||
theme="solid"
|
||||
@@ -328,6 +359,7 @@ export function FieldBrowser({
|
||||
)}
|
||||
</div>
|
||||
<FieldTable
|
||||
fill={!fixedScope}
|
||||
items={data.items}
|
||||
loading={loading}
|
||||
selected={Object.keys(selected)}
|
||||
@@ -344,7 +376,7 @@ export function FieldBrowser({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="preparation-tools">
|
||||
<div className="table-pagination workspace-table-footer">
|
||||
<span>
|
||||
{data.total_known === false
|
||||
? "平台未提供总数"
|
||||
@@ -353,20 +385,14 @@ export function FieldBrowser({
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
pageSize={size}
|
||||
total={data.total}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
<ResearchSelect
|
||||
label="字段每页条数"
|
||||
value={size}
|
||||
optionList={[25, 50, 100].map((v) => ({
|
||||
value: v,
|
||||
label: `${v} 条/页`,
|
||||
}))}
|
||||
onChange={(v) => {
|
||||
setSize(Number(v));
|
||||
showSizeChanger
|
||||
pageSizeOpts={[25, 50, 100]}
|
||||
onPageSizeChange={(next) => {
|
||||
setSize(next);
|
||||
setPage(1);
|
||||
}}
|
||||
total={data.total}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
{adding && (
|
||||
@@ -386,10 +412,23 @@ export function FieldDirectory({
|
||||
active: boolean;
|
||||
revision?: string;
|
||||
}) {
|
||||
const [tab, setTab] = useState("worldquant");
|
||||
const [tab, setTab] = useState("local");
|
||||
return (
|
||||
<section className="preparation-page">
|
||||
<Tabs activeKey={tab} onChange={setTab} keepDOM tabPaneMotion={false}>
|
||||
<Tabs
|
||||
className="workspace-tabs"
|
||||
type="card"
|
||||
activeKey={tab}
|
||||
onChange={setTab}
|
||||
keepDOM
|
||||
tabPaneMotion={false}
|
||||
>
|
||||
<TabPane tab="本地同步" itemKey="local">
|
||||
<FieldBrowser
|
||||
active={active && tab === "local"}
|
||||
revision={revision}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="worldquant接口" itemKey="worldquant">
|
||||
<FieldBrowser
|
||||
active={active && tab === "worldquant"}
|
||||
@@ -397,12 +436,6 @@ export function FieldDirectory({
|
||||
revision={revision}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="本地同步" itemKey="local">
|
||||
<FieldBrowser
|
||||
active={active && tab === "local"}
|
||||
revision={revision}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Button, Table } from "@douyinfe/semi-ui-19";
|
||||
import { WorkspaceTable as Table } from "../components/WorkspaceTable";
|
||||
import { Button, Tag } from "@douyinfe/semi-ui-19";
|
||||
import type { ColumnProps } from "@douyinfe/semi-ui-19/lib/es/table/interface";
|
||||
import { displayValue, formatTime } from "../api";
|
||||
import type { FieldRecord } from "./types";
|
||||
export function FieldTable({
|
||||
items,
|
||||
loading = false,
|
||||
fill = false,
|
||||
selected,
|
||||
onSelect,
|
||||
onOpen,
|
||||
}: {
|
||||
items: FieldRecord[];
|
||||
loading?: boolean;
|
||||
fill?: boolean;
|
||||
onOpen?: (field: FieldRecord) => void;
|
||||
selected?: string[];
|
||||
onSelect?: (ids: string[]) => void;
|
||||
@@ -22,7 +25,7 @@ export function FieldTable({
|
||||
width: 220,
|
||||
render: (_, r) =>
|
||||
onOpen ? (
|
||||
<Button theme="borderless" onClick={() => onOpen(r)}>
|
||||
<Button theme="borderless" title={r.id} onClick={() => onOpen(r)}>
|
||||
{r.id}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -44,7 +47,7 @@ export function FieldTable({
|
||||
title: "类型",
|
||||
dataIndex: "field_type",
|
||||
width: 100,
|
||||
render: displayValue,
|
||||
render: (value) => (value ? <Tag color="blue">{value}</Tag> : "未提供"),
|
||||
},
|
||||
{
|
||||
title: "覆盖率",
|
||||
@@ -67,11 +70,17 @@ export function FieldTable({
|
||||
{
|
||||
title: "同步时间",
|
||||
width: 170,
|
||||
render: (_, r) => (r.synced_at ? formatTime(r.synced_at) : "在线查询"),
|
||||
render: (_, r) =>
|
||||
r.synced_at ? (
|
||||
formatTime(r.synced_at)
|
||||
) : (
|
||||
<Tag color="blue">在线查询</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table<FieldRecord>
|
||||
fill={fill}
|
||||
size="small"
|
||||
className="preparation-table"
|
||||
rowKey={(r) => `${r?.dataset_id}|${r?.id}`}
|
||||
@@ -79,7 +88,7 @@ export function FieldTable({
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 1420, y: 400 }}
|
||||
scroll={{ x: 1420, y: fill ? "100%" : 400 }}
|
||||
rowSelection={
|
||||
onSelect
|
||||
? {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.preparation-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: var(--semi-color-bg-0);
|
||||
}
|
||||
.preparation-tools {
|
||||
@@ -13,7 +13,9 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin: 8px 0;
|
||||
margin: 0;
|
||||
padding: 4px 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.preparation-tools > .semi-input-wrapper {
|
||||
width: 240px;
|
||||
@@ -31,14 +33,17 @@
|
||||
.preparation-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.preparation-filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
margin: 0;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
.preparation-filters label {
|
||||
display: flex;
|
||||
@@ -60,9 +65,22 @@
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.preparation-page {
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.preparation-tools > .semi-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.preparation-detail .preparation-tools {
|
||||
padding: 0;
|
||||
}
|
||||
.preparation-page > .semi-banner {
|
||||
margin: 4px 12px;
|
||||
}
|
||||
.preparation-query-hint {
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
color: var(--semi-color-text-2);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user