diff --git a/README.md b/README.md index 0cd41a5..41c8864 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,34 @@ doc/ HTTP 与 4106 行情接口文档 市场排名与 AI 选期分别通过独立的 `HqRequestClient` 构造行情订阅参数,共享 `common/HqFieldId.ets` 中的 4106 字段 ID。 +## MarketRanking 生命周期 + +`MarketRankingNodeComponent` 出现时读取卡片配置、注册 `tcpReconnect` 事件监听,并在 +`firstPageVisible` 为 `true` 时获取当前 Tab 的 contracts。contracts 返回后先生成占位列表, +再由 `MarketRankingHqRequestClient` 发起行情订阅,行情回调会整体替换 `CardData`。 + +```text +aboutToAppear +→ 注册 TCP 重连事件 +→ 获取当前 Tab 的 contracts +→ 填充合约占位列表 +→ 页面可见时订阅行情 +``` + +运行期间按以下规则更新: + +- `refreshTrigger`:由父组件通过 `@Link` 传入;变化后停止旧行情、重新获取 contracts, + 并在可见时重新订阅。 +- `firstPageVisible`:由父组件通过 `@Provide` 提供,组件通过 `@Consume` 监听,默认 + 为 `true`。变为不可见时立即停止行情;恢复可见时优先使用已有 contracts 重新订阅, + 没有 contracts 才重新请求。 +- `tcpReconnect`:组件通过公共 `emitter` 监听。重连且页面可见时停止旧行情并重新订阅; + 页面不可见时忽略,恢复可见后再订阅。 +- Tab 切换:停止旧行情、清空列表、获取新 Tab 的 contracts,再订阅对应行情。 + +`aboutToDisappear` 会解绑 TCP 重连监听并停止行情订阅。HTTP 请求等待期间如果页面变为 +不可见,contracts 仍可更新,但不会创建行情订阅。 + ## 数据与接口 当前 HTTP Fetcher 仅构造请求参数,行情客户端也尚未连接真实客户端桥接层;页面使用 Mock diff --git a/entry/src/main/ets/common/EventEmitter.ets b/entry/src/main/ets/common/EventEmitter.ets new file mode 100644 index 0000000..36b3b85 --- /dev/null +++ b/entry/src/main/ets/common/EventEmitter.ets @@ -0,0 +1,53 @@ +export const EVENT_TCP_RECONNECT = 'tcpReconnect'; + +export type EventListener = () => void; + +// 模拟客户端事件总线,后续可在真实 TCP 重连回调中调用 emit。 +export class EventEmitter { + private static readonly instance: EventEmitter = new EventEmitter(); + private listeners: Map = new Map(); + + private constructor() { + } + + static getInstance(): EventEmitter { + return EventEmitter.instance; + } + + on(eventName: string, listener: EventListener): void { + const eventListeners = this.listeners.get(eventName); + if (eventListeners === undefined) { + this.listeners.set(eventName, [listener]); + return; + } + if (!eventListeners.includes(listener)) { + eventListeners.push(listener); + } + } + + off(eventName: string, listener: EventListener): void { + const eventListeners = this.listeners.get(eventName); + if (eventListeners === undefined) { + return; + } + const listenerIndex = eventListeners.indexOf(listener); + if (listenerIndex >= 0) { + eventListeners.splice(listenerIndex, 1); + } + if (eventListeners.length === 0) { + this.listeners.delete(eventName); + } + } + + emit(eventName: string): void { + const eventListeners = this.listeners.get(eventName); + if (eventListeners === undefined) { + return; + } + eventListeners.slice().forEach((listener: EventListener): void => { + listener(); + }); + } +} + +export const emitter: EventEmitter = EventEmitter.getInstance(); diff --git a/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets b/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets index 12497e1..f8bba1a 100644 --- a/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets +++ b/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets @@ -8,6 +8,11 @@ import { MarketRankingDataFetcher } from './MarketRankingDataFetcher'; import { MarketRankingHqRequestClient } from './MarketRankingHqRequestClient'; import { formatGreatNumber, formatPercent, riseFallColor } from '../common/NumberFormat'; import { AllCardsCard } from '../common/AllCardsModels'; +import { + emitter, + EVENT_TCP_RECONNECT, + EventListener, +} from '../common/EventEmitter'; import { INFO_IMAGE_DARK, INFO_IMAGE_LIGHT, @@ -32,11 +37,20 @@ export struct MarketRankingNodeComponent { @State tabContentWidth: number = 0; @State tabScrollOffset: number = 0; @State showExplainSheet: boolean = false; - @StorageLink('enableDarkMode') isDarkMode: boolean = false; + @Link @Watch('onRefreshTriggerChange') refreshTrigger: number; //todo + @Consume('firstPageVisible') @Watch('onFirstPageVisibleChange') firstPageVisible: boolean = true; + @StorageLink('enableDarkMode') isDarkMode: boolean = false; //todo private hqUnsubscribe: (() => void) | undefined; + private tcpReconnectListener: EventListener = (): void => { + this.onTcpReconnect(); + }; aboutToAppear(): void { this.updateCardConfig(); + emitter.on(EVENT_TCP_RECONNECT, this.tcpReconnectListener); + if (!this.firstPageVisible) { + return; + } // Mock 网络延迟:模拟真实接口返回前的等待过程,接入真实请求后移除该 setTimeout。 setTimeout(() => { this.updateCardData(this.primaryIdx, this.secondaryIdx); @@ -44,6 +58,7 @@ export struct MarketRankingNodeComponent { } aboutToDisappear(): void { + emitter.off(EVENT_TCP_RECONNECT, this.tcpReconnectListener); this.stopSubscribeHq(); } @@ -52,6 +67,37 @@ export struct MarketRankingNodeComponent { this.cardConfig = MarketRankingDataFetcher.getInstance().fetchCardConfig(); } + // refreshTrigger 变化时重新获取当前 Tab 的合约,并在请求完成后重建行情订阅。 + onRefreshTriggerChange(): void { + this.updateCardData(this.primaryIdx, this.secondaryIdx); + } + + // 页面不可见时停止行情;恢复可见后使用已有合约重建订阅。 + onFirstPageVisibleChange(): void { + if (!this.firstPageVisible) { + this.stopSubscribeHq(); + return; + } + if (this.cardData.tableList.length > 0) { + this.subscribeHq(this.primaryIdx, this.secondaryIdx); + return; + } + this.updateCardData(this.primaryIdx, this.secondaryIdx); + } + + // 收到 TCP 重连事件后,仅在页面可见时重新订阅行情。 + private onTcpReconnect(): void { + if (!this.firstPageVisible) { + return; + } + this.stopSubscribeHq(); + if (this.cardData.tableList.length > 0) { + this.subscribeHq(this.primaryIdx, this.secondaryIdx); + return; + } + this.updateCardData(this.primaryIdx, this.secondaryIdx); + } + @Builder CardTitle() { Row({ space: 4 }) { @@ -361,7 +407,9 @@ export struct MarketRankingNodeComponent { name: contract.contract_name, })), }; - this.subscribeHq(primaryIdx, secondaryIdx); + if (this.firstPageVisible) { + this.subscribeHq(primaryIdx, secondaryIdx); + } } catch { this.cardData = { tableList: [] }; } diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 31f7e13..a5184de 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -6,6 +6,9 @@ import { AiPickNodeComponent } from '../ai-pick/AiPickNodeComponent'; @Entry @Component struct Index { + @State refreshTrigger: number = 0; + @Provide('firstPageVisible') firstPageVisible: boolean = true; + build() { Scroll() { Column({ space: 8 }) { @@ -13,7 +16,9 @@ struct Index { AIView() } Card() { - MarketRankingNodeComponent() + MarketRankingNodeComponent({ + refreshTrigger: $refreshTrigger, + }) } Card() { AiPickNodeComponent()