From da66f7233d09318e468e7ab66affd79745a13612 Mon Sep 17 00:00:00 2001 From: clz Date: Mon, 27 Jul 2026 20:53:52 +0800 Subject: [PATCH] fix(ai-pick): complete quote handling and component organization --- src/main/ets/ai-pick/AiPickNodeComponent.ets | 234 ++++++++++-------- .../node/clients/AiPickHqRequestClient.ets | 24 +- 2 files changed, 150 insertions(+), 108 deletions(-) diff --git a/src/main/ets/ai-pick/AiPickNodeComponent.ets b/src/main/ets/ai-pick/AiPickNodeComponent.ets index 6b6949a..4c914b3 100644 --- a/src/main/ets/ai-pick/AiPickNodeComponent.ets +++ b/src/main/ets/ai-pick/AiPickNodeComponent.ets @@ -18,7 +18,6 @@ import { import { EMPTY_IMAGE_DARK, EMPTY_IMAGE_LIGHT, - MAX_ITEMS, } from './AiPickConstant'; import { AllCardsCard } from '../node/model/AiDiagnosisNodeModel'; import { RouterUtil } from '../util/RouterUtil'; @@ -28,36 +27,36 @@ interface StrategyDescParams { tabIdx: number; } -const TAB_BAR_HEIGHT = 30; -const STRATEGY_DESC_HEIGHT = 86; -const TABLE_HEADER_HEIGHT = 40; -const TABLE_ROW_HEIGHT = 45; -const TABLE_HEIGHT = TABLE_HEADER_HEIGHT + TABLE_ROW_HEIGHT * MAX_ITEMS; -const CUSTOM_STRATEGY_HEIGHT = 30; -const MAIN_CONTENT_HEIGHT = - TAB_BAR_HEIGHT + STRATEGY_DESC_HEIGHT + TABLE_HEIGHT + CUSTOM_STRATEGY_HEIGHT; -const NO_CONTRACT_HEIGHT = STRATEGY_DESC_HEIGHT + TABLE_HEIGHT; - @Component export struct AiPickNodeComponent { + // 外部状态 @Link @Watch('onRefreshTriggerChange') refreshTrigger: number; @Consume('firstPageVisible') @Watch('onFirstPageVisibleChange') firstPageVisible: boolean = true; @StorageProp(enableDarkMode) isDarkMode: boolean = false; + // 卡片业务数据 @State cardConfig: AllCardsCard | undefined = undefined; @State cardData: CardData = { tabs: [] }; + // 区分加载中和暂无数据 @State dataReady: boolean = false; + + // Tab 状态及滚动区域 @State tabIdx: number = 0; @State tabViewportWidth: number = 0; @State tabContentWidth: number = 0; @State tabScrollOffset: number = 0; + + // 弹层状态 @State showExplainSheet: boolean = false; + // 生命周期与异步请求状态 private isComponentActive: boolean = false; private dataRequestVersion: number = 0; - private hqSubscriptionVersion: number = 0; + + // 行情订阅与事件状态 private hqUnsubscribe: (() => void) | undefined; + private hqSubscriptionVersion: number = 0; private isTcpEventSubscribed: boolean = false; private tcpReconnectListener = (): void => { if (!this.canUseHq()) { @@ -65,11 +64,13 @@ export struct AiPickNodeComponent { } setTimeout((): void => { if (this.canUseHq()) { - this.restartSubscription(); + this.restartCurrentSubscription(); } }); }; + // ==================== 组件生命周期 ==================== + aboutToAppear(): void { this.isComponentActive = true; this.updateCardConfig(); @@ -86,6 +87,14 @@ export struct AiPickNodeComponent { this.unSubscribeTcpNetStatus(); } + // ==================== 配置与刷新 ==================== + + // 从首页统一卡片配置中读取 AI Pick 配置。 + private updateCardConfig(): void { + this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig(); + } + + // refreshTrigger 变化时重新获取策略和合约,并在请求完成后重建行情订阅。 onRefreshTriggerChange(): void { if (!this.isComponentActive) { return; @@ -94,6 +103,9 @@ export struct AiPickNodeComponent { this.updateCardData(); } + // ==================== 可见性与 TCP 重连 ==================== + + // 页面不可见时停止行情;恢复可见后使用已有数据重建订阅。 onFirstPageVisibleChange(): void { if (!this.isComponentActive) { return; @@ -111,10 +123,43 @@ export struct AiPickNodeComponent { } } - private updateCardConfig(): void { - this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig(); + private subscribeTcpNetStatus(): void { + if (this.isTcpEventSubscribed || !this.canUseHq()) { + return; + } + this.isTcpEventSubscribed = true; + emitter.on( + { eventId: EmitterConstants.NETWORK_TCP_RECONNECT }, + this.tcpReconnectListener, + ); } + private unSubscribeTcpNetStatus(): void { + if (!this.isTcpEventSubscribed) { + return; + } + this.isTcpEventSubscribed = false; + emitter.off( + EmitterConstants.NETWORK_TCP_RECONNECT, + this.tcpReconnectListener, + ); + } + + // 收到 TCP 重连事件后,仅在页面可见时重建原合约订阅。 + private restartCurrentSubscription(): void { + if (!this.canUseHq()) { + return; + } + if (this.cardData.tabs.length > 0) { + this.subscribeHq(); + } else if (this.dataReady) { + this.updateCardData(); + } + } + + // ==================== 数据请求与行情订阅 ==================== + + // 请求所有策略和合约;刷新成功后保留已有行情值,避免列表短暂回退到占位状态。 private async updateCardData(): Promise { if (!this.isComponentActive) { return; @@ -151,6 +196,7 @@ export struct AiPickNodeComponent { } } + // HTTP 刷新时按“合约 + 市场”复用旧行情,等待新订阅首包覆盖。 private mergeCachedQuotes(tabs: PickTabData[]): PickTabData[] { const cachedContracts: PickContractItem[] = getUniqueContracts(this.cardData); const cachedMap: Map = @@ -184,6 +230,7 @@ export struct AiPickNodeComponent { }); } + // AI Pick 一次订阅所有 Tab 的去重合约,推送后合并回完整卡片数据。 private subscribeHq(): void { this.stopSubscribeHq(); if (!this.canUseHq() || this.cardData.tabs.length === 0) { @@ -209,36 +256,6 @@ export struct AiPickNodeComponent { } } - private restartSubscription(): void { - if (this.cardData.tabs.length > 0) { - this.subscribeHq(); - } else if (this.dataReady) { - this.updateCardData(); - } - } - - private subscribeTcpNetStatus(): void { - if (this.isTcpEventSubscribed || !this.canUseHq()) { - return; - } - this.isTcpEventSubscribed = true; - emitter.on( - { eventId: EmitterConstants.NETWORK_TCP_RECONNECT }, - this.tcpReconnectListener, - ); - } - - private unSubscribeTcpNetStatus(): void { - if (!this.isTcpEventSubscribed) { - return; - } - this.isTcpEventSubscribed = false; - emitter.off( - EmitterConstants.NETWORK_TCP_RECONNECT, - this.tcpReconnectListener, - ); - } - private canUseHq(): boolean { return this.isComponentActive && this.firstPageVisible; } @@ -247,6 +264,63 @@ export struct AiPickNodeComponent { return this.isComponentActive && requestVersion === this.dataRequestVersion; } + // ==================== 页面跳转 ==================== + + // 跳转客户端行情页,并保持合约、市场和名称列表顺序一致。 + private jumpToDetail(item: PickContractItem): void { + const contracts: PickContractItem[] = getUniqueContracts(this.cardData); + const codeList: string = + contracts.map((contract: PickContractItem): string => contract.contract).join(','); + const marketIdList: string = + contracts.map((contract: PickContractItem): string => contract.market).join(','); + const nameList: string = + contracts.map((contract: PickContractItem): string => contract.name).join(','); + jumpToQuote(item.contract, item.market, codeList, marketIdList, nameList); + } + + // 跳转到卡片配置中的标题地址。 + private jumpToCardTitle(): void { + const jumpUrl: string = + this.cardConfig?.card_url.android || this.cardConfig?.card_url.ios || ''; + RouterUtil.jumpPage(jumpUrl, true); + } + + // 跳转 AI 策略定制首页。 + private jumpToNewHome(): void { + const pageUrl: string = + 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-home.html?sync=1'; + RouterUtil.jumpPage(this.buildClientUrl(pageUrl), true); + } + + // 跳转策略详情页。 + private jumpToStrategyDetail(strategy: PickStrategy): void { + const pageUrl: string = + 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-detail.html' + + `?id=${strategy.id}&type=${strategy.periodType}` + + `&name=${encodeURIComponent(strategy.name)}`; + RouterUtil.jumpPage(this.buildClientUrl(pageUrl), true); + } + + // 跳转策略标签页。 + private jumpToLabel(strategy: PickStrategy, label: string): void { + const pageUrl: string = + 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-label.html' + + `?id=${strategy.id}&type=${strategy.periodType}` + + `&label=${encodeURIComponent(label)}`; + RouterUtil.jumpPage(this.buildClientUrl(pageUrl), true); + } + + // 使用宿主统一的 client.html 协议包装 H5 地址。 + private buildClientUrl(pageUrl: string): string { + const title: string = + this.cardConfig?.card_title.value || + getResourceString(getContext(this), $r('app.string.ai_pick_title')); + return 'client.html?action=ymtz^ishiddenbar=1^mode=new^webid=2804^' + + `url=${pageUrl}^title=${title}`; + } + + // ==================== UI 构建 ==================== + private showTabGradient(): boolean { const maxOffset: number = this.tabContentWidth - this.tabViewportWidth; return maxOffset > 0 && this.tabScrollOffset < maxOffset; @@ -389,7 +463,7 @@ export struct AiPickNodeComponent { this.buildCustomStrategy() } .width('100%') - .height(MAIN_CONTENT_HEIGHT) + .height(321) } @Builder @@ -409,7 +483,7 @@ export struct AiPickNodeComponent { .constraintSize({ minWidth: 66 }) .maxLines(1) .textAlign(TextAlign.Center) - .height(TAB_BAR_HEIGHT) + .height(30) .padding({ left: 8, right: 8 }) .borderRadius(4) .backgroundColor(this.tabIdx === index @@ -426,7 +500,7 @@ export struct AiPickNodeComponent { }) } .width('100%') - .height(TAB_BAR_HEIGHT) + .height(30) .padding({ right: 10 }) .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) @@ -438,7 +512,7 @@ export struct AiPickNodeComponent { if (this.showTabGradient()) { Row() .width(24) - .height(TAB_BAR_HEIGHT) + .height(30) .linearGradient({ angle: 90, colors: [ @@ -450,7 +524,7 @@ export struct AiPickNodeComponent { } } .width('100%') - .height(TAB_BAR_HEIGHT) + .height(30) .onAreaChange((_oldValue: Area, newValue: Area) => { this.tabViewportWidth = Number(newValue.width); }) @@ -526,7 +600,7 @@ export struct AiPickNodeComponent { } } .width('100%') - .height(STRATEGY_DESC_HEIGHT) + .height(86) .padding({ top: 10, bottom: 10 }) .alignItems(VerticalAlign.Top) } @@ -551,7 +625,7 @@ export struct AiPickNodeComponent { .textAlign(TextAlign.End) } .width('100%') - .height(TABLE_HEADER_HEIGHT) + .height(40) ForEach( this.cardData.tabs[this.tabIdx].items, @@ -563,7 +637,7 @@ export struct AiPickNodeComponent { ) } .width('100%') - .height(TABLE_HEIGHT) + .height(175) } @Builder @@ -629,7 +703,7 @@ export struct AiPickNodeComponent { .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') - .height(TABLE_ROW_HEIGHT) + .height(45) .alignItems(VerticalAlign.Center) .border({ width: { top: 1 }, @@ -653,7 +727,7 @@ export struct AiPickNodeComponent { .fillColor($r('app.color.elements_icon_tertiary')) } .width('100%') - .height(CUSTOM_STRATEGY_HEIGHT) + .height(30) .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) .onClick(() => { @@ -679,7 +753,7 @@ export struct AiPickNodeComponent { } } .width('100%') - .height(MAIN_CONTENT_HEIGHT) + .height(321) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) } @@ -695,55 +769,9 @@ export struct AiPickNodeComponent { .fontColor($r('app.color.elements_text_tertiary')) } .width('100%') - .height(NO_CONTRACT_HEIGHT) + .height(261) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) } - private jumpToDetail(item: PickContractItem): void { - const contracts: PickContractItem[] = getUniqueContracts(this.cardData); - const codeList: string = - contracts.map((contract: PickContractItem): string => contract.contract).join(','); - const marketIdList: string = - contracts.map((contract: PickContractItem): string => contract.market).join(','); - const nameList: string = - contracts.map((contract: PickContractItem): string => contract.name).join(','); - jumpToQuote(item.contract, item.market, codeList, marketIdList, nameList); - } - - private jumpToCardTitle(): void { - const jumpUrl: string = - this.cardConfig?.card_url.android || this.cardConfig?.card_url.ios || ''; - RouterUtil.jumpPage(jumpUrl, true); - } - - private jumpToNewHome(): void { - const pageUrl: string = - 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-home.html?sync=1'; - RouterUtil.jumpPage(this.buildClientUrl(pageUrl), true); - } - - private jumpToStrategyDetail(strategy: PickStrategy): void { - const pageUrl: string = - 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-detail.html' + - `?id=${strategy.id}&type=${strategy.periodType}` + - `&name=${encodeURIComponent(strategy.name)}`; - RouterUtil.jumpPage(this.buildClientUrl(pageUrl), true); - } - - private jumpToLabel(strategy: PickStrategy, label: string): void { - const pageUrl: string = - 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-label.html' + - `?id=${strategy.id}&type=${strategy.periodType}` + - `&label=${encodeURIComponent(label)}`; - RouterUtil.jumpPage(this.buildClientUrl(pageUrl), true); - } - - private buildClientUrl(pageUrl: string): string { - const title: string = - this.cardConfig?.card_title.value || - getResourceString(getContext(this), $r('app.string.ai_pick_title')); - return 'client.html?action=ymtz^ishiddenbar=1^mode=new^webid=2804^' + - `url=${pageUrl}^title=${title}`; - } } diff --git a/src/main/ets/node/clients/AiPickHqRequestClient.ets b/src/main/ets/node/clients/AiPickHqRequestClient.ets index 018539a..3f787c4 100644 --- a/src/main/ets/node/clients/AiPickHqRequestClient.ets +++ b/src/main/ets/node/clients/AiPickHqRequestClient.ets @@ -1,21 +1,31 @@ import { AbsRowData, RowData, TableData } from '@b2b/hq_table'; -import { TableRequestClient } from '@b2c/lib_baseui'; +import { TableConstants, TableRequestClient } from '@b2c/lib_baseui'; import { RequestHelper, StuffTableStruct } from '@kernel/lib_communication'; +import { + QuoteCustomSettingManager, + StandardPriceType, + StandardPriceTypeHelper, + applyStandardPriceToTableData, +} from 'biz_quote'; import { CardData, PickContractItem, PickTabData, } from '../../ai-pick/AiPickModels'; import { getUniqueContracts } from '../../ai-pick/AiPickUtils'; -import { TableConstants } from '../../market-ranking/TableConstants'; const FRAME_ID = 2201; const PAGE_ID = 4106; const FIELD_IDS: number[] = [ TableConstants.DATA_ID_CODE_4, TableConstants.DATA_ID_PRICE, + TableConstants.DATA_ID_ZF, + TableConstants.DATA_ID_ZD, TableConstants.DATA_ID_MARKET, - TableConstants.DATA_ID_ZF_YTD, + TableConstants.DATA_ID_MARKET_OLD, + StandardPriceTypeHelper.FIELD_ZUO_SHOU, + StandardPriceTypeHelper.FIELD_JIN_KAI, + StandardPriceTypeHelper.FIELD_ZUO_JIE, ]; interface AiPickQuote { @@ -48,7 +58,8 @@ class AiPickTableRequestClient extends TableRequestClient { RequestHelper.build().base(this.frameId, this.pageId, this, this.requestParams).request(); } - protected afterParserModel(tableData: TableData, _structData: StuffTableStruct): void { + protected afterParserModel(tableData: TableData, structData: StuffTableStruct): void { + applyStandardPriceToTableData(tableData, structData, FIELD_IDS); const quoteMap: Map = this.toQuoteMap(tableData); const nextCardData: CardData = this.mergeQuoteData(quoteMap); this.sourceCardData = nextCardData; @@ -71,7 +82,7 @@ class AiPickTableRequestClient extends TableRequestClient { } const quote: AiPickQuote = { price: this.getNumber(row, TableConstants.DATA_ID_PRICE), - priceChg: this.getNumber(row, TableConstants.DATA_ID_ZF_YTD), + priceChg: this.getNumber(row, TableConstants.DATA_ID_ZF), }; quoteMap.set(`${codeValue}_${marketValue}`, quote); }); @@ -148,12 +159,15 @@ export class AiPickHqRequestClient { } private buildRequestParams(contracts: PickContractItem[]): string { + const computeMode: StandardPriceType = + QuoteCustomSettingManager.getInstance().getStandardPriceType(); return `\r\nsortid=-1\r\n` + `sortorder=0\r\n` + `push=1\r\n` + `dataitem=${FIELD_IDS.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`;