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 {
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<void> {
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<string, PickContractItem> =
@@ -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}`;
}
}
@@ -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<string, AiPickQuote> = 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`;