feat(FEAT-MO-001): 接入TDX市场宽度板块与成交额趋势

This commit is contained in:
2026-08-18 17:37:36 +08:00
parent 82619489a9
commit ff8ada8316
21 changed files with 616 additions and 69 deletions
+15 -3
View File
@@ -107,7 +107,7 @@ export interface components {
BreadthDataDto: {
advancing: number;
declining: number;
unchanged: number;
unchanged?: number | null;
};
BreadthResponseDto: {
/** Format: date-time */
@@ -126,7 +126,11 @@ export interface components {
status: 'healthy' | 'unavailable' | 'disabled';
};
FundDataDto: {
items: string[];
changePercent: number;
currentAmount: number;
previousAmount: number;
/** @example 亿元 */
unit: string;
};
FundsResponseDto: {
/** Format: date-time */
@@ -177,7 +181,15 @@ export interface components {
warnings: string[];
};
SectorDataDto: {
items: string[];
items: components['schemas']['SectorQuoteDto'][];
};
SectorQuoteDto: {
changePercent?: number | null;
code: string;
/** @enum {string} */
direction: 'up' | 'down' | 'flat';
name: string;
tradeDate: string;
};
SectorsResponseDto: {
/** Format: date-time */
+42
View File
@@ -472,12 +472,54 @@ textarea {
}
.sector-list button {
display: inline-flex;
align-items: center;
justify-content: space-between;
min-width: 112px;
gap: 10px;
padding: 7px 9px;
color: #4c5966;
background: #f3f7f7;
font-size: 12px;
}
.sector-list button strong {
font-weight: 650;
}
.sector-list button[data-direction='up'] strong,
.turnover-trend [data-direction='up'] {
color: #c33f3f;
}
.sector-list button[data-direction='down'] strong,
.turnover-trend [data-direction='down'] {
color: #19846e;
}
.turnover-trend {
display: grid;
gap: 12px;
margin-top: 24px;
}
.turnover-trend div {
display: flex;
align-items: baseline;
gap: 7px;
}
.turnover-trend strong {
color: #1c2a35;
font-size: 25px;
}
.turnover-trend small,
.turnover-trend p {
color: #7d8893;
font-size: 12px;
}
@media (max-width: 900px) {
.overview-grid-top,
.overview-grid-bottom {
@@ -8,16 +8,28 @@ defineProps<{ response: FundsResponse | null; loading: boolean }>();
<section class="overview-panel" aria-labelledby="funds-title">
<div class="panel-heading">
<div>
<span class="section-kicker">资金观察</span>
<h2 id="funds-title">资金趋势</h2>
<span class="section-kicker">量能观察</span>
<h2 id="funds-title">成交额趋势</h2>
</div>
<span class="panel-meta">数据口径</span>
<span class="panel-meta">TDX · 亿元</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 class="module-placeholder">暂无资金趋势数据</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>
</div>
<p :data-direction="response.data.changePercent >= 0 ? 'up' : 'down'">
较前一交易日
{{ response.data.changePercent > 0 ? '+' : ''
}}{{ response.data.changePercent.toFixed(2) }}%
</p>
</div>
<div v-else class="module-placeholder">暂无成交额趋势数据</div>
</section>
</template>
@@ -26,7 +26,7 @@ defineProps<{ response: BreadthResponse | null; loading: boolean }>();
<span>下跌</span><strong class="negative">{{ response.data.declining }}</strong>
</div>
<div>
<span>平盘</span><strong>{{ response.data.unchanged }}</strong>
<span>平盘</span><strong>{{ response.data.unchanged ?? '—' }}</strong>
</div>
</div>
<div v-else class="module-placeholder">暂无宽度数据</div>
@@ -0,0 +1,78 @@
import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import FundTrend from './FundTrend.vue';
import MarketBreadth from './MarketBreadth.vue';
import SectorOverview from './SectorOverview.vue';
const meta = {
snapshotId: '20260818-150000',
asOf: '2026-08-18T15:00:00.000Z',
source: 'tdx' as const,
status: 'fresh' as const,
warnings: [],
};
describe('市场概览真实数据面板', () => {
it('市场宽度对未知平盘家数显示占位符而不是零', () => {
const wrapper = mount(MarketBreadth, {
props: {
loading: false,
response: { ...meta, data: { advancing: 2121, declining: 3292, unchanged: null } },
},
});
expect(wrapper.text()).toContain('2121');
expect(wrapper.text()).toContain('3292');
expect(wrapper.text()).toContain('平盘—');
});
it('板块展示名称和涨跌幅并以代码触发详情入口', async () => {
const wrapper = mount(SectorOverview, {
props: {
loading: false,
response: {
...meta,
data: {
items: [
{
code: '881319.SH',
name: '半导体',
changePercent: 2.56,
direction: 'up',
tradeDate: '20260818',
},
],
},
},
},
});
expect(wrapper.text()).toContain('半导体');
expect(wrapper.text()).toContain('+2.56%');
await wrapper.get('button').trigger('click');
expect(wrapper.emitted('select')).toEqual([['881319.SH']]);
});
it('资金模块明确展示成交额和与上一交易日的变化', () => {
const wrapper = mount(FundTrend, {
props: {
loading: false,
response: {
...meta,
data: {
currentAmount: 12345.67,
previousAmount: 11000,
changePercent: 12.23,
unit: '亿元' as const,
},
},
},
});
expect(wrapper.text()).toContain('成交额趋势');
expect(wrapper.text()).toContain('12,345.67');
expect(wrapper.text()).toContain('较前一交易日 +12.23%');
expect(wrapper.text()).not.toContain('主力净流入');
});
});
@@ -22,11 +22,20 @@ const emit = defineEmits<{ select: [sector: string] }>();
<div v-else-if="response?.data?.items.length" class="sector-list">
<button
v-for="sector in response.data.items"
:key="sector"
:key="sector.code"
type="button"
@click="emit('select', sector)"
:data-direction="sector.direction"
:title="`进入${sector.name}详细分析`"
@click="emit('select', sector.code)"
>
{{ sector }}
<span>{{ sector.name }}</span>
<strong>
{{
sector.changePercent == null
? '—'
: `${sector.changePercent > 0 ? '+' : ''}${sector.changePercent.toFixed(2)}%`
}}
</strong>
</button>
</div>
<div v-else class="module-placeholder">暂无板块数据</div>