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}`;
}
}