From 82619489a91246844121e1442d26c649b3286764 Mon Sep 17 00:00:00 2001 From: Citrons <909229731@qq.com> Date: Tue, 18 Aug 2026 15:28:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(FEAT-MO-001):=20=E5=AE=8C=E6=88=90TDX?= =?UTF-8?q?=E5=B8=82=E5=9C=BA=E6=A6=82=E8=A7=88=E9=A6=96=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FEAT-MO-001-market-overview/design.md | 25 +- .../development-summary.md | 48 ++ .../gitnexus/after.md | 61 +++ .../gitnexus/comparison.md | 16 + .../gitnexus/detected-changes.md | 19 + .../implementation-plan.md | 13 +- .../FEAT-MO-001-market-overview/manifest.yaml | 4 +- .../FEAT-MO-001-market-overview/prd.md | 9 +- .../qa/test-results.md | 15 + .../qa/visual-qa.md | 16 + .../release-notes.md | 13 + .../requirements.md | 9 +- .../review/code-review.md | 30 ++ .../review/standards-review.md | 18 + .../FEAT-MO-001-market-overview/rollback.md | 17 + .../FEAT-MO-001-market-overview/test-plan.md | 28 +- eslint.config.mjs | 9 + .../market-overview.service.test.ts | 11 +- .../market-overview.service.ts | 15 +- .../market-overview/tdx/tdx.client.test.ts | 28 +- .../apps/api/test/market-overview.e2e-spec.ts | 19 +- server/tools/export-openapi.ts | 5 +- server/tools/run-test-api.mjs | 3 + web/src/api/market-overview/index.ts | 15 +- web/src/app/App.test.ts | 25 +- web/src/app/App.vue | 10 +- web/src/app/router.ts | 8 +- web/src/styles/index.scss | 475 +++++++++++++++++- .../market-overview/MarketOverviewPage.vue | 96 ++++ .../market-overview/components/FundTrend.vue | 23 + .../components/IndexOverview.vue | 41 ++ .../components/MarketBreadth.vue | 34 ++ .../components/MarketConclusion.vue | 42 ++ .../components/MarketDrivers.vue | 33 ++ .../components/SectorOverview.vue | 34 ++ .../market-overview/useMarketOverview.test.ts | 9 +- .../market-overview/useMarketOverview.ts | 7 +- web/tests/smoke.spec.ts | 1 + 38 files changed, 1230 insertions(+), 54 deletions(-) create mode 100644 docs/features/FEAT-MO-001-market-overview/development-summary.md create mode 100644 docs/features/FEAT-MO-001-market-overview/gitnexus/after.md create mode 100644 docs/features/FEAT-MO-001-market-overview/gitnexus/comparison.md create mode 100644 docs/features/FEAT-MO-001-market-overview/gitnexus/detected-changes.md create mode 100644 docs/features/FEAT-MO-001-market-overview/qa/test-results.md create mode 100644 docs/features/FEAT-MO-001-market-overview/qa/visual-qa.md create mode 100644 docs/features/FEAT-MO-001-market-overview/release-notes.md create mode 100644 docs/features/FEAT-MO-001-market-overview/review/code-review.md create mode 100644 docs/features/FEAT-MO-001-market-overview/review/standards-review.md create mode 100644 docs/features/FEAT-MO-001-market-overview/rollback.md create mode 100644 web/src/views/market-overview/MarketOverviewPage.vue create mode 100644 web/src/views/market-overview/components/FundTrend.vue create mode 100644 web/src/views/market-overview/components/IndexOverview.vue create mode 100644 web/src/views/market-overview/components/MarketBreadth.vue create mode 100644 web/src/views/market-overview/components/MarketConclusion.vue create mode 100644 web/src/views/market-overview/components/MarketDrivers.vue create mode 100644 web/src/views/market-overview/components/SectorOverview.vue diff --git a/docs/features/FEAT-MO-001-market-overview/design.md b/docs/features/FEAT-MO-001-market-overview/design.md index f3df785..fa42c15 100644 --- a/docs/features/FEAT-MO-001-market-overview/design.md +++ b/docs/features/FEAT-MO-001-market-overview/design.md @@ -1,5 +1,28 @@ # 设计 -首屏顺序为:市场结论、驱动与风险、核心指数、市场宽度与量能、板块概览、情绪与资金摘要。分析偏好保存在浏览器本地,页面只显示轻量的当前状态入口。 +首屏顺序为:市场结论、驱动与风险、核心指数、市场宽度与量能、板块概览、资金趋势。分析偏好保存在浏览器本地,页面显示轻量的当前状态入口。Vue 不直连 TDX,由 Nest API 按模块拆分接口并隔离失败。 + +## 数据可用性 + +- `summary` 和 `indices`:使用 TDX K 线真实响应,展示来源、快照时间和指数涨跌。 +- `breadth`、`sectors`、`funds`:桥接层暂未注册对应方法,使用 `unavailable` 响应和明确原因。 +- 页面不把不可用指标降级为模拟值;后续补齐桥接方法时只替换后端映射,不改变前端契约。 + +## 运行边界 + +本功能只读调用 TDX。MySQL、Redis、RocketMQ 仍为已规划基础设施,但当前没有业务状态、缓存或异步任务需要它们,因此本切片不启用。 + +## 安全策略与评估 + +- TDX 地址在配置层固定为 `http://127.0.0.1:17710`,API 是唯一访问边界,Vue 不直连行情服务。 +- 请求只允许固定方法和公开行情参数;客户端限制超时与 1 MiB 响应,异常脱敏,不记录原始载荷、凭据或网络地址。 +- 本功能不提供交易写入、账户操作、用户输入拼接查询或 AI 密钥下发;MySQL/Redis/RocketMQ 未启用,不读取生产数据库。 +- 已通过客户端、服务、HTTP/OpenAPI、边界和 E2E 测试;生产授权、长期断线恢复和公网安全仍需后续评估。 + +## 性能目标与评估 + +- 五个模块并行请求,单个 TDX 请求受配置超时保护,模块失败不阻断其他模块。 +- 客户端限制响应大小,避免异常行情载荷占用过多内存;本切片不增加数据库、缓存或消息队列负载。 +- 已完成构建、浏览器桌面/390px 移动端验证;未进行 7×24 采集、峰值并发和长期网络抖动压测。 ETF监控、两融数据、大盘云图和实时消息仍是独立一级菜单,不属于短线和中长线切换。 diff --git a/docs/features/FEAT-MO-001-market-overview/development-summary.md b/docs/features/FEAT-MO-001-market-overview/development-summary.md new file mode 100644 index 0000000..0ef6e29 --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/development-summary.md @@ -0,0 +1,48 @@ +# FEAT-MO-001 开发总结 + +## 原始开发需求 + +实现市场概览首屏:结论优先展示市场状态、驱动与风险,并支持综合、短线、中长线视角和规则、AI、混合、对照分析选择;用户可从指数和板块入口进入后续详细分析。 + +## 需求与范围变化 + +- 数据源从早期“确定性模拟快照”调整为 TDX 只读直连,Nest API 访问本机 `127.0.0.1:17710`,Vue 不直接访问 TDX。 +- API 按摘要、指数、市场宽度、板块和资金拆分,避免把整个首屏塞入单个接口。 +- 当前 TDX 桥接层仅确认 `get_market_data` 可用;宽度、板块和资金方法未注册,因此对应模块明确返回 `unavailable`。 +- MySQL、Redis、RocketMQ 和 AI 仍不进入本切片运行链路;它们保留为后续基础设施边界。 + +## 实际完成需求 + +- 新增 TDX 配置校验、超时/响应大小限制、错误脱敏的 Node 客户端。 +- 新增市场概览 DTO、服务、控制器、OpenAPI 契约和前端 Axios API 文件。 +- 完成结论、驱动与风险、核心指数、市场宽度、板块、资金趋势模块,以及加载/错误/不可用状态。 +- 完成分析视角和分析引擎切换,本地保存用户偏好。 +- 真实 TDX 指数和摘要已通过本机隧道冒烟;未用指数或离线样本伪造缺失指标。 + +## 修复缺陷 + +- 修正 PRD 和实施计划中仍描述“模拟数据”的旧表述。 +- 避免单个 TDX 模块失败导致整页失败,改为五个模块并行、独立状态。 +- 对未注册桥接方法返回结构化原因,避免前端显示“实时”或空白数据。 +- 修复 Vue TypeScript 单文件组件未被 ESLint 正确解析的问题,并为 E2E 测试进程补齐必需的本机 TDX 配置边界。 +- 隔离应用外壳单测中的真实 Axios 请求,消除 JSDOM 网络错误噪声,保持页面测试只验证外壳职责。 + +## 未完成与延期项 + +- TDX 桥接层补齐市场宽度、板块排行、资金趋势方法后再实现真实图表和口径。 +- “查看依据”抽屉、指数/板块详情页、AI 请求和用户体系另立功能。 +- 本地验证不代表生产 7×24 采集稳定性、商业授权或公网部署安全性。 + +## 关键提交 + +- `9a965fa`、`d283ee3`、`6eb34b5`、`c2fa929`、`15a2500`、`27bc3c1` +- 首屏 UI 收口提交待 Review 通过后补充。 + +## 验证证据 + +- GitNexus 已完成最终重新索引;提交前 CLI `detect-changes --scope staged` 映射 11 条概览/E2E 执行流并标记 high。高风险来自本功能修改核心概览服务和测试启动入口,已由定向单测、HTTP/OpenAPI、构建和 E2E 覆盖;未发现额外业务模块调用方。MCP context 在当前 worktree 未初始化,因此以 CLI 索引和静态调用证据补充记录。 +- API 冒烟:`/summary`、`/indices` 返回 `source=tdx,status=fresh`。 +- 前端单测、typecheck、build、后端 TDX/映射/服务/HTTP/OpenAPI 测试通过。 +- `pnpm verify:e2e`:4/4 通过(桌面和移动端各覆盖页面与健康契约)。 +- 浏览器桌面与 390px 移动视口验证无横向溢出,视角与引擎切换可用。 +- 开发 Schema 按用户确认跳过,沿用 `v0.0.10 captured_empty` 追溯边界,不读取生产库。 diff --git a/docs/features/FEAT-MO-001-market-overview/gitnexus/after.md b/docs/features/FEAT-MO-001-market-overview/gitnexus/after.md new file mode 100644 index 0000000..bc4c8a0 --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/gitnexus/after.md @@ -0,0 +1,61 @@ +# GitNexus 开发后基线 + +## GitNexus 状态 + +```text +Repository: /Users/citrons/Documents/Codex/2026-08-12/wo-x/a-share-analysis/.worktrees/feat-mo-001-tdx-overview +Indexed: 8/18/2026, 3:16:44 PM +Indexed commit: 27bc3c1 +Current commit: 27bc3c1 +Status: ✅ up-to-date +``` + +## Git 已跟踪变更摘要 + +```text +AGENTS.md | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) +``` + +## Git 工作区摘要 + +```text +M AGENTS.md +M docs/features/FEAT-MO-001-market-overview/design.md +A docs/features/FEAT-MO-001-market-overview/development-summary.md +A docs/features/FEAT-MO-001-market-overview/gitnexus/comparison.md +A docs/features/FEAT-MO-001-market-overview/gitnexus/detected-changes.md +M docs/features/FEAT-MO-001-market-overview/implementation-plan.md +M docs/features/FEAT-MO-001-market-overview/manifest.yaml +M docs/features/FEAT-MO-001-market-overview/prd.md +A docs/features/FEAT-MO-001-market-overview/qa/test-results.md +A docs/features/FEAT-MO-001-market-overview/qa/visual-qa.md +A docs/features/FEAT-MO-001-market-overview/release-notes.md +M docs/features/FEAT-MO-001-market-overview/requirements.md +A docs/features/FEAT-MO-001-market-overview/review/code-review.md +A docs/features/FEAT-MO-001-market-overview/review/standards-review.md +A docs/features/FEAT-MO-001-market-overview/rollback.md +M docs/features/FEAT-MO-001-market-overview/test-plan.md +M eslint.config.mjs +M server/apps/api/src/market-overview/market-overview.service.test.ts +M server/apps/api/src/market-overview/market-overview.service.ts +M server/apps/api/src/market-overview/tdx/tdx.client.test.ts +M server/apps/api/test/market-overview.e2e-spec.ts +M server/tools/export-openapi.ts +M server/tools/run-test-api.mjs +M web/src/api/market-overview/index.ts +M web/src/app/App.test.ts +M web/src/app/App.vue +M web/src/app/router.ts +M web/src/styles/index.scss +A web/src/views/market-overview/MarketOverviewPage.vue +A web/src/views/market-overview/components/FundTrend.vue +A web/src/views/market-overview/components/IndexOverview.vue +A web/src/views/market-overview/components/MarketBreadth.vue +A web/src/views/market-overview/components/MarketConclusion.vue +A web/src/views/market-overview/components/MarketDrivers.vue +A web/src/views/market-overview/components/SectorOverview.vue +M web/src/views/market-overview/useMarketOverview.test.ts +M web/src/views/market-overview/useMarketOverview.ts +M web/tests/smoke.spec.ts +``` diff --git a/docs/features/FEAT-MO-001-market-overview/gitnexus/comparison.md b/docs/features/FEAT-MO-001-market-overview/gitnexus/comparison.md new file mode 100644 index 0000000..cabe71e --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/gitnexus/comparison.md @@ -0,0 +1,16 @@ +# GitNexus 前后对比 + +## 开发前 + +- 基线:`gitnexus/before.md`、`gitnexus/implementation-baseline.md`。 +- 目标是把模拟首屏改为 TDX 只读概览,并拆分五个 HTTP 模块接口。 + +## 开发后 + +- CLI 已完成最终重新索引,保留 77 条执行流并识别本功能影响路径。 +- 变更检测识别 11 条本功能执行流,核心路径均有测试和 E2E 验证。 +- 未新增消息、数据库、Redis、Scheduler、Worker 或部署运行时依赖。 + +## 差异结论 + +直接影响集中于 TDX API、市场概览服务、前端 API/页面和测试夹具;生产数据边界、基础设施暂不启用边界和回退路径保持不变。 diff --git a/docs/features/FEAT-MO-001-market-overview/gitnexus/detected-changes.md b/docs/features/FEAT-MO-001-market-overview/gitnexus/detected-changes.md new file mode 100644 index 0000000..540c60b --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/gitnexus/detected-changes.md @@ -0,0 +1,19 @@ +# GitNexus 开发后变更检测 + +## 检测命令 + +```text +npx gitnexus analyze +npx gitnexus detect-changes --repo "$PWD" --scope staged +``` + +## 结果 + +- 索引:已完成最终增量更新。 +- 暂存文件:38 个文件、87 个符号(包含完整功能档案)。 +- 影响执行流:11 条,CLI 风险:high。 +- 主要执行流:`GetSummary`、`GetBreadth`、`GetSectors`、`GetFunds` 和 `RunTestApi`。 + +## 处置 + +high 来自本功能新增并修改市场概览核心服务、共享快照元数据函数和 E2E 启动入口,不是未知跨业务扩散。已用 TDX 客户端/映射/服务/HTTP/OpenAPI 单测、边界测试、构建和 4/4 E2E 覆盖;未发现用户、消息、数据库或 Scheduler/Worker 业务调用方。MCP context 在当前 worktree 未初始化,未将其缺失误判为低风险。 diff --git a/docs/features/FEAT-MO-001-market-overview/implementation-plan.md b/docs/features/FEAT-MO-001-market-overview/implementation-plan.md index 7734490..e9248f2 100644 --- a/docs/features/FEAT-MO-001-market-overview/implementation-plan.md +++ b/docs/features/FEAT-MO-001-market-overview/implementation-plan.md @@ -1,8 +1,9 @@ # 实施计划 -1. 初始化 Vue 3、Vite、TypeScript和 Dart Sass。 -2. 建立市场快照类型和确定性模拟数据。 -3. 实现终端外壳、数据状态和结论区。 -4. 实现指数、宽度、量能、板块、情绪与资金模块。 -5. 实现分析偏好菜单和依据抽屉。 -6. 完成测试、构建、浏览器验证和视觉对比。 +1. ~~初始化 Vue 3、Vite、TypeScript 和 Dart Sass。~~ 已完成。 +2. ~~建立 TDX 配置、Node fetch 客户端、DTO、映射和五个模块接口。~~ 已完成。 +3. ~~实现终端外壳、分模块加载状态和结论区。~~ 已完成。 +4. ~~实现指数、宽度、板块和资金模块;不可用指标使用结构化状态。~~ 已完成。 +5. ~~实现分析偏好菜单和本地持久化。~~ 已完成;依据抽屉和详情页入口延期至独立功能。 +6. ~~完成单元测试、类型检查、构建、接口冒烟、浏览器桌面/移动端视觉验证。~~ 已完成。 +7. 补齐 TDX 桥接层的宽度、板块和资金方法后,再单独更新对应数据口径和图表需求。 diff --git a/docs/features/FEAT-MO-001-market-overview/manifest.yaml b/docs/features/FEAT-MO-001-market-overview/manifest.yaml index 4d892aa..daa7df6 100644 --- a/docs/features/FEAT-MO-001-market-overview/manifest.yaml +++ b/docs/features/FEAT-MO-001-market-overview/manifest.yaml @@ -1,12 +1,12 @@ id: FEAT-MO-001 name: 市场综合概览 slug: market-overview -status: approved +status: in_review owner: project created_at: 2026-08-12 requirements: requirements.md prd: prd.md design: design.md implementation_plan: implementation-plan.md -traceability_note: 开发尚未开始,GitNexus与QA证据将在开发门禁执行时创建 +traceability_note: 已完成 TDX 直连开发、GitNexus 前后分析、接口冒烟和桌面/移动端视觉验证;待代码 Review、规范 Review 和 governance:finish 收口 release: unreleased diff --git a/docs/features/FEAT-MO-001-market-overview/prd.md b/docs/features/FEAT-MO-001-market-overview/prd.md index f7337b5..a52c989 100644 --- a/docs/features/FEAT-MO-001-market-overview/prd.md +++ b/docs/features/FEAT-MO-001-market-overview/prd.md @@ -23,7 +23,7 @@ ## 非目标 - 不做自选股、持仓、笔记、交易计划、社区、荐股、跟单和公开组合。 -- 本切片不接正式行情源、RocketMQ、MySQL、Redis和付费 AI。 +- 本切片直接接入已验证的 TDX 只读桥接层;不接入 RocketMQ、MySQL、Redis 和付费 AI。 - 本切片不实现用户、邀请、会员、订阅和支付。 - ETF监控、两融数据、大盘云图和实时消息仍是独立一级模块。 @@ -41,7 +41,7 @@ | 编号 | 优先级 | 需求 | 可观察结果 | |---|---|---|---| | FR-001 | P0 | 结论优先展示市场定性 | 首屏顶部出现一句结论、市场状态、量能状态和情绪状态 | -| FR-002 | P0 | 展示驱动与风险 | 结论下方并列显示至少三条驱动和三条风险证据 | +| FR-002 | P0 | 展示驱动与风险 | 结论下方并列显示已由 TDX 支持的驱动和风险证据;缺少口径时明确标记 | | FR-003 | P0 | 展示数据新鲜度 | 页面显示交易状态、数据更新时间和正常/延迟/不完整状态 | | FR-004 | P0 | 提供分析偏好 | 可选择综合/短线/中长线和规则/AI/混合/对照,并在本地保存默认值 | | FR-005 | P1 | 展示市场证据模块 | 指数、宽度、量能、板块、情绪和资金摘要可阅读 | @@ -50,7 +50,7 @@ ## 数据与口径 -首个切片使用确定性模拟快照。字段仍必须包含交易日期、快照时间、来源状态、完整性和单位,以便以后无结构变更地替换为后端 API。 +首个切片由 Nest API 直接读取 TDX 只读桥接层。指数和摘要使用真实 TDX K 线映射;当前桥接层未提供市场宽度、板块排行和资金趋势方法,因此这些模块返回结构化 `unavailable`,不得用指数或离线样本冒充。所有响应仍包含交易日期、快照时间、来源状态、完整性和单位。 A 股使用红涨绿跌。成交额统一展示为亿元,比例明确使用百分比。历史数据和低频资金数据必须显示实际数据日期,旧数据不得标记为实时。 @@ -77,7 +77,7 @@ A 股使用红涨绿跌。成交额统一展示为亿元,比例明确使用百 - Given 页面加载有效快照,When 用户进入市场概览,Then 结论、驱动、风险和更新时间在首屏可见。 - Given 用户打开分析状态入口,When 切换周期或引擎,Then 页面状态立即更新且可选择设为默认。 - Given 数据快照被标记为延迟,When 页面渲染,Then 最近有效值保留并明确显示延迟,不能显示“实时”。 -- Given 用户点击查看依据,When 抽屉打开,Then 能看到指标名称、数值、口径和更新时间。 +- Given 用户看到查看依据入口,When 后续依据抽屉功能立项,Then 能看到指标名称、数值、口径和更新时间;本切片只保留入口。 ## 风险与依赖 @@ -92,3 +92,4 @@ A 股使用红涨绿跌。成交额统一展示为亿元,比例明确使用百 ## 变更记录 - 0.1:建立首版 PRD,补充结论优先、分析偏好和异常状态。 +- 0.2:根据 TDX 实连验证修正数据源、可用性边界和未接入基础设施范围。 diff --git a/docs/features/FEAT-MO-001-market-overview/qa/test-results.md b/docs/features/FEAT-MO-001-market-overview/qa/test-results.md new file mode 100644 index 0000000..e10ce53 --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/qa/test-results.md @@ -0,0 +1,15 @@ +# FEAT-MO-001 测试结果 + +## 自动化结果 + +- 前端 Vitest:7 个文件、14 个测试通过。 +- 后端 Vitest/Node:11 个文件、40 个测试通过;Schema、治理、边界和部署门禁通过。 +- `pnpm verify:e2e`:桌面/移动端页面和健康契约共 4 个测试通过。 +- `pnpm typecheck`、`pnpm build`、`pnpm format:check`:通过。 +- ESLint:0 error,保留 35 条 Vue 风格 warning,不阻断门禁。 + +## 手工结果 + +- `GET /api/v1/market-overview/indices` 和 `summary` 返回真实 TDX `fresh` 快照。 +- `breadth`、`sectors`、`funds` 返回明确 `unavailable`,未伪造指标。 +- 桌面默认视口与 390px 移动视口无横向溢出;视角和引擎切换可用。 diff --git a/docs/features/FEAT-MO-001-market-overview/qa/visual-qa.md b/docs/features/FEAT-MO-001-market-overview/qa/visual-qa.md new file mode 100644 index 0000000..1c65393 --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/qa/visual-qa.md @@ -0,0 +1,16 @@ +# FEAT-MO-001 视觉 QA + +## 验证范围 + +- 桌面默认视口:顶部应用识别、结论区、驱动/风险、核心指数和不可用模块均可见。 +- 移动视口:390×844,首屏按单列排列,标题、状态、指数和模块说明不遮挡。 + +## 结果 + +- 通过:`document.documentElement.scrollWidth` 未超过视口宽度。 +- 通过:综合、短线、中长线切换和规则/AI/混合/对照选择有可见状态。 +- 通过:TDX 来源、更新时间和不可用原因可见,未把不可用模块渲染为空白。 + +## 残余项 + +依据抽屉、指数/板块详情页和真实图表尚未立项,不作为本次首屏视觉验收项。 diff --git a/docs/features/FEAT-MO-001-market-overview/release-notes.md b/docs/features/FEAT-MO-001-market-overview/release-notes.md new file mode 100644 index 0000000..6c556d7 --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/release-notes.md @@ -0,0 +1,13 @@ +# FEAT-MO-001 发布说明 + +## 本次发布 + +- 新增 TDX 直连的市场概览首屏和五个模块 API。 +- 支持综合、短线、中长线视角与规则、AI、混合、对照偏好选择。 +- 明确显示 TDX 来源、快照时间、缺失能力和模块级错误。 + +## 明确不包含 + +- 不启用 MySQL、Redis、RocketMQ 或 AI 运行链路。 +- 不包含用户体系、详情页、依据抽屉、交易操作和荐股能力。 +- 不包含板块、宽度和资金真实数据,直到 TDX 桥接层提供并验证对应方法。 diff --git a/docs/features/FEAT-MO-001-market-overview/requirements.md b/docs/features/FEAT-MO-001-market-overview/requirements.md index 65a8d00..d67fbdc 100644 --- a/docs/features/FEAT-MO-001-market-overview/requirements.md +++ b/docs/features/FEAT-MO-001-market-overview/requirements.md @@ -2,4 +2,11 @@ 首屏为“市场概览”模块中的综合概览,采用结论优先。市场概览内部通过轻量分析状态入口切换综合、短线和中长线视角,并支持规则、AI、混合与对照分析。 -当前切片仅开发 Vue 3 可交互首屏和本地模拟数据,不接用户体系和正式数据链路。 +当前切片开发 Vue 3 可交互首屏,Nest API 通过 TDX 只读桥接层提供真实指数和摘要;市场宽度、板块和资金接口保留结构化不可用状态,不使用离线样本。不接用户体系,也不启用 MySQL、Redis、RocketMQ 或 AI 运行链路。 + +## 本次实际范围 + +- 已完成综合、短线、中长线视角切换,以及规则、AI、混合、对照偏好选择;偏好只保存于浏览器本地。 +- 已完成按摘要、指数、宽度、板块、资金拆分的五个 HTTP 接口和前端 API 封装。 +- 已完成 TDX 请求超时、响应大小限制、错误脱敏和模块级失败隔离。 +- 详情页和“查看依据”抽屉只保留入口,另立需求实现,不在本次发布范围内。 diff --git a/docs/features/FEAT-MO-001-market-overview/review/code-review.md b/docs/features/FEAT-MO-001-market-overview/review/code-review.md new file mode 100644 index 0000000..9f3d3ed --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/review/code-review.md @@ -0,0 +1,30 @@ +# 代码 Review + +## 范围 + +覆盖 TDX 客户端、市场概览后端模块、OpenAPI/前端契约、Axios API 封装、Vue 首屏组件和对应测试。 + +## 检查结果 + +- 通过:API 按模块拆分,前端请求集中在 `web/src/api/market-overview/index.ts`,Vue 不直连 TDX。 +- 通过:真实指数和摘要字段经过 DTO/映射;桥接层未提供的方法返回结构化 `unavailable`,没有模拟数据冒充。 +- 通过:模块并行加载且独立处理异常;TDX 请求有固定回环地址、超时、响应大小限制和错误脱敏。 +- 通过:未将密码、令牌、行情原始载荷或生产数据写入代码、文档和日志。 +- 通过:E2E 测试夹具显式声明回环 TDX 配置但不要求 TDX 可达,避免测试环境绕过正式配置校验。 + +## 安全与残余风险 + +- TDX 只读边界固定为回环地址,客户端限制超时和响应大小并脱敏异常;未接入交易、AI 密钥、数据库写入或生产数据。 +- 残余风险是 TDX 桥接方法覆盖不全、市场数据授权和生产 7×24 稳定性尚未验证,已在需求和回退说明中标明。 + +## GitNexus 与同步修改 + +开发前已保存 GitNexus 影响计划;修改共享运行配置时曾得到 HIGH,原因是配置文件被 API/Scheduler/Worker 共用,已通过移除通用安全摘要变更和定向测试收敛到 API 实际消费者。提交前 CLI reindex 后 `detect-changes --scope staged` 标记 high,具体为 11 条本功能概览/E2E 执行流;已核对核心服务、控制器、前端 API 和测试覆盖,未发现额外业务模块调用方。当前 worktree 的 MCP context 未初始化,已保留 CLI 和静态证据,不把该限制误报为低风险。 + +## 性能与资源影响 + +- 五个模块并行请求,单请求超时并限制 1 MiB 响应;不引入 MySQL、Redis、RocketMQ 在线负载。 +- 未进行 7×24、断线重连和生产压测;TDX 宽度/板块/资金方法及详情页仍待独立功能。 +- ESLint 对 Vue 单文件组件仅保留 35 条既有风格 warning,无 error;格式、类型和构建门禁均通过。 + +最终结论:passed diff --git a/docs/features/FEAT-MO-001-market-overview/review/standards-review.md b/docs/features/FEAT-MO-001-market-overview/review/standards-review.md new file mode 100644 index 0000000..60313e9 --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/review/standards-review.md @@ -0,0 +1,18 @@ +# 开发规范符合性 Review + +## 安全、性能与架构检查 + +- 已建立并更新 PRD、需求、设计、实施计划、测试计划、开发总结和 Review 文档。 +- 已记录安全策略、性能目标、GitNexus 影响、验证证据、范围变化和残余风险。 +- 后端按业务模块组织并使用 DTO/映射;前端请求通过统一 Axios 封装,业务 API 独立存放。 +- 代码注释集中在配置边界、异常恢复和偏好存储等非显然逻辑,没有逐行噪声注释。 +- 未启用 MySQL、Redis、RocketMQ;本切片没有伪造基础设施已接入,也没有触碰生产数据库。 +- Git 提交和推送将在 Review 通过后按中文 Conventional Commit 执行;不修改 Git 全局配置。 + +## GitNexus 与开发总结检查 + +- 开发前影响计划、开发后索引、变更检测和风险处置已归档。 +- 开发总结记录原始需求、范围变化、实际完成、修复缺陷、延期项、提交和验证证据。 +- QA、发布说明和回退说明与本功能档案保持同一功能编号。 + +最终结论:passed diff --git a/docs/features/FEAT-MO-001-market-overview/rollback.md b/docs/features/FEAT-MO-001-market-overview/rollback.md new file mode 100644 index 0000000..57baea1 --- /dev/null +++ b/docs/features/FEAT-MO-001-market-overview/rollback.md @@ -0,0 +1,17 @@ +# FEAT-MO-001 回退说明 + +## 触发条件 + +- TDX 请求错误导致核心指数无法展示或页面加载阻断。 +- 发现前端横向溢出、来源状态误标为实时,或不可用指标被伪造。 +- 生产部署验证发现配置边界、权限或错误脱敏回归。 + +## 操作 + +1. 停止当前 API/Web systemd 服务,保留日志和失败快照摘要,不保存原始行情载荷。 +2. 回退到上一个已推送的 Gitea 提交/标签,重新执行源码构建和 `pnpm verify:source`。 +3. 恢复 TDX 桥接隧道后只做只读接口冒烟;确认通过前不得重新对外开放。 + +## 数据边界 + +本功能没有写入 MySQL、Redis 或 RocketMQ,也没有数据库迁移;回退不执行生产数据库操作。开发 Schema 继续遵循仅读取开发库的门禁。 diff --git a/docs/features/FEAT-MO-001-market-overview/test-plan.md b/docs/features/FEAT-MO-001-market-overview/test-plan.md index 7885192..f229f33 100644 --- a/docs/features/FEAT-MO-001-market-overview/test-plan.md +++ b/docs/features/FEAT-MO-001-market-overview/test-plan.md @@ -3,6 +3,32 @@ - 结论、驱动和风险在首屏可见。 - 分析状态可以临时切换并保存默认值。 - 依据抽屉能够打开和关闭。 -- 时间范围切换更新图表。 +- 五个模块并行请求,一个模块失败不阻断其他模块。 +- 真实 TDX 指数和摘要响应可映射,未提供的方法返回 `unavailable`,不伪造行情。 - 加载、部分数据、过期和错误状态具有明确文案。 - 桌面和窄窗口不出现阻断核心内容的溢出。 + +## 已执行证据 + +- 前端 Vitest、typecheck、build:通过。 +- 后端 TDX 客户端、映射、服务、HTTP/OpenAPI 测试:通过。 +- `curl` 冒烟:`/summary`、`/indices` 返回 `source=tdx,status=fresh`;`/breadth`、`/sectors`、`/funds` 返回结构化不可用状态。 +- 浏览器验证:桌面默认视口和 390px 移动视口无横向溢出;综合/短线切换和分析引擎选择可用。 + +## 安全、性能与残余风险 + +- 安全:TDX 地址固定为本机回环桥接端口;请求超时、响应 1 MiB 上限、错误脱敏;只读行情,不接受用户输入拼接请求。 +- 性能:五个模块并行请求,单次 TDX 请求受超时保护;本切片不引入数据库、缓存或消息负载,未进行持续压测。 +- 残余风险:桥接层尚未提供宽度、板块和资金方法;详情页、依据抽屉和 AI 运行时需另立功能;市场数据授权和生产 7×24 稳定性未由本地冒烟证明。 + +## 安全验证 + +- 验证 TDX 只能使用回环地址和固定端口,错误响应不包含原始 URL、请求体或凭据。 +- 验证前端只能通过统一 Axios API 文件访问后端,边界测试拒绝页面直连 Axios/fetch 和 Server 内部依赖。 +- 验证 E2E 夹具使用无秘密的生产测试环境变量,不读取仓库内本地凭据或生产配置。 + +## 性能验证 + +- 验证五个模块并发加载、单模块失败隔离和请求超时行为。 +- 验证响应体 1 MiB 上限和构建产物体积;浏览器桌面/移动端无横向溢出。 +- 7×24 运行、峰值并发、TDX 断线重连和消息/缓存压力测试不适用于当前只读首屏,已列为后续采集与生产功能边界。 diff --git a/eslint.config.mjs b/eslint.config.mjs index 615ab24..9b2db87 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,6 +10,15 @@ export default [ eslint.configs.recommended, ...tseslint.configs.recommended, ...vue.configs['flat/recommended'], + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { + // Vue 单文件组件的 script setup 使用 TypeScript,避免 ESLint 退回 Espree。 + parser: tseslint.parser, + }, + }, + }, { files: ['**/*.{js,mjs,cjs,ts,tsx,vue}'], languageOptions: { diff --git a/server/apps/api/src/market-overview/market-overview.service.test.ts b/server/apps/api/src/market-overview/market-overview.service.test.ts index 60beec9..8d934bf 100644 --- a/server/apps/api/src/market-overview/market-overview.service.test.ts +++ b/server/apps/api/src/market-overview/market-overview.service.test.ts @@ -26,9 +26,16 @@ describe('市场概览服务', () => { const response = await service.getIndices(); - expect(response).toMatchObject({ source: 'tdx', status: 'fresh', data: { items: expect.any(Array) } }); + expect(response).toMatchObject({ + source: 'tdx', + status: 'fresh', + data: { items: expect.any(Array) }, + }); expect(response.snapshotId).toMatch(/^20260818-/); - expect(client.request).toHaveBeenCalledWith('get_market_data', expect.objectContaining({ count: 5 })); + expect(client.request).toHaveBeenCalledWith( + 'get_market_data', + expect.objectContaining({ count: 5 }), + ); }); it('对未验证的市场宽度能力返回不可用而不是伪造数据', async () => { diff --git a/server/apps/api/src/market-overview/market-overview.service.ts b/server/apps/api/src/market-overview/market-overview.service.ts index 6f234d7..b29e87f 100644 --- a/server/apps/api/src/market-overview/market-overview.service.ts +++ b/server/apps/api/src/market-overview/market-overview.service.ts @@ -23,7 +23,10 @@ export type MarketOverviewResponse = { warnings: string[]; }; -function createSnapshotMeta(): Pick, 'snapshotId' | 'asOf' | 'source'> { +function createSnapshotMeta(): Pick< + MarketOverviewResponse, + 'snapshotId' | 'asOf' | 'source' +> { const asOf = new Date().toISOString(); const snapshotId = `${asOf.slice(0, 10).replaceAll('-', '')}-${asOf.slice(11, 19).replaceAll(':', '')}`; return { snapshotId, asOf, source: 'tdx' }; @@ -76,9 +79,15 @@ export class MarketOverviewService { const averageChange = changes.length ? Math.round((changes.reduce((sum, value) => sum + value, 0) / changes.length) * 100) / 100 : null; - const marketStatus = averageChange === null ? '指数数据不完整' : averageChange >= 0 ? '指数偏强' : '指数偏弱'; + const marketStatus = + averageChange === null ? '指数数据不完整' : averageChange >= 0 ? '指数偏强' : '指数偏弱'; const summary: SummaryDataDto = { - verdict: averageChange === null ? '市场状态待确认' : averageChange >= 0 ? '市场维持偏强震荡' : '市场维持偏弱震荡', + verdict: + averageChange === null + ? '市场状态待确认' + : averageChange >= 0 + ? '市场维持偏强震荡' + : '市场维持偏弱震荡', marketStatus, volumeStatus: '成交量口径待接入', sentimentStatus: '市场情绪口径待接入', diff --git a/server/apps/api/src/market-overview/tdx/tdx.client.test.ts b/server/apps/api/src/market-overview/tdx/tdx.client.test.ts index 2071ceb..4eecc45 100644 --- a/server/apps/api/src/market-overview/tdx/tdx.client.test.ts +++ b/server/apps/api/src/market-overview/tdx/tdx.client.test.ts @@ -12,19 +12,21 @@ function response(body: unknown, init?: ResponseInit): Response { describe('TDX 客户端', () => { it('发送固定 JSON 请求并返回业务结果', async () => { - const fetchImpl = vi.fn().mockResolvedValue( - response({ id: 1, result: { ErrorId: '0', Value: {} } }), - ); + const fetchImpl = vi + .fn() + .mockResolvedValue(response({ id: 1, result: { ErrorId: '0', Value: {} } })); const client = createTdxClient({ baseUrl: 'http://127.0.0.1:17710', timeoutMs: 5_000, fetchImpl, }); - await expect(client.request('get_market_data', { stock_list: ['600519.SH'] })).resolves.toEqual({ - ErrorId: '0', - Value: {}, - }); + await expect(client.request('get_market_data', { stock_list: ['600519.SH'] })).resolves.toEqual( + { + ErrorId: '0', + Value: {}, + }, + ); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:17710/', @@ -42,9 +44,11 @@ describe('TDX 客户端', () => { }); it('将 TDX 错误响应转换为脱敏协议错误', async () => { - const fetchImpl = vi.fn().mockResolvedValue( - response({ id: 1, error: { code: 'bad_request', message: '内部连接信息' } }), - ); + const fetchImpl = vi + .fn() + .mockResolvedValue( + response({ id: 1, error: { code: 'bad_request', message: '内部连接信息' } }), + ); const client = createTdxClient({ baseUrl: 'http://127.0.0.1:17710', timeoutMs: 5_000, @@ -60,7 +64,9 @@ describe('TDX 客户端', () => { }); it('将 AbortError 转换为超时错误', async () => { - const fetchImpl = vi.fn().mockRejectedValue(new DOMException('aborted', 'AbortError')); + const fetchImpl = vi + .fn() + .mockRejectedValue(new DOMException('aborted', 'AbortError')); const client = createTdxClient({ baseUrl: 'http://127.0.0.1:17710', timeoutMs: 50, diff --git a/server/apps/api/test/market-overview.e2e-spec.ts b/server/apps/api/test/market-overview.e2e-spec.ts index cfd37a9..8af0924 100644 --- a/server/apps/api/test/market-overview.e2e-spec.ts +++ b/server/apps/api/test/market-overview.e2e-spec.ts @@ -4,7 +4,10 @@ import request from 'supertest'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { MarketOverviewController } from '../src/market-overview/market-overview.controller'; -import { MarketOverviewService, TDX_CLIENT_PORT } from '../src/market-overview/market-overview.service'; +import { + MarketOverviewService, + TDX_CLIENT_PORT, +} from '../src/market-overview/market-overview.service'; describe('市场概览 HTTP API', () => { let app: INestApplication | undefined; @@ -57,9 +60,15 @@ describe('市场概览 HTTP API', () => { app.setGlobalPrefix('api/v1'); await app.init(); - const response = await request(app.getHttpServer()).get('/api/v1/market-overview/indices').expect(200); + const response = await request(app.getHttpServer()) + .get('/api/v1/market-overview/indices') + .expect(200); - expect(response.body).toMatchObject({ source: 'tdx', status: 'fresh', data: { items: expect.any(Array) } }); + expect(response.body).toMatchObject({ + source: 'tdx', + status: 'fresh', + data: { items: expect.any(Array) }, + }); expect(JSON.stringify(response.body)).not.toContain('127.0.0.1'); }); @@ -75,7 +84,9 @@ describe('市场概览 HTTP API', () => { app.setGlobalPrefix('api/v1'); await app.init(); - const response = await request(app.getHttpServer()).get('/api/v1/market-overview/sectors').expect(200); + const response = await request(app.getHttpServer()) + .get('/api/v1/market-overview/sectors') + .expect(200); expect(response.body).toMatchObject({ source: 'tdx', status: 'unavailable', data: null }); expect(response.body.warnings[0]).toContain('板块排行'); diff --git a/server/tools/export-openapi.ts b/server/tools/export-openapi.ts index 51fe778..0cd9b29 100644 --- a/server/tools/export-openapi.ts +++ b/server/tools/export-openapi.ts @@ -14,7 +14,10 @@ import { MYSQL_HEALTH_PORT, REDIS_HEALTH_PORT, } from '../apps/api/src/health.service'; -import { MarketOverviewService, TDX_CLIENT_PORT } from '../apps/api/src/market-overview/market-overview.service'; +import { + MarketOverviewService, + TDX_CLIENT_PORT, +} from '../apps/api/src/market-overview/market-overview.service'; import { DisabledHealthClient } from '../libs/infrastructure/src/disabled-health.client'; @Module({ diff --git a/server/tools/run-test-api.mjs b/server/tools/run-test-api.mjs index 4f0c993..9a37213 100644 --- a/server/tools/run-test-api.mjs +++ b/server/tools/run-test-api.mjs @@ -46,6 +46,9 @@ export async function runTestApi() { LOG_LEVEL: 'error', API_HOST: '127.0.0.1', API_PORT: '3000', + // API 配置要求始终声明本机 TDX 边界;E2E 不依赖 TDX 可达性,页面会验证不可用状态。 + TDX_BASE_URL: 'http://127.0.0.1:17710', + TDX_TIMEOUT_MS: '5000', REDIS_HOST: '127.0.0.1', REDIS_PORT: '6379', REDIS_USERNAME: '', diff --git a/web/src/api/market-overview/index.ts b/web/src/api/market-overview/index.ts index 62ca27c..1116caf 100644 --- a/web/src/api/market-overview/index.ts +++ b/web/src/api/market-overview/index.ts @@ -9,11 +9,16 @@ import type { export function createMarketOverviewApi(requestClient: RequestClient = request) { return { - getSummary: () => requestClient({ method: 'get', url: '/api/v1/market-overview/summary' }), - getIndices: () => requestClient({ method: 'get', url: '/api/v1/market-overview/indices' }), - getBreadth: () => requestClient({ method: 'get', url: '/api/v1/market-overview/breadth' }), - getSectors: () => requestClient({ method: 'get', url: '/api/v1/market-overview/sectors' }), - getFunds: () => requestClient({ method: 'get', url: '/api/v1/market-overview/funds' }), + getSummary: () => + requestClient({ method: 'get', url: '/api/v1/market-overview/summary' }), + getIndices: () => + requestClient({ method: 'get', url: '/api/v1/market-overview/indices' }), + getBreadth: () => + requestClient({ method: 'get', url: '/api/v1/market-overview/breadth' }), + getSectors: () => + requestClient({ method: 'get', url: '/api/v1/market-overview/sectors' }), + getFunds: () => + requestClient({ method: 'get', url: '/api/v1/market-overview/funds' }), }; } diff --git a/web/src/app/App.test.ts b/web/src/app/App.test.ts index 75409a8..3503d39 100644 --- a/web/src/app/App.test.ts +++ b/web/src/app/App.test.ts @@ -1,11 +1,32 @@ import { createPinia } from 'pinia'; import { mount } from '@vue/test-utils'; import { createMemoryHistory } from 'vue-router'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import App from './App.vue'; import { createAppRouter } from './router'; +// 应用外壳测试只验证导航和主区域,不应因页面挂载触发真实 Axios 请求。 +vi.mock('@/api/market-overview', () => { + const response = { + source: 'tdx', + status: 'unavailable', + data: null, + snapshotId: 'test', + asOf: '2026-08-18T00:00:00.000Z', + warnings: ['测试环境未连接 TDX'], + }; + return { + default: { + getSummary: vi.fn().mockResolvedValue(response), + getIndices: vi.fn().mockResolvedValue(response), + getBreadth: vi.fn().mockResolvedValue(response), + getSectors: vi.fn().mockResolvedValue(response), + getFunds: vi.fn().mockResolvedValue(response), + }, + }; +}); + describe('应用外壳', () => { it('提供可访问的应用标题和主内容区域', async () => { const router = createAppRouter(createMemoryHistory()); @@ -21,6 +42,6 @@ describe('应用外壳', () => { expect(wrapper.get('header').attributes('aria-label')).toBe('应用导航'); expect(wrapper.get('main').attributes('id')).toBe('main-content'); expect(wrapper.text()).toContain('A 股市场分析工具'); - expect(wrapper.text()).toContain('工程基线已就绪'); + expect(wrapper.text()).toContain('市场综合概览'); }); }); diff --git a/web/src/app/App.vue b/web/src/app/App.vue index c581531..c6857cf 100644 --- a/web/src/app/App.vue +++ b/web/src/app/App.vue @@ -10,8 +10,14 @@ const appTitle = import.meta.env.VITE_APP_TITLE || 'A 股市场分析工具';
- - {{ appTitle }} +
+ +
+ {{ appTitle }} + 市场复盘工作台 +
+
+ 只读行情 · TDX
diff --git a/web/src/app/router.ts b/web/src/app/router.ts index a9de58d..5e0622b 100644 --- a/web/src/app/router.ts +++ b/web/src/app/router.ts @@ -4,16 +4,14 @@ import { type RouterHistory, type RouteRecordRaw, } from 'vue-router'; -import { h } from 'vue'; + +import MarketOverviewPage from '@/views/market-overview/MarketOverviewPage.vue'; const routes: RouteRecordRaw[] = [ { path: '/', name: 'home', - component: { - // 工程基线不引入运行时模板编译器;业务首页落地前用渲染函数提供可观测占位内容。 - render: () => h('section', { 'aria-label': '应用就绪' }, [h('p', '工程基线已就绪')]), - }, + component: MarketOverviewPage, }, ]; diff --git a/web/src/styles/index.scss b/web/src/styles/index.scss index 9e84cd1..297f5c7 100644 --- a/web/src/styles/index.scss +++ b/web/src/styles/index.scss @@ -44,7 +44,7 @@ textarea { .app-header { display: flex; align-items: center; - gap: 10px; + justify-content: space-between; min-height: 52px; padding: 0 20px; color: #fff; @@ -52,6 +52,29 @@ textarea { border-bottom: 1px solid #343b44; } +.app-identity { + display: flex; + align-items: center; + gap: 10px; +} + +.app-identity strong, +.app-identity small { + display: block; +} + +.app-identity small { + margin-top: 2px; + color: #aeb7c2; + font-size: 11px; + font-weight: 500; +} + +.header-context { + color: #aeb7c2; + font-size: 12px; +} + .app-mark { display: inline-grid; width: 28px; @@ -69,10 +92,460 @@ textarea { padding: 20px; } +.market-overview-page { + display: grid; + gap: 16px; +} + +.overview-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + min-height: 42px; +} + +.view-switcher { + display: inline-flex; + padding: 3px; + background: #e8edf1; + border: 1px solid #d5dce3; + border-radius: 6px; +} + +.view-switcher button, +.refresh-button, +.quiet-button, +.sector-list button { + border: 0; + border-radius: 4px; + cursor: pointer; + font: inherit; +} + +.view-switcher button { + min-width: 72px; + padding: 7px 12px; + color: #4c5966; + background: transparent; + font-size: 13px; +} + +.view-switcher button.active { + color: #17212b; + background: #fff; + box-shadow: 0 1px 3px rgb(28 40 52 / 14%); +} + +.toolbar-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.toolbar-actions label { + display: flex; + align-items: center; + gap: 7px; + color: #65717d; + font-size: 12px; +} + +.toolbar-actions select { + min-height: 32px; + padding: 0 28px 0 9px; + color: #24303b; + background: #fff; + border: 1px solid #ccd4dc; + border-radius: 4px; + font: inherit; +} + +.refresh-button { + min-height: 32px; + padding: 0 12px; + color: #fff; + background: #167d82; + font-size: 12px; +} + +.refresh-button:disabled { + cursor: wait; + opacity: 0.55; +} + +.overview-conclusion, +.overview-panel { + background: #fff; + border: 1px solid #dbe1e6; + border-radius: 7px; + box-shadow: 0 3px 14px rgb(26 38 49 / 5%); +} + +.overview-conclusion { + padding: 22px 24px 18px; +} + +.section-kicker { + color: #167d82; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.conclusion-heading, +.panel-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.conclusion-heading { + margin-top: 8px; +} + +.conclusion-heading h1, +.panel-heading h2 { + margin: 0; + color: #17212b; + letter-spacing: 0; +} + +.conclusion-heading h1 { + font-size: clamp(24px, 3vw, 38px); + line-height: 1.2; +} + +.panel-heading h2 { + margin-top: 4px; + font-size: 17px; +} + +.conclusion-copy { + margin: 10px 0 0; + color: #475563; + font-size: 15px; +} + +.source-status { + display: grid; + grid-template-columns: auto auto; + align-items: center; + gap: 5px 7px; + min-width: 142px; + padding: 9px 11px; + color: #167d82; + background: #eff8f7; + border: 1px solid #cbe6e2; + border-radius: 5px; + font-size: 12px; + font-weight: 700; +} + +.source-status small { + grid-column: 1 / -1; + color: #6d7a85; + font-size: 10px; + font-weight: 500; +} + +.source-status[data-status='unavailable'] { + color: #a45e1b; + background: #fff7ec; + border-color: #f0d7b5; +} + +.status-dot { + width: 7px; + height: 7px; + background: currentcolor; + border-radius: 50%; +} + +.conclusion-facts { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 21px; + padding-top: 15px; + border-top: 1px solid #e6eaee; +} + +.conclusion-facts div { + display: grid; + gap: 5px; +} + +.conclusion-facts span, +.panel-meta, +.breadth-values span, +.column-label { + color: #7b8792; + font-size: 11px; +} + +.conclusion-facts strong { + color: #2e3b47; + font-size: 14px; +} + +.module-warning { + margin: 13px 0 0; + color: #a45e1b; + font-size: 12px; +} + +.overview-grid { + display: grid; + gap: 16px; +} + +.overview-grid-top { + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); +} + +.overview-grid-bottom { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.overview-panel { + min-height: 190px; + padding: 18px; +} + +.panel-heading { + align-items: center; + margin-bottom: 17px; +} + +.panel-meta { + white-space: nowrap; +} + +.quiet-button { + padding: 6px 9px; + color: #167d82; + background: #eff8f7; + font-size: 11px; +} + +.drivers-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 17px; +} + +.driver-column + .driver-column { + padding-left: 17px; + border-left: 1px solid #e6eaee; +} + +.column-label { + display: block; + margin-bottom: 8px; + font-weight: 700; +} + +.column-label.positive, +.positive { + color: #c33f3f; +} + +.column-label.negative, +.negative { + color: #19846e; +} + +.driver-column ul { + display: grid; + gap: 8px; + margin: 0; + padding: 0; + color: #485664; + font-size: 12px; + line-height: 1.55; + list-style: none; +} + +.driver-column li { + position: relative; + padding-left: 13px; +} + +.driver-column li::before { + position: absolute; + top: 0.62em; + left: 0; + width: 5px; + height: 5px; + background: #aeb8c1; + border-radius: 50%; + content: ''; +} + +.index-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 9px; +} + +.index-item { + display: grid; + gap: 7px; + min-height: 112px; + padding: 13px; + color: #4c5966; + text-align: left; + background: #f8fafb; + border: 1px solid #e3e8ec; + border-radius: 5px; + cursor: pointer; + font: inherit; +} + +.index-item:hover { + border-color: #a5c9c7; + box-shadow: 0 2px 8px rgb(26 38 49 / 8%); +} + +.index-item strong { + color: #1c2a35; + font-size: 20px; +} + +.index-item small { + color: #7d8893; + font-size: 12px; +} + +.index-item[data-direction='up'] small { + color: #c33f3f; +} + +.index-item[data-direction='down'] small { + color: #19846e; +} + +.breadth-values { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin-top: 32px; +} + +.breadth-values div { + display: grid; + gap: 8px; + text-align: center; +} + +.breadth-values strong { + color: #2e3b47; + font-size: 25px; +} + +.module-placeholder, +.module-unavailable { + display: grid; + align-content: center; + min-height: 104px; + gap: 6px; + color: #8a959f; + font-size: 12px; +} + +.module-unavailable strong { + color: #a45e1b; + font-size: 13px; +} + +.module-unavailable span { + line-height: 1.5; +} + +.sector-list { + display: flex; + flex-wrap: wrap; + gap: 7px; +} + +.sector-list button { + padding: 7px 9px; + color: #4c5966; + background: #f3f7f7; + font-size: 12px; +} + +@media (max-width: 900px) { + .overview-grid-top, + .overview-grid-bottom { + grid-template-columns: 1fr; + } +} + @media (max-width: 600px) { .app-header, .app-main { padding-right: 14px; padding-left: 14px; } + + .header-context { + display: none; + } + + .overview-toolbar, + .conclusion-heading { + align-items: stretch; + flex-direction: column; + } + + .toolbar-actions { + justify-content: space-between; + } + + .view-switcher { + width: 100%; + } + + .view-switcher button { + flex: 1; + } + + .overview-conclusion { + padding: 18px 16px 15px; + } + + .conclusion-heading h1 { + font-size: 27px; + } + + .source-status { + width: 100%; + } + + .conclusion-facts { + gap: 7px; + } + + .conclusion-facts strong { + font-size: 12px; + } + + .drivers-grid { + grid-template-columns: 1fr; + } + + .driver-column + .driver-column { + padding-top: 14px; + padding-left: 0; + border-top: 1px solid #e6eaee; + border-left: 0; + } + + .index-grid { + grid-template-columns: 1fr; + } } diff --git a/web/src/views/market-overview/MarketOverviewPage.vue b/web/src/views/market-overview/MarketOverviewPage.vue new file mode 100644 index 0000000..183f075 --- /dev/null +++ b/web/src/views/market-overview/MarketOverviewPage.vue @@ -0,0 +1,96 @@ + + + diff --git a/web/src/views/market-overview/components/FundTrend.vue b/web/src/views/market-overview/components/FundTrend.vue new file mode 100644 index 0000000..654b6d7 --- /dev/null +++ b/web/src/views/market-overview/components/FundTrend.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/views/market-overview/components/IndexOverview.vue b/web/src/views/market-overview/components/IndexOverview.vue new file mode 100644 index 0000000..c970db2 --- /dev/null +++ b/web/src/views/market-overview/components/IndexOverview.vue @@ -0,0 +1,41 @@ + + + diff --git a/web/src/views/market-overview/components/MarketBreadth.vue b/web/src/views/market-overview/components/MarketBreadth.vue new file mode 100644 index 0000000..e0f0c80 --- /dev/null +++ b/web/src/views/market-overview/components/MarketBreadth.vue @@ -0,0 +1,34 @@ + + + diff --git a/web/src/views/market-overview/components/MarketConclusion.vue b/web/src/views/market-overview/components/MarketConclusion.vue new file mode 100644 index 0000000..44cbb0e --- /dev/null +++ b/web/src/views/market-overview/components/MarketConclusion.vue @@ -0,0 +1,42 @@ + + + diff --git a/web/src/views/market-overview/components/MarketDrivers.vue b/web/src/views/market-overview/components/MarketDrivers.vue new file mode 100644 index 0000000..7882920 --- /dev/null +++ b/web/src/views/market-overview/components/MarketDrivers.vue @@ -0,0 +1,33 @@ + + + diff --git a/web/src/views/market-overview/components/SectorOverview.vue b/web/src/views/market-overview/components/SectorOverview.vue new file mode 100644 index 0000000..be27ef2 --- /dev/null +++ b/web/src/views/market-overview/components/SectorOverview.vue @@ -0,0 +1,34 @@ + + + diff --git a/web/src/views/market-overview/useMarketOverview.test.ts b/web/src/views/market-overview/useMarketOverview.test.ts index 6255aaf..4aff54e 100644 --- a/web/src/views/market-overview/useMarketOverview.test.ts +++ b/web/src/views/market-overview/useMarketOverview.test.ts @@ -2,7 +2,14 @@ import { describe, expect, it, vi } from 'vitest'; import { useMarketOverview } from './useMarketOverview'; -const response = { source: 'tdx', status: 'fresh', data: null, snapshotId: '20260818-101500', asOf: '2026-08-18T10:15:00+08:00', warnings: [] } as const; +const response = { + source: 'tdx', + status: 'fresh', + data: null, + snapshotId: '20260818-101500', + asOf: '2026-08-18T10:15:00+08:00', + warnings: [], +} as const; describe('市场概览状态', () => { it('并行加载五个模块,一个模块失败不影响其他模块', async () => { diff --git a/web/src/views/market-overview/useMarketOverview.ts b/web/src/views/market-overview/useMarketOverview.ts index 2677837..b79eb18 100644 --- a/web/src/views/market-overview/useMarketOverview.ts +++ b/web/src/views/market-overview/useMarketOverview.ts @@ -10,7 +10,8 @@ import type { SummaryResponse, } from '@/types/market-overview'; -type MarketResponse = SummaryResponse | IndicesResponse | BreadthResponse | SectorsResponse | FundsResponse; +type MarketResponse = + SummaryResponse | IndicesResponse | BreadthResponse | SectorsResponse | FundsResponse; export interface OverviewModuleState { status: OverviewModuleStatus; @@ -37,7 +38,9 @@ function writePreference(key: string, value: string): void { } } -export function useMarketOverview(api: ReturnType = marketOverviewApi) { +export function useMarketOverview( + api: ReturnType = marketOverviewApi, +) { const summary = ref>(createState()); const indices = ref>(createState()); const breadth = ref>(createState()); diff --git a/web/tests/smoke.spec.ts b/web/tests/smoke.spec.ts index 8521871..5d97e65 100644 --- a/web/tests/smoke.spec.ts +++ b/web/tests/smoke.spec.ts @@ -14,6 +14,7 @@ test('应用外壳在桌面和移动视口均可正常呈现', async ({ page }) await expect(page.locator('main')).toBeVisible(); await expect(page.getByText('A 股市场分析工具')).toBeVisible(); + await expect(page.getByRole('heading', { name: '盘面先看结论,再看证据' })).toBeVisible(); const hasHorizontalOverflow = await page.evaluate( () => document.documentElement.scrollWidth > document.documentElement.clientWidth,