This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Alpha 视图标签与筛选弹层
|
||||||
|
|
||||||
|
Type: task
|
||||||
|
Status: ready-for-agent
|
||||||
|
|
||||||
|
按 ../spec.md 实现并验证。
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
开始实现。使用现有 Semi 组件和研究素材接口;主代理负责修改及最终验证。
|
||||||
|
|
||||||
|
完成实现。生产构建、类型检查、Prettier 与 diff 空白检查通过。受影响的 6 项浏览器回归全部通过(分批运行):Alpha 管理、工作空间同步/导出、批量标签/列/分页、保存视图与研究评估、AI 筛选上下文及多宽度布局。
|
||||||
|
|
||||||
|
通过 Playwright CLI 在独立模拟服务进一步核验:搜索+地区+Turnover 组合筛选、筛选数量与导出参数、视图创建/重命名/更新/复制/删除及恢复、排序更改标记;390px 宽度无横向溢出,长筛选面板内滚动且底部操作固定可见。浏览器最后一次导航后无控制台错误。
|
||||||
|
|
||||||
|
截图:output/playwright/alpha-view-toolbar.png、alpha-filter-panel.png、alpha-view-menu.png、alpha-filter-mobile.png、alpha-views-mobile.png。所有验证使用隔离临时数据库和模拟平台,未修改真实 Alpha 或部署。构建仅保留依赖已有的 lottie eval 与包体积警告。
|
||||||
|
|
||||||
|
Completion: completed
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Alpha 视图与筛选工具栏
|
||||||
|
|
||||||
|
按用户截图调整交互,保留 Semi Design 2.103.0。
|
||||||
|
|
||||||
|
- 待提交、已提交及保存的视图统一为标签栏;新增视图通过命名弹窗完成,标签菜单支持更新、重命名、复制及删除。
|
||||||
|
- 工具栏集中筛选、排序、列设置、导入导出、同步及表格批量操作。搜索仅在筛选弹层内显示。
|
||||||
|
- 保留全部现有筛选字段及查询/重置行为;应用后关闭弹层,入口显示已应用数量;切换视图清理草稿与行选择。
|
||||||
|
- 复用研究素材视图接口及版本锁,不改后端数据模型。没有后端支持的保护视图/设为首项不在范围内。
|
||||||
|
- 本地生产构建、受影响的既有浏览器回归,以及隔离模拟环境中的视图保存恢复、菜单、筛选弹层、窄屏视觉验收。
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
.alpha-views-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--semi-color-fill-0);
|
||||||
|
border-bottom: 1px solid var(--semi-color-border);
|
||||||
|
}
|
||||||
|
.alpha-view-tabs {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.alpha-view-tabs > .semi-tabs-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.alpha-view-tabs .semi-tabs-bar {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
.alpha-view-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
.alpha-view-name {
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.alpha-view-modified {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--semi-color-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.alpha-add-view {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
.alpha-toolbar {
|
||||||
|
gap: 8px 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--semi-color-border);
|
||||||
|
}
|
||||||
|
.alpha-view-tools,
|
||||||
|
.alpha-table-actions {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.alpha-table-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.alpha-filter-count {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.alpha-selection-actions {
|
||||||
|
padding: 8px 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--semi-color-primary-light-default);
|
||||||
|
border-bottom: 1px solid var(--semi-color-border);
|
||||||
|
}
|
||||||
|
.alpha-selection-actions .selection-text {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.alpha-filter-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: min(640px, calc(100vw - 32px));
|
||||||
|
max-height: min(700px, calc(100dvh - 180px));
|
||||||
|
color: var(--semi-color-text-0);
|
||||||
|
}
|
||||||
|
.alpha-filter-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px 20px 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.alpha-filter-header p {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.alpha-filter-body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
.alpha-filter-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.alpha-filter-fields > label {
|
||||||
|
margin: 0;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.alpha-filter-fields .semi-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.alpha-filter-search {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.alpha-filter-panel .advanced-filters {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.alpha-filter-panel .advanced-filters > summary {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--semi-color-text-1);
|
||||||
|
}
|
||||||
|
.alpha-filter-panel .advanced-filters .alpha-filter-fields {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.alpha-filter-panel .range-inputs {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.alpha-filter-panel .range-inputs .semi-input-wrapper {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.alpha-filter-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid var(--semi-color-border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.alpha-filter-footer > :first-child {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.alpha-sort-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 96px;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
width: min(360px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
.alpha-sort-panel > h2 {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.alpha-sort-panel .semi-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.alpha-table-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px 16px;
|
||||||
|
}
|
||||||
|
.alpha-table-meta .alpha-last-sync {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.alpha-table-meta .count-pill {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.alpha-toolbar {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.alpha-table-actions {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.alpha-view-name {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
.alpha-add-view {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
.alpha-table-meta .alpha-last-sync {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.alpha-filter-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.alpha-filter-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
.alpha-filter-fields {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.alpha-filter-fields {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
+454
-339
@@ -11,12 +11,22 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
Select,
|
Select,
|
||||||
Table,
|
Table,
|
||||||
Tabs,
|
|
||||||
Tag,
|
Tag,
|
||||||
TextArea,
|
TextArea,
|
||||||
Toast,
|
Toast,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@douyinfe/semi-ui-19";
|
} from "@douyinfe/semi-ui-19";
|
||||||
|
import {
|
||||||
|
IconFilter,
|
||||||
|
IconSetting,
|
||||||
|
IconSort,
|
||||||
|
IconSearch,
|
||||||
|
IconClose,
|
||||||
|
IconDownload,
|
||||||
|
IconUpload,
|
||||||
|
IconRefresh,
|
||||||
|
} from "@douyinfe/semi-icons";
|
||||||
|
import "./AlphaPage.css";
|
||||||
import type { ColumnProps } from "@douyinfe/semi-ui-19/lib/es/table/interface";
|
import type { ColumnProps } from "@douyinfe/semi-ui-19/lib/es/table/interface";
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
@@ -144,6 +154,8 @@ export function AlphaPage({
|
|||||||
const [facets, setFacets] = useState<Facets>({});
|
const [facets, setFacets] = useState<Facets>({});
|
||||||
const [draft, setDraft] = useState<Record<string, string>>({});
|
const [draft, setDraft] = useState<Record<string, string>>({});
|
||||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
|
const [selectedViewId, setSelectedViewId] = useState<string | null>(null);
|
||||||
const [submission, setSubmission] = useState<Submission>("UNSUBMITTED");
|
const [submission, setSubmission] = useState<Submission>("UNSUBMITTED");
|
||||||
const [syncingScope, setSyncingScope] = useState<Submission | null>(null);
|
const [syncingScope, setSyncingScope] = useState<Submission | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -238,6 +250,8 @@ export function AlphaPage({
|
|||||||
setDetailId(action.alpha_id);
|
setDetailId(action.alpha_id);
|
||||||
onOverlay();
|
onOverlay();
|
||||||
} else {
|
} else {
|
||||||
|
setSelectedViewId(null);
|
||||||
|
setFilterOpen(false);
|
||||||
const {
|
const {
|
||||||
sort: nextSort,
|
sort: nextSort,
|
||||||
direction: nextDirection,
|
direction: nextDirection,
|
||||||
@@ -296,15 +310,20 @@ export function AlphaPage({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
|
setFilterOpen(false);
|
||||||
setFilters({ ...draft });
|
setFilters({ ...draft });
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
|
setFilterOpen(false);
|
||||||
setDraft({});
|
setDraft({});
|
||||||
setFilters({});
|
setFilters({});
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}
|
||||||
function changeSubmission(value: string) {
|
function changeSubmission(value: string) {
|
||||||
|
setSelectedViewId(null);
|
||||||
|
setFilterOpen(false);
|
||||||
|
setDirection("desc");
|
||||||
setSubmission(value as Submission);
|
setSubmission(value as Submission);
|
||||||
setDraft({});
|
setDraft({});
|
||||||
setFilters({});
|
setFilters({});
|
||||||
@@ -610,6 +629,233 @@ export function AlphaPage({
|
|||||||
const connected =
|
const connected =
|
||||||
account?.connection_status === "connected" ||
|
account?.connection_status === "connected" ||
|
||||||
account?.connection_status === "expired";
|
account?.connection_status === "expired";
|
||||||
|
const filterPanel = (
|
||||||
|
<form
|
||||||
|
className="alpha-filter-panel"
|
||||||
|
aria-label="Alpha 筛选条件"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
applyFilters();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header className="alpha-filter-header">
|
||||||
|
<div>
|
||||||
|
<h2>筛选 Alpha</h2>
|
||||||
|
<p className="muted">设置搜索和筛选条件,查询后应用到当前视图。</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon={<IconClose aria-hidden="true" />}
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
aria-label="关闭筛选"
|
||||||
|
onClick={() => setFilterOpen(false)}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<div className="alpha-filter-body">
|
||||||
|
<div className="alpha-filter-fields">
|
||||||
|
<label className="alpha-filter-search">
|
||||||
|
搜索
|
||||||
|
<Input
|
||||||
|
prefix={<IconSearch aria-hidden="true" />}
|
||||||
|
aria-label="搜索 Alpha"
|
||||||
|
placeholder="搜索 ID、名称或表达式"
|
||||||
|
value={draft.q ?? ""}
|
||||||
|
onChange={(v) => updateDraft("q", v)}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
地区
|
||||||
|
<Select
|
||||||
|
aria-label="地区"
|
||||||
|
placeholder="全部地区"
|
||||||
|
showClear
|
||||||
|
value={draft.region || undefined}
|
||||||
|
optionList={options("region")}
|
||||||
|
onChange={(v) => updateDraft("region", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
平台状态
|
||||||
|
<Select
|
||||||
|
aria-label="平台状态"
|
||||||
|
placeholder="全部平台状态"
|
||||||
|
showClear
|
||||||
|
value={draft.status || undefined}
|
||||||
|
optionList={options("status")}
|
||||||
|
onChange={(v) => updateDraft("status", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
研究状态
|
||||||
|
<Select
|
||||||
|
aria-label="研究状态筛选"
|
||||||
|
placeholder="全部研究状态"
|
||||||
|
showClear
|
||||||
|
value={draft.research_state || undefined}
|
||||||
|
optionList={stateOptions}
|
||||||
|
onChange={(v) => updateDraft("research_state", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
检查结果
|
||||||
|
<Select
|
||||||
|
aria-label="检查结果筛选"
|
||||||
|
placeholder="全部检查结果"
|
||||||
|
showClear
|
||||||
|
value={draft.check_type || undefined}
|
||||||
|
optionList={Object.entries(checkLabels).map(([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
}))}
|
||||||
|
onChange={(v) => updateDraft("check_type", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
自定义标签
|
||||||
|
<Select
|
||||||
|
aria-label="自定义标签筛选"
|
||||||
|
showClear
|
||||||
|
filter
|
||||||
|
value={draft.tag || undefined}
|
||||||
|
optionList={options("tags")}
|
||||||
|
placeholder="全部标签"
|
||||||
|
onChange={(v) => updateDraft("tag", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<details className="advanced-filters">
|
||||||
|
<summary>更多筛选</summary>
|
||||||
|
<div className="alpha-filter-fields">
|
||||||
|
<label>
|
||||||
|
研究来源
|
||||||
|
<Select
|
||||||
|
aria-label="研究来源筛选"
|
||||||
|
showClear
|
||||||
|
value={draft.source || undefined}
|
||||||
|
optionList={options("source").map((option) => ({
|
||||||
|
...option,
|
||||||
|
label: sourceLabel(option.value),
|
||||||
|
}))}
|
||||||
|
placeholder="全部来源"
|
||||||
|
onChange={(v) => updateDraft("source", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
研究编号
|
||||||
|
<Input
|
||||||
|
aria-label="研究编号筛选"
|
||||||
|
value={draft.research_id || ""}
|
||||||
|
onChange={(v) => updateDraft("research_id", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{[
|
||||||
|
["universe", "Universe"],
|
||||||
|
["alpha_type", "Alpha 类型"],
|
||||||
|
["language", "语言"],
|
||||||
|
["stage", "IS / OS"],
|
||||||
|
].map(([key, label]) => (
|
||||||
|
<label key={key}>
|
||||||
|
{label}
|
||||||
|
<Select
|
||||||
|
showClear
|
||||||
|
aria-label={label}
|
||||||
|
value={draft[key] || undefined}
|
||||||
|
optionList={options(key)}
|
||||||
|
placeholder="全部"
|
||||||
|
onChange={(v) => updateDraft(key, v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<label>
|
||||||
|
平台隐藏状态
|
||||||
|
<Select
|
||||||
|
showClear
|
||||||
|
value={draft.hidden || undefined}
|
||||||
|
optionList={[
|
||||||
|
{ value: "false", label: "可见" },
|
||||||
|
{ value: "true", label: "隐藏" },
|
||||||
|
]}
|
||||||
|
placeholder="全部"
|
||||||
|
onChange={(v) => updateDraft("hidden", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
中性化
|
||||||
|
<Input
|
||||||
|
aria-label="中性化筛选"
|
||||||
|
value={draft.neutralization || ""}
|
||||||
|
placeholder="例如 INDUSTRY"
|
||||||
|
onChange={(v) => updateDraft("neutralization", v)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
创建时间起(UTC)
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={draft.created_from?.slice(0, 10) || ""}
|
||||||
|
onChange={(v) =>
|
||||||
|
updateDraft("created_from", v ? `${v}T00:00:00Z` : "")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
创建时间止(UTC)
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={draft.created_to?.slice(0, 10) || ""}
|
||||||
|
onChange={(v) =>
|
||||||
|
updateDraft("created_to", v ? `${v}T23:59:59.999999Z` : "")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{Object.entries(metricLabels).map(([key, label]) => (
|
||||||
|
<label key={key}>
|
||||||
|
{label}
|
||||||
|
<div className="range-inputs">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
aria-label={`${label} 最小值`}
|
||||||
|
placeholder="最小值"
|
||||||
|
value={draft[`${key}_min`] || ""}
|
||||||
|
onChange={(v) => updateDraft(`${key}_min`, v)}
|
||||||
|
/>
|
||||||
|
<span>—</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
aria-label={`${label} 最大值`}
|
||||||
|
placeholder="最大值"
|
||||||
|
value={draft[`${key}_max`] || ""}
|
||||||
|
onChange={(v) => updateDraft(`${key}_max`, v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<Checkbox
|
||||||
|
checked={draft.favorite === "true"}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateDraft("favorite", e.target.checked ? "true" : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
只看收藏
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<p className="muted">
|
||||||
|
数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。
|
||||||
|
检查结果仅汇总已同步的平台检查;预检通过不代表可以提交,活动资格可用自定义标签记录。
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<footer className="alpha-filter-footer">
|
||||||
|
<Button type="tertiary" theme="borderless" onClick={clearFilters}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button htmlType="submit" theme="solid">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!connected && (
|
{!connected && (
|
||||||
@@ -627,223 +873,17 @@ export function AlphaPage({
|
|||||||
)}
|
)}
|
||||||
{error && <Banner type="danger" description={error} />}
|
{error && <Banner type="danger" description={error} />}
|
||||||
<section className="library-panel" ref={tablePanel}>
|
<section className="library-panel" ref={tablePanel}>
|
||||||
<div className="alpha-tabs-header">
|
|
||||||
<Tabs
|
|
||||||
className="alpha-submission-tabs"
|
|
||||||
activeKey={submission}
|
|
||||||
onChange={changeSubmission}
|
|
||||||
tabList={[
|
|
||||||
{ itemKey: "UNSUBMITTED", tab: "待提交" },
|
|
||||||
{ itemKey: "SUBMITTED", tab: "已提交" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<span className="alpha-last-sync">
|
|
||||||
最近数据更新{" "}
|
|
||||||
<span>
|
|
||||||
{formatTime(
|
|
||||||
typeof facets.last_sync === "string" ? facets.last_sync : null,
|
|
||||||
account?.timezone,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
className="filter-panel"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
applyFilters();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="primary-filters">
|
|
||||||
<Input
|
|
||||||
aria-label="搜索 Alpha"
|
|
||||||
placeholder="搜索 ID、名称或表达式"
|
|
||||||
value={draft.q ?? ""}
|
|
||||||
onChange={(v) => updateDraft("q", v)}
|
|
||||||
showClear
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
aria-label="地区"
|
|
||||||
placeholder="全部地区"
|
|
||||||
showClear
|
|
||||||
value={draft.region || undefined}
|
|
||||||
optionList={options("region")}
|
|
||||||
onChange={(v) => updateDraft("region", v)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
aria-label="平台状态"
|
|
||||||
placeholder="全部平台状态"
|
|
||||||
showClear
|
|
||||||
value={draft.status || undefined}
|
|
||||||
optionList={options("status")}
|
|
||||||
onChange={(v) => updateDraft("status", v)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
aria-label="研究状态筛选"
|
|
||||||
placeholder="全部研究状态"
|
|
||||||
showClear
|
|
||||||
value={draft.research_state || undefined}
|
|
||||||
optionList={stateOptions}
|
|
||||||
onChange={(v) => updateDraft("research_state", v)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
aria-label="检查结果筛选"
|
|
||||||
placeholder="全部检查结果"
|
|
||||||
showClear
|
|
||||||
value={draft.check_type || undefined}
|
|
||||||
optionList={Object.entries(checkLabels).map(([value, label]) => ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
}))}
|
|
||||||
onChange={(v) => updateDraft("check_type", v)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
aria-label="自定义标签筛选"
|
|
||||||
showClear
|
|
||||||
filter
|
|
||||||
value={draft.tag || undefined}
|
|
||||||
optionList={options("tags")}
|
|
||||||
placeholder="全部标签"
|
|
||||||
onChange={(v) => updateDraft("tag", v)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<details className="advanced-filters">
|
|
||||||
<summary>
|
|
||||||
更多筛选{" "}
|
|
||||||
{activeFilterCount ? `· ${activeFilterCount} 项已应用` : ""}
|
|
||||||
</summary>
|
|
||||||
<div className="advanced-grid">
|
|
||||||
<label>
|
|
||||||
研究来源
|
|
||||||
<Select
|
|
||||||
aria-label="研究来源筛选"
|
|
||||||
showClear
|
|
||||||
value={draft.source || undefined}
|
|
||||||
optionList={options("source").map((option) => ({
|
|
||||||
...option,
|
|
||||||
label: sourceLabel(option.value),
|
|
||||||
}))}
|
|
||||||
placeholder="全部来源"
|
|
||||||
onChange={(v) => updateDraft("source", v)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
研究编号
|
|
||||||
<Input
|
|
||||||
aria-label="研究编号筛选"
|
|
||||||
value={draft.research_id || ""}
|
|
||||||
onChange={(v) => updateDraft("research_id", v)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{[
|
|
||||||
["universe", "Universe"],
|
|
||||||
["alpha_type", "Alpha 类型"],
|
|
||||||
["language", "语言"],
|
|
||||||
["stage", "IS / OS"],
|
|
||||||
].map(([key, label]) => (
|
|
||||||
<label key={key}>
|
|
||||||
{label}
|
|
||||||
<Select
|
|
||||||
showClear
|
|
||||||
aria-label={label}
|
|
||||||
value={draft[key] || undefined}
|
|
||||||
optionList={options(key)}
|
|
||||||
placeholder="全部"
|
|
||||||
onChange={(v) => updateDraft(key, v)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
<label>
|
|
||||||
平台隐藏状态
|
|
||||||
<Select
|
|
||||||
showClear
|
|
||||||
value={draft.hidden || undefined}
|
|
||||||
optionList={[
|
|
||||||
{ value: "false", label: "可见" },
|
|
||||||
{ value: "true", label: "隐藏" },
|
|
||||||
]}
|
|
||||||
placeholder="全部"
|
|
||||||
onChange={(v) => updateDraft("hidden", v)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
中性化
|
|
||||||
<Input
|
|
||||||
aria-label="中性化筛选"
|
|
||||||
value={draft.neutralization || ""}
|
|
||||||
placeholder="例如 INDUSTRY"
|
|
||||||
onChange={(v) => updateDraft("neutralization", v)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
创建时间起(UTC)
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={draft.created_from?.slice(0, 10) || ""}
|
|
||||||
onChange={(v) =>
|
|
||||||
updateDraft("created_from", v ? `${v}T00:00:00Z` : "")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
创建时间止(UTC)
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={draft.created_to?.slice(0, 10) || ""}
|
|
||||||
onChange={(v) =>
|
|
||||||
updateDraft("created_to", v ? `${v}T23:59:59.999999Z` : "")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{Object.entries(metricLabels).map(([key, label]) => (
|
|
||||||
<label key={key}>
|
|
||||||
{label}
|
|
||||||
<div className="range-inputs">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
aria-label={`${label} 最小值`}
|
|
||||||
placeholder="最小值"
|
|
||||||
value={draft[`${key}_min`] || ""}
|
|
||||||
onChange={(v) => updateDraft(`${key}_min`, v)}
|
|
||||||
/>
|
|
||||||
<span>—</span>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
aria-label={`${label} 最大值`}
|
|
||||||
placeholder="最大值"
|
|
||||||
value={draft[`${key}_max`] || ""}
|
|
||||||
onChange={(v) => updateDraft(`${key}_max`, v)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
<Checkbox
|
|
||||||
checked={draft.favorite === "true"}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateDraft("favorite", e.target.checked ? "true" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
只看收藏
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<p className="muted">
|
|
||||||
数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。
|
|
||||||
检查结果仅汇总已同步的平台检查;预检通过不代表可以提交,活动资格可用自定义标签记录。
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
<div className="inline-actions filter-actions">
|
|
||||||
<Button htmlType="submit" theme="solid">
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
<Button type="tertiary" theme="borderless" onClick={clearFilters}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<SavedViews
|
<SavedViews
|
||||||
|
submission={submission}
|
||||||
|
selectedId={selectedViewId}
|
||||||
|
onSelect={setSelectedViewId}
|
||||||
|
onSubmissionChange={changeSubmission}
|
||||||
|
suspended={overlaySuspended || !active}
|
||||||
|
onOverlay={onOverlay}
|
||||||
filters={{ ...filters, submission, sort, direction, limit: pageSize }}
|
filters={{ ...filters, submission, sort, direction, limit: pageSize }}
|
||||||
columns={visibleColumns}
|
columns={visibleColumns}
|
||||||
onRestore={(view) => {
|
onRestore={(view) => {
|
||||||
|
setFilterOpen(false);
|
||||||
const {
|
const {
|
||||||
submission: savedSubmission,
|
submission: savedSubmission,
|
||||||
sort: savedSort,
|
sort: savedSort,
|
||||||
@@ -861,11 +901,9 @@ export function AlphaPage({
|
|||||||
setDraft(values);
|
setDraft(values);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
if (
|
setSubmission(
|
||||||
savedSubmission === "SUBMITTED" ||
|
savedSubmission === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED",
|
||||||
savedSubmission === "UNSUBMITTED"
|
);
|
||||||
)
|
|
||||||
setSubmission(savedSubmission);
|
|
||||||
setSort(String(savedSort || "date_created"));
|
setSort(String(savedSort || "date_created"));
|
||||||
setDirection(String(savedDirection || "desc"));
|
setDirection(String(savedDirection || "desc"));
|
||||||
if (typeof limit === "number") setPageSize(limit);
|
if (typeof limit === "number") setPageSize(limit);
|
||||||
@@ -879,104 +917,148 @@ export function AlphaPage({
|
|||||||
localStorage.setItem("alpha-columns", JSON.stringify(next));
|
localStorage.setItem("alpha-columns", JSON.stringify(next));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
<div className="table-toolbar">
|
className="table-toolbar alpha-toolbar"
|
||||||
<div>
|
aria-label="Alpha 表格工具栏"
|
||||||
<strong>
|
>
|
||||||
{submission === "UNSUBMITTED" ? "待提交" : "已提交"} Alpha
|
<div className="inline-actions alpha-view-tools">
|
||||||
</strong>
|
<Popover
|
||||||
<span className="count-pill">{data.total.toLocaleString()}</span>
|
trigger="custom"
|
||||||
{selected.length > 0 && (
|
position="bottomLeft"
|
||||||
<span className="selection-text">
|
visible={filterOpen && active && !overlaySuspended}
|
||||||
已选择 {selected.length} 条
|
onClickOutSide={() => setFilterOpen(false)}
|
||||||
</span>
|
onEscKeyDown={() => setFilterOpen(false)}
|
||||||
)}
|
content={filterPanel}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<IconFilter aria-hidden="true" />}
|
||||||
|
type={filterOpen || activeFilterCount ? "primary" : "tertiary"}
|
||||||
|
theme={filterOpen || activeFilterCount ? "light" : "borderless"}
|
||||||
|
aria-label="筛选 Alpha"
|
||||||
|
onClick={() => {
|
||||||
|
setFilterOpen((open) => !open);
|
||||||
|
onOverlay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
筛选
|
||||||
|
{activeFilterCount > 0 && (
|
||||||
|
<span className="alpha-filter-count">
|
||||||
|
{activeFilterCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
position="bottomRight"
|
||||||
|
content={
|
||||||
|
<div className="columns-picker">
|
||||||
|
<strong>显示列</strong>
|
||||||
|
<CheckboxGroup
|
||||||
|
direction="vertical"
|
||||||
|
value={visibleColumns}
|
||||||
|
options={Object.entries(columnLabels).map(
|
||||||
|
([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
disabled: value === "name",
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
onChange={(values) => {
|
||||||
|
const next = values.map(String);
|
||||||
|
setVisibleColumns(next);
|
||||||
|
localStorage.setItem(
|
||||||
|
"alpha-columns",
|
||||||
|
JSON.stringify(next),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<IconSetting aria-hidden="true" />}
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
aria-label="显示列设置"
|
||||||
|
>
|
||||||
|
列设置
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
position="bottomLeft"
|
||||||
|
content={
|
||||||
|
<div className="alpha-sort-panel">
|
||||||
|
<h2>排序</h2>
|
||||||
|
<Select
|
||||||
|
aria-label="排序字段"
|
||||||
|
size="small"
|
||||||
|
value={sort}
|
||||||
|
optionList={[
|
||||||
|
{ value: "date_created", label: "创建时间" },
|
||||||
|
{ value: "date_submitted", label: "提交时间" },
|
||||||
|
{ value: "synced_at", label: "同步时间" },
|
||||||
|
...Object.entries(metricLabels).map(([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
})),
|
||||||
|
{ value: "name", label: "名称" },
|
||||||
|
]}
|
||||||
|
onChange={(v) => {
|
||||||
|
setSort(String(v));
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
aria-label="排序方向"
|
||||||
|
size="small"
|
||||||
|
value={direction}
|
||||||
|
optionList={[
|
||||||
|
{ value: "desc", label: "降序" },
|
||||||
|
{ value: "asc", label: "升序" },
|
||||||
|
]}
|
||||||
|
onChange={(v) => {
|
||||||
|
setDirection(String(v));
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<IconSort aria-hidden="true" />}
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
aria-label="排序设置"
|
||||||
|
>
|
||||||
|
排序
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-actions">
|
<div className="inline-actions alpha-table-actions">
|
||||||
{selected.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
size="small"
|
|
||||||
loading={busy === "self_correlation"}
|
|
||||||
onClick={() => void newTask("self_correlation", selected)}
|
|
||||||
>
|
|
||||||
检测自相关
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setBulkVersions(
|
|
||||||
Object.fromEntries(
|
|
||||||
data.items
|
|
||||||
.filter((row) => selected.includes(row.id))
|
|
||||||
.map((row) => [row.id, row.research.version]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setBulkOpen(true);
|
|
||||||
onOverlay();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量编辑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="tertiary"
|
|
||||||
size="small"
|
|
||||||
disabled={!connected}
|
|
||||||
loading={busy === "alpha_refresh"}
|
|
||||||
onClick={() => void newTask("alpha_refresh", selected)}
|
|
||||||
>
|
|
||||||
刷新选中
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Select
|
|
||||||
aria-label="排序字段"
|
|
||||||
size="small"
|
|
||||||
value={sort}
|
|
||||||
optionList={[
|
|
||||||
{ value: "date_created", label: "创建时间" },
|
|
||||||
{ value: "date_submitted", label: "提交时间" },
|
|
||||||
{ value: "synced_at", label: "同步时间" },
|
|
||||||
...Object.entries(metricLabels).map(([value, label]) => ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
})),
|
|
||||||
{ value: "name", label: "名称" },
|
|
||||||
]}
|
|
||||||
onChange={(v) => {
|
|
||||||
setSort(String(v));
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
aria-label="排序方向"
|
|
||||||
size="small"
|
|
||||||
value={direction}
|
|
||||||
optionList={[
|
|
||||||
{ value: "desc", label: "降序" },
|
|
||||||
{ value: "asc", label: "升序" },
|
|
||||||
]}
|
|
||||||
onChange={(v) => {
|
|
||||||
setDirection(String(v));
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<a href={`/api/v1/alphas/export?${query}`}>
|
<a href={`/api/v1/alphas/export?${query}`}>
|
||||||
<Button type="tertiary" size="small">
|
<Button
|
||||||
|
icon={<IconDownload aria-hidden="true" />}
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
>
|
||||||
导出
|
导出
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
icon={<IconUpload aria-hidden="true" />}
|
||||||
|
theme="borderless"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
onClick={() => setImporting(true)}
|
onClick={() => {
|
||||||
|
setImporting(true);
|
||||||
|
onOverlay();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
导入 Alpha ID
|
导入 Alpha ID
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
icon={<IconRefresh aria-hidden="true" />}
|
||||||
theme="solid"
|
theme="solid"
|
||||||
disabled={!connected}
|
disabled={!connected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1007,40 +1089,56 @@ export function AlphaPage({
|
|||||||
全量同步已提交
|
全量同步已提交
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Popover
|
|
||||||
trigger="click"
|
|
||||||
position="bottomRight"
|
|
||||||
content={
|
|
||||||
<div className="columns-picker">
|
|
||||||
<strong>显示列</strong>
|
|
||||||
<CheckboxGroup
|
|
||||||
direction="vertical"
|
|
||||||
value={visibleColumns}
|
|
||||||
options={Object.entries(columnLabels).map(
|
|
||||||
([value, label]) => ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
disabled: value === "name",
|
|
||||||
}),
|
|
||||||
)}
|
|
||||||
onChange={(values) => {
|
|
||||||
const next = values.map(String);
|
|
||||||
setVisibleColumns(next);
|
|
||||||
localStorage.setItem(
|
|
||||||
"alpha-columns",
|
|
||||||
JSON.stringify(next),
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button type="tertiary" aria-label="显示列设置" size="small">
|
|
||||||
列设置
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{selected.length > 0 && (
|
||||||
|
<div className="alpha-selection-actions inline-actions">
|
||||||
|
<span className="selection-text">已选择 {selected.length} 条</span>
|
||||||
|
<Button
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
loading={busy === "self_correlation"}
|
||||||
|
onClick={() => void newTask("self_correlation", selected)}
|
||||||
|
>
|
||||||
|
检测自相关
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setBulkVersions(
|
||||||
|
Object.fromEntries(
|
||||||
|
data.items
|
||||||
|
.filter((row) => selected.includes(row.id))
|
||||||
|
.map((row) => [row.id, row.research.version]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setBulkOpen(true);
|
||||||
|
onOverlay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
批量编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
disabled={!connected}
|
||||||
|
loading={busy === "alpha_refresh"}
|
||||||
|
onClick={() => void newTask("alpha_refresh", selected)}
|
||||||
|
>
|
||||||
|
刷新选中
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
onClick={() => setSelected([])}
|
||||||
|
>
|
||||||
|
取消选择
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table<Alpha>
|
<Table<Alpha>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data.items}
|
dataSource={data.items}
|
||||||
@@ -1075,7 +1173,24 @@ export function AlphaPage({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<footer className="table-pagination" aria-label="Alpha 分页">
|
<footer className="table-pagination" aria-label="Alpha 分页">
|
||||||
<span className="muted">共 {data.total.toLocaleString()} 条</span>
|
<div className="alpha-table-meta">
|
||||||
|
<span className="muted">
|
||||||
|
{submission === "UNSUBMITTED" ? "待提交" : "已提交"} Alpha{" "}
|
||||||
|
<span className="count-pill">{data.total.toLocaleString()}</span>{" "}
|
||||||
|
条
|
||||||
|
</span>
|
||||||
|
<span className="alpha-last-sync">
|
||||||
|
最近数据更新{" "}
|
||||||
|
<span>
|
||||||
|
{formatTime(
|
||||||
|
typeof facets.last_sync === "string"
|
||||||
|
? facets.last_sync
|
||||||
|
: null,
|
||||||
|
account?.timezone,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<Pagination
|
<Pagination
|
||||||
size={compact ? "small" : "default"}
|
size={compact ? "small" : "default"}
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
|
|||||||
@@ -1,113 +1,344 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Input, Toast } from "@douyinfe/semi-ui-19";
|
import {
|
||||||
import { api, post } from "../api";
|
Button,
|
||||||
import { ResearchSelect } from "./ResearchSelect";
|
Dropdown,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Tabs,
|
||||||
|
Toast,
|
||||||
|
} from "@douyinfe/semi-ui-19";
|
||||||
|
import {
|
||||||
|
IconCopy,
|
||||||
|
IconDelete,
|
||||||
|
IconEdit,
|
||||||
|
IconMore,
|
||||||
|
IconPlus,
|
||||||
|
IconSave,
|
||||||
|
IconList,
|
||||||
|
} from "@douyinfe/semi-icons";
|
||||||
|
import { api, ApiError, post } from "../api";
|
||||||
|
import type { Submission } from "../types";
|
||||||
|
|
||||||
export type ViewContent = {
|
export type ViewContent = {
|
||||||
name: string;
|
name: string;
|
||||||
filters: Record<string, unknown>;
|
filters: Record<string, unknown>;
|
||||||
columns: string[];
|
columns: string[];
|
||||||
};
|
};
|
||||||
type View = { id: string; version: number; content: ViewContent };
|
type View = { id: string; version: number; content: ViewContent };
|
||||||
|
type Editor = {
|
||||||
|
mode: "create" | "rename" | "copy";
|
||||||
|
content: ViewContent;
|
||||||
|
view?: View;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The API normalizes numbers and adds offset; neither changes the saved view.
|
||||||
|
function filterSignature(filters: Record<string, unknown>) {
|
||||||
|
return JSON.stringify(
|
||||||
|
Object.entries(filters)
|
||||||
|
.filter(
|
||||||
|
([key, value]) => key !== "offset" && value !== "" && value != null,
|
||||||
|
)
|
||||||
|
.map(([key, value]) => [key, String(value)])
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SavedViews({
|
export function SavedViews({
|
||||||
filters,
|
filters,
|
||||||
columns,
|
columns,
|
||||||
|
submission,
|
||||||
|
selectedId,
|
||||||
|
onSelect,
|
||||||
|
onSubmissionChange,
|
||||||
onRestore,
|
onRestore,
|
||||||
|
suspended,
|
||||||
|
onOverlay,
|
||||||
}: {
|
}: {
|
||||||
filters: Record<string, unknown>;
|
filters: Record<string, unknown>;
|
||||||
columns: string[];
|
columns: string[];
|
||||||
|
submission: Submission;
|
||||||
|
selectedId: string | null;
|
||||||
|
onSelect: (id: string | null) => void;
|
||||||
|
onSubmissionChange: (submission: string) => void;
|
||||||
onRestore: (view: ViewContent) => void;
|
onRestore: (view: ViewContent) => void;
|
||||||
|
suspended: boolean;
|
||||||
|
onOverlay: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [items, setItems] = useState<View[]>([]);
|
const [items, setItems] = useState<View[]>([]);
|
||||||
|
const [editor, setEditor] = useState<Editor | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState<View | null>(null);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [selected, setSelected] = useState<string>();
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [q, setQ] = useState("");
|
const [revision, setRevision] = useState(0);
|
||||||
const [version, setVersion] = useState(0);
|
const current = items.find((item) => item.id === selectedId);
|
||||||
|
const modified =
|
||||||
|
current &&
|
||||||
|
(filterSignature(filters) !== filterSignature(current.content.filters) ||
|
||||||
|
JSON.stringify(columns) !== JSON.stringify(current.content.columns));
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
api<{ items: View[] }>(
|
async function load() {
|
||||||
`/research/assets?kind=view&q=${encodeURIComponent(q)}&limit=100`,
|
const views: View[] = [];
|
||||||
{ signal: controller.signal },
|
// Tabs replace remote search, so retain access to views beyond the first page.
|
||||||
)
|
while (!controller.signal.aborted) {
|
||||||
.then((r) => setItems(r.items))
|
const result = await api<{ items: View[]; total: number }>(
|
||||||
.catch((e) => {
|
`/research/assets?kind=view&limit=100&offset=${views.length}`,
|
||||||
if (!controller.signal.aborted) Toast.error(e.message);
|
{ signal: controller.signal },
|
||||||
});
|
);
|
||||||
|
views.push(...result.items);
|
||||||
|
if (views.length >= result.total || result.items.length === 0) break;
|
||||||
|
}
|
||||||
|
if (!controller.signal.aborted) setItems(views);
|
||||||
|
}
|
||||||
|
void load().catch((error: Error) => {
|
||||||
|
if (!controller.signal.aborted) Toast.error(error.message);
|
||||||
|
});
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [q, version]);
|
}, [revision]);
|
||||||
async function save(replace: boolean) {
|
|
||||||
|
function openEditor(mode: Editor["mode"], view?: View) {
|
||||||
|
const content = view?.content ?? { name: "", filters, columns };
|
||||||
|
setEditor({ mode, content, view });
|
||||||
|
setName(
|
||||||
|
mode === "copy" ? `${content.name} 副本`.slice(0, 200) : content.name,
|
||||||
|
);
|
||||||
|
onOverlay();
|
||||||
|
}
|
||||||
|
function reportError(error: unknown) {
|
||||||
|
Toast.error((error as Error).message);
|
||||||
|
if (error instanceof ApiError && error.status === 409) {
|
||||||
|
setRevision((value) => value + 1);
|
||||||
|
setEditor(null);
|
||||||
|
setDeleting(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function save(content: ViewContent, old?: View, restore = false) {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const old = items.find((i) => i.id === selected);
|
const value = old
|
||||||
if (replace && !old) return;
|
? await api<View>(`/research/assets/${old.id}`, {
|
||||||
const content = {
|
|
||||||
name: name.trim() || old?.content.name || "研究队列",
|
|
||||||
filters: Object.fromEntries(
|
|
||||||
Object.entries(filters).filter(
|
|
||||||
([, v]) => v !== "" && v !== null && v !== undefined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
columns,
|
|
||||||
};
|
|
||||||
const value = replace
|
|
||||||
? await api<View>(`/research/assets/${old!.id}`, {
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
kind: "view",
|
kind: "view",
|
||||||
version: old!.version,
|
version: old.version,
|
||||||
content,
|
content,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
: await post<View>("/research/assets", { kind: "view", content });
|
: await post<View>("/research/assets", { kind: "view", content });
|
||||||
setSelected(value.id);
|
setItems((previous) =>
|
||||||
setName(value.content.name);
|
old
|
||||||
setVersion((n) => n + 1);
|
? previous.map((item) => (item.id === value.id ? value : item))
|
||||||
|
: [...previous, value],
|
||||||
|
);
|
||||||
|
if (restore) {
|
||||||
|
onSelect(value.id);
|
||||||
|
onRestore(value.content);
|
||||||
|
}
|
||||||
|
setEditor(null);
|
||||||
Toast.success("筛选视图已保存");
|
Toast.success("筛选视图已保存");
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
Toast.error((e as Error).message);
|
reportError(error);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function remove() {
|
||||||
|
if (!deleting) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await api(`/research/assets/${deleting.id}?version=${deleting.version}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
setItems((previous) =>
|
||||||
|
previous.filter((item) => item.id !== deleting.id),
|
||||||
|
);
|
||||||
|
if (selectedId === deleting.id) onSubmissionChange(submission);
|
||||||
|
setDeleting(null);
|
||||||
|
Toast.success("视图已删除");
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="research-saved-views inline-actions">
|
<>
|
||||||
<ResearchSelect
|
<div className="alpha-views-header">
|
||||||
label="保存的 Alpha 视图"
|
<Tabs
|
||||||
placeholder="搜索并恢复研究队列"
|
className="alpha-view-tabs"
|
||||||
filter
|
type="card"
|
||||||
remote
|
collapsible="auto"
|
||||||
onSearch={setQ}
|
activeKey={selectedId ?? submission}
|
||||||
value={selected}
|
onChange={(key) => {
|
||||||
optionList={items.map((i) => ({
|
const view = items.find((item) => item.id === key);
|
||||||
value: i.id,
|
if (view) {
|
||||||
label: `${i.content.name} · v${i.version}`,
|
onSelect(view.id);
|
||||||
}))}
|
onRestore(view.content);
|
||||||
onChange={(v) => {
|
} else onSubmissionChange(key);
|
||||||
const item = items.find((i) => i.id === v);
|
}}
|
||||||
if (item) {
|
tabList={[
|
||||||
setSelected(item.id);
|
{
|
||||||
setName(item.content.name);
|
itemKey: "UNSUBMITTED",
|
||||||
onRestore(item.content);
|
tab: "待提交",
|
||||||
}
|
icon: <IconList aria-hidden="true" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
itemKey: "SUBMITTED",
|
||||||
|
tab: "已提交",
|
||||||
|
icon: <IconList aria-hidden="true" />,
|
||||||
|
},
|
||||||
|
...items.map((view) => ({
|
||||||
|
itemKey: view.id,
|
||||||
|
icon: <IconList aria-hidden="true" />,
|
||||||
|
tab: (
|
||||||
|
<span className="alpha-view-label">
|
||||||
|
<span className="alpha-view-name" title={view.content.name}>
|
||||||
|
{view.content.name}
|
||||||
|
</span>
|
||||||
|
{view.id === selectedId && modified && (
|
||||||
|
<span
|
||||||
|
className="alpha-view-modified"
|
||||||
|
title="视图有未保存的更改"
|
||||||
|
aria-label="有未保存的更改"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onKeyDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
trigger="click"
|
||||||
|
position="bottomLeft"
|
||||||
|
stopPropagation
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Item
|
||||||
|
icon={<IconSave aria-hidden="true" />}
|
||||||
|
disabled={
|
||||||
|
busy || view.id !== selectedId || !modified
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
void save(
|
||||||
|
{ name: view.content.name, filters, columns },
|
||||||
|
view,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
更新当前视图
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
|
icon={<IconEdit aria-hidden="true" />}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => openEditor("rename", view)}
|
||||||
|
>
|
||||||
|
重命名
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
|
icon={<IconCopy aria-hidden="true" />}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => openEditor("copy", view)}
|
||||||
|
>
|
||||||
|
复制视图
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Divider />
|
||||||
|
<Dropdown.Item
|
||||||
|
icon={<IconDelete aria-hidden="true" />}
|
||||||
|
type="danger"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => {
|
||||||
|
setDeleting(view);
|
||||||
|
onOverlay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除视图
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<IconMore aria-hidden="true" />}
|
||||||
|
size="small"
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
aria-label={`${view.content.name} 视图操作`}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="alpha-add-view"
|
||||||
|
icon={<IconPlus aria-hidden="true" />}
|
||||||
|
theme="borderless"
|
||||||
|
type="tertiary"
|
||||||
|
disabled={busy}
|
||||||
|
aria-label="保存为新视图"
|
||||||
|
title="将当前筛选、排序和显示列保存为新视图"
|
||||||
|
onClick={() => openEditor("create")}
|
||||||
|
>
|
||||||
|
新建视图
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
editor?.mode === "rename"
|
||||||
|
? "重命名视图"
|
||||||
|
: editor?.mode === "copy"
|
||||||
|
? "复制视图"
|
||||||
|
: "保存为新视图"
|
||||||
|
}
|
||||||
|
visible={Boolean(editor) && !suspended}
|
||||||
|
onCancel={() => {
|
||||||
|
if (!busy) setEditor(null);
|
||||||
|
}}
|
||||||
|
confirmLoading={busy}
|
||||||
|
okText="保存视图"
|
||||||
|
okButtonProps={{ disabled: !name.trim(), "aria-label": "保存视图" }}
|
||||||
|
onOk={() => {
|
||||||
|
if (editor && name.trim())
|
||||||
|
void save(
|
||||||
|
{ ...editor.content, name: name.trim() },
|
||||||
|
editor.mode === "rename" ? editor.view : undefined,
|
||||||
|
editor.mode !== "rename",
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
aria-label="视图名称"
|
|
||||||
placeholder="视图名称"
|
|
||||||
value={name}
|
|
||||||
onChange={setName}
|
|
||||||
style={{ width: 160 }}
|
|
||||||
/>
|
|
||||||
<Button size="small" disabled={busy} onClick={() => void save(false)}>
|
|
||||||
保存为新视图
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
disabled={busy || !selected}
|
|
||||||
onClick={() => void save(true)}
|
|
||||||
>
|
>
|
||||||
更新当前视图
|
<label>
|
||||||
</Button>
|
视图名称
|
||||||
</div>
|
<Input
|
||||||
|
aria-label="视图名称"
|
||||||
|
placeholder="例如:低换手候选"
|
||||||
|
value={name}
|
||||||
|
onChange={setName}
|
||||||
|
maxLength={200}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{editor?.mode !== "rename" && (
|
||||||
|
<p className="muted">
|
||||||
|
保存筛选条件、排序、每页条数和显示列,之后可从标签栏一键切换。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
title="删除视图"
|
||||||
|
visible={Boolean(deleting) && !suspended}
|
||||||
|
onCancel={() => {
|
||||||
|
if (!busy) setDeleting(null);
|
||||||
|
}}
|
||||||
|
confirmLoading={busy}
|
||||||
|
okText="删除视图"
|
||||||
|
okButtonProps={{ type: "danger", "aria-label": "删除视图" }}
|
||||||
|
onOk={() => void remove()}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
删除“{deleting?.content.name}”后,该视图将从标签栏移除,Alpha
|
||||||
|
数据不受影响。
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ test("model settings, text stream, contextual query, confirmation and preserved
|
|||||||
await page.locator(".settings-drawer .semi-sidesheet-close").click();
|
await page.locator(".settings-drawer .semi-sidesheet-close").click();
|
||||||
await configure(page);
|
await configure(page);
|
||||||
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0001");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0001");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
// Global tools sit below drawers; open the assistant before the Alpha detail.
|
// Global tools sit below drawers; open the assistant before the Alpha detail.
|
||||||
@@ -138,9 +139,11 @@ test("model settings, text stream, contextual query, confirmation and preserved
|
|||||||
chat.getByText("筛选换手率不超过 15% 的 Alpha", { exact: true }),
|
chat.getByText("筛选换手率不超过 15% 的 Alpha", { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await chat.getByRole("button", { name: "应用到列表" }).click();
|
await chat.getByRole("button", { name: "应用到列表" }).click();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await expect(page.getByRole("textbox", { name: "搜索 Alpha" })).toHaveValue(
|
await expect(page.getByRole("textbox", { name: "搜索 Alpha" })).toHaveValue(
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
await page.getByRole("button", { name: "关闭筛选", exact: true }).click();
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: "../output/playwright/ai-query.png",
|
path: "../output/playwright/ai-query.png",
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ test("submission tabs, day selection, local correlation and reload", async ({
|
|||||||
expect(complete.processed).toBe(103);
|
expect(complete.processed).toBe(103);
|
||||||
await expect(page.locator(".job-panel")).toContainText("2025-02-02");
|
await expect(page.locator(".job-panel")).toContainText("2025-02-02");
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0003");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0003");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
||||||
@@ -96,11 +97,15 @@ test("submission tabs, day selection, local correlation and reload", async ({
|
|||||||
await page.getByRole("tab", { name: "待提交", exact: true }).hover();
|
await page.getByRole("tab", { name: "待提交", exact: true }).hover();
|
||||||
|
|
||||||
await page.getByRole("tab", { name: "待提交", exact: true }).click();
|
await page.getByRole("tab", { name: "待提交", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await expect(page.getByRole("textbox", { name: "搜索 Alpha" })).toHaveValue(
|
await expect(page.getByRole("textbox", { name: "搜索 Alpha" })).toHaveValue(
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
await page.getByRole("button", { name: "关闭筛选", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "按天同步", exact: true }).click();
|
await page.getByRole("button", { name: "按天同步", exact: true }).click();
|
||||||
await expect(page.getByRole("dialog")).toContainText("创建日期");
|
await expect(
|
||||||
|
page.getByRole("dialog", { name: "按天同步待提交 Alpha" }),
|
||||||
|
).toContainText("创建日期");
|
||||||
await page.getByLabel("同步开始日期").fill("2025-01-01");
|
await page.getByLabel("同步开始日期").fill("2025-01-01");
|
||||||
await page.getByLabel("同步结束日期").fill("2025-01-01");
|
await page.getByLabel("同步结束日期").fill("2025-01-01");
|
||||||
await page.getByRole("button", { name: "开始同步", exact: true }).click();
|
await page.getByRole("button", { name: "开始同步", exact: true }).click();
|
||||||
@@ -115,6 +120,7 @@ test("submission tabs, day selection, local correlation and reload", async ({
|
|||||||
)
|
)
|
||||||
.toBe(413);
|
.toBe(413);
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0004");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0004");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
await page.route("**/api/v1/alphas/TEST0004", async (route) => {
|
await page.route("**/api/v1/alphas/TEST0004", async (route) => {
|
||||||
@@ -147,6 +153,7 @@ test("submission tabs, day selection, local correlation and reload", async ({
|
|||||||
detail.is_metrics.unavailable = null;
|
detail.is_metrics.unavailable = null;
|
||||||
await route.fulfill({ response, json: detail });
|
await route.fulfill({ response, json: detail });
|
||||||
});
|
});
|
||||||
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
||||||
await page.locator(".alpha-link").click();
|
await page.locator(".alpha-link").click();
|
||||||
const sheet = page.locator(".alpha-detail");
|
const sheet = page.locator(".alpha-detail");
|
||||||
await expect(sheet.getByRole("tab")).toHaveText([
|
await expect(sheet.getByRole("tab")).toHaveText([
|
||||||
@@ -290,6 +297,7 @@ test("submission tabs, day selection, local correlation and reload", async ({
|
|||||||
});
|
});
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0004");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0004");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-table:visible")).toContainText(
|
await expect(page.locator(".alpha-table:visible")).toContainText(
|
||||||
|
|||||||
@@ -100,8 +100,9 @@ test("feature conversion, saved views and immutable evaluations", async ({
|
|||||||
]);
|
]);
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
await page.getByRole("button", { name: "Alpha 管理", exact: true }).click();
|
||||||
await page.getByLabel("视图名称", { exact: true }).fill("浏览器常用队列");
|
|
||||||
await page.getByRole("button", { name: "保存为新视图" }).click();
|
await page.getByRole("button", { name: "保存为新视图" }).click();
|
||||||
|
await page.getByLabel("视图名称", { exact: true }).fill("浏览器常用队列");
|
||||||
|
await page.getByRole("button", { name: "保存视图", exact: true }).click();
|
||||||
await expect(page.getByText("筛选视图已保存", { exact: true })).toBeVisible();
|
await expect(page.getByText("筛选视图已保存", { exact: true })).toBeVisible();
|
||||||
const views = await (
|
const views = await (
|
||||||
await page.request.get("/api/v1/research/assets?kind=view&q=浏览器常用队列")
|
await page.request.get("/api/v1/research/assets?kind=view&q=浏览器常用队列")
|
||||||
@@ -137,6 +138,7 @@ test("feature conversion, saved views and immutable evaluations", async ({
|
|||||||
)
|
)
|
||||||
.toBe("completed");
|
.toBe("completed");
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0001");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0001");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
||||||
|
|||||||
@@ -84,11 +84,12 @@ test("account → scoped sync → research → resync → PnL → filtered expor
|
|||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
await expect(page.locator(".job-panel")).not.toBeVisible();
|
await expect(page.locator(".job-panel")).not.toBeVisible();
|
||||||
await page.getByRole("tab", { name: "待提交", exact: true }).click();
|
await page.getByRole("tab", { name: "待提交", exact: true }).click();
|
||||||
await expect(page.locator(".table-toolbar .count-pill")).toHaveText("413");
|
await expect(page.locator(".alpha-table-meta .count-pill")).toHaveText("413");
|
||||||
await page.screenshot({
|
await page.screenshot({
|
||||||
path: "../output/playwright/alpha-library.png",
|
path: "../output/playwright/alpha-library.png",
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
});
|
});
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
||||||
@@ -141,6 +142,7 @@ test("account → scoped sync → research → resync → PnL → filtered expor
|
|||||||
)
|
)
|
||||||
.toBe("completed");
|
.toBe("completed");
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST0007");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
await expect(page.locator(".alpha-link")).toHaveCount(1);
|
||||||
@@ -153,6 +155,7 @@ test("account → scoped sync → research → resync → PnL → filtered expor
|
|||||||
page.getByRole("checkbox", { name: "收藏此 Alpha" }),
|
page.getByRole("checkbox", { name: "收藏此 Alpha" }),
|
||||||
).toBeChecked();
|
).toBeChecked();
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "重置", exact: true }).click();
|
await page.getByRole("button", { name: "重置", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-link")).toHaveCount(25);
|
await expect(page.locator(".alpha-link")).toHaveCount(25);
|
||||||
const downloading = page.waitForEvent("download");
|
const downloading = page.waitForEvent("download");
|
||||||
@@ -217,6 +220,7 @@ test("batch tags, column visibility, server pagination and saved preferences", a
|
|||||||
)
|
)
|
||||||
.toBe(620);
|
.toBe(620);
|
||||||
}
|
}
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST006");
|
await page.getByRole("textbox", { name: "搜索 Alpha" }).fill("TEST006");
|
||||||
await page.getByRole("button", { name: "查询", exact: true }).click();
|
await page.getByRole("button", { name: "查询", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-link")).toHaveCount(6);
|
await expect(page.locator(".alpha-link")).toHaveCount(6);
|
||||||
@@ -258,6 +262,7 @@ test("batch tags, column visibility, server pagination and saved preferences", a
|
|||||||
page.getByRole("columnheader", { name: "自定义标签", exact: true }),
|
page.getByRole("columnheader", { name: "自定义标签", exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await page.getByRole("button", { name: "显示列设置" }).click();
|
await page.getByRole("button", { name: "显示列设置" }).click();
|
||||||
|
await page.getByRole("button", { name: "筛选 Alpha", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "重置", exact: true }).click();
|
await page.getByRole("button", { name: "重置", exact: true }).click();
|
||||||
await expect(page.locator(".alpha-link")).toHaveCount(25);
|
await expect(page.locator(".alpha-link")).toHaveCount(25);
|
||||||
const firstId = await page.locator(".alpha-link").first().innerText();
|
const firstId = await page.locator(".alpha-link").first().innerText();
|
||||||
|
|||||||
Reference in New Issue
Block a user