diff --git a/doc/market-ranking-known-issues.md b/doc/market-ranking-known-issues.md index a28f9a6..c962974 100644 --- a/doc/market-ranking-known-issues.md +++ b/doc/market-ranking-known-issues.md @@ -70,24 +70,22 @@ 逐项验证涨幅、跌幅、1/5/10/15 分钟涨速和跌速、成交额、日增仓 Tab。预期每个 Tab 的 `sortid`、 排序方向、第三列字段和值均正确,不出现整列 `--` 或排序方向相反。 -### 2. 基准价计算(高风险) +### 2. 基准价计算(代码已补齐,待宿主验证) -`MarketRankingHqRequestClient` 已接入 `applyStandardPriceToTableData`,但当前传入的 `fieldIds` -与仓库内 `FirstPageSelfStockRequestClient` 的基准价必需字段存在差异。市场排名当前未包含 -`DATA_ID_ZD`、`DATA_ID_MARKET_OLD`,并且昨收、今开、昨结使用本地 `TableConstants` 字段, -没有使用 `StandardPriceTypeHelper.FIELD_ZUO_SHOU`、`FIELD_JIN_KAI`、`FIELD_ZUO_JIE`。 +`MarketRankingHqRequestClient` 已按 `FirstPageSelfStockRequestClient` 补齐 +`applyStandardPriceToTableData` 所需字段,包括 `DATA_ID_ZD`、`DATA_ID_MARKET_OLD` 以及 +`StandardPriceTypeHelper` 提供的昨收、今开、昨结字段。4106 请求同时携带 `update=1` 和当前 +`computemode`,使服务端排序口径与客户端展示口径保持一致。 -在无法查看宿主 `biz_quote` 和 `@b2c/lib_baseui` 实现的情况下,尚不能确认这些字段是否等价或可省略。 -如果宿主实现依赖上述完整字段,用户切换基准价后可能出现涨跌幅未更新、计算口径错误或整列显示 `--`。 -因此该项应按高风险处理,在字段要求得到确认并完成宿主验证前,不应认定市场排名的基准价功能可发布。 +当前 HAR 无法查看宿主 `biz_quote`、`@b2c/lib_baseui` 和 4106 服务实现,因此仍需确认排序场景 +`qht_qihuo_sort` 支持 `computemode`,并在宿主完成以下验证: 分别选择昨收、今开、昨结作为基准价,验证: - 最新价不变,涨跌幅按当前基准价重新计算。 - 页面隐藏后修改基准价,再返回首页会重新订阅并更新。 - 首包和后续实时推送采用相同计算口径。 -- 对照 `FirstPageSelfStockRequestClient` 确认必需字段集合,并优先复用宿主 - `TableConstants` / `StandardPriceTypeHelper`,避免维护重复字段 ID。 +- 涨幅、跌幅列表顺序与当前基准价下展示的涨跌幅一致。 ### 3. HTTP 推荐合约接口 diff --git a/src/main/ets/market-ranking/MarketRankingDataMapper.ets b/src/main/ets/market-ranking/MarketRankingDataMapper.ets new file mode 100644 index 0000000..62c7789 --- /dev/null +++ b/src/main/ets/market-ranking/MarketRankingDataMapper.ets @@ -0,0 +1,24 @@ +import { + CardData, + RankItem, + RecommendFuturesItem, +} from './MarketRankingModels'; + +export function cardDataToContracts(cardData: CardData): RecommendFuturesItem[] { + return cardData.tableList.map((item: RankItem): RecommendFuturesItem => ({ + contract_code: item.code, + contract_name: item.name, + market: item.market, + })); +} + +export function contractsToPlaceholderCardData(contracts: RecommendFuturesItem[]): CardData { + const cardData: CardData = { + tableList: contracts.map((contract: RecommendFuturesItem): RankItem => ({ + code: contract.contract_code, + market: contract.market, + name: contract.contract_name, + })), + }; + return cardData; +} diff --git a/src/main/ets/market-ranking/MarketRankingNodeComponent.ets b/src/main/ets/market-ranking/MarketRankingNodeComponent.ets index f5d9985..bac1706 100644 --- a/src/main/ets/market-ranking/MarketRankingNodeComponent.ets +++ b/src/main/ets/market-ranking/MarketRankingNodeComponent.ets @@ -6,17 +6,23 @@ import { RankItem, RecommendFuturesItem, TabMetric, } from './MarketRankingModels'; import { MarketRankingDataFetcher } from './MarketRankingDataFetcher'; +import { + cardDataToContracts, + contractsToPlaceholderCardData, +} from './MarketRankingDataMapper'; import { MarketRankingHqRequestClient } from '../node/clients/MarketRankingHqRequestClient'; import { formatGreatNumber, formatPercent, formatPrice, riseFallColor } from '../common/NumberFormat'; import { AllCardsCard } from '../node/model/AiDiagnosisNodeModel'; import { PERIOD_RANGES, TAB_METRICS } from './MarketRankingConstant'; import { enableDarkMode } from '@kernel/theme_manager'; -import { EmitterConstants, GlobalContext, HXLog, jumpToQuote } from 'biz_common'; -import { QuoteCustomSettingManager, StandardPriceType } from 'biz_quote'; +import { EmitterConstants, GlobalContext, jumpToQuote } from 'biz_common'; import { RouterUtil } from '../util/RouterUtil'; const NORMAL_SIZE = 3; const MAX_SIZE = 5; +const UNFOLDED_STORAGE_KEY = 'firstPageMarketRankingUnfolded'; + +PersistentStorage.persistProp(UNFOLDED_STORAGE_KEY, false); @Component export struct MarketRankingNodeComponent { @@ -29,13 +35,14 @@ export struct MarketRankingNodeComponent { @State cardConfig: AllCardsCard | undefined = undefined; @State cardData: CardData = { tableList: [] }; private contracts: RecommendFuturesItem[] = []; + private tabDataCache: Map = new Map(); // 区分加载中和暂无数据,与 HotListNodeComponent 的 dataReady 约定保持一致 @State dataReady: boolean = false; // Tab 与展开状态 @State primaryIdx: number = 0; @State secondaryIdx: number = 0; - @State unfolded: boolean = false; + @StorageLink(UNFOLDED_STORAGE_KEY) unfolded: boolean = false; // Tab 滚动区域状态 @State tabViewportWidth: number = 0; @@ -53,8 +60,6 @@ export struct MarketRankingNodeComponent { private hqUnsubscribe: (() => void) | undefined; private hqSubscriptionVersion: number = 0; private isTcpEventSubscribed: boolean = false; - private lastStandardPriceType: StandardPriceType = - QuoteCustomSettingManager.getInstance().getStandardPriceType(); private tcpReconnectListener = (): void => { if (!this.canUseHq()) { return; @@ -114,19 +119,9 @@ export struct MarketRankingNodeComponent { return; } this.subscribeTcpNetStatus(); - this.syncStandardPriceType(); this.restartCurrentSubscription(); } - private syncStandardPriceType(): void { - const currentStandardPriceType: StandardPriceType = - QuoteCustomSettingManager.getInstance().getStandardPriceType(); - if (this.lastStandardPriceType !== currentStandardPriceType) { - HXLog.d('MarketRankingNodeComponent', 'Standard price type changed, resubscribe'); - this.lastStandardPriceType = currentStandardPriceType; - } - } - private subscribeTcpNetStatus(): void { if (this.isTcpEventSubscribed || !this.canUseHq()) { return; @@ -185,11 +180,21 @@ export struct MarketRankingNodeComponent { } const requestVersion: number = this.dataRequestVersion + 1; this.dataRequestVersion = requestVersion; - // 切换 Tab 后立即清理旧列表,避免新请求返回前闪现上一 Tab 的数据。 + const cacheKey: string = this.getTabCacheKey(primaryIdx, secondaryIdx); + const cachedData: CardData | undefined = this.tabDataCache.get(cacheKey); this.stopSubscribeHq(); - this.contracts = []; - this.cardData = { tableList: [] }; - this.dataReady = false; + if (cachedData !== undefined) { + this.cardData = cachedData; + this.contracts = cardDataToContracts(cachedData); + this.dataReady = true; + if (this.firstPageVisible) { + this.subscribeHq(primaryIdx, secondaryIdx); + } + } else { + this.contracts = []; + this.cardData = { tableList: [] }; + this.dataReady = false; + } try { const contracts = await MarketRankingDataFetcher .getInstance() @@ -198,14 +203,11 @@ export struct MarketRankingNodeComponent { return; } this.contracts = contracts; - this.cardData = { - tableList: contracts.map((contract: RecommendFuturesItem): RankItem => ({ - code: contract.contract_code, - market: contract.market, - name: contract.contract_name, - })), - }; + if (cachedData === undefined) { + this.cardData = contractsToPlaceholderCardData(contracts); + } this.dataReady = true; + this.tabDataCache.set(cacheKey, this.cardData); if (this.firstPageVisible) { this.subscribeHq(primaryIdx, secondaryIdx); } @@ -213,8 +215,10 @@ export struct MarketRankingNodeComponent { if (!this.isCurrentDataRequest(requestVersion, primaryIdx, secondaryIdx)) { return; } - this.contracts = []; - this.cardData = { tableList: [] }; + if (cachedData === undefined) { + this.contracts = []; + this.cardData = { tableList: [] }; + } this.dataReady = true; } } @@ -235,6 +239,7 @@ export struct MarketRankingNodeComponent { primaryIdx === this.primaryIdx && secondaryIdx === this.secondaryIdx) { this.cardData = cardData; + this.tabDataCache.set(this.getTabCacheKey(primaryIdx, secondaryIdx), cardData); } }); } @@ -266,6 +271,14 @@ export struct MarketRankingNodeComponent { secondaryIdx === this.secondaryIdx; } + private getTabCacheKey(primaryIdx: number, secondaryIdx: number): string { + const metric: TabMetric = TAB_METRICS[primaryIdx]; + if (metric.hasChildren) { + return `${metric.id}_${PERIOD_RANGES[secondaryIdx].id}`; + } + return `${metric.id}`; + } + // ==================== 页面跳转 ==================== // 对齐 futures-homepage jumpToFenShi,跳转客户端行情分时页。 @@ -357,26 +370,24 @@ export struct MarketRankingNodeComponent { }) // 展开/收起按钮,样式对齐 HotListNodeComponent 的展开/收起按钮 - if (this.cardData.tableList.length > NORMAL_SIZE) { - Row() { - Text(this.unfolded - ? $r('app.string.hotlist_collapse') - : $r('app.string.hotlist_expand')) - .fontColor($r('app.color.elements_text_tertiary')) - .fontSize(12) - .height(16) - .width('100%') - .textAlign(TextAlign.Center) - } - .width(40) - .height(20) - .borderRadius(10) - .backgroundColor($r('app.color.elements_button_bg_disable_02')) - .margin({ left: 8 }) - .onClick(() => { - this.unfolded = !this.unfolded; - }) + Row() { + Text(this.unfolded + ? $r('app.string.hotlist_collapse') + : $r('app.string.hotlist_expand')) + .fontColor($r('app.color.elements_text_tertiary')) + .fontSize(12) + .height(16) + .width('100%') + .textAlign(TextAlign.Center) } + .width(40) + .height(20) + .borderRadius(10) + .backgroundColor($r('app.color.elements_button_bg_disable_02')) + .margin({ left: 8 }) + .onClick(() => { + this.toggleUnfolded(); + }) Blank() } @@ -540,7 +551,6 @@ export struct MarketRankingNodeComponent { } .width('100%') .height(this.emptyHeight()) - .margin({ top: 10 }) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) } @@ -581,6 +591,8 @@ export struct MarketRankingNodeComponent { ) } .width('100%') + .height(this.emptyHeight()) + .animation({ duration: 180, curve: Curve.EaseInOut }) } @Builder @@ -626,6 +638,10 @@ export struct MarketRankingNodeComponent { // ==================== 展示计算 ==================== + private toggleUnfolded(): void { + this.unfolded = !this.unfolded; + } + private showTabGradient(): boolean { const maxOffset = this.tabContentWidth - this.tabViewportWidth; return maxOffset > 0 && this.tabScrollOffset < maxOffset; diff --git a/src/main/ets/node/clients/MarketRankingHqRequestClient.ets b/src/main/ets/node/clients/MarketRankingHqRequestClient.ets index c62c1e8..4a82265 100644 --- a/src/main/ets/node/clients/MarketRankingHqRequestClient.ets +++ b/src/main/ets/node/clients/MarketRankingHqRequestClient.ets @@ -1,7 +1,12 @@ import { AbsRowData, RowData, TableData } from '@b2b/hq_table'; -import { TableRequestClient } from '@b2c/lib_baseui'; +import { TableConstants as BaseTableConstants, TableRequestClient } from '@b2c/lib_baseui'; import { RequestHelper, StuffTableStruct } from '@kernel/lib_communication'; -import { applyStandardPriceToTableData } from 'biz_quote'; +import { + QuoteCustomSettingManager, + StandardPriceType, + StandardPriceTypeHelper, + applyStandardPriceToTableData, +} from 'biz_quote'; import { CardData, RankItem, @@ -16,6 +21,17 @@ import { TableConstants } from '../../market-ranking/TableConstants'; const FRAME_ID = 2201; const PAGE_ID = 4106; +const REQUIRED_IDS_FOR_STANDARD_PRICE: number[] = [ + BaseTableConstants.DATA_ID_CODE_4, + BaseTableConstants.DATA_ID_PRICE, + BaseTableConstants.DATA_ID_ZF, + BaseTableConstants.DATA_ID_ZD, + BaseTableConstants.DATA_ID_MARKET, + BaseTableConstants.DATA_ID_MARKET_OLD, + StandardPriceTypeHelper.FIELD_ZUO_SHOU, + StandardPriceTypeHelper.FIELD_JIN_KAI, + StandardPriceTypeHelper.FIELD_ZUO_JIE, +]; export type MarketRankingCardDataListener = (cardData: CardData) => void; @@ -107,7 +123,9 @@ class MarketRankingTableRequestClient extends TableRequestClient { return value; } if (typeof value === 'string' && value !== '' && value !== '--') { - const result: number = Number(value); + // 行情解析及标准价换算后的字段可能带 "%" 等展示后缀。 + // 与仓库内其他行情卡片保持一致,读取开头的有效数值。 + const result: number = parseFloat(value); return Number.isNaN(result) ? undefined : result; } return undefined; @@ -142,14 +160,12 @@ export class MarketRankingHqRequestClient { const sortId: number = this.getSortId(primaryIdx, secondaryIdx); const fieldIds: number[] = [ TableConstants.DATA_ID_NAME, - TableConstants.DATA_ID_PRICE, - TableConstants.DATA_ID_CODE_4, - TableConstants.DATA_ID_MARKET, - TableConstants.DATA_ID_ZF, - TableConstants.DATA_ID_PRE_PRICE, - TableConstants.DATA_ID_OPEN_PRICE, - TableConstants.DATA_ID_JSJ, ]; + REQUIRED_IDS_FOR_STANDARD_PRICE.forEach((fieldId: number): void => { + if (!fieldIds.includes(fieldId)) { + fieldIds.push(fieldId); + } + }); if (!fieldIds.includes(sortId)) { fieldIds.push(sortId); } @@ -182,11 +198,15 @@ export class MarketRankingHqRequestClient { ): string { const metric: TabMetric = TAB_METRICS[primaryIdx]; const sortOrder: string = metric.sortOrder; + const computeMode: StandardPriceType = + QuoteCustomSettingManager.getInstance().getStandardPriceType(); return `\r\nsortid=${sortId}\r\n` + `sortorder=${sortOrder}\r\n` + `push=1\r\n` + `dataitem=${fieldIds.map((fieldId: number): string => `${fieldId},`).join('')}\r\n` + `codelist=${this.formatCodeList(contracts)}\r\n` + + `update=1\r\n` + + `computemode=${computeMode}\r\n` + `scenario=qht_qihuo_sort\r\n` + `precision=1\r\n` + `pushtime=2.5\r\n`;