fix(FEAT-MO-001): 修正概览时间时区显示
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatShanghaiDateTime, formatShanghaiTime } from './datetime';
|
||||
|
||||
describe('A 股时间显示格式化', () => {
|
||||
it('将 UTC ISO 时间转换为上海时区的完整日期时间', () => {
|
||||
expect(formatShanghaiDateTime('2026-08-24T14:16:26.813Z')).toBe('2026-08-24 22:16:26');
|
||||
});
|
||||
|
||||
it('将 UTC ISO 时间转换为上海时区的时分秒', () => {
|
||||
expect(formatShanghaiTime('2026-08-24T14:16:26.813Z')).toBe('22:16:26');
|
||||
});
|
||||
|
||||
it('收到空值或非法日期时返回占位符', () => {
|
||||
expect(formatShanghaiDateTime(undefined)).toBe('—');
|
||||
expect(formatShanghaiTime('not-a-date')).toBe('—');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
const SHANGHAI_TIME_ZONE = 'Asia/Shanghai';
|
||||
|
||||
type DatePart = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second';
|
||||
|
||||
function getShanghaiDateParts(value: string | undefined): Record<DatePart, string> | null {
|
||||
if (!value) return null;
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: SHANGHAI_TIME_ZONE,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
}).formatToParts(date);
|
||||
|
||||
const result = Object.fromEntries(
|
||||
parts
|
||||
.filter((part): part is Intl.DateTimeFormatPart & { type: DatePart } =>
|
||||
['year', 'month', 'day', 'hour', 'minute', 'second'].includes(part.type),
|
||||
)
|
||||
.map((part) => [part.type, part.value]),
|
||||
) as Partial<Record<DatePart, string>>;
|
||||
|
||||
if (Object.values(result).some((part) => !part)) return null;
|
||||
return result as Record<DatePart, string>;
|
||||
}
|
||||
|
||||
/** A 股页面统一使用上海时区,接口仍保留标准 UTC ISO 时间。 */
|
||||
export function formatShanghaiDateTime(value: string | undefined): string {
|
||||
const parts = getShanghaiDateParts(value);
|
||||
return parts
|
||||
? `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`
|
||||
: '—';
|
||||
}
|
||||
|
||||
export function formatShanghaiTime(value: string | undefined): string {
|
||||
const parts = getShanghaiDateParts(value);
|
||||
return parts ? `${parts.hour}:${parts.minute}:${parts.second}` : '—';
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndicesResponse } from '@/types/market-overview';
|
||||
import { formatShanghaiTime } from '@/utils/datetime';
|
||||
|
||||
defineProps<{ response: IndicesResponse | null; loading: boolean }>();
|
||||
const emit = defineEmits<{ select: [code: string] }>();
|
||||
@@ -12,7 +13,7 @@ const emit = defineEmits<{ select: [code: string] }>();
|
||||
<span class="section-kicker">核心指数</span>
|
||||
<h2 id="indices-title">主要市场温度</h2>
|
||||
</div>
|
||||
<span class="panel-meta">{{ response?.asOf ? response.asOf.slice(11, 19) : '—' }}</span>
|
||||
<span class="panel-meta">{{ formatShanghaiTime(response?.asOf) }}</span>
|
||||
</div>
|
||||
<div v-if="loading" class="module-placeholder">正在读取指数…</div>
|
||||
<div v-else-if="!response?.data?.items.length" class="module-placeholder">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SummaryResponse } from '@/types/market-overview';
|
||||
import { formatShanghaiDateTime } from '@/utils/datetime';
|
||||
|
||||
defineProps<{
|
||||
response: SummaryResponse | null;
|
||||
@@ -20,7 +21,9 @@ defineProps<{
|
||||
<div class="source-status" :data-status="response?.status || 'unavailable'">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<span>{{ response?.source === 'tdx' ? 'TDX 行情' : '等待数据' }}</span>
|
||||
<small>{{ response?.asOf ? `更新于 ${response.asOf}` : '尚未连接' }}</small>
|
||||
<small>{{
|
||||
response?.asOf ? `更新于 ${formatShanghaiDateTime(response.asOf)}` : '尚未连接'
|
||||
}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conclusion-facts">
|
||||
|
||||
@@ -2,7 +2,9 @@ import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import FundTrend from './FundTrend.vue';
|
||||
import IndexOverview from './IndexOverview.vue';
|
||||
import MarketBreadth from './MarketBreadth.vue';
|
||||
import MarketConclusion from './MarketConclusion.vue';
|
||||
import SectorOverview from './SectorOverview.vue';
|
||||
import ShortTermEmotion from './ShortTermEmotion.vue';
|
||||
|
||||
@@ -15,6 +17,54 @@ const meta = {
|
||||
};
|
||||
|
||||
describe('市场概览真实数据面板', () => {
|
||||
it('市场结论按上海时区展示更新时间而不是 UTC 原文', () => {
|
||||
const wrapper = mount(MarketConclusion, {
|
||||
props: {
|
||||
loading: false,
|
||||
response: {
|
||||
...meta,
|
||||
data: {
|
||||
verdict: '市场维持偏强震荡',
|
||||
marketStatus: '指数偏强',
|
||||
volumeStatus: '成交额趋势见下方',
|
||||
sentimentStatus: '涨跌家数见下方',
|
||||
drivers: [],
|
||||
risks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('更新于 2026-08-18 23:00:00');
|
||||
expect(wrapper.text()).not.toContain('2026-08-18T15:00:00.000Z');
|
||||
});
|
||||
|
||||
it('核心指数按上海时区展示更新时间', () => {
|
||||
const wrapper = mount(IndexOverview, {
|
||||
props: {
|
||||
loading: false,
|
||||
response: {
|
||||
...meta,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
code: '000001.SH',
|
||||
name: '上证指数',
|
||||
price: 3380,
|
||||
changePercent: 1.2,
|
||||
direction: 'up',
|
||||
tradeDate: '20260818',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('23:00:00');
|
||||
expect(wrapper.text()).not.toContain('15:00:00');
|
||||
});
|
||||
|
||||
it('市场宽度对未知平盘家数显示占位符而不是零', () => {
|
||||
const wrapper = mount(MarketBreadth, {
|
||||
props: {
|
||||
|
||||
Reference in New Issue
Block a user