fix(ai-pick): complete quote handling and component organization

This commit is contained in:
clz
2026-07-27 20:53:52 +08:00
parent 0e5a82c518
commit da66f7233d
2 changed files with 150 additions and 108 deletions
+131 -103
View File
@@ -18,7 +18,6 @@ import {
import { import {
EMPTY_IMAGE_DARK, EMPTY_IMAGE_DARK,
EMPTY_IMAGE_LIGHT, EMPTY_IMAGE_LIGHT,
MAX_ITEMS,
} from './AiPickConstant'; } from './AiPickConstant';
import { AllCardsCard } from '../node/model/AiDiagnosisNodeModel'; import { AllCardsCard } from '../node/model/AiDiagnosisNodeModel';
import { RouterUtil } from '../util/RouterUtil'; import { RouterUtil } from '../util/RouterUtil';
@@ -28,36 +27,36 @@ interface StrategyDescParams {
tabIdx: number; 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 @Component
export struct AiPickNodeComponent { export struct AiPickNodeComponent {
// 外部状态
@Link @Watch('onRefreshTriggerChange') refreshTrigger: number; @Link @Watch('onRefreshTriggerChange') refreshTrigger: number;
@Consume('firstPageVisible') @Watch('onFirstPageVisibleChange') @Consume('firstPageVisible') @Watch('onFirstPageVisibleChange')
firstPageVisible: boolean = true; firstPageVisible: boolean = true;
@StorageProp(enableDarkMode) isDarkMode: boolean = false; @StorageProp(enableDarkMode) isDarkMode: boolean = false;
// 卡片业务数据
@State cardConfig: AllCardsCard | undefined = undefined; @State cardConfig: AllCardsCard | undefined = undefined;
@State cardData: CardData = { tabs: [] }; @State cardData: CardData = { tabs: [] };
// 区分加载中和暂无数据
@State dataReady: boolean = false; @State dataReady: boolean = false;
// Tab 状态及滚动区域
@State tabIdx: number = 0; @State tabIdx: number = 0;
@State tabViewportWidth: number = 0; @State tabViewportWidth: number = 0;
@State tabContentWidth: number = 0; @State tabContentWidth: number = 0;
@State tabScrollOffset: number = 0; @State tabScrollOffset: number = 0;
// 弹层状态
@State showExplainSheet: boolean = false; @State showExplainSheet: boolean = false;
// 生命周期与异步请求状态
private isComponentActive: boolean = false; private isComponentActive: boolean = false;
private dataRequestVersion: number = 0; private dataRequestVersion: number = 0;
private hqSubscriptionVersion: number = 0;
// 行情订阅与事件状态
private hqUnsubscribe: (() => void) | undefined; private hqUnsubscribe: (() => void) | undefined;
private hqSubscriptionVersion: number = 0;
private isTcpEventSubscribed: boolean = false; private isTcpEventSubscribed: boolean = false;
private tcpReconnectListener = (): void => { private tcpReconnectListener = (): void => {
if (!this.canUseHq()) { if (!this.canUseHq()) {
@@ -65,11 +64,13 @@ export struct AiPickNodeComponent {
} }
setTimeout((): void => { setTimeout((): void => {
if (this.canUseHq()) { if (this.canUseHq()) {
this.restartSubscription(); this.restartCurrentSubscription();
} }
}); });
}; };
// ==================== 组件生命周期 ====================
aboutToAppear(): void { aboutToAppear(): void {
this.isComponentActive = true; this.isComponentActive = true;
this.updateCardConfig(); this.updateCardConfig();
@@ -86,6 +87,14 @@ export struct AiPickNodeComponent {
this.unSubscribeTcpNetStatus(); this.unSubscribeTcpNetStatus();
} }
// ==================== 配置与刷新 ====================
// 从首页统一卡片配置中读取 AI Pick 配置。
private updateCardConfig(): void {
this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig();
}
// refreshTrigger 变化时重新获取策略和合约,并在请求完成后重建行情订阅。
onRefreshTriggerChange(): void { onRefreshTriggerChange(): void {
if (!this.isComponentActive) { if (!this.isComponentActive) {
return; return;
@@ -94,6 +103,9 @@ export struct AiPickNodeComponent {
this.updateCardData(); this.updateCardData();
} }
// ==================== 可见性与 TCP 重连 ====================
// 页面不可见时停止行情;恢复可见后使用已有数据重建订阅。
onFirstPageVisibleChange(): void { onFirstPageVisibleChange(): void {
if (!this.isComponentActive) { if (!this.isComponentActive) {
return; return;
@@ -111,10 +123,43 @@ export struct AiPickNodeComponent {
} }
} }
private updateCardConfig(): void { private subscribeTcpNetStatus(): void {
this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig(); 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<void> { private async updateCardData(): Promise<void> {
if (!this.isComponentActive) { if (!this.isComponentActive) {
return; return;
@@ -151,6 +196,7 @@ export struct AiPickNodeComponent {
} }
} }
// HTTP 刷新时按“合约 + 市场”复用旧行情,等待新订阅首包覆盖。
private mergeCachedQuotes(tabs: PickTabData[]): PickTabData[] { private mergeCachedQuotes(tabs: PickTabData[]): PickTabData[] {
const cachedContracts: PickContractItem[] = getUniqueContracts(this.cardData); const cachedContracts: PickContractItem[] = getUniqueContracts(this.cardData);
const cachedMap: Map<string, PickContractItem> = const cachedMap: Map<string, PickContractItem> =
@@ -184,6 +230,7 @@ export struct AiPickNodeComponent {
}); });
} }
// AI Pick 一次订阅所有 Tab 的去重合约,推送后合并回完整卡片数据。
private subscribeHq(): void { private subscribeHq(): void {
this.stopSubscribeHq(); this.stopSubscribeHq();
if (!this.canUseHq() || this.cardData.tabs.length === 0) { 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 { private canUseHq(): boolean {
return this.isComponentActive && this.firstPageVisible; return this.isComponentActive && this.firstPageVisible;
} }
@@ -247,6 +264,63 @@ export struct AiPickNodeComponent {
return this.isComponentActive && requestVersion === this.dataRequestVersion; 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 { private showTabGradient(): boolean {
const maxOffset: number = this.tabContentWidth - this.tabViewportWidth; const maxOffset: number = this.tabContentWidth - this.tabViewportWidth;
return maxOffset > 0 && this.tabScrollOffset < maxOffset; return maxOffset > 0 && this.tabScrollOffset < maxOffset;
@@ -389,7 +463,7 @@ export struct AiPickNodeComponent {
this.buildCustomStrategy() this.buildCustomStrategy()
} }
.width('100%') .width('100%')
.height(MAIN_CONTENT_HEIGHT) .height(321)
} }
@Builder @Builder
@@ -409,7 +483,7 @@ export struct AiPickNodeComponent {
.constraintSize({ minWidth: 66 }) .constraintSize({ minWidth: 66 })
.maxLines(1) .maxLines(1)
.textAlign(TextAlign.Center) .textAlign(TextAlign.Center)
.height(TAB_BAR_HEIGHT) .height(30)
.padding({ left: 8, right: 8 }) .padding({ left: 8, right: 8 })
.borderRadius(4) .borderRadius(4)
.backgroundColor(this.tabIdx === index .backgroundColor(this.tabIdx === index
@@ -426,7 +500,7 @@ export struct AiPickNodeComponent {
}) })
} }
.width('100%') .width('100%')
.height(TAB_BAR_HEIGHT) .height(30)
.padding({ right: 10 }) .padding({ right: 10 })
.scrollable(ScrollDirection.Horizontal) .scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off) .scrollBar(BarState.Off)
@@ -438,7 +512,7 @@ export struct AiPickNodeComponent {
if (this.showTabGradient()) { if (this.showTabGradient()) {
Row() Row()
.width(24) .width(24)
.height(TAB_BAR_HEIGHT) .height(30)
.linearGradient({ .linearGradient({
angle: 90, angle: 90,
colors: [ colors: [
@@ -450,7 +524,7 @@ export struct AiPickNodeComponent {
} }
} }
.width('100%') .width('100%')
.height(TAB_BAR_HEIGHT) .height(30)
.onAreaChange((_oldValue: Area, newValue: Area) => { .onAreaChange((_oldValue: Area, newValue: Area) => {
this.tabViewportWidth = Number(newValue.width); this.tabViewportWidth = Number(newValue.width);
}) })
@@ -526,7 +600,7 @@ export struct AiPickNodeComponent {
} }
} }
.width('100%') .width('100%')
.height(STRATEGY_DESC_HEIGHT) .height(86)
.padding({ top: 10, bottom: 10 }) .padding({ top: 10, bottom: 10 })
.alignItems(VerticalAlign.Top) .alignItems(VerticalAlign.Top)
} }
@@ -551,7 +625,7 @@ export struct AiPickNodeComponent {
.textAlign(TextAlign.End) .textAlign(TextAlign.End)
} }
.width('100%') .width('100%')
.height(TABLE_HEADER_HEIGHT) .height(40)
ForEach( ForEach(
this.cardData.tabs[this.tabIdx].items, this.cardData.tabs[this.tabIdx].items,
@@ -563,7 +637,7 @@ export struct AiPickNodeComponent {
) )
} }
.width('100%') .width('100%')
.height(TABLE_HEIGHT) .height(175)
} }
@Builder @Builder
@@ -629,7 +703,7 @@ export struct AiPickNodeComponent {
.textOverflow({ overflow: TextOverflow.Ellipsis }) .textOverflow({ overflow: TextOverflow.Ellipsis })
} }
.width('100%') .width('100%')
.height(TABLE_ROW_HEIGHT) .height(45)
.alignItems(VerticalAlign.Center) .alignItems(VerticalAlign.Center)
.border({ .border({
width: { top: 1 }, width: { top: 1 },
@@ -653,7 +727,7 @@ export struct AiPickNodeComponent {
.fillColor($r('app.color.elements_icon_tertiary')) .fillColor($r('app.color.elements_icon_tertiary'))
} }
.width('100%') .width('100%')
.height(CUSTOM_STRATEGY_HEIGHT) .height(30)
.justifyContent(FlexAlign.Center) .justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center) .alignItems(VerticalAlign.Center)
.onClick(() => { .onClick(() => {
@@ -679,7 +753,7 @@ export struct AiPickNodeComponent {
} }
} }
.width('100%') .width('100%')
.height(MAIN_CONTENT_HEIGHT) .height(321)
.alignItems(HorizontalAlign.Center) .alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center) .justifyContent(FlexAlign.Center)
} }
@@ -695,55 +769,9 @@ export struct AiPickNodeComponent {
.fontColor($r('app.color.elements_text_tertiary')) .fontColor($r('app.color.elements_text_tertiary'))
} }
.width('100%') .width('100%')
.height(NO_CONTRACT_HEIGHT) .height(261)
.alignItems(HorizontalAlign.Center) .alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.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}`;
}
} }
@@ -1,21 +1,31 @@
import { AbsRowData, RowData, TableData } from '@b2b/hq_table'; 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 { RequestHelper, StuffTableStruct } from '@kernel/lib_communication';
import {
QuoteCustomSettingManager,
StandardPriceType,
StandardPriceTypeHelper,
applyStandardPriceToTableData,
} from 'biz_quote';
import { import {
CardData, CardData,
PickContractItem, PickContractItem,
PickTabData, PickTabData,
} from '../../ai-pick/AiPickModels'; } from '../../ai-pick/AiPickModels';
import { getUniqueContracts } from '../../ai-pick/AiPickUtils'; import { getUniqueContracts } from '../../ai-pick/AiPickUtils';
import { TableConstants } from '../../market-ranking/TableConstants';
const FRAME_ID = 2201; const FRAME_ID = 2201;
const PAGE_ID = 4106; const PAGE_ID = 4106;
const FIELD_IDS: number[] = [ const FIELD_IDS: number[] = [
TableConstants.DATA_ID_CODE_4, TableConstants.DATA_ID_CODE_4,
TableConstants.DATA_ID_PRICE, TableConstants.DATA_ID_PRICE,
TableConstants.DATA_ID_ZF,
TableConstants.DATA_ID_ZD,
TableConstants.DATA_ID_MARKET, 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 { interface AiPickQuote {
@@ -48,7 +58,8 @@ class AiPickTableRequestClient extends TableRequestClient {
RequestHelper.build().base(this.frameId, this.pageId, this, this.requestParams).request(); 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<string, AiPickQuote> = this.toQuoteMap(tableData); const quoteMap: Map<string, AiPickQuote> = this.toQuoteMap(tableData);
const nextCardData: CardData = this.mergeQuoteData(quoteMap); const nextCardData: CardData = this.mergeQuoteData(quoteMap);
this.sourceCardData = nextCardData; this.sourceCardData = nextCardData;
@@ -71,7 +82,7 @@ class AiPickTableRequestClient extends TableRequestClient {
} }
const quote: AiPickQuote = { const quote: AiPickQuote = {
price: this.getNumber(row, TableConstants.DATA_ID_PRICE), 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); quoteMap.set(`${codeValue}_${marketValue}`, quote);
}); });
@@ -148,12 +159,15 @@ export class AiPickHqRequestClient {
} }
private buildRequestParams(contracts: PickContractItem[]): string { private buildRequestParams(contracts: PickContractItem[]): string {
const computeMode: StandardPriceType =
QuoteCustomSettingManager.getInstance().getStandardPriceType();
return `\r\nsortid=-1\r\n` + return `\r\nsortid=-1\r\n` +
`sortorder=0\r\n` + `sortorder=0\r\n` +
`push=1\r\n` + `push=1\r\n` +
`dataitem=${FIELD_IDS.map((fieldId: number): string => `${fieldId},`).join('')}\r\n` + `dataitem=${FIELD_IDS.map((fieldId: number): string => `${fieldId},`).join('')}\r\n` +
`codelist=${this.formatCodeList(contracts)}\r\n` + `codelist=${this.formatCodeList(contracts)}\r\n` +
`update=1\r\n` + `update=1\r\n` +
`computemode=${computeMode}\r\n` +
`scenario=qht_qihuo_sort\r\n` + `scenario=qht_qihuo_sort\r\n` +
`precision=1\r\n` + `precision=1\r\n` +
`pushtime=2.5\r\n`; `pushtime=2.5\r\n`;