fix(ai-pick): complete quote handling and component organization
This commit is contained in:
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user