fix(FEAT-MO-001): 修复TDX数据完整性与状态展示
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
- 对未注册桥接方法返回结构化原因,避免前端显示“实时”或空白数据。
|
||||
- 修复 Vue TypeScript 单文件组件未被 ESLint 正确解析的问题,并为 E2E 测试进程补齐必需的本机 TDX 配置边界。
|
||||
- 隔离应用外壳单测中的真实 Axios 请求,消除 JSDOM 网络错误噪声,保持页面测试只验证外壳职责。
|
||||
- 根据代码 Review 修复畸形 TDX 字段被折算为零、缺市场成交额误报三市、交易日错位跨日求和、空行业目录误报 `fresh`、前端隐藏 `partial/delayed` 警告和零变化使用上涨色等缺陷,并补齐对应回归测试。
|
||||
|
||||
## 未完成与延期项
|
||||
|
||||
@@ -41,9 +42,10 @@
|
||||
|
||||
## 验证证据
|
||||
|
||||
- GitNexus 已完成最终重新索引;提交前 CLI `detect-changes --scope staged` 映射 11 条概览/E2E 执行流并标记 high。高风险来自本功能修改核心概览服务和测试启动入口,已由定向单测、HTTP/OpenAPI、构建和 E2E 覆盖;未发现额外业务模块调用方。MCP context 在当前 worktree 未初始化,因此以 CLI 索引和静态调用证据补充记录。
|
||||
- GitNexus 已完成提交前重新索引;CLI `detect-changes --scope staged` 映射 16 条概览执行流并标记 critical。风险来自本功能修改核心概览服务、TDX 映射和前端数据面板,已由定向单测、HTTP/OpenAPI、前端状态回归、构建、浏览器视觉验证和 E2E 覆盖;未发现额外业务模块调用方。当前环境未暴露 GitNexus MCP 变更检测工具,因此使用同版本 CLI 并指定 worktree 绝对路径执行。
|
||||
- API 冒烟:`/breadth`、`/sectors`、`/funds` 均返回 `source=tdx,status=fresh`;完整结果和耗时见 `tdx-smoke-report.md`。
|
||||
- 前端单测、typecheck、build、后端 TDX/映射/服务/HTTP/OpenAPI 测试通过。
|
||||
- `pnpm verify:e2e`:4/4 通过(桌面和移动端各覆盖页面与健康契约)。
|
||||
- 浏览器桌面与 390px 移动视口验证无横向溢出,视角与引擎切换可用。
|
||||
- Review 首轮在 `ff8ada8` 发现 6 项重要问题;已在当前修复提交中逐项处置,需二次代码 Review 确认。
|
||||
- 开发 Schema 按用户确认跳过,沿用 `v0.0.10 captured_empty` 追溯边界,不读取生产库。
|
||||
|
||||
@@ -10,10 +10,10 @@ npx gitnexus detect-changes --repo "$PWD" --scope staged
|
||||
## 结果
|
||||
|
||||
- 索引:已完成最终增量更新。
|
||||
- 暂存文件:38 个文件、87 个符号(包含完整功能档案)。
|
||||
- 影响执行流:11 条,CLI 风险:high。
|
||||
- 主要执行流:`GetSummary`、`GetBreadth`、`GetSectors`、`GetFunds` 和 `RunTestApi`。
|
||||
- 暂存文件:20 个文件、67 个符号;GitNexus 自动改写的 `AGENTS.md` 已明确排除。
|
||||
- 影响执行流:16 条,CLI 风险:critical。
|
||||
- 主要执行流:`GetSummary`、`GetIndices`、`GetBreadth`、`GetSectors` 和 `GetFunds`,以及对应映射和状态创建流程。
|
||||
|
||||
## 处置
|
||||
|
||||
high 来自本功能新增并修改市场概览核心服务、共享快照元数据函数和 E2E 启动入口,不是未知跨业务扩散。已用 TDX 客户端/映射/服务/HTTP/OpenAPI 单测、边界测试、构建和 4/4 E2E 覆盖;未发现用户、消息、数据库或 Scheduler/Worker 业务调用方。MCP context 在当前 worktree 未初始化,未将其缺失误判为低风险。
|
||||
critical 来自本功能修改市场概览核心服务、TDX 映射和三个前端数据面板,不是未知跨业务扩散。已用 TDX 映射/服务/HTTP/OpenAPI 单测、前端状态回归、边界测试、构建、浏览器视觉验证和 4/4 E2E 覆盖;未发现用户、消息、数据库、Redis、RocketMQ、Scheduler、Worker、认证或部署调用方。当前环境未暴露 GitNexus MCP 变更检测工具,已使用同版本 CLI 并指定 worktree 绝对路径执行,没有将工具限制误判为低风险。
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
## 数据与口径
|
||||
|
||||
首个切片由 Nest API 直接读取 TDX 只读桥接层。指数和摘要使用真实 TDX K 线映射;当前桥接层未提供市场宽度、板块排行和资金趋势方法,因此这些模块返回结构化 `unavailable`,不得用指数或离线样本冒充。所有响应仍包含交易日期、快照时间、来源状态、完整性和单位。
|
||||
首个切片由 Nest API 直接读取 TDX 只读桥接层。指数和摘要使用真实 TDX K 线映射;市场宽度使用三市场指数快照,板块使用一级行业目录与日行情,量能使用三市场指数成交额汇总。主力净流入和盘中新闻不在本切片 TDX 主链路中,必须保持明确的补充源边界,不得用指数或离线样本冒充。所有响应仍包含交易日期、快照时间、来源状态、完整性和单位。
|
||||
|
||||
A 股使用红涨绿跌。成交额统一展示为亿元,比例明确使用百分比。历史数据和低频资金数据必须显示实际数据日期,旧数据不得标记为实时。
|
||||
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
|
||||
## 结果
|
||||
|
||||
- 通过:`document.documentElement.scrollWidth` 未超过视口宽度。
|
||||
- 通过:2026-08-18 使用本地 API/Web 实际页面复验;桌面视口为 `1440×900`,移动视口为 `390×844`。
|
||||
- 通过:桌面 `clientWidth=1440, scrollWidth=1440`,移动端 `clientWidth=390, scrollWidth=390`;移动端六个主要区块左右边界均位于 `14px~376px`,未发生页面级或区块级横向溢出。
|
||||
- 通过:综合、短线、中长线切换和规则/AI/混合/对照选择有可见状态。
|
||||
- 通过:TDX 来源、更新时间和不可用原因可见,未把不可用模块渲染为空白。
|
||||
- 通过:实际切换到“综合”和“混合”后,按钮激活态和下拉选中项同步更新。
|
||||
- 通过:TDX 来源、更新时间和不可用原因可见;当前 `127.0.0.1:17710` 未监听时,摘要、指数、宽度、板块和成交额趋势均显示结构化不可用状态,没有空白或伪造实时数据。
|
||||
- 通过:移动端浏览器控制台无 `warning` 或 `error`。
|
||||
|
||||
## 残余项
|
||||
|
||||
依据抽屉、指数/板块详情页和真实图表尚未立项,不作为本次首屏视觉验收项。
|
||||
依据抽屉、指数/板块详情页和真实图表尚未立项,不作为本次首屏视觉验收项。真实 TDX 数据展示的接口冒烟证据沿用 `tdx-smoke-report.md`;本次隧道未监听,因此没有覆盖或伪造新的真实行情截图。
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
- 新增 TDX 直连的市场概览首屏和五个模块 API。
|
||||
- 支持综合、短线、中长线视角与规则、AI、混合、对照偏好选择。
|
||||
- 明确显示 TDX 来源、快照时间、缺失能力和模块级错误。
|
||||
- 市场宽度、一级行业强弱和成交额趋势接入真实 TDX 数据,并对空数据、部分数据、日期不一致和延迟状态显示明确边界。
|
||||
- 明确显示 TDX 来源、快照时间、交易日期、市场覆盖数和模块级错误。
|
||||
|
||||
## 明确不包含
|
||||
|
||||
- 不启用 MySQL、Redis、RocketMQ 或 AI 运行链路。
|
||||
- 不包含用户体系、详情页、依据抽屉、交易操作和荐股能力。
|
||||
- 不包含板块、宽度和资金真实数据,直到 TDX 桥接层提供并验证对应方法。
|
||||
- 不包含主力净流入、盘中新闻、真实图表和详情页;这些能力需另立补充数据源和功能需求。
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# 代码 Review
|
||||
|
||||
## 首轮独立 Review
|
||||
|
||||
- 对比基线:`8261948`
|
||||
- 审查提交:`ff8ada8`
|
||||
- 结论:`Ready to merge: No`
|
||||
|
||||
首轮发现及处置:
|
||||
|
||||
1. 畸形 `UpHome/DownHome` 被折算为零并误标 `fresh`:改为有限、非负且成对校验,无效市场整体排除并按有效市场数返回 `fresh/partial/unavailable`。
|
||||
2. 成交额缺市场或交易日错位仍误标三市实时数据:只汇总非负金额和一致交易日,公开 `marketCount`、`tradeDate`、`previousTradeDate`,缺市场返回 `partial`。
|
||||
3. 空板块目录误标 `fresh`:一级行业为空时直接返回 `unavailable`,且只保留存在相邻两日收盘价和有效涨跌幅的板块。
|
||||
4. 前端隐藏 `partial/delayed` 警告:宽度、板块和成交额面板均展示非 `fresh` 状态提示,并展示板块与成交额真实交易日期。
|
||||
5. PRD、发布说明和 Review 文档残留旧口径:已同步更正为真实 TDX 宽度、一级行业与成交额趋势口径,主力净流入和新闻仍明确延期。
|
||||
6. 异常分支测试不足:新增畸形宽度、部分市场、错位交易日、空行业目录、额外字段隔离、前端状态提示和零变化中性色回归测试。
|
||||
|
||||
次要问题也已同步处理:OpenAPI `required + nullable` 定义、板块 DTO 额外 `price` 字段泄露、成交额零变化上涨色和新增映射中文口径注释。
|
||||
|
||||
## 范围
|
||||
|
||||
覆盖 TDX 客户端、市场概览后端模块、OpenAPI/前端契约、Axios API 封装、Vue 首屏组件和对应测试。
|
||||
@@ -27,4 +44,8 @@
|
||||
- 未进行 7×24、断线重连和生产压测;TDX 宽度/板块/资金方法及详情页仍待独立功能。
|
||||
- ESLint 对 Vue 单文件组件仅保留 35 条既有风格 warning,无 error;格式、类型和构建门禁均通过。
|
||||
|
||||
最终结论:passed
|
||||
## 二次 Review
|
||||
|
||||
等待修复提交完成后执行独立二次 Review。
|
||||
|
||||
最终结论:pending
|
||||
|
||||
@@ -15,4 +15,10 @@
|
||||
- 开发总结记录原始需求、范围变化、实际完成、修复缺陷、延期项、提交和验证证据。
|
||||
- QA、发布说明和回退说明与本功能档案保持同一功能编号。
|
||||
|
||||
最终结论:passed
|
||||
## 首轮问题同步检查
|
||||
|
||||
- 数据完整性、状态语义、OpenAPI nullable、DTO 输出边界和前端降级提示均已补齐回归测试。
|
||||
- PRD、发布说明、测试计划和开发总结已同步实际口径,避免实现与文档分叉。
|
||||
- 浏览器复验覆盖桌面与 `390×844` 移动视口;当前 TDX 隧道未监听时只确认结构化降级,历史真实行情冒烟证据保持独立。
|
||||
|
||||
最终结论:pending(等待独立二次代码 Review 与完整门禁)
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
- 前端 Vitest、typecheck、build:通过。
|
||||
- 后端 TDX 客户端、映射、服务、HTTP/OpenAPI 测试:通过。
|
||||
- `curl` 冒烟:`/breadth`、`/sectors`、`/funds` 均返回 `source=tdx,status=fresh`;分别验证 `UpHome/DownHome` 汇总、一级行业排序和成交额单位转换。
|
||||
- 异常映射:覆盖畸形涨跌家数字段、空一级行业目录、单根板块 K 线、成交额缺市场/交易日错位/负数,以及三市完整性状态。
|
||||
- 前端状态:覆盖宽度、板块和成交额模块的 `partial`/`delayed` 警告、真实交易日期和成交额零变化中性色。
|
||||
- 浏览器验证:桌面默认视口和 390px 移动视口无横向溢出;综合/短线切换和分析引擎选择可用。
|
||||
|
||||
## 安全、性能与残余风险
|
||||
|
||||
@@ -72,7 +72,7 @@ export class BreadthDataDto {
|
||||
@ApiProperty({ type: Number })
|
||||
declining!: number;
|
||||
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
unchanged!: number | null;
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ export class SectorQuoteDto {
|
||||
@ApiProperty({ type: String })
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
changePercent!: number | null;
|
||||
@ApiProperty({ type: Number })
|
||||
changePercent!: number;
|
||||
|
||||
@ApiProperty({ enum: ['up', 'down', 'flat'] })
|
||||
direction!: QuoteDirection;
|
||||
@@ -110,6 +110,15 @@ export class FundDataDto {
|
||||
|
||||
@ApiProperty({ type: String, example: '亿元' })
|
||||
unit!: '亿元';
|
||||
|
||||
@ApiProperty({ type: Number, minimum: 1, maximum: 3 })
|
||||
marketCount!: number;
|
||||
|
||||
@ApiProperty({ type: String, example: '20260818' })
|
||||
tradeDate!: string;
|
||||
|
||||
@ApiProperty({ type: String, example: '20260817' })
|
||||
previousTradeDate!: string;
|
||||
}
|
||||
|
||||
export class SummaryResponseDto extends MarketOverviewMetaDto {
|
||||
|
||||
@@ -118,6 +118,26 @@ describe('市场概览服务', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('市场宽度排除畸形快照并标记部分数据', async () => {
|
||||
const client = {
|
||||
request: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(breadthSnapshot('10', '5'))
|
||||
.mockResolvedValueOnce({ ErrorId: '0', UpHome: '', DownHome: '3' })
|
||||
.mockRejectedValueOnce(new Error('raw network detail')),
|
||||
};
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getBreadth();
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'partial',
|
||||
data: { advancing: 10, declining: 5, unchanged: null },
|
||||
});
|
||||
expect(response.warnings[0]).toContain('深证市场、北证市场');
|
||||
expect(response.warnings.join(' ')).not.toContain('raw network detail');
|
||||
});
|
||||
|
||||
it('按一级行业涨跌幅排序板块并保留 TDX 代码', async () => {
|
||||
const client = {
|
||||
request: vi.fn().mockResolvedValueOnce(sectorResult).mockResolvedValueOnce(sectorKlineResult),
|
||||
@@ -142,6 +162,46 @@ describe('市场概览服务', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('一级行业目录为空时返回不可用而不是 fresh 空数据', async () => {
|
||||
const client = { request: vi.fn().mockResolvedValue({ ErrorId: '0', Value: [] }) };
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getSectors();
|
||||
|
||||
expect(response).toMatchObject({ status: 'unavailable', data: null });
|
||||
expect(response.warnings).toEqual(['一级行业目录为空']);
|
||||
expect(client.request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('一级行业强弱结果最多展示前八项', async () => {
|
||||
const definitions = Array.from({ length: 9 }, (_, index) => ({
|
||||
Code: `88100${index}.SH`,
|
||||
Name: `行业${index}`,
|
||||
}));
|
||||
const value = Object.fromEntries(
|
||||
definitions.map(({ Code }, index) => [
|
||||
Code,
|
||||
{
|
||||
...indexSeries,
|
||||
Close: ['100', `${101 + index}`],
|
||||
},
|
||||
]),
|
||||
);
|
||||
const client = {
|
||||
request: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ ErrorId: '0', Value: definitions })
|
||||
.mockResolvedValueOnce({ ErrorId: '0', Value: value }),
|
||||
};
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getSectors();
|
||||
|
||||
expect(response.status).toBe('fresh');
|
||||
expect(response.data?.items).toHaveLength(8);
|
||||
expect(response.data?.items[0].name).toBe('行业8');
|
||||
});
|
||||
|
||||
it('按交易所指数成交额计算亿元级趋势而不宣称主力净流入', async () => {
|
||||
const client = { request: vi.fn().mockResolvedValue(turnoverResult) };
|
||||
const service = new MarketOverviewService(client);
|
||||
@@ -156,8 +216,26 @@ describe('市场概览服务', () => {
|
||||
previousAmount: 30,
|
||||
changePercent: 20,
|
||||
unit: '亿元',
|
||||
marketCount: 3,
|
||||
tradeDate: '20260818',
|
||||
previousTradeDate: '20260817',
|
||||
},
|
||||
});
|
||||
expect(response.warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('成交额缺少一个市场时标记 partial 并展示实际市场数', async () => {
|
||||
const partialResult = structuredClone(turnoverResult);
|
||||
delete partialResult.Value['899050.BJ'];
|
||||
const client = { request: vi.fn().mockResolvedValue(partialResult) };
|
||||
const service = new MarketOverviewService(client);
|
||||
|
||||
const response = await service.getFunds();
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'partial',
|
||||
data: { currentAmount: 24, previousAmount: 20, marketCount: 2, tradeDate: '20260818' },
|
||||
});
|
||||
expect(response.warnings).toEqual(['部分市场成交额不可用,当前汇总 2 个市场']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,28 +112,34 @@ export class MarketOverviewService {
|
||||
}),
|
||||
),
|
||||
);
|
||||
const snapshots = results.flatMap((result) =>
|
||||
result.status === 'fulfilled' ? [result.value] : [],
|
||||
);
|
||||
if (!snapshots.length) {
|
||||
const validSnapshots = results.flatMap((result) => {
|
||||
if (result.status === 'rejected') return [];
|
||||
return mapBreadthSnapshots([result.value]).validCount === 1 ? [result.value] : [];
|
||||
});
|
||||
const mapped = mapBreadthSnapshots(validSnapshots);
|
||||
const unavailableMarkets = BREADTH_INDEXES.filter((_, index) => {
|
||||
const result = results[index];
|
||||
return (
|
||||
!result ||
|
||||
result.status === 'rejected' ||
|
||||
mapBreadthSnapshots([result.value]).validCount === 0
|
||||
);
|
||||
}).map((item) => item.name);
|
||||
if (!mapped.data) {
|
||||
return {
|
||||
...meta,
|
||||
status: 'unavailable',
|
||||
data: null,
|
||||
warnings: [
|
||||
this.toWarning(
|
||||
results[0]?.status === 'rejected' ? results[0].reason : undefined,
|
||||
'市场宽度',
|
||||
),
|
||||
],
|
||||
warnings: [`市场宽度数据不可用:${unavailableMarkets.join('、')}`],
|
||||
};
|
||||
}
|
||||
const status = snapshots.length === BREADTH_INDEXES.length ? 'fresh' : 'partial';
|
||||
const status = mapped.validCount === BREADTH_INDEXES.length ? 'fresh' : 'partial';
|
||||
return {
|
||||
...meta,
|
||||
status,
|
||||
data: mapBreadthSnapshots(snapshots),
|
||||
warnings: status === 'partial' ? ['部分市场指数未返回涨跌家数'] : [],
|
||||
data: mapped.data,
|
||||
warnings:
|
||||
status === 'partial' ? [`部分市场涨跌家数不可用:${unavailableMarkets.join('、')}`] : [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,6 +151,14 @@ export class MarketOverviewService {
|
||||
list_type: 1,
|
||||
});
|
||||
const definitions = catalog.Value.map(({ Code: code, Name: name }) => ({ code, name }));
|
||||
if (!definitions.length) {
|
||||
return {
|
||||
...meta,
|
||||
status: 'unavailable',
|
||||
data: null,
|
||||
warnings: ['一级行业目录为空'],
|
||||
};
|
||||
}
|
||||
const result = await this.tdxClient.request<TdxMarketDataResult>('get_market_data', {
|
||||
stock_list: definitions.map((item) => item.code),
|
||||
count: 2,
|
||||
@@ -187,10 +201,19 @@ export class MarketOverviewService {
|
||||
dividend_type: 'none',
|
||||
period: '1d',
|
||||
});
|
||||
const data = mapTurnoverTrend(result, TURNOVER_INDEXES);
|
||||
if (!data)
|
||||
const mapped = mapTurnoverTrend(result, TURNOVER_INDEXES);
|
||||
if (!mapped.data)
|
||||
return { ...meta, status: 'unavailable', data: null, warnings: ['成交额趋势数据不完整'] };
|
||||
return { ...meta, status: 'fresh', data, warnings: [] };
|
||||
const status = mapped.validMarketCount === TURNOVER_INDEXES.length ? 'fresh' : 'partial';
|
||||
return {
|
||||
...meta,
|
||||
status,
|
||||
data: mapped.data,
|
||||
warnings:
|
||||
status === 'partial'
|
||||
? [`部分市场成交额不可用,当前汇总 ${mapped.validMarketCount} 个市场`]
|
||||
: [],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...meta,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { mapBreadthSnapshots, mapIndexQuotes, mapTurnoverTrend } from './tdx.mapper';
|
||||
import {
|
||||
mapBreadthSnapshots,
|
||||
mapIndexQuotes,
|
||||
mapSectorQuotes,
|
||||
mapTurnoverTrend,
|
||||
} from './tdx.mapper';
|
||||
|
||||
describe('TDX 行情映射', () => {
|
||||
it('将 K 线数组转换为 A 股红涨绿跌的指数报价', () => {
|
||||
@@ -62,7 +67,66 @@ describe('TDX 行情映射', () => {
|
||||
{ UpHome: '10', DownHome: '5' },
|
||||
{ UpHome: '2', DownHome: '3' },
|
||||
]),
|
||||
).toEqual({ advancing: 12, declining: 8, unchanged: null });
|
||||
).toEqual({
|
||||
data: { advancing: 12, declining: 8, unchanged: null },
|
||||
validCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('市场宽度排除字段缺失、非数值和负数的快照', () => {
|
||||
expect(
|
||||
mapBreadthSnapshots([
|
||||
{ UpHome: '10', DownHome: '5' },
|
||||
{ UpHome: '', DownHome: '3' },
|
||||
{ UpHome: 'NaN', DownHome: '2' },
|
||||
{ UpHome: '-1', DownHome: '2' },
|
||||
]),
|
||||
).toEqual({
|
||||
data: { advancing: 10, declining: 5, unchanged: null },
|
||||
validCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('板块只保留有两个交易日和有效涨跌幅的精确字段', () => {
|
||||
const result = mapSectorQuotes(
|
||||
{
|
||||
ErrorId: '0',
|
||||
Value: {
|
||||
'881001.SH': {
|
||||
Date: ['20260817', '20260818'],
|
||||
Open: ['1', '1'],
|
||||
High: ['1', '1'],
|
||||
Low: ['1', '1'],
|
||||
Close: ['100', '102'],
|
||||
Amount: ['1', '1'],
|
||||
Volume: ['1', '1'],
|
||||
},
|
||||
'881002.SH': {
|
||||
Date: ['20260818'],
|
||||
Open: ['1'],
|
||||
High: ['1'],
|
||||
Low: ['1'],
|
||||
Close: ['100'],
|
||||
Amount: ['1'],
|
||||
Volume: ['1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
[
|
||||
{ code: '881001.SH', name: '行业一' },
|
||||
{ code: '881002.SH', name: '行业二' },
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
code: '881001.SH',
|
||||
name: '行业一',
|
||||
changePercent: 2,
|
||||
direction: 'up',
|
||||
tradeDate: '20260818',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('成交额映射将 TDX 万元转换为亿元并计算环比', () => {
|
||||
@@ -84,6 +148,49 @@ describe('TDX 行情映射', () => {
|
||||
},
|
||||
[{ code: '000001.SH', name: '上证市场' }],
|
||||
),
|
||||
).toEqual({ currentAmount: 12, previousAmount: 10, changePercent: 20, unit: '亿元' });
|
||||
).toEqual({
|
||||
data: {
|
||||
currentAmount: 12,
|
||||
previousAmount: 10,
|
||||
changePercent: 20,
|
||||
unit: '亿元',
|
||||
marketCount: 1,
|
||||
tradeDate: '20260818',
|
||||
previousTradeDate: '20260817',
|
||||
},
|
||||
validMarketCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('成交额只汇总交易日期一致且金额非负的市场', () => {
|
||||
const series = (dates: string[], amounts: string[]) => ({
|
||||
Date: dates,
|
||||
Open: ['1', '1'],
|
||||
High: ['1', '1'],
|
||||
Low: ['1', '1'],
|
||||
Close: ['1', '1'],
|
||||
Amount: amounts,
|
||||
Volume: ['1', '1'],
|
||||
});
|
||||
const result = mapTurnoverTrend(
|
||||
{
|
||||
ErrorId: '0',
|
||||
Value: {
|
||||
'000001.SH': series(['20260817', '20260818'], ['100000', '120000']),
|
||||
'399001.SZ': series(['20260816', '20260817'], ['200000', '240000']),
|
||||
'899050.BJ': series(['20260817', '20260818'], ['10000', '-1']),
|
||||
},
|
||||
},
|
||||
[
|
||||
{ code: '000001.SH', name: '上证市场' },
|
||||
{ code: '399001.SZ', name: '深证市场' },
|
||||
{ code: '899050.BJ', name: '北证市场' },
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
validMarketCount: 1,
|
||||
data: { currentAmount: 12, previousAmount: 10, marketCount: 1, tradeDate: '20260818' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,11 @@ function toNumber(value: string | undefined): number | null {
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function toNonNegativeNumber(value: string | undefined): number | null {
|
||||
const parsed = toNumber(value);
|
||||
return parsed !== null && parsed >= 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function mapDirection(changePercent: number | null): IndexQuoteDto['direction'] {
|
||||
if (changePercent === null || changePercent === 0) return 'flat';
|
||||
return changePercent > 0 ? 'up' : 'down';
|
||||
@@ -52,50 +57,98 @@ export function mapIndexQuotes(
|
||||
});
|
||||
}
|
||||
|
||||
export function mapBreadthSnapshots(
|
||||
snapshots: readonly { UpHome?: string; DownHome?: string }[],
|
||||
): BreadthDataDto {
|
||||
const advancing = snapshots.reduce((total, item) => total + (toNumber(item.UpHome) ?? 0), 0);
|
||||
const declining = snapshots.reduce((total, item) => total + (toNumber(item.DownHome) ?? 0), 0);
|
||||
return { advancing, declining, unchanged: null };
|
||||
/** TDX 涨跌家数必须成对有效且非负;无效市场整体排除,禁止把缺失字段折算为零。 */
|
||||
export function mapBreadthSnapshots(snapshots: readonly { UpHome?: string; DownHome?: string }[]): {
|
||||
data: BreadthDataDto | null;
|
||||
validCount: number;
|
||||
} {
|
||||
const validSnapshots = snapshots.flatMap((item) => {
|
||||
const advancing = toNonNegativeNumber(item.UpHome);
|
||||
const declining = toNonNegativeNumber(item.DownHome);
|
||||
return advancing === null || declining === null ? [] : [{ advancing, declining }];
|
||||
});
|
||||
if (!validSnapshots.length) return { data: null, validCount: 0 };
|
||||
|
||||
return {
|
||||
data: {
|
||||
advancing: validSnapshots.reduce((total, item) => total + item.advancing, 0),
|
||||
declining: validSnapshots.reduce((total, item) => total + item.declining, 0),
|
||||
unchanged: null,
|
||||
},
|
||||
validCount: validSnapshots.length,
|
||||
};
|
||||
}
|
||||
|
||||
/** 一级行业只有在具备相邻两日收盘价时才参与强弱排序,返回对象严格匹配公开 DTO。 */
|
||||
export function mapSectorQuotes(
|
||||
result: TdxMarketDataResult,
|
||||
definitions: readonly IndexDefinition[],
|
||||
): SectorQuoteDto[] {
|
||||
return mapIndexQuotes(result, definitions).sort((left, right) => {
|
||||
return (
|
||||
(right.changePercent ?? Number.NEGATIVE_INFINITY) -
|
||||
(left.changePercent ?? Number.NEGATIVE_INFINITY)
|
||||
);
|
||||
});
|
||||
return definitions
|
||||
.flatMap((definition) => {
|
||||
const series = result.Value[definition.code];
|
||||
if (!series) return [];
|
||||
const quote = mapIndexQuote(series, definition);
|
||||
if (!quote || quote.changePercent === null) return [];
|
||||
return [
|
||||
{
|
||||
code: quote.code,
|
||||
name: quote.name,
|
||||
changePercent: quote.changePercent,
|
||||
direction: quote.direction,
|
||||
tradeDate: quote.tradeDate,
|
||||
},
|
||||
];
|
||||
})
|
||||
.sort((left, right) => right.changePercent - left.changePercent);
|
||||
}
|
||||
|
||||
/** 成交额仅汇总交易日期对齐、金额非负的市场;TDX Amount 从万元转换为亿元。 */
|
||||
export function mapTurnoverTrend(
|
||||
result: TdxMarketDataResult,
|
||||
definitions: readonly IndexDefinition[],
|
||||
): FundDataDto | null {
|
||||
let currentAmount = 0;
|
||||
let previousAmount = 0;
|
||||
let validSeries = 0;
|
||||
|
||||
for (const definition of definitions) {
|
||||
): { data: FundDataDto | null; validMarketCount: number } {
|
||||
const candidates = definitions.flatMap((definition) => {
|
||||
const series = result.Value[definition.code];
|
||||
if (!series) continue;
|
||||
const current = toNumber(series.Amount.at(-1));
|
||||
const previous = toNumber(series.Amount.at(-2));
|
||||
if (current === null || previous === null) continue;
|
||||
currentAmount += current;
|
||||
previousAmount += previous;
|
||||
validSeries += 1;
|
||||
}
|
||||
if (!series) return [];
|
||||
const current = toNonNegativeNumber(series.Amount.at(-1));
|
||||
const previous = toNonNegativeNumber(series.Amount.at(-2));
|
||||
const tradeDate = series.Date.at(-1);
|
||||
const previousTradeDate = series.Date.at(-2);
|
||||
if (
|
||||
current === null ||
|
||||
previous === null ||
|
||||
previous === 0 ||
|
||||
!tradeDate ||
|
||||
!previousTradeDate ||
|
||||
tradeDate === previousTradeDate
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [{ current, previous, tradeDate, previousTradeDate }];
|
||||
});
|
||||
if (!candidates.length) return { data: null, validMarketCount: 0 };
|
||||
|
||||
const expected = candidates[0];
|
||||
const aligned = candidates.filter(
|
||||
(item) =>
|
||||
item.tradeDate === expected.tradeDate &&
|
||||
item.previousTradeDate === expected.previousTradeDate,
|
||||
);
|
||||
const currentAmount = aligned.reduce((total, item) => total + item.current, 0);
|
||||
const previousAmount = aligned.reduce((total, item) => total + item.previous, 0);
|
||||
if (previousAmount === 0) return { data: null, validMarketCount: aligned.length };
|
||||
|
||||
if (!validSeries || previousAmount === 0) return null;
|
||||
return {
|
||||
currentAmount: Math.round((currentAmount / 10000) * 100) / 100,
|
||||
previousAmount: Math.round((previousAmount / 10000) * 100) / 100,
|
||||
changePercent: Math.round(((currentAmount - previousAmount) / previousAmount) * 10000) / 100,
|
||||
unit: '亿元',
|
||||
data: {
|
||||
currentAmount: Math.round((currentAmount / 10000) * 100) / 100,
|
||||
previousAmount: Math.round((previousAmount / 10000) * 100) / 100,
|
||||
changePercent: Math.round(((currentAmount - previousAmount) / previousAmount) * 10000) / 100,
|
||||
unit: '亿元',
|
||||
marketCount: aligned.length,
|
||||
tradeDate: expected.tradeDate,
|
||||
previousTradeDate: expected.previousTradeDate,
|
||||
},
|
||||
validMarketCount: aligned.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"declining": { "type": "number" },
|
||||
"unchanged": { "nullable": true, "type": "number" }
|
||||
},
|
||||
"required": ["advancing", "declining"],
|
||||
"required": ["advancing", "declining", "unchanged"],
|
||||
"type": "object"
|
||||
},
|
||||
"BreadthResponseDto": {
|
||||
@@ -44,10 +44,21 @@
|
||||
"properties": {
|
||||
"changePercent": { "type": "number" },
|
||||
"currentAmount": { "type": "number" },
|
||||
"marketCount": { "maximum": 3, "minimum": 1, "type": "number" },
|
||||
"previousAmount": { "type": "number" },
|
||||
"previousTradeDate": { "example": "20260817", "type": "string" },
|
||||
"tradeDate": { "example": "20260818", "type": "string" },
|
||||
"unit": { "example": "亿元", "type": "string" }
|
||||
},
|
||||
"required": ["currentAmount", "previousAmount", "changePercent", "unit"],
|
||||
"required": [
|
||||
"currentAmount",
|
||||
"previousAmount",
|
||||
"changePercent",
|
||||
"unit",
|
||||
"marketCount",
|
||||
"tradeDate",
|
||||
"previousTradeDate"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FundsResponseDto": {
|
||||
@@ -143,13 +154,13 @@
|
||||
},
|
||||
"SectorQuoteDto": {
|
||||
"properties": {
|
||||
"changePercent": { "nullable": true, "type": "number" },
|
||||
"changePercent": { "type": "number" },
|
||||
"code": { "type": "string" },
|
||||
"direction": { "enum": ["up", "down", "flat"], "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"tradeDate": { "type": "string" }
|
||||
},
|
||||
"required": ["code", "name", "direction", "tradeDate"],
|
||||
"required": ["code", "name", "changePercent", "direction", "tradeDate"],
|
||||
"type": "object"
|
||||
},
|
||||
"SectorsResponseDto": {
|
||||
|
||||
@@ -107,7 +107,7 @@ export interface components {
|
||||
BreadthDataDto: {
|
||||
advancing: number;
|
||||
declining: number;
|
||||
unchanged?: number | null;
|
||||
unchanged: number | null;
|
||||
};
|
||||
BreadthResponseDto: {
|
||||
/** Format: date-time */
|
||||
@@ -128,7 +128,12 @@ export interface components {
|
||||
FundDataDto: {
|
||||
changePercent: number;
|
||||
currentAmount: number;
|
||||
marketCount: number;
|
||||
previousAmount: number;
|
||||
/** @example 20260817 */
|
||||
previousTradeDate: string;
|
||||
/** @example 20260818 */
|
||||
tradeDate: string;
|
||||
/** @example 亿元 */
|
||||
unit: string;
|
||||
};
|
||||
@@ -184,7 +189,7 @@ export interface components {
|
||||
items: components['schemas']['SectorQuoteDto'][];
|
||||
};
|
||||
SectorQuoteDto: {
|
||||
changePercent?: number | null;
|
||||
changePercent: number;
|
||||
code: string;
|
||||
/** @enum {string} */
|
||||
direction: 'up' | 'down' | 'flat';
|
||||
|
||||
@@ -297,6 +297,18 @@ textarea {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.overview-panel .module-warning {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 7px 9px;
|
||||
border-left: 3px solid #b67a2d;
|
||||
background: #fff8ec;
|
||||
}
|
||||
|
||||
.overview-panel .module-warning strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -497,6 +509,10 @@ textarea {
|
||||
color: #19846e;
|
||||
}
|
||||
|
||||
.turnover-trend [data-direction='flat'] {
|
||||
color: #6f7a84;
|
||||
}
|
||||
|
||||
.turnover-trend {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import type { FundsResponse } from '@/types/market-overview';
|
||||
|
||||
defineProps<{ response: FundsResponse | null; loading: boolean }>();
|
||||
|
||||
function formatTradeDate(value: string | undefined): string {
|
||||
return value?.length === 8 ? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6)}` : '—';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -11,25 +15,39 @@ defineProps<{ response: FundsResponse | null; loading: boolean }>();
|
||||
<span class="section-kicker">量能观察</span>
|
||||
<h2 id="funds-title">成交额趋势</h2>
|
||||
</div>
|
||||
<span class="panel-meta">TDX · 亿元</span>
|
||||
<span class="panel-meta">{{ formatTradeDate(response?.data?.tradeDate) }}</span>
|
||||
</div>
|
||||
<div v-if="loading" class="module-placeholder">正在读取资金趋势…</div>
|
||||
<div v-else-if="response?.status === 'unavailable'" class="module-unavailable">
|
||||
<strong>暂不可用</strong>
|
||||
<span>{{ response.warnings[0] }}</span>
|
||||
</div>
|
||||
<div v-else-if="response?.data" class="turnover-trend">
|
||||
<div>
|
||||
<span>三市成交额</span>
|
||||
<strong>{{ response.data.currentAmount.toLocaleString('zh-CN') }}</strong>
|
||||
<small>{{ response.data.unit }}</small>
|
||||
<template v-else-if="response?.data">
|
||||
<div v-if="response.status !== 'fresh'" class="module-warning">
|
||||
<strong>{{ response.status === 'partial' ? '部分数据' : '数据延迟' }}</strong>
|
||||
<span>{{ response.warnings[0] }}</span>
|
||||
</div>
|
||||
<p :data-direction="response.data.changePercent >= 0 ? 'up' : 'down'">
|
||||
较前一交易日
|
||||
{{ response.data.changePercent > 0 ? '+' : ''
|
||||
}}{{ response.data.changePercent.toFixed(2) }}%
|
||||
</p>
|
||||
</div>
|
||||
<div class="turnover-trend">
|
||||
<div>
|
||||
<span>{{ response.data.marketCount }} 市成交额</span>
|
||||
<strong>{{ response.data.currentAmount.toLocaleString('zh-CN') }}</strong>
|
||||
<small>{{ response.data.unit }}</small>
|
||||
</div>
|
||||
<p
|
||||
:data-direction="
|
||||
response.data.changePercent === 0
|
||||
? 'flat'
|
||||
: response.data.changePercent > 0
|
||||
? 'up'
|
||||
: 'down'
|
||||
"
|
||||
>
|
||||
较前一交易日
|
||||
{{ response.data.changePercent > 0 ? '+' : ''
|
||||
}}{{ response.data.changePercent.toFixed(2) }}%
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="module-placeholder">暂无成交额趋势数据</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -18,17 +18,23 @@ defineProps<{ response: BreadthResponse | null; loading: boolean }>();
|
||||
<strong>暂不可用</strong>
|
||||
<span>{{ response.warnings[0] }}</span>
|
||||
</div>
|
||||
<div v-else-if="response?.data" class="breadth-values">
|
||||
<div>
|
||||
<span>上涨</span><strong class="positive">{{ response.data.advancing }}</strong>
|
||||
<template v-else-if="response?.data">
|
||||
<div v-if="response.status !== 'fresh'" class="module-warning">
|
||||
<strong>{{ response.status === 'partial' ? '部分数据' : '数据延迟' }}</strong>
|
||||
<span>{{ response.warnings[0] }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>下跌</span><strong class="negative">{{ response.data.declining }}</strong>
|
||||
<div class="breadth-values">
|
||||
<div>
|
||||
<span>上涨</span><strong class="positive">{{ response.data.advancing }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>下跌</span><strong class="negative">{{ response.data.declining }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>平盘</span><strong>{{ response.data.unchanged ?? '—' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>平盘</span><strong>{{ response.data.unchanged ?? '—' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="module-placeholder">暂无宽度数据</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -54,6 +54,52 @@ describe('市场概览真实数据面板', () => {
|
||||
expect(wrapper.emitted('select')).toEqual([['881319.SH']]);
|
||||
});
|
||||
|
||||
it('市场宽度 partial 状态显示警告且保留有效数据', () => {
|
||||
const wrapper = mount(MarketBreadth, {
|
||||
props: {
|
||||
loading: false,
|
||||
response: {
|
||||
...meta,
|
||||
status: 'partial',
|
||||
warnings: ['部分市场涨跌家数不可用:北证市场'],
|
||||
data: { advancing: 10, declining: 5, unchanged: null },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('部分数据');
|
||||
expect(wrapper.text()).toContain('北证市场');
|
||||
expect(wrapper.text()).toContain('10');
|
||||
});
|
||||
|
||||
it('板块 delayed 状态显示警告和真实交易日期', () => {
|
||||
const wrapper = mount(SectorOverview, {
|
||||
props: {
|
||||
loading: false,
|
||||
response: {
|
||||
...meta,
|
||||
status: 'delayed',
|
||||
warnings: ['板块数据已延迟'],
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
code: '881319.SH',
|
||||
name: '半导体',
|
||||
changePercent: 2.56,
|
||||
direction: 'up',
|
||||
tradeDate: '20260818',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('数据延迟');
|
||||
expect(wrapper.text()).toContain('板块数据已延迟');
|
||||
expect(wrapper.text()).toContain('2026-08-18');
|
||||
});
|
||||
|
||||
it('资金模块明确展示成交额和与上一交易日的变化', () => {
|
||||
const wrapper = mount(FundTrend, {
|
||||
props: {
|
||||
@@ -65,6 +111,9 @@ describe('市场概览真实数据面板', () => {
|
||||
previousAmount: 11000,
|
||||
changePercent: 12.23,
|
||||
unit: '亿元' as const,
|
||||
marketCount: 3,
|
||||
tradeDate: '20260818',
|
||||
previousTradeDate: '20260817',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -75,4 +124,32 @@ describe('市场概览真实数据面板', () => {
|
||||
expect(wrapper.text()).toContain('较前一交易日 +12.23%');
|
||||
expect(wrapper.text()).not.toContain('主力净流入');
|
||||
});
|
||||
|
||||
it('部分数据保留数值并显示警告和实际交易日期', () => {
|
||||
const wrapper = mount(FundTrend, {
|
||||
props: {
|
||||
loading: false,
|
||||
response: {
|
||||
...meta,
|
||||
status: 'partial',
|
||||
warnings: ['部分市场成交额不可用,当前汇总 2 个市场'],
|
||||
data: {
|
||||
currentAmount: 100,
|
||||
previousAmount: 100,
|
||||
changePercent: 0,
|
||||
unit: '亿元' as const,
|
||||
marketCount: 2,
|
||||
tradeDate: '20260818',
|
||||
previousTradeDate: '20260817',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('部分数据');
|
||||
expect(wrapper.text()).toContain('部分市场成交额不可用');
|
||||
expect(wrapper.text()).toContain('2 市成交额');
|
||||
expect(wrapper.text()).toContain('2026-08-18');
|
||||
expect(wrapper.get('[data-direction="flat"]').attributes('data-direction')).toBe('flat');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { SectorsResponse } from '@/types/market-overview';
|
||||
|
||||
defineProps<{ response: SectorsResponse | null; loading: boolean }>();
|
||||
const emit = defineEmits<{ select: [sector: string] }>();
|
||||
|
||||
function formatTradeDate(value: string | undefined): string {
|
||||
return value?.length === 8 ? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6)}` : '—';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,32 +16,34 @@ const emit = defineEmits<{ select: [sector: string] }>();
|
||||
<span class="section-kicker">板块扫描</span>
|
||||
<h2 id="sectors-title">强弱分布</h2>
|
||||
</div>
|
||||
<span class="panel-meta">点击查看详情</span>
|
||||
<span class="panel-meta">{{ formatTradeDate(response?.data?.items[0]?.tradeDate) }}</span>
|
||||
</div>
|
||||
<div v-if="loading" class="module-placeholder">正在读取板块…</div>
|
||||
<div v-else-if="response?.status === 'unavailable'" class="module-unavailable">
|
||||
<strong>暂不可用</strong>
|
||||
<span>{{ response.warnings[0] }}</span>
|
||||
</div>
|
||||
<div v-else-if="response?.data?.items.length" class="sector-list">
|
||||
<button
|
||||
v-for="sector in response.data.items"
|
||||
:key="sector.code"
|
||||
type="button"
|
||||
:data-direction="sector.direction"
|
||||
:title="`进入${sector.name}详细分析`"
|
||||
@click="emit('select', sector.code)"
|
||||
>
|
||||
<span>{{ sector.name }}</span>
|
||||
<strong>
|
||||
{{
|
||||
sector.changePercent == null
|
||||
? '—'
|
||||
: `${sector.changePercent > 0 ? '+' : ''}${sector.changePercent.toFixed(2)}%`
|
||||
}}
|
||||
</strong>
|
||||
</button>
|
||||
</div>
|
||||
<template v-else-if="response?.data?.items.length">
|
||||
<div v-if="response.status !== 'fresh'" class="module-warning">
|
||||
<strong>{{ response.status === 'partial' ? '部分数据' : '数据延迟' }}</strong>
|
||||
<span>{{ response.warnings[0] }}</span>
|
||||
</div>
|
||||
<div class="sector-list">
|
||||
<button
|
||||
v-for="sector in response.data.items"
|
||||
:key="sector.code"
|
||||
type="button"
|
||||
:data-direction="sector.direction"
|
||||
:title="`进入${sector.name}详细分析`"
|
||||
@click="emit('select', sector.code)"
|
||||
>
|
||||
<span>{{ sector.name }}</span>
|
||||
<strong>{{
|
||||
`${sector.changePercent > 0 ? '+' : ''}${sector.changePercent.toFixed(2)}%`
|
||||
}}</strong>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="module-placeholder">暂无板块数据</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user