From 10dc2787df78d564328098ee43bda185658f8403 Mon Sep 17 00:00:00 2001 From: clz Date: Mon, 27 Jul 2026 17:22:29 +0800 Subject: [PATCH] add AI pick card --- src/main/ets/ai-pick/AiPickConstant.ets | 22 + src/main/ets/ai-pick/AiPickDataFetcher.ets | 122 +++ src/main/ets/ai-pick/AiPickModels.ets | 83 ++ src/main/ets/ai-pick/AiPickNodeComponent.ets | 746 ++++++++++++++++++ src/main/ets/ai-pick/AiPickUtils.ets | 177 +++++ .../ets/components/mainpage/FirstPage.ets | 10 +- src/main/ets/datacenter/FirstPageConstant.ets | 3 +- .../ets/market-ranking/TableConstants.ets | 2 +- .../node/clients/AiPickHqRequestClient.ets | 178 +++++ .../datacenter/FirstPageCardsConfigLoader.ets | 1 + src/main/resources/base/element/string.json | 28 + src/main/resources/en_US/element/string.json | 30 +- .../rawfile/first_page_cards_config.json | 26 + src/main/resources/zh_CN/element/string.json | 30 +- 14 files changed, 1453 insertions(+), 5 deletions(-) create mode 100644 src/main/ets/ai-pick/AiPickConstant.ets create mode 100644 src/main/ets/ai-pick/AiPickDataFetcher.ets create mode 100644 src/main/ets/ai-pick/AiPickModels.ets create mode 100644 src/main/ets/ai-pick/AiPickNodeComponent.ets create mode 100644 src/main/ets/ai-pick/AiPickUtils.ets create mode 100644 src/main/ets/node/clients/AiPickHqRequestClient.ets diff --git a/src/main/ets/ai-pick/AiPickConstant.ets b/src/main/ets/ai-pick/AiPickConstant.ets new file mode 100644 index 0000000..1030bd4 --- /dev/null +++ b/src/main/ets/ai-pick/AiPickConstant.ets @@ -0,0 +1,22 @@ +export const MAX_TABS = 5; +export const MAX_ITEMS = 3; +export const MAX_TAGS = 3; +export const MAX_SHOW_KEYS = 3; +export const MAX_DOUBLE_KEYS = 2; + +export const EMPTY_IMAGE_LIGHT = + 'https://u.thsi.cn/imgsrc/bbs/8eee9cd4fa7e359032de11a3f7c2a70b_300_230.png'; +export const EMPTY_IMAGE_DARK = + 'https://u.thsi.cn/imgsrc/bbs/1c4e679244ed87231befad2b04cddfe2_300_230.png'; + +export const BLACK_LIST: string[] = [ + '合约代码', + '合约简称', + '收盘价', + '最新价', + '涨跌幅', + 'code', + 'market_id', + 'market_code', + '最新涨跌幅', +]; diff --git a/src/main/ets/ai-pick/AiPickDataFetcher.ets b/src/main/ets/ai-pick/AiPickDataFetcher.ets new file mode 100644 index 0000000..72658d3 --- /dev/null +++ b/src/main/ets/ai-pick/AiPickDataFetcher.ets @@ -0,0 +1,122 @@ +import { http } from '@kit.NetworkKit'; +import { GlobalContext, HXLog } from 'biz_common'; +import { buildHeader } from '../util/Header'; +import { FirstPageCardsConfigLoader } from '../node/datacenter/FirstPageCardsConfigLoader'; +import { AllCardsCard, ModDataItem } from '../node/model/AiDiagnosisNodeModel'; +import { + PickContractItem, + PickStrategy, + PickTabData, + RawPickResponse, + RawPickTabItem, + RawStrategyDetail, + RawStrategyItem, +} from './AiPickModels'; +import { getResData, getTableList, ResData } from './AiPickUtils'; +import { MAX_ITEMS, MAX_TABS, MAX_TAGS } from './AiPickConstant'; + +const EMPTY_RAW_STRATEGY_ITEM: RawStrategyItem = {}; +const EMPTY_RAW_STRATEGY_DETAIL: RawStrategyDetail = { + id: '', + strategy_name: '', + strategy_type: '', +}; + +function parseRawTab(item: RawPickTabItem): PickTabData { + const isSystemPeriod: boolean = item.type === 'system_period'; + const strategyItem: RawStrategyItem = ( + isSystemPeriod ? item.system_strategy : item.custom_strategy + ) ?? EMPTY_RAW_STRATEGY_ITEM; + const strategyDetail: RawStrategyDetail = ( + isSystemPeriod ? strategyItem.system_strategy : strategyItem.custom_strategy + ) ?? EMPTY_RAW_STRATEGY_DETAIL; + const resData: ResData = getResData(strategyItem); + const allItems: PickContractItem[] = + getTableList(resData.list, resData.resultValueList); + const strategy: PickStrategy = { + id: strategyDetail.id, + name: strategyDetail.strategy_name, + detail: strategyDetail.user_question || strategyDetail.content || '', + type: strategyDetail.strategy_type || 'custom_strategy', + periodType: item.type, + tagList: (strategyDetail.tag_list ?? + [GlobalContext.get().resourceManager + .getStringSync($r('app.string.ai_pick_custom_tag'))]) + .slice(0, MAX_TAGS), + imgs: isSystemPeriod + ? [strategyDetail.white_img ?? '', strategyDetail.black_img ?? ''] + : [], + count: strategyItem.detail_count ?? 0, + }; + const tabData: PickTabData = { + strategy: strategy, + items: allItems.slice(0, MAX_ITEMS), + keysList: resData.showKeyList, + fieldList: resData.fieldList, + }; + return tabData; +} + +function parseRawTabs(rawTabs: RawPickTabItem[]): PickTabData[] { + return rawTabs + .slice(0, MAX_TABS) + .map((item: RawPickTabItem): PickTabData => parseRawTab(item)); +} + +export class AiPickDataFetcher { + private static readonly instance: AiPickDataFetcher = new AiPickDataFetcher(); + + private constructor() { + } + + static getInstance(): AiPickDataFetcher { + return AiPickDataFetcher.instance; + } + + fetchCardConfig(): AllCardsCard | undefined { + return FirstPageCardsConfigLoader.getConfig()?.aiPick; + } + + async fetchCardData(): Promise { + const cardConfig: AllCardsCard | undefined = this.fetchCardConfig(); + const modData: ModDataItem | undefined = cardConfig?.mod_data[0]; + const configuredUrl: string = modData?.url ?? ''; + if (configuredUrl === '') { + return []; + } + + const requestOptions: http.HttpRequestOptions = { + method: http.RequestMethod.GET, + header: buildHeader(), + connectTimeout: 60000, + readTimeout: 60000, + }; + const httpRequest: http.HttpRequest = http.createHttp(); + try { + const requestUrl: string = this.buildRequestUrl(configuredUrl, Date.now()); + const response: http.HttpResponse = + await httpRequest.request(requestUrl, requestOptions); + if (response.responseCode !== http.ResponseCode.OK) { + throw new Error('request failed: ' + response.responseCode); + } + const responseText: string = response.result as string; + const result: RawPickResponse = JSON.parse(responseText) as RawPickResponse; + const responseCode: number = result.code ?? result.status_code ?? 0; + if (responseCode !== 0 || result.data === undefined) { + throw new Error('response code: ' + responseCode); + } + return parseRawTabs(result.data); + } catch (requestError) { + const errorInfo: Error = requestError as Error; + HXLog.e('AiPickDataFetcher', 'fetchCardData error: ' + errorInfo.message); + throw new Error(errorInfo.message); + } finally { + httpRequest.destroy(); + } + } + + private buildRequestUrl(configuredUrl: string, timestamp: number): string { + const separator: string = configuredUrl.includes('?') ? '&' : '?'; + return `${configuredUrl}${separator}_=${timestamp}`; + } +} diff --git a/src/main/ets/ai-pick/AiPickModels.ets b/src/main/ets/ai-pick/AiPickModels.ets new file mode 100644 index 0000000..d4c6bb8 --- /dev/null +++ b/src/main/ets/ai-pick/AiPickModels.ets @@ -0,0 +1,83 @@ +export interface PickContractItem { + contract: string; + market: string; + name: string; + price?: number; + priceChg?: number; + isMain: boolean; + extraFields: Record; +} + +export interface PickFieldItem { + key: string; + unit: string; +} + +export interface PickStrategy { + id: string; + name: string; + detail: string; + type: string; + periodType: string; + tagList: string[]; + imgs: string[]; + count: number; +} + +export interface PickTabData { + strategy: PickStrategy; + items: PickContractItem[]; + keysList: string[]; + fieldList: PickFieldItem[]; +} + +export interface CardData { + tabs: PickTabData[]; +} + +export interface RawStrategyDetail { + id: string; + strategy_name: string; + user_question?: string; + content?: string; + strategy_type: string; + tag_list?: string[]; + white_img?: string; + black_img?: string; +} + +export interface RawFieldItem { + key: string; + type: string; + unit?: string; +} + +export interface RawDetailItem { + contract: string; + market: string; + contract_name: string; + main_contract_type: string | number; +} + +export type RawResultItem = Record; + +export interface RawStrategyItem { + custom_strategy?: RawStrategyDetail; + system_strategy?: RawStrategyDetail; + detail_list?: RawDetailItem[]; + field_list?: RawFieldItem[]; + origin_result_list?: RawResultItem[]; + detail_count?: number; +} + +export interface RawPickTabItem { + type: string; + system_strategy?: RawStrategyItem; + custom_strategy?: RawStrategyItem; +} + +export interface RawPickResponse { + code?: number; + status_code?: number; + data?: RawPickTabItem[]; +} diff --git a/src/main/ets/ai-pick/AiPickNodeComponent.ets b/src/main/ets/ai-pick/AiPickNodeComponent.ets new file mode 100644 index 0000000..4e357d9 --- /dev/null +++ b/src/main/ets/ai-pick/AiPickNodeComponent.ets @@ -0,0 +1,746 @@ +import { emitter } from '@kit.BasicServicesKit'; +import { enableDarkMode } from '@kernel/theme_manager'; +import { EmitterConstants, GlobalContext, jumpToQuote } from 'biz_common'; +import { AiPickDataFetcher } from './AiPickDataFetcher'; +import { AiPickHqRequestClient } from '../node/clients/AiPickHqRequestClient'; +import { + CardData, + PickContractItem, + PickStrategy, + PickTabData, +} from './AiPickModels'; +import { + cellColor, + cellValue, + formatColName, + getUniqueContracts, +} from './AiPickUtils'; +import { + EMPTY_IMAGE_DARK, + EMPTY_IMAGE_LIGHT, + MAX_ITEMS, +} from './AiPickConstant'; +import { AllCardsCard } from '../node/model/AiDiagnosisNodeModel'; +import { RouterUtil } from '../util/RouterUtil'; +import { getResourceString } from '../util/ResourceUtil'; + +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; + @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 isTcpEventSubscribed: boolean = false; + private tcpReconnectListener = (): void => { + if (!this.canUseHq()) { + return; + } + setTimeout((): void => { + if (this.canUseHq()) { + this.restartSubscription(); + } + }); + }; + + aboutToAppear(): void { + this.isComponentActive = true; + this.updateCardConfig(); + if (this.firstPageVisible) { + this.subscribeTcpNetStatus(); + } + this.updateCardData(); + } + + aboutToDisappear(): void { + this.isComponentActive = false; + this.dataRequestVersion += 1; + this.stopSubscribeHq(); + this.unSubscribeTcpNetStatus(); + } + + onRefreshTriggerChange(): void { + if (!this.isComponentActive) { + return; + } + this.updateCardConfig(); + this.updateCardData(); + } + + onFirstPageVisibleChange(): void { + if (!this.isComponentActive) { + return; + } + if (!this.firstPageVisible) { + this.stopSubscribeHq(); + this.unSubscribeTcpNetStatus(); + return; + } + this.subscribeTcpNetStatus(); + if (this.cardData.tabs.length > 0) { + this.subscribeHq(); + } else { + this.updateCardData(); + } + } + + private updateCardConfig(): void { + this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig(); + } + + private async updateCardData(): Promise { + if (!this.isComponentActive) { + return; + } + const requestVersion: number = this.dataRequestVersion + 1; + this.dataRequestVersion = requestVersion; + this.stopSubscribeHq(); + if (this.cardData.tabs.length === 0) { + this.dataReady = false; + } + try { + const tabs: PickTabData[] = + await AiPickDataFetcher.getInstance().fetchCardData(); + if (!this.isCurrentDataRequest(requestVersion)) { + return; + } + const nextTabs: PickTabData[] = this.mergeCachedQuotes(tabs); + if (this.tabIdx >= nextTabs.length) { + this.tabIdx = 0; + } + this.cardData = { tabs: nextTabs }; + this.dataReady = true; + if (nextTabs.length > 0 && this.firstPageVisible) { + this.subscribeHq(); + } + } catch { + if (!this.isCurrentDataRequest(requestVersion)) { + return; + } + this.dataReady = true; + if (this.cardData.tabs.length > 0 && this.firstPageVisible) { + this.subscribeHq(); + } + } + } + + private mergeCachedQuotes(tabs: PickTabData[]): PickTabData[] { + const cachedContracts: PickContractItem[] = getUniqueContracts(this.cardData); + const cachedMap: Map = + new Map(); + cachedContracts.forEach((item: PickContractItem): void => { + cachedMap.set(`${item.contract}_${item.market}`, item); + }); + return tabs.map((tab: PickTabData): PickTabData => { + const items: PickContractItem[] = + tab.items.map((item: PickContractItem): PickContractItem => { + const cachedItem: PickContractItem | undefined = + cachedMap.get(`${item.contract}_${item.market}`); + const nextItem: PickContractItem = { + contract: item.contract, + market: item.market, + name: item.name, + price: cachedItem?.price, + priceChg: cachedItem?.priceChg, + isMain: item.isMain, + extraFields: item.extraFields, + }; + return nextItem; + }); + const nextTab: PickTabData = { + strategy: tab.strategy, + items: items, + keysList: tab.keysList, + fieldList: tab.fieldList, + }; + return nextTab; + }); + } + + private subscribeHq(): void { + this.stopSubscribeHq(); + if (!this.canUseHq() || this.cardData.tabs.length === 0) { + return; + } + const subscriptionVersion: number = this.hqSubscriptionVersion; + this.hqUnsubscribe = AiPickHqRequestClient.getInstance().subscribeHq( + this.cardData, + (cardData: CardData): void => { + if (this.canUseHq() && + subscriptionVersion === this.hqSubscriptionVersion) { + this.cardData = cardData; + } + }, + ); + } + + private stopSubscribeHq(): void { + this.hqSubscriptionVersion += 1; + if (this.hqUnsubscribe !== undefined) { + this.hqUnsubscribe(); + this.hqUnsubscribe = undefined; + } + } + + 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; + } + + private isCurrentDataRequest(requestVersion: number): boolean { + return this.isComponentActive && requestVersion === this.dataRequestVersion; + } + + private showTabGradient(): boolean { + const maxOffset: number = this.tabContentWidth - this.tabViewportWidth; + return maxOffset > 0 && this.tabScrollOffset < maxOffset; + } + + build() { + Column() { + this.buildTitleBar() + if (this.cardData.tabs.length > 0) { + this.buildMainContent() + } else { + this.buildEmptyState() + } + } + .width('100%') + .backgroundColor($r('app.color.surface_layer1_foreground')) + .borderRadius(4) + .padding({ left: 10, right: 10, bottom: 10 }) + .bindSheet(this.showExplainSheet, this.buildExplainSheetContent, { + height: SheetSize.FIT_CONTENT, + dragBar: true, + showClose: false, + maskColor: $r('app.color.mask_level2'), + onDisappear: () => { + this.showExplainSheet = false; + }, + radius: { + topLeft: 10, + topRight: 10, + }, + }) + } + + @Builder + buildTitleBar() { + Row() { + Row({ space: 4 }) { + Text(this.cardConfig?.card_title.value || $r('app.string.ai_pick_title')) + .fontSize(18) + .fontWeight(500) + .fontColor($r('app.color.elements_text_primary_02')) + .maxLines(1) + + if ((this.cardConfig?.card_url.android ?? '') !== '' || + (this.cardConfig?.card_url.ios ?? '') !== '') { + Image($r('app.media.icon_arrow_forward_20')) + .width(20) + .height(20) + .fillColor($r('app.color.elements_icon_primary_02')) + } + } + .height('100%') + .alignItems(VerticalAlign.Center) + .onClick(() => { + this.jumpToCardTitle(); + }) + + if ((this.cardConfig?.explain_message ?? '') !== '') { + Image($r('app.media.icon_info_24')) + .width(16) + .height(16) + .fillColor($r('app.color.elements_icon_tertiary')) + .margin({ left: 8 }) + .onClick(() => { + this.showExplainSheet = true; + }) + } + + Blank() + } + .width('100%') + .height(48) + .alignItems(VerticalAlign.Center) + } + + @Builder + buildExplainSheetContent() { + Column() { + Stack({ alignContent: Alignment.End }) { + Text(this.cardConfig?.explain_title || + this.cardConfig?.card_title.value || + $r('app.string.ai_pick_title')) + .fontSize(18) + .fontWeight(500) + .textAlign(TextAlign.Center) + .width('100%') + + Image($r('app.media.icon_close_24')) + .width(24) + .height(24) + .fillColor($r('app.color.elements_icon_primary_02')) + .onClick(() => { + this.showExplainSheet = false; + }) + } + .height(64) + .width('100%') + + Scroll() { + Text((this.cardConfig?.explain_message ?? '') + .replace(new RegExp('\\\\n', 'g'), '\n')) + .width('100%') + .fontSize(14) + .lineHeight(22) + .fontColor($r('app.color.elements_text_primary_02')) + } + .width('100%') + .scrollBar(BarState.Off) + + Button($r('app.string.card_explain_confirm')) + .type(ButtonType.Normal) + .backgroundColor($r('app.color.elements_button_bg_primary')) + .width('100%') + .height(44) + .borderRadius(4) + .margin({ top: 16 }) + .onClick(() => { + this.showExplainSheet = false; + }) + } + .width('100%') + .backgroundColor($r('app.color.surface_layer3_background')) + .padding({ + bottom: px2vp(GlobalContext.navigationBarHeight), + left: 16, + right: 16, + }) + } + + @Builder + buildMainContent() { + Column() { + this.buildTabBar() + if (this.cardData.tabs[this.tabIdx].items.length > 0) { + this.buildStrategyDesc({ tabIdx: this.tabIdx }) + this.buildTable() + } else { + this.buildNoContractState() + } + this.buildCustomStrategy() + } + .width('100%') + .height(MAIN_CONTENT_HEIGHT) + } + + @Builder + buildTabBar() { + 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.elements_text_primary_01') + : $r('app.color.elements_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.elements_others_bg_primary') + : $r('app.color.elements_button_bg_tertiary')) + .onClick(() => { + this.tabIdx = index; + }) + }, (tab: PickTabData): string => + `${tab.strategy.periodType}_${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.transparent_white'), 0], + [$r('app.color.surface_layer1_foreground'), 1], + ], + }) + .hitTestBehavior(HitTestMode.None) + } + } + .width('100%') + .height(TAB_BAR_HEIGHT) + .onAreaChange((_oldValue: Area, newValue: Area) => { + this.tabViewportWidth = Number(newValue.width); + }) + } + + @Builder + buildStrategyDesc($$: StrategyDescParams) { + 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.elements_text_primary_02')) + .maxLines(2) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .onClick(() => { + this.jumpToStrategyDetail( + this.cardData.tabs[$$.tabIdx].strategy, + ); + }) + } + + if (this.cardData.tabs[$$.tabIdx].strategy.tagList.length > 0) { + Row({ space: 4 }) { + ForEach( + this.cardData.tabs[$$.tabIdx].strategy.tagList, + (tag: string) => { + Text(tag) + .fontSize(11) + .lineHeight(18) + .fontColor($r('app.color.elements_text_primary_01')) + .maxLines(1) + .height(18) + .padding({ left: 3, right: 3 }) + .borderRadius(2) + .border({ + width: 0.5, + color: $r('app.color.elements_text_primary_01'), + }) + .onClick(() => { + this.jumpToLabel( + this.cardData.tabs[$$.tabIdx].strategy, + tag, + ); + }) + }, + (tag: string): string => tag, + ) + } + .width('100%') + } + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + 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 }) + .onClick(() => { + this.jumpToStrategyDetail( + this.cardData.tabs[$$.tabIdx].strategy, + ); + }) + } + } + .width('100%') + .height(STRATEGY_DESC_HEIGHT) + .padding({ top: 10, bottom: 10 }) + .alignItems(VerticalAlign.Top) + } + + @Builder + buildTable() { + Column() { + Row({ space: 8 }) { + Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[0])) + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + .width('35%') + Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[1])) + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + .layoutWeight(1) + .textAlign(TextAlign.End) + Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[2])) + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + .layoutWeight(1) + .textAlign(TextAlign.End) + } + .width('100%') + .height(TABLE_HEADER_HEIGHT) + + ForEach( + this.cardData.tabs[this.tabIdx].items, + (item: PickContractItem) => { + this.buildContractRow(item) + }, + (item: PickContractItem): string => + `${item.contract}_${item.market}_${item.price ?? ''}_${item.priceChg ?? ''}`, + ) + } + .width('100%') + .height(TABLE_HEIGHT) + } + + @Builder + buildContractRow(item: PickContractItem) { + Row({ space: 8 }) { + Row({ space: 5 }) { + Text(item.name) + .fontSize(16) + .maxFontSize(16) + .minFontSize(9) + .fontColor($r('app.color.elements_text_primary_02')) + .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.hotlist_orange')) + .width(16) + .height(16) + .textAlign(TextAlign.Center) + .borderRadius(2) + .backgroundColor($r('app.color.elements_button_bg_tertiary')) + } + } + .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) + .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) + .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.elements_others_divider_primary'), + }) + .onClick(() => { + this.jumpToDetail(item); + }) + } + + @Builder + buildCustomStrategy() { + Row({ space: 2 }) { + Text($r('app.string.ai_pick_custom_strategy')) + .fontSize(14) + .lineHeight(20) + .fontColor($r('app.color.elements_text_tertiary')) + Image($r('app.media.icon_arrow_forward_20')) + .width(14) + .height(14) + .fillColor($r('app.color.elements_icon_tertiary')) + } + .width('100%') + .height(CUSTOM_STRATEGY_HEIGHT) + .justifyContent(FlexAlign.Center) + .alignItems(VerticalAlign.Center) + .onClick(() => { + this.jumpToNewHome(); + }) + } + + @Builder + buildEmptyState() { + Column() { + if (this.dataReady) { + Image(this.isDarkMode ? EMPTY_IMAGE_DARK : EMPTY_IMAGE_LIGHT) + .width(120) + .height(120) + .margin({ bottom: 8 }) + Text($r('app.string.ai_pick_no_contract')) + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + } else { + Text($r('app.string.ai_pick_loading')) + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + } + } + .width('100%') + .height(MAIN_CONTENT_HEIGHT) + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + } + + @Builder + buildNoContractState() { + Column() { + Image(this.isDarkMode ? EMPTY_IMAGE_DARK : EMPTY_IMAGE_LIGHT) + .width(100) + .height(100) + Text($r('app.string.ai_pick_no_contract')) + .fontSize(14) + .fontColor($r('app.color.elements_text_tertiary')) + } + .width('100%') + .height(NO_CONTRACT_HEIGHT) + .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}`; + } +} diff --git a/src/main/ets/ai-pick/AiPickUtils.ets b/src/main/ets/ai-pick/AiPickUtils.ets new file mode 100644 index 0000000..bb933ee --- /dev/null +++ b/src/main/ets/ai-pick/AiPickUtils.ets @@ -0,0 +1,177 @@ +import { + CardData, + PickContractItem, + PickFieldItem, + PickTabData, + RawDetailItem, + RawFieldItem, + RawResultItem, + RawStrategyItem, +} from './AiPickModels'; +import { BLACK_LIST, MAX_DOUBLE_KEYS, MAX_SHOW_KEYS } from './AiPickConstant'; +import { formatPercent, formatPrice, riseFallColor } from '../common/NumberFormat'; + +const EMPTY_EXTRA_FIELDS: Record = {}; + +export interface ResData { + showKeyList: string[]; + fieldList: PickFieldItem[]; + resultValueList: Record[]; + list: RawDetailItem[]; +} + +export function filterFieldKey(key: string): boolean { + const extBlackList: string[] = + BLACK_LIST.map((value: string): string => `期货@${value}`); + const inBlack: boolean = BLACK_LIST.includes(key) || extBlackList.includes(key); + const isChangeField: boolean = + key.startsWith('涨跌幅[') || key.startsWith('期货@涨跌幅['); + return !inBlack && !isChangeField; +} + +export function formatBigData(value: number | string): string { + const numericValue: number = Number(value); + if (Number.isNaN(numericValue)) { + return `${value}`; + } + const tenThousand = 1e4; + const billion = 1e8; + const trillion = 1e12; + const formattedValue: number = Number(numericValue.toFixed(3)); + const absoluteValue: number = Math.abs(formattedValue); + if (absoluteValue >= trillion) { + return `${(formattedValue / trillion).toFixed(2)}万亿`; + } + if (absoluteValue >= billion) { + if (Number((absoluteValue / billion).toFixed(2)) >= tenThousand) { + return `${(formattedValue / trillion).toFixed(2)}万亿`; + } + return `${(formattedValue / billion).toFixed(2)}亿`; + } + if (absoluteValue >= tenThousand) { + if (Number((absoluteValue / tenThousand).toFixed(2)) >= tenThousand) { + return `${(formattedValue / billion).toFixed(2)}亿`; + } + return `${(formattedValue / tenThousand).toFixed(2)}万`; + } + return `${formattedValue}`; +} + +export function getShowKeyList(doubleKeys: string[]): string[] { + if (doubleKeys.length === 0) { + return ['合约名称', '最新价', '涨跌幅(结)']; + } + return ['合约名称'] + .concat(doubleKeys.concat(['涨跌幅(结)', '最新价'])) + .slice(0, MAX_SHOW_KEYS); +} + +export function getResultValueList( + resultList: RawResultItem[], + doubleListKeys: string[], +): Record[] { + return resultList.map((value: RawResultItem): Record => { + const result: Record = {}; + doubleListKeys.forEach((key: string): void => { + const rawValue: number | string | undefined = value[key]; + if (rawValue !== undefined) { + result[key] = formatBigData(rawValue); + } + }); + return result; + }); +} + +export function getResData(strategyItem: RawStrategyItem): ResData { + const rawFieldList: RawFieldItem[] = strategyItem.field_list ?? []; + const resultList: RawResultItem[] = strategyItem.origin_result_list ?? []; + const list: RawDetailItem[] = strategyItem.detail_list ?? []; + const doubleList: RawFieldItem[] = rawFieldList.filter( + (field: RawFieldItem): boolean => + field.type === 'DOUBLE' && filterFieldKey(field.key), + ); + const doubleListKeys: string[] = + doubleList.map((field: RawFieldItem): string => field.key); + const showKeys: string[] = doubleListKeys.slice(0, MAX_DOUBLE_KEYS); + const fieldList: PickFieldItem[] = + doubleList.map((field: RawFieldItem): PickFieldItem => { + const item: PickFieldItem = { + key: field.key, + unit: field.unit ?? '', + }; + return item; + }); + const result: ResData = { + showKeyList: getShowKeyList(showKeys), + fieldList: fieldList, + resultValueList: getResultValueList(resultList, doubleListKeys), + list: list, + }; + return result; +} + +export function getTableList( + list: RawDetailItem[], + resultValueList: Record[], +): PickContractItem[] { + return list.map((item: RawDetailItem, index: number): PickContractItem => { + const contract: PickContractItem = { + contract: item.contract, + market: item.market, + name: item.contract_name, + isMain: String(item.main_contract_type) === '1', + extraFields: resultValueList[index] ?? EMPTY_EXTRA_FIELDS, + }; + return contract; + }); +} + +export function getUniqueContracts(cardData: CardData): PickContractItem[] { + const contracts: PickContractItem[] = []; + const contractKeys: string[] = []; + cardData.tabs.forEach((tab: PickTabData): void => { + tab.items.forEach((item: PickContractItem): void => { + const contractKey: string = `${item.contract}_${item.market}`; + if (!contractKeys.includes(contractKey)) { + contractKeys.push(contractKey); + contracts.push(item); + } + }); + }); + return contracts; +} + +export function formatColName(name: string): string { + return name + .replace(new RegExp('期货@', 'g'), '') + .replace(new RegExp('\\[.*?\\]', 'g'), ''); +} + +export function getUnit(key: string, fieldList: PickFieldItem[]): string { + const item: PickFieldItem | undefined = + fieldList.find((field: PickFieldItem): boolean => field.key === key); + return item?.unit ?? ''; +} + +export function cellValue( + columnName: string, + item: PickContractItem, + fieldList: PickFieldItem[], +): string { + if (columnName === '最新价') { + return formatPrice(item.price); + } + if (columnName === '涨跌幅(结)') { + return formatPercent(item.priceChg); + } + const rawValue: string = item.extraFields[columnName] ?? '--'; + const unit: string = getUnit(columnName, fieldList); + return unit === '' ? rawValue : `${rawValue}${unit}`; +} + +export function cellColor(columnName: string, item: PickContractItem): Resource { + if (columnName === '最新价' || columnName === '涨跌幅(结)') { + return riseFallColor(item.priceChg); + } + return $r('app.color.elements_text_primary_02'); +} diff --git a/src/main/ets/components/mainpage/FirstPage.ets b/src/main/ets/components/mainpage/FirstPage.ets index a640a16..84736d0 100644 --- a/src/main/ets/components/mainpage/FirstPage.ets +++ b/src/main/ets/components/mainpage/FirstPage.ets @@ -24,6 +24,7 @@ import { AiRiseFallNodeComponent } from '../../node/view/AiRiseFallNodeComponent import { HotListNodeComponent } from '../../node/view/HotListNodeComponent' import { CommodityOptionsNodeComponent } from '../../node/view/CommodityOptionsNodeComponent' import { MarketRankingNodeComponent } from '../../market-ranking/MarketRankingNodeComponent' +import { AiPickNodeComponent } from '../../ai-pick/AiPickNodeComponent' /** @@ -146,11 +147,14 @@ export struct FirstPage { aiRiseFallModel.targetTypeId = FirstPageConstant.KEY_AI_RISEFALL.toString() let marketRankingModel = new FirstPageNodeModel() marketRankingModel.targetTypeId = FirstPageConstant.KEY_MARKET_RANKING.toString() + let aiPickModel = new FirstPageNodeModel() + aiPickModel.targetTypeId = FirstPageConstant.KEY_AI_PICK.toString() let feedBackModel = new FirstPageNodeModel() feedBackModel.targetTypeId = FirstPageConstant.KEY_FEED_BACK.toString() let endTipModel = new FirstPageNodeModel() endTipModel.targetTypeId = FirstPageConstant.KEY_END_TIP.toString() - response.push(hotListModel, commodityOptionModel, aiRiseFallModel, aiDiagnosisModel, marketRankingModel, feedBackModel, endTipModel) + response.push(hotListModel, commodityOptionModel, aiRiseFallModel, aiDiagnosisModel, + aiPickModel, marketRankingModel, feedBackModel, endTipModel) this.nodeModelArr = response } } @@ -358,6 +362,10 @@ export struct FirstPage { MarketRankingNodeComponent({ refreshTrigger: this.refreshTrigger, }).margin({ top: index > 0 ? 10 : 0, left: 10, right: 10 }) + } else if (model.targetTypeId == FirstPageConstant.KEY_AI_PICK.toString()) { + AiPickNodeComponent({ + refreshTrigger: this.refreshTrigger, + }).margin({ top: index > 0 ? 10 : 0, left: 10, right: 10 }) } else if (model.targetTypeId == FirstPageConstant.KEY_TOP_BG.toString()) { TopImageNodeComponent({ nodeModel: model diff --git a/src/main/ets/datacenter/FirstPageConstant.ets b/src/main/ets/datacenter/FirstPageConstant.ets index eef7c5d..c642b45 100644 --- a/src/main/ets/datacenter/FirstPageConstant.ets +++ b/src/main/ets/datacenter/FirstPageConstant.ets @@ -14,4 +14,5 @@ export class FirstPageConstant { static KEY_HOT_LIST = 18 //热点排行 static KEY_COMMODITY_OPTIONS = 19 //商品期权 static KEY_MARKET_RANKING = 20 //市场排名 -} \ No newline at end of file + static KEY_AI_PICK = 21 //AI选期 +} diff --git a/src/main/ets/market-ranking/TableConstants.ets b/src/main/ets/market-ranking/TableConstants.ets index cb6f712..6e39954 100644 --- a/src/main/ets/market-ranking/TableConstants.ets +++ b/src/main/ets/market-ranking/TableConstants.ets @@ -13,7 +13,7 @@ export class TableConstants { static readonly DATA_ID_CHG_SPEED_1MIN: number = 34874; // 1 分钟涨跌速 static readonly DATA_ID_CHG_SPEED_10MIN: number = 34875; // 10 分钟涨跌速 static readonly DATA_ID_CHG_SPEED_15MIN: number = 34876; // 15 分钟涨跌速 - static readonly DATA_ID_ZF_YTD: number = 34877; // 昨收涨跌幅 + static readonly DATA_ID_ZF_YTD: number = 34877; // 涨跌幅(昨结算价基准) static readonly DATA_ID_MARKET: number = 36103; // 市场 ID private constructor() { diff --git a/src/main/ets/node/clients/AiPickHqRequestClient.ets b/src/main/ets/node/clients/AiPickHqRequestClient.ets new file mode 100644 index 0000000..018539a --- /dev/null +++ b/src/main/ets/node/clients/AiPickHqRequestClient.ets @@ -0,0 +1,178 @@ +import { AbsRowData, RowData, TableData } from '@b2b/hq_table'; +import { TableRequestClient } from '@b2c/lib_baseui'; +import { RequestHelper, StuffTableStruct } from '@kernel/lib_communication'; +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_MARKET, + TableConstants.DATA_ID_ZF_YTD, +]; + +interface AiPickQuote { + price?: number; + priceChg?: number; +} + +export type AiPickCardDataListener = (cardData: CardData) => void; + +class AiPickTableRequestClient extends TableRequestClient { + private requestParams: string; + private sourceCardData: CardData; + private listener: AiPickCardDataListener; + + constructor( + requestParams: string, + sourceCardData: CardData, + listener: AiPickCardDataListener, + ) { + super(FRAME_ID, PAGE_ID, [], null, undefined); + this.requestParams = requestParams; + this.sourceCardData = sourceCardData; + this.listener = listener; + this.setSubscribeIds(FIELD_IDS); + } + + protected doRequest(): void { + RequestHelper.getHqManager() + .addRequestStructToBuff(this.frameId, this.pageId, this.getInstanceId(), this.requestParams); + RequestHelper.build().base(this.frameId, this.pageId, this, this.requestParams).request(); + } + + protected afterParserModel(tableData: TableData, _structData: StuffTableStruct): void { + const quoteMap: Map = this.toQuoteMap(tableData); + const nextCardData: CardData = this.mergeQuoteData(quoteMap); + this.sourceCardData = nextCardData; + this.listener(nextCardData); + } + + private toQuoteMap(tableData: TableData): Map { + const quoteMap: Map = new Map(); + if (!tableData.tableRows) { + return quoteMap; + } + tableData.tableRows.forEach((row: AbsRowData): void => { + if (!(row instanceof RowData)) { + return; + } + const codeValue = row.originalData.get(TableConstants.DATA_ID_CODE_4); + const marketValue = row.originalData.get(TableConstants.DATA_ID_MARKET); + if (codeValue === undefined || marketValue === undefined) { + return; + } + const quote: AiPickQuote = { + price: this.getNumber(row, TableConstants.DATA_ID_PRICE), + priceChg: this.getNumber(row, TableConstants.DATA_ID_ZF_YTD), + }; + quoteMap.set(`${codeValue}_${marketValue}`, quote); + }); + return quoteMap; + } + + private mergeQuoteData(quoteMap: Map): CardData { + const tabs: PickTabData[] = + this.sourceCardData.tabs.map((tab: PickTabData): PickTabData => { + const items: PickContractItem[] = + tab.items.map((item: PickContractItem): PickContractItem => { + const quote: AiPickQuote | undefined = + quoteMap.get(`${item.contract}_${item.market}`); + const nextItem: PickContractItem = { + contract: item.contract, + market: item.market, + name: item.name, + price: quote?.price ?? item.price, + priceChg: quote?.priceChg ?? item.priceChg, + isMain: item.isMain, + extraFields: item.extraFields, + }; + return nextItem; + }); + const nextTab: PickTabData = { + strategy: tab.strategy, + items: items, + keysList: tab.keysList, + fieldList: tab.fieldList, + }; + return nextTab; + }); + const nextCardData: CardData = { tabs: tabs }; + return nextCardData; + } + + private getNumber(row: RowData, fieldId: number): number | undefined { + const value = row.originalData.get(fieldId); + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string' && value !== '' && value !== '--') { + const result: number = parseFloat(value); + return Number.isNaN(result) ? undefined : result; + } + return undefined; + } +} + +export class AiPickHqRequestClient { + private static readonly instance: AiPickHqRequestClient = + new AiPickHqRequestClient(); + + private constructor() { + } + + static getInstance(): AiPickHqRequestClient { + return AiPickHqRequestClient.instance; + } + + subscribeHq(cardData: CardData, listener: AiPickCardDataListener): () => void { + const contracts: PickContractItem[] = getUniqueContracts(cardData); + if (contracts.length === 0) { + return (): void => { + }; + } + const requestParams: string = this.buildRequestParams(contracts); + const requestClient: AiPickTableRequestClient = + new AiPickTableRequestClient(requestParams, cardData, listener); + requestClient.request(0, contracts.length); + return (): void => { + requestClient.release(true); + }; + } + + private buildRequestParams(contracts: PickContractItem[]): string { + 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` + + `scenario=qht_qihuo_sort\r\n` + + `precision=1\r\n` + + `pushtime=2.5\r\n`; + } + + private formatCodeList(contracts: PickContractItem[]): string { + const markets: string[] = []; + const codeGroups: string[][] = []; + contracts.forEach((contract: PickContractItem): void => { + const marketIndex: number = markets.indexOf(contract.market); + if (marketIndex >= 0) { + codeGroups[marketIndex].push(contract.contract); + } else { + markets.push(contract.market); + codeGroups.push([contract.contract]); + } + }); + return markets.map((market: string, index: number): string => + `${market}(${codeGroups[index].map((code: string): string => `${code},`).join('')});` + ).join(''); + } +} diff --git a/src/main/ets/node/datacenter/FirstPageCardsConfigLoader.ets b/src/main/ets/node/datacenter/FirstPageCardsConfigLoader.ets index a20793d..ad34a14 100644 --- a/src/main/ets/node/datacenter/FirstPageCardsConfigLoader.ets +++ b/src/main/ets/node/datacenter/FirstPageCardsConfigLoader.ets @@ -47,6 +47,7 @@ export interface FirstPageCardsConfig { aiRiseFall: AllCardsCard commodityOptions: CommodityOptionsCardConfig marketRanking: AllCardsCard + aiPick: AllCardsCard } export class FirstPageCardsConfigLoader { diff --git a/src/main/resources/base/element/string.json b/src/main/resources/base/element/string.json index 45b25c2..1b10cc0 100644 --- a/src/main/resources/base/element/string.json +++ b/src/main/resources/base/element/string.json @@ -279,6 +279,34 @@ { "name": "market_ranking_15min_fall_speed", "value": "15分钟跌速" + }, + { + "name": "ai_pick_title", + "value": "AI选期" + }, + { + "name": "ai_pick_loading", + "value": "数据加载中" + }, + { + "name": "ai_pick_no_contract", + "value": "没有符合的合约" + }, + { + "name": "ai_pick_main_contract", + "value": "主" + }, + { + "name": "ai_pick_custom_strategy", + "value": "一句话定制策略" + }, + { + "name": "ai_pick_custom_tag", + "value": "自编" + }, + { + "name": "card_explain_confirm", + "value": "我知道了" } ] } diff --git a/src/main/resources/en_US/element/string.json b/src/main/resources/en_US/element/string.json index 9659948..b72eddb 100644 --- a/src/main/resources/en_US/element/string.json +++ b/src/main/resources/en_US/element/string.json @@ -3,6 +3,34 @@ { "name": "page_show", "value": "page from package" + }, + { + "name": "ai_pick_title", + "value": "AI Picks" + }, + { + "name": "ai_pick_loading", + "value": "Loading" + }, + { + "name": "ai_pick_no_contract", + "value": "No matching contracts" + }, + { + "name": "ai_pick_main_contract", + "value": "M" + }, + { + "name": "ai_pick_custom_strategy", + "value": "Create a strategy" + }, + { + "name": "ai_pick_custom_tag", + "value": "Custom" + }, + { + "name": "card_explain_confirm", + "value": "Got it" } ] -} \ No newline at end of file +} diff --git a/src/main/resources/rawfile/first_page_cards_config.json b/src/main/resources/rawfile/first_page_cards_config.json index fb8cb8c..1a5329c 100644 --- a/src/main/resources/rawfile/first_page_cards_config.json +++ b/src/main/resources/rawfile/first_page_cards_config.json @@ -112,5 +112,31 @@ "explain_message": null, "mod_data": [], "bury_point": "marketRank" + }, + "aiPick": { + "card_id": 5106, + "card_key": "futuresHomepageAIPickCard", + "title_type": 1, + "is_toggle": false, + "render_key": "ai-pick", + "card_size": 4, + "explain_title": "AI选期说明", + "explain_message": "1.合约数据更新说明:合约最新价和涨跌幅信息实时更新,无需手动刷新。\n2.符合策略的合约内容需要手动刷新。\n3.安卓3.96.1版本和苹果3.97.01版本以上已支持自定义选期。\n4.新版首页AI选期将与AI分组同步,默认取AI分组前5条。\n5.新版首页AI选期策略可在AI分组管理中可进行删减。", + "card_title": { + "value": "AI选期", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/ai_diagnosis/home_strategy/v1/user_data", + "param_list": [""], + "req_desc": "http_get" + } + ], + "bury_point": "aiPick" } } diff --git a/src/main/resources/zh_CN/element/string.json b/src/main/resources/zh_CN/element/string.json index 9659948..89f1187 100644 --- a/src/main/resources/zh_CN/element/string.json +++ b/src/main/resources/zh_CN/element/string.json @@ -3,6 +3,34 @@ { "name": "page_show", "value": "page from package" + }, + { + "name": "ai_pick_title", + "value": "AI选期" + }, + { + "name": "ai_pick_loading", + "value": "数据加载中" + }, + { + "name": "ai_pick_no_contract", + "value": "没有符合的合约" + }, + { + "name": "ai_pick_main_contract", + "value": "主" + }, + { + "name": "ai_pick_custom_strategy", + "value": "一句话定制策略" + }, + { + "name": "ai_pick_custom_tag", + "value": "自编" + }, + { + "name": "card_explain_confirm", + "value": "我知道了" } ] -} \ No newline at end of file +}