import router from '@ohos.router'; import { CardData, PickContractItem, PickStrategy, PickTabData, } from './AiPickModels'; import { AllCardsCard } from '../common/AllCardsModels'; import { AiPickDataFetcher } from './AiPickDataFetcher'; import { AiPickHqRequestClient } from './AiPickHqRequestClient'; import { cellColor, cellValue, formatColName } from './AiPickUtils'; import { emitter, EVENT_TCP_RECONNECT, EventListener, } from '../common/EventEmitter'; import { EMPTY_IMAGE_DARK, EMPTY_IMAGE_LIGHT, INFO_IMAGE_DARK, INFO_IMAGE_LIGHT, MAX_ITEMS, RIGHT_ARROW_DARK, RIGHT_ARROW_LIGHT, TITLE_ARROW_DARK, TITLE_ARROW_LIGHT, } from './AiPickConstant'; interface StrategyDescParams { tabIdx: number; } const CARD_TITLE_HEIGHT = 48; 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 CARD_CONTENT_HEIGHT = CARD_TITLE_HEIGHT + MAIN_CONTENT_HEIGHT; const NO_CONTRACT_HEIGHT = STRATEGY_DESC_HEIGHT + TABLE_HEIGHT; @Component export struct AiPickNodeComponent { // 当前卡片配置。 @State cardConfig: AllCardsCard | undefined = undefined; @State cardData: CardData = { tabs: [] }; @State tabIdx: number = 0; @StorageLink('enableDarkMode') isDarkMode: boolean = false; @State tabViewportWidth: number = 0; @State tabContentWidth: number = 0; @State tabScrollOffset: number = 0; @State showExplainSheet: boolean = false; @Link @Watch('onRefreshTriggerChange') refreshTrigger: number; @Consume('firstPageVisible') @Watch('onFirstPageVisibleChange') firstPageVisible: boolean = true; private hqUnsubscribe: (() => void) | undefined; private tcpReconnectListener: EventListener = (): void => { this.onTcpReconnect(); }; private showTabGradient(): boolean { const maxOffset = this.tabContentWidth - this.tabViewportWidth; return maxOffset > 0 && this.tabScrollOffset < maxOffset; } aboutToAppear(): void { this.updateCardConfig(); emitter.on(EVENT_TCP_RECONNECT, this.tcpReconnectListener); if (!this.firstPageVisible) { return; } setTimeout(() => { this.updateCardData(); }, 600); } aboutToDisappear(): void { emitter.off(EVENT_TCP_RECONNECT, this.tcpReconnectListener); this.stopSubscribeHq(); } // 只负责从公共 Mock 获取并更新卡片配置。 private updateCardConfig(): void { this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig(); } // refreshTrigger 变化时重新获取策略数据,并在页面可见时重建行情订阅。 onRefreshTriggerChange(): void { this.updateCardData(); } // 页面不可见时停止行情;恢复可见后使用已有策略数据重建订阅。 onFirstPageVisibleChange(): void { if (!this.firstPageVisible) { this.stopSubscribeHq(); return; } if (this.cardData.tabs.length > 0) { this.subscribeHq(); return; } this.updateCardData(); } // 收到 TCP 重连事件后,仅在页面可见时重新订阅行情。 private onTcpReconnect(): void { if (!this.firstPageVisible) { return; } this.stopSubscribeHq(); if (this.cardData.tabs.length > 0) { this.subscribeHq(); return; } this.updateCardData(); } // 只负责请求并更新卡片展示数据。 private async updateCardData(): Promise { this.stopSubscribeHq(); this.cardData = { tabs: [] }; const fetcher = AiPickDataFetcher.getInstance(); try { const tabs = await fetcher.fetchCardData(); this.cardData = { tabs }; if (tabs.length > 0 && this.firstPageVisible) { this.subscribeHq(); } } catch { this.stopSubscribeHq(); this.cardData = { tabs: [] }; } } // 使用当前 CardData 的全部 Tab 合约订阅行情,推送后整体更新 CardData。 private subscribeHq(): void { this.stopSubscribeHq(); this.hqUnsubscribe = AiPickHqRequestClient.getInstance().subscribeHq( this.cardData, (cardData: CardData): void => { this.cardData = cardData; }, ); } private stopSubscribeHq(): void { if (this.hqUnsubscribe !== undefined) { this.hqUnsubscribe(); this.hqUnsubscribe = undefined; } } @Builder CardTitle() { Row() { Text(this.cardConfig?.card_title.value || $r('app.string.ai_pick_title')) .fontSize(18) .fontWeight(500) .fontColor($r('app.color.text_primary')) .maxLines(1) if ((this.cardConfig?.card_url.android ?? '') !== '' || (this.cardConfig?.card_url.ios ?? '') !== '') { Image(this.isDarkMode ? TITLE_ARROW_DARK : TITLE_ARROW_LIGHT) .width(20) .height(20) } if ((this.cardConfig?.explain_message ?? '') !== '') { Image(this.isDarkMode ? INFO_IMAGE_DARK : INFO_IMAGE_LIGHT) .width(16) .height(16) .margin({ left: 8 }) .onClick((event: ClickEvent) => { this.showExplainSheet = true; }) } } .width('100%') .height(CARD_TITLE_HEIGHT) .alignItems(VerticalAlign.Center) .onClick(() => { this.jumpToCardTitle(); }) } @Builder ExplainSheet() { Column() { Text(this.cardConfig?.explain_title || this.cardConfig?.card_title.value || $r('app.string.ai_pick_title')) .width('100%') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_primary')) .textAlign(TextAlign.Center) .margin({ bottom: 16 }) Scroll() { Text(this.cardConfig?.explain_message ?? '') .width('100%') .fontSize(16) .lineHeight(24) .fontColor($r('app.color.text_primary')) } .width('100%') .scrollBar(BarState.Off) Text($r('app.string.card_explain_confirm')) .width('100%') .height(44) .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor(Color.White) .textAlign(TextAlign.Center) .backgroundColor($r('app.color.text_blue')) .borderRadius(4) .margin({ top: 16 }) .onClick(() => { this.showExplainSheet = false; }) } .width('100%') .padding({ left: 16, right: 16, top: 16, bottom: 18 }) } @Builder StrategyDesc($$: StrategyDescParams) { // 图片:imgs[0] 白天,imgs[1] 夜间 Row() { // 左侧:问句 + 标签 Column({ space: 4 }) { if (this.cardData.tabs[$$.tabIdx].strategy.detail) { Text(this.cardData.tabs[$$.tabIdx].strategy.detail) .fontSize(14) .lineHeight(21) .fontColor($r('app.color.text_primary')) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } if (this.cardData.tabs[$$.tabIdx].strategy.tagList.length > 0) { Row({ space: 4 }) { ForEach(this.cardData.tabs[$$.tabIdx].strategy.tagList, (tag: string) => { Row() { Text(tag) .fontSize(11) .fontColor($r('app.color.text_blue')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .height(18) .padding({ left: 3, right: 3 }) .borderRadius(2) .border({ width: 0.5, color: $r('app.color.text_blue') }) .alignItems(VerticalAlign.Center) .onClick(() => { this.jumpToLabel( this.cardData.tabs[$$.tabIdx].strategy, tag, ); }) }, (tag: string) => tag) } .width('100%') } } .layoutWeight(1) .alignItems(HorizontalAlign.Start) // 右侧:策略图片(仅系统策略有图,右侧 image) // imgs 长度>0 且当前选中位(imgs[0],暂无主题切换)非空才渲染 if ((this.cardData.tabs[$$.tabIdx].strategy.imgs[this.isDarkMode ? 1 : 0] ?? '') !== '') { Image(this.cardData.tabs[$$.tabIdx].strategy.imgs[this.isDarkMode ? 1 : 0] ?? '') .width(104) .height(66) .objectFit(ImageFit.Contain) .margin({ left: 10 }) } } .width('100%') .height(STRATEGY_DESC_HEIGHT) .padding({ top: 10, bottom: 10 }) .alignItems(VerticalAlign.Top) .onClick(() => { this.jumpToStrategyDetail( this.cardData.tabs[$$.tabIdx].strategy, ); }) } build() { Column() { // 数据未就绪时不渲染依赖 tabs[tabIdx] 的内容,避免越界。 if (this.cardData.tabs.length === 0) { this.CardTitle() Column() { Text($r('app.string.ai_pick_loading')) .fontSize(14) .fontColor($r('app.color.text_grey')) } .width('100%') .height(MAIN_CONTENT_HEIGHT) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) } else { this.MainContent() } } .width('100%') .backgroundColor($r('app.color.card_bg')) .borderRadius(4) .padding({ left: 10, right: 10, bottom: 10 }) .bindSheet(this.showExplainSheet, this.ExplainSheet, { height: SheetSize.FIT_CONTENT, dragBar: true, showClose: false, maskColor: '#99000000', // todo: 后续替换 onDisappear: () => { this.showExplainSheet = false; }, radius: { topLeft: 10, topRight: 10, } }) } @Builder MainContent() { Column() { // ── 标题栏 ────────────────────────────────────────────── this.CardTitle() // ── Tab 栏 ────────────────────────────────────────────── Stack({ alignContent: Alignment.End }) { Scroll() { Row({ space: 8 }) { ForEach(this.cardData.tabs, (tab: PickTabData, index: number) => { Text(`${tab.strategy.name}(${tab.strategy.count})`) .fontSize(14) .fontWeight(this.tabIdx === index ? FontWeight.Medium : FontWeight.Normal) .fontColor(this.tabIdx === index ? $r('app.color.text_blue') : $r('app.color.text_secondary')) .constraintSize({ minWidth: 66 }) .maxLines(1) .textAlign(TextAlign.Center) .height(TAB_BAR_HEIGHT) .padding({ left: 8, right: 8 }) .borderRadius(4) .backgroundColor(this.tabIdx === index ? $r('app.color.pill_active_bg') : $r('app.color.pill_inactive_bg')) .onClick(() => { this.tabIdx = index; }) }, (tab: PickTabData) => tab.strategy.id) } .onAreaChange((oldValue: Area, newValue: Area) => { this.tabContentWidth = Number(newValue.width); }) } .width('100%') .height(TAB_BAR_HEIGHT) .padding({ right: 10 }) .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .align(Alignment.Start) .onScroll((xOffset: number, _yOffset: number) => { this.tabScrollOffset = xOffset; }) if (this.showTabGradient()) { Row() .width(24) .height(TAB_BAR_HEIGHT) .linearGradient({ angle: 90, colors: [[$r('app.color.tab_gradient_start'), 0], [$r('app.color.card_bg'), 1]], }) .hitTestBehavior(HitTestMode.None) } } .width('100%') .height(TAB_BAR_HEIGHT) .onAreaChange((oldValue: Area, newValue: Area) => { this.tabViewportWidth = Number(newValue.width); }) // ── 策略问句(按引用传递,tabIdx 变化时自动刷新)──────── if (this.cardData.tabs[this.tabIdx].items.length > 0) { this.StrategyDesc({ tabIdx: this.tabIdx }) } if (this.cardData.tabs[this.tabIdx].items.length === 0) { this.EmptyState($r('app.string.ai_pick_no_contract')) } else { Column() { // ── 表头 ──────────────────────────────────────────── Row({ space: 8 }) { Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[0])) .fontSize(14) .fontColor($r('app.color.text_tertiary')) .width('35%') Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[1])) .fontSize(14) .fontColor($r('app.color.text_tertiary')) .layoutWeight(1) .textAlign(TextAlign.End) Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[2])) .fontSize(14) .fontColor($r('app.color.text_tertiary')) .layoutWeight(1) .textAlign(TextAlign.End) } .width('100%') .height(TABLE_HEADER_HEIGHT) // ── 合约列表 ──────────────────────────────────────── ForEach( this.cardData.tabs[this.tabIdx].items, (item: PickContractItem) => { Row({ space: 8 }) { Row({ space: 5 }) { Text(item.name) .fontSize(16) .maxFontSize(16) .minFontSize(9) .fontColor($r('app.color.text_primary')) .layoutWeight(1) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) if (item.isMain) { Text($r('app.string.ai_pick_main_contract')) .fontSize(11) .fontColor($r('app.color.color_orange')) .width(16) .height(16) .textAlign(TextAlign.Center) .borderRadius(2) .backgroundColor($r('app.color.bg_orange')) } } .width('35%') .alignItems(VerticalAlign.Center) Text(cellValue( this.cardData.tabs[this.tabIdx].keysList[1], item, this.cardData.tabs[this.tabIdx].fieldList, )) .fontSize(16) .maxFontSize(16) .minFontSize(9) .fontWeight(500) .fontFamily('monospace') .fontColor(cellColor(this.cardData.tabs[this.tabIdx].keysList[1], item)) .layoutWeight(1) .textAlign(TextAlign.End) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(cellValue( this.cardData.tabs[this.tabIdx].keysList[2], item, this.cardData.tabs[this.tabIdx].fieldList, )) .fontSize(16) .maxFontSize(16) .minFontSize(9) .fontWeight(500) .fontFamily('monospace') .fontColor(cellColor(this.cardData.tabs[this.tabIdx].keysList[2], item)) .layoutWeight(1) .textAlign(TextAlign.End) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) } .width('100%') .height(TABLE_ROW_HEIGHT) .alignItems(VerticalAlign.Center) .border({ width: { top: 1 }, color: $r('app.color.divider_color') }) .onClick(() => { this.jumpToDetail(item); }) }, // 行情字段参与 key,确保整体替换 CardData 后列表行同步刷新。 (item: PickContractItem) => `${item.contract}_${item.market}_${item.price ?? ''}_${item.priceChg ?? ''}`, ) } .width('100%') .height(TABLE_HEIGHT) } // ── 一句话定制策略 ────────────────────────────────────── Row({ space: 2 }) { Text($r('app.string.ai_pick_custom_strategy')) .fontSize(14) .lineHeight(20) .fontColor($r('app.color.text_tertiary')) Image(this.isDarkMode ? RIGHT_ARROW_DARK : RIGHT_ARROW_LIGHT) .width(14) .height(14) .margin({ top: 2 }) } .width('100%') .height(CUSTOM_STRATEGY_HEIGHT) .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) .onClick(() => { this.jumpToNewHome(); }) } .width('100%') .height(CARD_CONTENT_HEIGHT) } @Builder EmptyState(message: ResourceStr) { Column() { Image(this.isDarkMode ? EMPTY_IMAGE_DARK : EMPTY_IMAGE_LIGHT) .width(100) .height(100) Text(message) .fontSize(14) .fontColor($r('app.color.text_grey')) } .width('100%') .height(NO_CONTRACT_HEIGHT) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) } // 跳转到分时详情页。 // todo: 后续跳转为真实的客户端页面。 private jumpToDetail(item: PickContractItem): void { const clientUrl = `client://client.html?action=ymtz^webid=2205^stockcode=${item.contract}^marketid=${item.market}`; console.info(`[AiPick] detail url: ${clientUrl}`); // 当前仍跳转本地 Mock 页面;接入客户端能力后改为使用 clientUrl。 router.pushUrl({ url: 'pages/Detail', params: { code: item.contract, market: item.market, clientUrl: clientUrl, }, }); } // 跳转到卡片配置中的标题地址。 // todo: 后续跳转为真实的客户端页面。 private jumpToCardTitle(): void { const androidUrl = this.cardConfig?.card_url.android ?? ''; const clientUrl = androidUrl !== '' ? androidUrl : this.cardConfig?.card_url.ios ?? ''; if (clientUrl === '') { return; } console.info(`[AiPick] card title url: ${clientUrl}`); // 当前仍跳转本地 Mock 页面;接入客户端能力后改为使用 clientUrl。 router.pushUrl({ url: 'pages/StrategyDetail', params: { type: 'home', clientUrl: clientUrl, }, }); } // 跳转到一句话定制策略主页。 // todo: 后续跳转为真实的客户端页面。 private jumpToNewHome(): void { const pageUrl = 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-home.html?sync=1'; const clientUrl = `client://client.html?action=ymtz^ishiddenbar=1^mode=new^webid=2804^url=${pageUrl}^title=AI选期`; console.info(`[AiPick] home url: ${clientUrl}`); // 当前仍跳转本地 Mock 页面;接入客户端能力后改为使用 clientUrl。 router.pushUrl({ url: 'pages/StrategyDetail', params: { type: 'home', clientUrl: clientUrl, }, }); } // 跳转到策略详情页。 // todo: 后续跳转为真实的客户端页面。 private jumpToStrategyDetail(strategy: PickStrategy): void { const pageUrl = 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-detail.html' + `?id=${strategy.id}&type=${strategy.periodType}&name=${encodeURIComponent(strategy.name)}`; const clientUrl = `client://client.html?action=ymtz^ishiddenbar=1^mode=new^webid=2804^url=${pageUrl}^title=AI选期`; console.info(`[AiPick] strategy detail url: ${clientUrl}`); // 当前仍跳转本地 Mock 页面;接入客户端能力后改为使用 clientUrl。 router.pushUrl({ url: 'pages/StrategyDetail', params: { type: 'detail', id: strategy.id, periodType: strategy.periodType, name: strategy.name, clientUrl: clientUrl, }, }); } // 跳转到策略标签页。 // todo: 后续跳转为真实的客户端页面。 private jumpToLabel(strategy: PickStrategy, label: string): void { const pageUrl = 'https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-label.html' + `?id=${strategy.id}&type=${strategy.periodType}&label=${encodeURIComponent(label)}`; const clientUrl = `client://client.html?action=ymtz^ishiddenbar=1^mode=new^webid=2804^url=${pageUrl}^title=AI选期`; console.info(`[AiPick] label url: ${clientUrl}`); // 当前仍跳转本地 Mock 页面;接入客户端能力后改为使用 clientUrl。 router.pushUrl({ url: 'pages/StrategyDetail', params: { type: 'label', id: strategy.id, periodType: strategy.periodType, label: label, clientUrl: clientUrl, }, }); } }