From 1e67f43012c66f8e0f2051e88c2265063317fa40 Mon Sep 17 00:00:00 2001 From: yuxuanhui Date: Thu, 10 Sep 2026 14:55:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=20Alpha=20=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E7=AD=BE=E4=B8=8E=E7=AD=9B=E9=80=89=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A0=8F=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../issues/01-implementation.md | 18 + .scratch/alpha-view-toolbar/spec.md | 9 + frontend/src/pages/AlphaPage.css | 190 +++++ frontend/src/pages/AlphaPage.tsx | 793 ++++++++++-------- frontend/src/research/SavedViews.tsx | 375 +++++++-- frontend/tests/ai.spec.ts | 3 + frontend/tests/alpha-management.spec.ts | 10 +- frontend/tests/research-outcomes.spec.ts | 4 +- frontend/tests/workspace.spec.ts | 7 +- 9 files changed, 995 insertions(+), 414 deletions(-) create mode 100644 .scratch/alpha-view-toolbar/issues/01-implementation.md create mode 100644 .scratch/alpha-view-toolbar/spec.md create mode 100644 frontend/src/pages/AlphaPage.css diff --git a/.scratch/alpha-view-toolbar/issues/01-implementation.md b/.scratch/alpha-view-toolbar/issues/01-implementation.md new file mode 100644 index 0000000..b367a60 --- /dev/null +++ b/.scratch/alpha-view-toolbar/issues/01-implementation.md @@ -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 diff --git a/.scratch/alpha-view-toolbar/spec.md b/.scratch/alpha-view-toolbar/spec.md new file mode 100644 index 0000000..8e0c58e --- /dev/null +++ b/.scratch/alpha-view-toolbar/spec.md @@ -0,0 +1,9 @@ +# Alpha 视图与筛选工具栏 + +按用户截图调整交互,保留 Semi Design 2.103.0。 + +- 待提交、已提交及保存的视图统一为标签栏;新增视图通过命名弹窗完成,标签菜单支持更新、重命名、复制及删除。 +- 工具栏集中筛选、排序、列设置、导入导出、同步及表格批量操作。搜索仅在筛选弹层内显示。 +- 保留全部现有筛选字段及查询/重置行为;应用后关闭弹层,入口显示已应用数量;切换视图清理草稿与行选择。 +- 复用研究素材视图接口及版本锁,不改后端数据模型。没有后端支持的保护视图/设为首项不在范围内。 +- 本地生产构建、受影响的既有浏览器回归,以及隔离模拟环境中的视图保存恢复、菜单、筛选弹层、窄屏视觉验收。 diff --git a/frontend/src/pages/AlphaPage.css b/frontend/src/pages/AlphaPage.css new file mode 100644 index 0000000..46c1920 --- /dev/null +++ b/frontend/src/pages/AlphaPage.css @@ -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); + } +} diff --git a/frontend/src/pages/AlphaPage.tsx b/frontend/src/pages/AlphaPage.tsx index 4e0ec68..92206ef 100644 --- a/frontend/src/pages/AlphaPage.tsx +++ b/frontend/src/pages/AlphaPage.tsx @@ -11,12 +11,22 @@ import { Pagination, Select, Table, - Tabs, Tag, TextArea, Toast, Typography, } 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 { api, @@ -144,6 +154,8 @@ export function AlphaPage({ const [facets, setFacets] = useState({}); const [draft, setDraft] = useState>({}); const [filters, setFilters] = useState>({}); + const [filterOpen, setFilterOpen] = useState(false); + const [selectedViewId, setSelectedViewId] = useState(null); const [submission, setSubmission] = useState("UNSUBMITTED"); const [syncingScope, setSyncingScope] = useState(null); const [page, setPage] = useState(1); @@ -238,6 +250,8 @@ export function AlphaPage({ setDetailId(action.alpha_id); onOverlay(); } else { + setSelectedViewId(null); + setFilterOpen(false); const { sort: nextSort, direction: nextDirection, @@ -296,15 +310,20 @@ export function AlphaPage({ })); } function applyFilters() { + setFilterOpen(false); setFilters({ ...draft }); setPage(1); } function clearFilters() { + setFilterOpen(false); setDraft({}); setFilters({}); setPage(1); } function changeSubmission(value: string) { + setSelectedViewId(null); + setFilterOpen(false); + setDirection("desc"); setSubmission(value as Submission); setDraft({}); setFilters({}); @@ -610,6 +629,233 @@ export function AlphaPage({ const connected = account?.connection_status === "connected" || account?.connection_status === "expired"; + const filterPanel = ( +
{ + e.preventDefault(); + applyFilters(); + }} + > +
+
+

筛选 Alpha

+

设置搜索和筛选条件,查询后应用到当前视图。

+
+
+
+
+ + + + + + {[ + ["universe", "Universe"], + ["alpha_type", "Alpha 类型"], + ["language", "语言"], + ["stage", "IS / OS"], + ].map(([key, label]) => ( + + + + + {Object.entries(metricLabels).map(([key, label]) => ( + + ))} + + updateDraft("favorite", e.target.checked ? "true" : "") + } + > + 只看收藏 + +
+

+ 数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。 + 检查结果仅汇总已同步的平台检查;预检通过不代表可以提交,活动资格可用自定义标签记录。 +

+ +
+
+ + +
+
+ ); return ( <> {!connected && ( @@ -627,223 +873,17 @@ export function AlphaPage({ )} {error && }
-
- - - 最近数据更新{" "} - - {formatTime( - typeof facets.last_sync === "string" ? facets.last_sync : null, - account?.timezone, - )} - - -
-
{ - e.preventDefault(); - applyFilters(); - }} - > -
- updateDraft("q", v)} - showClear - /> - updateDraft("status", v)} - /> - ({ - value, - label, - }))} - onChange={(v) => updateDraft("check_type", v)} - /> - ({ - ...option, - label: sourceLabel(option.value), - }))} - placeholder="全部来源" - onChange={(v) => updateDraft("source", v)} - /> - - - {[ - ["universe", "Universe"], - ["alpha_type", "Alpha 类型"], - ["language", "语言"], - ["stage", "IS / OS"], - ].map(([key, label]) => ( - - - - - {Object.entries(metricLabels).map(([key, label]) => ( - - ))} - - updateDraft("favorite", e.target.checked ? "true" : "") - } - > - 只看收藏 - -
-

- 数值筛选使用平台原始值,例如 Turnover 0.15 表示 15%。 - 检查结果仅汇总已同步的平台检查;预检通过不代表可以提交,活动资格可用自定义标签记录。 -

- -
- - -
-
{ + setFilterOpen(false); const { submission: savedSubmission, sort: savedSort, @@ -861,11 +901,9 @@ export function AlphaPage({ setDraft(values); setPage(1); setSelected([]); - if ( - savedSubmission === "SUBMITTED" || - savedSubmission === "UNSUBMITTED" - ) - setSubmission(savedSubmission); + setSubmission( + savedSubmission === "SUBMITTED" ? "SUBMITTED" : "UNSUBMITTED", + ); setSort(String(savedSort || "date_created")); setDirection(String(savedDirection || "desc")); if (typeof limit === "number") setPageSize(limit); @@ -879,104 +917,148 @@ export function AlphaPage({ localStorage.setItem("alpha-columns", JSON.stringify(next)); }} /> - -
-
- - {submission === "UNSUBMITTED" ? "待提交" : "已提交"} Alpha - - {data.total.toLocaleString()} - {selected.length > 0 && ( - - 已选择 {selected.length} 条 - - )} +
+
+ setFilterOpen(false)} + onEscKeyDown={() => setFilterOpen(false)} + content={filterPanel} + > + + + + 显示列 + ({ + value, + label, + disabled: value === "name", + }), + )} + onChange={(values) => { + const next = values.map(String); + setVisibleColumns(next); + localStorage.setItem( + "alpha-columns", + JSON.stringify(next), + ); + }} + /> +
+ } + > + + + +

排序

+ { + setDirection(String(v)); + setPage(1); + }} + /> +
+ } + > + +
-
- {selected.length > 0 && ( - <> - - - - - )} - { - setDirection(String(v)); - setPage(1); - }} - /> +
- )} - - 显示列 - ({ - value, - label, - disabled: value === "name", - }), - )} - onChange={(values) => { - const next = values.map(String); - setVisibleColumns(next); - localStorage.setItem( - "alpha-columns", - JSON.stringify(next), - ); - }} - /> -
- } - > - -
+ {selected.length > 0 && ( +
+ 已选择 {selected.length} 条 + + + + + +
+ )} columns={columns} dataSource={data.items} @@ -1075,7 +1173,24 @@ export function AlphaPage({ } />