feat(FEAT-MO-005): 接入市场结构前端数据层

This commit is contained in:
2026-08-25 02:10:22 +08:00
parent ad40ffffc4
commit b95b9e5c92
7 changed files with 139 additions and 1 deletions
@@ -17,6 +17,9 @@ describe('市场概览业务 API', () => {
await api.getBreadth();
await api.getSectors();
await api.getFunds();
await api.getSectorRankings({ category: 'industry', sort: 'rise', limit: 10 });
await api.getCapitalFlow({ category: 'industry', sort: 'inflow', limit: 10 });
await api.getTurnoverTrend({ interval: '5' });
await api.getShortTermEmotion();
await api.getShortTermConclusion();
@@ -26,6 +29,9 @@ describe('市场概览业务 API', () => {
'/api/v1/market-overview/breadth',
'/api/v1/market-overview/sectors',
'/api/v1/market-overview/funds',
'/api/v1/market-overview/sector-rankings',
'/api/v1/market-overview/capital-flow',
'/api/v1/market-overview/turnover-trend',
'/api/v1/market-overview/short-term/emotion',
'/api/v1/market-overview/short-term/conclusion',
]);
+2
View File
@@ -8,9 +8,11 @@ import type {
ShortTermEmotionResponse,
SummaryResponse,
} from '@/types/market-overview';
import { createMarketStructureApi } from './structure';
export function createMarketOverviewApi(requestClient: RequestClient = request) {
return {
...createMarketStructureApi(requestClient),
getSummary: () =>
requestClient<SummaryResponse>({ method: 'get', url: '/api/v1/market-overview/summary' }),
getIndices: () =>
@@ -0,0 +1,42 @@
import { describe, expect, it, vi } from 'vitest';
import type { RequestClient } from '@/utils/request';
import { createMarketStructureApi } from './structure';
describe('市场结构业务 API', () => {
it('使用独立版本化路径和白名单查询参数', async () => {
const requestMock = vi.fn((config: Parameters<RequestClient>[0]) => {
void config;
return Promise.resolve({});
});
const api = createMarketStructureApi(requestMock as unknown as RequestClient);
await api.getSectorRankings({ category: 'industry', sort: 'rise', limit: 10 });
await api.getCapitalFlow({ category: 'concept', sort: 'outflow', limit: 20 });
await api.getTurnoverTrend({ interval: '5', tradeDate: '20260825' });
expect(requestMock.mock.calls).toEqual([
[
expect.objectContaining({
method: 'get',
url: '/api/v1/market-overview/sector-rankings',
params: { category: 'industry', sort: 'rise', limit: 10 },
}),
],
[
expect.objectContaining({
method: 'get',
url: '/api/v1/market-overview/capital-flow',
params: { category: 'concept', sort: 'outflow', limit: 20 },
}),
],
[
expect.objectContaining({
method: 'get',
url: '/api/v1/market-overview/turnover-trend',
params: { interval: '5', tradeDate: '20260825' },
}),
],
]);
});
});
+51
View File
@@ -0,0 +1,51 @@
import request, { type RequestClient } from '@/utils/request';
import type { components } from '@/api/generated/schema';
export type SectorRankingsResponse = components['schemas']['SectorRankingsResponseDto'];
export type CapitalFlowResponse = components['schemas']['CapitalFlowResponseDto'];
export type TurnoverTrendResponse = components['schemas']['TurnoverTrendResponseDto'];
export interface SectorRankingsQuery {
category: 'industry' | 'concept' | 'board';
sort: 'rise' | 'fall' | 'turnover';
limit: number;
}
export interface CapitalFlowQuery {
category: 'industry' | 'concept' | 'board';
sort: 'inflow' | 'outflow';
limit: number;
}
export interface TurnoverTrendQuery {
interval: '5' | '15';
tradeDate?: string;
}
/** 市场结构请求只调用固定的 Nest 路径,页面不感知 AKTools/TDX 地址。 */
export function createMarketStructureApi(requestClient: RequestClient = request) {
return {
getSectorRankings: (params: SectorRankingsQuery) =>
requestClient<SectorRankingsResponse>({
method: 'get',
url: '/api/v1/market-overview/sector-rankings',
params,
}),
getCapitalFlow: (params: CapitalFlowQuery) =>
requestClient<CapitalFlowResponse>({
method: 'get',
url: '/api/v1/market-overview/capital-flow',
params,
}),
getTurnoverTrend: (params: TurnoverTrendQuery) =>
requestClient<TurnoverTrendResponse>({
method: 'get',
url: '/api/v1/market-overview/turnover-trend',
params,
}),
};
}
const marketStructureApi = createMarketStructureApi();
export default marketStructureApi;
+6
View File
@@ -7,6 +7,9 @@ export type SectorsResponse = components['schemas']['SectorsResponseDto'];
export type FundsResponse = components['schemas']['FundsResponseDto'];
export type ShortTermEmotionResponse = components['schemas']['ShortTermEmotionResponseDto'];
export type ShortTermConclusionResponse = components['schemas']['ShortTermConclusionResponseDto'];
export type SectorRankingsResponse = components['schemas']['SectorRankingsResponseDto'];
export type CapitalFlowResponse = components['schemas']['CapitalFlowResponseDto'];
export type TurnoverTrendResponse = components['schemas']['TurnoverTrendResponseDto'];
export type SummaryData = NonNullable<SummaryResponse['data']>;
export type IndicesData = NonNullable<IndicesResponse['data']>;
export type BreadthData = NonNullable<BreadthResponse['data']>;
@@ -14,5 +17,8 @@ export type SectorsData = NonNullable<SectorsResponse['data']>;
export type FundsData = NonNullable<FundsResponse['data']>;
export type ShortTermEmotionData = NonNullable<ShortTermEmotionResponse['data']>;
export type ShortTermConclusionData = NonNullable<ShortTermConclusionResponse['data']>;
export type SectorRankingsData = NonNullable<SectorRankingsResponse['data']>;
export type CapitalFlowData = NonNullable<CapitalFlowResponse['data']>;
export type TurnoverTrendData = NonNullable<TurnoverTrendResponse['data']>;
export type OverviewModuleStatus = 'idle' | 'loading' | 'ready' | 'error';
@@ -19,6 +19,9 @@ describe('市场概览状态', () => {
getBreadth: vi.fn().mockResolvedValue(response),
getSectors: vi.fn().mockResolvedValue(response),
getFunds: vi.fn().mockResolvedValue(response),
getSectorRankings: vi.fn().mockResolvedValue(response),
getCapitalFlow: vi.fn().mockResolvedValue(response),
getTurnoverTrend: vi.fn().mockResolvedValue(response),
getShortTermEmotion: vi.fn().mockResolvedValue(response),
getShortTermConclusion: vi.fn().mockResolvedValue(response),
};
@@ -32,6 +35,9 @@ describe('市场概览状态', () => {
expect(state.isLoading.value).toBe(false);
expect(state.shortTermEmotion.value.status).toBe('ready');
expect(state.shortTermConclusion.value.status).toBe('ready');
expect(state.sectorRankings.value.status).toBe('ready');
expect(state.capitalFlow.value.status).toBe('ready');
expect(state.turnoverTrend.value.status).toBe('ready');
});
it('分析偏好写入本地存储并可恢复', () => {
@@ -7,6 +7,9 @@ import type {
IndicesResponse,
OverviewModuleStatus,
SectorsResponse,
SectorRankingsResponse,
CapitalFlowResponse,
TurnoverTrendResponse,
ShortTermConclusionResponse,
ShortTermEmotionResponse,
SummaryResponse,
@@ -19,7 +22,10 @@ type MarketResponse =
| SectorsResponse
| FundsResponse
| ShortTermEmotionResponse
| ShortTermConclusionResponse;
| ShortTermConclusionResponse
| SectorRankingsResponse
| CapitalFlowResponse
| TurnoverTrendResponse;
export interface OverviewModuleState<TResponse extends MarketResponse> {
status: OverviewModuleStatus;
@@ -54,6 +60,9 @@ export function useMarketOverview(
const breadth = ref<OverviewModuleState<BreadthResponse>>(createState());
const sectors = ref<OverviewModuleState<SectorsResponse>>(createState());
const funds = ref<OverviewModuleState<FundsResponse>>(createState());
const sectorRankings = ref<OverviewModuleState<SectorRankingsResponse>>(createState());
const capitalFlow = ref<OverviewModuleState<CapitalFlowResponse>>(createState());
const turnoverTrend = ref<OverviewModuleState<TurnoverTrendResponse>>(createState());
const shortTermEmotion = ref<OverviewModuleState<ShortTermEmotionResponse>>(createState());
const shortTermConclusion = ref<OverviewModuleState<ShortTermConclusionResponse>>(createState());
const analysisHorizon = ref(readPreference('a-share-analysis:overview-horizon', '综合'));
@@ -65,6 +74,9 @@ export function useMarketOverview(
breadth.value,
sectors.value,
funds.value,
sectorRankings.value,
capitalFlow.value,
turnoverTrend.value,
shortTermEmotion.value,
shortTermConclusion.value,
].some((module) => module.status === 'loading'),
@@ -76,6 +88,9 @@ export function useMarketOverview(
breadth.value.status = 'loading';
sectors.value.status = 'loading';
funds.value.status = 'loading';
sectorRankings.value.status = 'loading';
capitalFlow.value.status = 'loading';
turnoverTrend.value.status = 'loading';
shortTermEmotion.value.status = 'loading';
shortTermConclusion.value.status = 'loading';
}
@@ -101,6 +116,13 @@ export function useMarketOverview(
loadModule(breadth, api.getBreadth),
loadModule(sectors, api.getSectors),
loadModule(funds, api.getFunds),
loadModule(sectorRankings, () =>
api.getSectorRankings({ category: 'industry', sort: 'rise', limit: 10 }),
),
loadModule(capitalFlow, () =>
api.getCapitalFlow({ category: 'industry', sort: 'inflow', limit: 10 }),
),
loadModule(turnoverTrend, () => api.getTurnoverTrend({ interval: '5' })),
loadModule(shortTermEmotion, api.getShortTermEmotion),
loadModule(shortTermConclusion, api.getShortTermConclusion),
]);
@@ -122,6 +144,9 @@ export function useMarketOverview(
breadth,
sectors,
funds,
sectorRankings,
capitalFlow,
turnoverTrend,
shortTermEmotion,
shortTermConclusion,
isLoading,