From 63fcb9cbaad00e7a7f704b15ac5cd2516f2a1216 Mon Sep 17 00:00:00 2001 From: clz Date: Thu, 23 Jul 2026 16:56:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor(cards):=20=E7=BB=9F=E4=B8=80=20AI=20?= =?UTF-8?q?=E9=80=89=E6=9C=9F=E4=B8=8E=E5=B8=82=E5=9C=BA=E6=8E=92=E5=90=8D?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 AI Pick 重构为 NodeComponent、DataFetcher、Models 分层结构 - 新增公共 AllCardsCard 模型与卡片配置 Mock 数据 - 分离 cardConfig 与 cardData 的状态及更新职责 - 对齐两个卡片的标题、配置跳转、深色模式和说明 Sheet 设计 - 完善 AI Pick HTTP 请求、行情订阅、跳转参数及接口文档 - 保留 MarketRanking 榜单请求、行情合并和展开收起交互 --- doc/ai-pick-interfaces.md | 86 ++ entry/src/main/ets/ai-pick/AiPick.ets | 377 ------ entry/src/main/ets/ai-pick/AiPickApi.ets | 97 -- entry/src/main/ets/ai-pick/AiPickConstant.ets | 19 +- .../main/ets/ai-pick/AiPickDataFetcher.ets | 278 ++++ entry/src/main/ets/ai-pick/AiPickMock.ets | 2 +- entry/src/main/ets/ai-pick/AiPickModel.ets | 135 -- .../{AiPickTypes.ets => AiPickModels.ets} | 56 +- .../main/ets/ai-pick/AiPickNodeComponent.ets | 561 ++++++++ entry/src/main/ets/ai-pick/AiPickUtils.ets | 22 +- entry/src/main/ets/common/AllCardsModels.ets | 48 + entry/src/main/ets/common/NumberFormat.ets | 11 +- .../main/ets/common/mock/AllCardsMock.json | 1149 +++++++++++++++++ .../market-ranking/MarketRankingConstant.ets | 9 + .../MarketRankingDataFetcher.ets | 24 + .../MarketRankingNodeComponent.ets | 164 ++- entry/src/main/ets/pages/Index.ets | 20 +- .../main/resources/base/element/string.json | 24 + 18 files changed, 2383 insertions(+), 699 deletions(-) create mode 100644 doc/ai-pick-interfaces.md delete mode 100644 entry/src/main/ets/ai-pick/AiPick.ets delete mode 100644 entry/src/main/ets/ai-pick/AiPickApi.ets create mode 100644 entry/src/main/ets/ai-pick/AiPickDataFetcher.ets delete mode 100644 entry/src/main/ets/ai-pick/AiPickModel.ets rename entry/src/main/ets/ai-pick/{AiPickTypes.ets => AiPickModels.ets} (74%) create mode 100644 entry/src/main/ets/ai-pick/AiPickNodeComponent.ets create mode 100644 entry/src/main/ets/common/AllCardsModels.ets create mode 100644 entry/src/main/ets/common/mock/AllCardsMock.json diff --git a/doc/ai-pick-interfaces.md b/doc/ai-pick-interfaces.md new file mode 100644 index 0000000..c466b23 --- /dev/null +++ b/doc/ai-pick-interfaces.md @@ -0,0 +1,86 @@ +# AI 选期接口文档 + +本文记录 `AiPickNodeComponent` 使用的策略 HTTP 接口及 4106 行情订阅协议。当前实现位于 +`entry/src/main/ets/ai-pick/AiPickDataFetcher.ets`。 + +## 策略 HTTP 接口 + +### 请求 + +```text +GET {API_HOST}futgwapi/api/ai_diagnosis/home_strategy/v1/user_data?_=TIMESTAMP +``` + +| 构建模式 | API_HOST | +| --- | --- | +| Debug | `https://futures-test.10jqka.com.cn/` | +| Release | `https://ftapi.10jqka.com.cn/` | + +请求头为 `Content-Type: application/json` 和客户端 Cookie。`_` 是毫秒时间戳,用于避免缓存。 + +### 响应 + +```json +{ + "code": 0, + "data": [ + { + "type": "custom_strategy", + "custom_strategy": { + "custom_strategy": { + "id": "1", + "strategy_name": "策略名", + "user_question": "策略描述", + "strategy_type": "custom_strategy", + "tag_list": ["自编"] + }, + "detail_list": [ + { + "contract": "CU2501", + "market": "70", + "contract_name": "沪铜2501", + "main_contract_type": "1" + } + ], + "field_list": [], + "origin_result_list": [], + "detail_count": 1 + } + } + ] +} +``` + +HTTP 数据先生成 Tab 和合约列表;最新价、涨跌幅由行情推送补齐。 + +## 4106 行情订阅 + +```json +{ + "protocolId": "4106", + "pageId": "2201", + "onlineId": "contractHQData", + "columnOrder": "55|10|4|36103|34818|6|7|66|34877", + "requestDic": "sortid=-1\r\nsortorder=0\r\npush=1\r\ndataitem=55,10,4,36103,34818,6,7,66,34877,\r\ncodelist=70(CU2501,);\r\nscenario=qht_qihuo_sort\r\nprecision=1\r\npushtime=2.5\r\n" +} +``` + +合约按市场分组写入 `codelist`,`push=1` 表示持续推送,推送间隔为 2.5 秒。关键字段: + +| 字段 | ID | +| --- | ---: | +| 名称 | 55 | +| 最新价 | 10 | +| 合约代码 | 4 | +| 市场 | 36103 | +| 涨跌幅 | 34818 | +| 昨收 | 6 | +| 开盘价 | 7 | +| 昨结算 | 66 | +| 主力标记 | 34877 | + +## 当前 Mock 行为 + +- HTTP 使用 `@ohos.net.http` 构造请求并打印参数,但不调用 `request()`。 +- Fetcher 延迟 300ms 返回 Mock 策略数据,不读取或更新卡片缓存。 +- 行情订阅打印完整 4106 参数,每 2500ms 生成一次行情,并整体替换 `CardData`。 diff --git a/entry/src/main/ets/ai-pick/AiPick.ets b/entry/src/main/ets/ai-pick/AiPick.ets deleted file mode 100644 index 1e64d1d..0000000 --- a/entry/src/main/ets/ai-pick/AiPick.ets +++ /dev/null @@ -1,377 +0,0 @@ -import { common, ConfigurationConstant } from '@kit.AbilityKit'; -import { CardData, PickContractItem, PickTabData } from './AiPickTypes'; -import { AiPickModel } from './AiPickModel'; -import { cellColor, cellValue, formatColName } from './AiPickUtils'; -import { MAX_ITEMS } from './AiPickConstant'; - -const RIGHT_ARROW_LIGHT = - 'https://u.thsi.cn/imgsrc/bbs/8c8290d904cd89250d4e828da5f1010c_300_230.png'; -const RIGHT_ARROW_DARK = - 'https://u.thsi.cn/imgsrc/bbs/f5ff0a4fcd103d7b3da180ad90d9db23_300_230.png'; -const EMPTY_IMAGE_LIGHT = - 'https://u.thsi.cn/imgsrc/bbs/8eee9cd4fa7e359032de11a3f7c2a70b_300_230.png'; -const EMPTY_IMAGE_DARK = - 'https://u.thsi.cn/imgsrc/bbs/1c4e679244ed87231befad2b04cddfe2_300_230.png'; - -interface StrategyDescParams { - tabIdx: number; -} - -@Component -export struct AiPick { - // cardData prop:从第一层 floorList 透传下来的单卡片配置 - @Watch('onCardDataChanged') @Prop cardData: CardData | undefined; - // 初值用 placeholder cardData 占位(仅类型占位,实际值由 aboutToAppear 用真实 cardData 创建) - @State vm: AiPickModel = new AiPickModel(this.cardData); - @State tabIdx: number = 0; - @State isDarkMode: boolean = false; - @State tabViewportWidth: number = 0; - @State tabContentWidth: number = 0; - @State tabScrollOffset: number = 0; - - private truncateText(text: string): string { - const maxLength = 20; - let currentLength = 0; - let truncatedText = ''; - for (const char of text) { - currentLength += /[\u4e00-\u9fa5]/.test(char) ? 2 : 1; - if (currentLength > maxLength) { - return `${truncatedText}...`; - } - truncatedText += char; - } - return truncatedText; - } - - private tabTitle(tab: PickTabData): string { - return `${this.truncateText(tab.strategy.name)}(${tab.strategy.count})`; - } - - private showTabGradient(): boolean { - const maxOffset = this.tabContentWidth - this.tabViewportWidth; - return maxOffset > 0 && this.tabScrollOffset < maxOffset; - } - - private syncColorMode(): void { - const context = getContext(this) as common.UIAbilityContext; - this.isDarkMode = context.config.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK; - } - - aboutToAppear(): void { - this.syncColorMode(); - this.vm = new AiPickModel(this.cardData); - this.vm.loadData(); - } - - // 父组件传入不同卡片配置时,重新构造 Model 并重新加载数据 - onCardDataChanged(): void { - this.vm = new AiPickModel(this.cardData); - this.tabIdx = 0; - this.vm.loadData(); - } - - @Builder - StrategyDesc($$: StrategyDescParams) { - // 图片:imgs[0] 白天,imgs[1] 夜间 - Row() { - // 左侧:问句 + 标签 - Column({ space: 4 }) { - if (this.vm.tabs[$$.tabIdx].strategy.detail) { - Text(this.vm.tabs[$$.tabIdx].strategy.detail) - .fontSize(14) - .lineHeight(21) - .fontColor($r('app.color.text_primary')) - .maxLines(2) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - } - - if (this.vm.tabs[$$.tabIdx].strategy.tagList.length > 0) { - Row({ space: 4 }) { - ForEach(this.vm.tabs[$$.tabIdx].strategy.tagList, (tag: string) => { - Text(tag) - .fontSize(11) - .lineHeight(18) - .fontColor($r('app.color.text_blue')) - .height(18) - .padding({ left: 3, right: 3 }) - .borderRadius(2) - .border({ width: 0.5, color: $r('app.color.text_blue') }) - .onClick((event: ClickEvent) => { - - this.vm.jumpToLabel( - this.vm.tabs[$$.tabIdx].strategy.id, - this.vm.tabs[$$.tabIdx].strategy.periodType, - tag, - ); - }) - }, (tag: string) => tag) - } - .width('100%') - } - } - .layoutWeight(1) - .alignItems(HorizontalAlign.Start) - .margin({ bottom: 10 }) - - // 右侧:策略图片(仅系统策略有图,右侧 image) - // imgs 长度>0 且当前选中位(imgs[0],暂无主题切换)非空才渲染 - if (this.vm.tabs[$$.tabIdx].strategy.imgs.length > 0 && this.vm.tabs[$$.tabIdx].strategy.imgs[0]) { - Image(this.vm.tabs[$$.tabIdx].strategy.imgs[0]) - .width(104) - .height(66) - .objectFit(ImageFit.Contain) - .margin({ left: 12 }) - } - } - .width('100%') - .margin({ top: 10 }) - .alignItems(VerticalAlign.Top) - .onClick(() => { - this.vm.jumpToDetail2( - this.vm.tabs[$$.tabIdx].strategy.id, - this.vm.tabs[$$.tabIdx].strategy.periodType, - this.vm.tabs[$$.tabIdx].strategy.name, - ); - }) - } - - build() { - Column() { - // 数据未就绪(加载中/加载失败)时不渲染依赖 tabs[tabIdx] 的内容,避免越界 - if (this.vm.tabs.length === 0) { - Row() { - Text('AI选期') - .fontSize(18) - .fontWeight(FontWeight.Bold) - .fontColor($r('app.color.text_primary')) - } - .width('100%') - .padding({ bottom: 16 }) - - if (this.vm.dataError) { - this.EmptyState('暂无满足条件的期货合约') - } else { - Column() { - Text('数据加载中') - .fontSize(16) - .fontColor($r('app.color.text_grey')) - } - .width('100%') - .height(186) - .alignItems(HorizontalAlign.Center) - .justifyContent(FlexAlign.Center) - } - } else { - this.MainContent() - } - } - .width('100%') - } - - @Builder - MainContent() { - Column() { - // ── 标题栏 ────────────────────────────────────────────── - Row() { - Text('AI选期') - .fontSize(18) - .fontWeight(FontWeight.Bold) - .fontColor($r('app.color.text_primary')) - } - .width('100%') - .padding({ bottom: 16 }) - - // ── Tab 栏 ────────────────────────────────────────────── - Stack({ alignContent: Alignment.End }) { - Scroll() { - Row({ space: 8 }) { - ForEach(this.vm.tabs, (tab: PickTabData, index: number) => { - Text(this.tabTitle(tab)) - .fontSize(14) - .fontWeight(this.tabIdx === index ? FontWeight.Bold : FontWeight.Normal) - .fontColor(this.tabIdx === index - ? $r('app.color.text_blue') - : $r('app.color.text_secondary')) - .constraintSize({ minWidth: 63 }) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .textAlign(TextAlign.Center) - .padding({ top: 6, bottom: 6, left: 12, right: 12 }) - .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(30) - .scrollable(ScrollDirection.Horizontal) - .scrollBar(BarState.Off) - .align(Alignment.Start) - .onScroll((xOffset: number, _yOffset: number) => { - this.tabScrollOffset = xOffset; - }) - - if (this.showTabGradient()) { - Row() - .width(24) - .height(30) - .linearGradient({ - angle: 90, - colors: [['#00FFFFFF', 0], [$r('app.color.card_bg'), 1]], - }) - .hitTestBehavior(HitTestMode.None) - } - } - .width('100%') - .height(30) - .onAreaChange((oldValue: Area, newValue: Area) => { - this.tabViewportWidth = Number(newValue.width); - }) - - // ── 策略问句(按引用传递,tabIdx 变化时自动刷新)──────── - this.StrategyDesc({ tabIdx: this.tabIdx }) - - if (this.vm.tabs[this.tabIdx].items.length === 0) { - this.EmptyState('没有符合的合约') - } else { - // ── 表头 ────────────────────────────────────────────── - Row() { - Text(formatColName(this.vm.tabs[this.tabIdx].keysList[0])) - .fontSize(12) - .fontColor($r('app.color.text_tertiary')) - .width('35%') - Text(formatColName(this.vm.tabs[this.tabIdx].keysList[1])) - .fontSize(12) - .fontColor($r('app.color.text_tertiary')) - .layoutWeight(1) - .margin({ left: 8 }) - .textAlign(TextAlign.End) - Text(formatColName(this.vm.tabs[this.tabIdx].keysList[2])) - .fontSize(12) - .fontColor($r('app.color.text_tertiary')) - .layoutWeight(1) - .margin({ left: 8 }) - .textAlign(TextAlign.End) - } - .width('100%') - .height(24) - .margin({ top: 8 }) - .padding({ bottom: 2 }) - .alignItems(VerticalAlign.Center) - .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) - - // ── 合约列表 ────────────────────────────────────────── - ForEach( - this.vm.tabs[this.tabIdx].items.slice(0, MAX_ITEMS), - (item: PickContractItem, index: number) => { - Row() { - Row({ space: 5 }) { - Text(item.name) - .fontSize(16) - .fontColor($r('app.color.text_primary')) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - if (item.isMain) { - Text('主') - .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.vm.tabs[this.tabIdx].keysList[1], - item, - this.vm.tabs[this.tabIdx].fieldList, - )) - .fontSize(16) - .fontFamily('monospace') - .fontColor(cellColor(this.vm.tabs[this.tabIdx].keysList[1], item)) - .layoutWeight(1) - .margin({ left: 8 }) - .textAlign(TextAlign.End) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - - Text(cellValue( - this.vm.tabs[this.tabIdx].keysList[2], - item, - this.vm.tabs[this.tabIdx].fieldList, - )) - .fontSize(16) - .fontFamily('monospace') - .fontColor(cellColor(this.vm.tabs[this.tabIdx].keysList[2], item)) - .layoutWeight(1) - .margin({ left: 8 }) - .textAlign(TextAlign.End) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - } - .width('100%') - .height(44) - .alignItems(VerticalAlign.Center) - .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) - .onClick(() => { - this.vm.jumpToDetail(item.contract, item.market); - }) - }, - (item: PickContractItem) => item.contract + item.market, - ) - } - - // ── 一句话定制策略 ────────────────────────────────────── - Row({ space: 2 }) { - Text('一句话定制策略') - .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%') - .justifyContent(FlexAlign.Center) - .alignItems(VerticalAlign.Center) - .margin({ top: 12 }) - .onClick(() => { - this.vm.jumpToNewHome( - this.vm.tabs[this.tabIdx].strategy.id, - this.vm.tabs[this.tabIdx].strategy.type, - ); - }) - } - .width('100%') - } - - @Builder - EmptyState(message: string) { - Column() { - Image(this.isDarkMode ? EMPTY_IMAGE_DARK : EMPTY_IMAGE_LIGHT) - .width(120) - .height(120) - .margin({ bottom: 8 }) - Text(message) - .fontSize(16) - .fontColor($r('app.color.text_grey')) - } - .width('100%') - .height(186) - .alignItems(HorizontalAlign.Center) - .justifyContent(FlexAlign.Center) - } -} diff --git a/entry/src/main/ets/ai-pick/AiPickApi.ets b/entry/src/main/ets/ai-pick/AiPickApi.ets deleted file mode 100644 index c6bfe14..0000000 --- a/entry/src/main/ets/ai-pick/AiPickApi.ets +++ /dev/null @@ -1,97 +0,0 @@ -// AI选期接口层:负责从网络获取原始数据并解析为视图数据 -// 目前网络请求未接入,直接返回 Mock 数据;接入时替换 fetchAiPickTabs 内部实现即可。 -import { - CardData, - PickContractItem, - PickStrategy, - PickTabData, - RawPickTabItem, - RawStrategyDetail, - RawStrategyItem, -} from './AiPickTypes'; -import { getResData, getTableList, ResData } from './AiPickUtils'; -import { MOCK_RAW_TABS } from './AiPickMock'; -import { MAX_ITEMS, MAX_TABS, MAX_TAGS } from './AiPickConstant'; - -/** - * 新版接口地址(isCustomVersion=true):{API_HOST}futgwapi/api/ai_diagnosis/home_strategy/v1/user_data - * 旧版接口地址(isCustomVersion=false):{API_HOST}futgwapi/api/ai_diagnosis/system_strategy/v1/home_data - **/ - -// 模拟网络延迟(毫秒),验证 loading 状态和异步流程用;接入真实接口后删除 -const MOCK_DELAY_MS = 300; - -// 对应 Vue 端 index.vue 中 formatTabData 的单项处理(新版 isCustomVersion=true) -export function parseRawTab(item: RawPickTabItem): PickTabData { - const isSystemPeriod = item.type === 'system_period'; - const strategyItem: RawStrategyItem = ( - isSystemPeriod ? item.system_strategy : item.custom_strategy - ) ?? {}; - - const strategyDetail: RawStrategyDetail = ( - isSystemPeriod - ? strategyItem.system_strategy - : strategyItem.custom_strategy - ) ?? { id: '', strategy_name: '', strategy_type: '' }; - - const resData: ResData = getResData(strategyItem); - const allItems: PickContractItem[] = getTableList(resData.list, resData.resultValueList); - // count 取自 strategyItem 层级(与 detail_list/field_list 同级), - // 对应 Vue 端 strategyItem['detail_count'],不是嵌套的策略详情对象里的字段 - const count = strategyItem.detail_count ?? 0; - - // Tab 标题:策略名 + "(count个)"(isCustomVersion 全局为 true 时,系统策略 Tab 也会拼此后缀) - const title = `${strategyDetail.strategy_name}(${count}个)`; - - // 图片:系统策略有 white_img/black_img,自定义策略为空 - // 保留两个位置(哪怕为空字符串),对应 Vue 端 [white_img, black_img] 不做过滤 - const imgs: string[] = isSystemPeriod - ? [strategyDetail.white_img ?? '', strategyDetail.black_img ?? ''] - : []; - - 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 ?? ['自编']).slice(0, MAX_TAGS), - imgs, - count, - }; - - const tabData: PickTabData = { - title, - strategy, - items: allItems.slice(0, MAX_ITEMS), - keysList: resData.showKeyList, - fieldList: resData.fieldList, - }; - return tabData; -} - -// 把 RawPickTabItem[] 解析成 PickTabData[](上限 MAX_TABS) -export function parseRawTabs(rawTabs: RawPickTabItem[]): PickTabData[] { - return rawTabs.slice(0, MAX_TABS).map(parseRawTab); -} - -// 获取 AI 选期视图数据(对应 Vue 端 getData() + useCardHandler.showData 决策) -// data_completed=true:使用 mod_data[0].data 内嵌数据,不发请求 -// data_completed=false:发起 HTTP 请求(当前走 Mock),并通过 AiPickStorage 写入卡片级缓存 -// TODO: 接入真实接口后,改为调用 @ohos.net.http 请求 cardData.mod_data[0].url,解析 JSON 响应体为 RawPickTabItem[] 后传入 parseRawTabs -export function fetchAiPickTabs(cardData: CardData): Promise { - return new Promise((resolve) => { - setTimeout(() => { - // 第一档:data_completed 内嵌数据 - if (cardData.data_completed) { - const embedded = cardData.mod_data?.[0]?.data; - if (embedded && embedded.length > 0) { - resolve(parseRawTabs(embedded)); - return; - } - } - // 第二档:HTTP 请求结果(当前用 mock 模拟) - resolve(parseRawTabs(MOCK_RAW_TABS)); - }, MOCK_DELAY_MS); - }); -} diff --git a/entry/src/main/ets/ai-pick/AiPickConstant.ets b/entry/src/main/ets/ai-pick/AiPickConstant.ets index 3093074..eafb40f 100644 --- a/entry/src/main/ets/ai-pick/AiPickConstant.ets +++ b/entry/src/main/ets/ai-pick/AiPickConstant.ets @@ -1,4 +1,4 @@ -// AI选期卡片常量配置,供 AiPickModel / AiPickUtils / AiPick 共用 +// AI选期卡片常量配置,供 DataFetcher、Utils 和 NodeComponent 共用。 // 最多展示的策略 Tab 数 export const MAX_TABS = 5; @@ -15,6 +15,23 @@ export const MAX_SHOW_KEYS = 3; // 自定义 DOUBLE 字段最多取前 2 个用于表头 export const MAX_DOUBLE_KEYS = 2; +export const RIGHT_ARROW_LIGHT = + 'https://u.thsi.cn/imgsrc/bbs/8c8290d904cd89250d4e828da5f1010c_300_230.png'; +export const RIGHT_ARROW_DARK = + 'https://u.thsi.cn/imgsrc/bbs/f5ff0a4fcd103d7b3da180ad90d9db23_300_230.png'; +export const TITLE_ARROW_LIGHT = + 'https://s.thsi.cn/staticS3/mobileweb-upload-static-server.img/offline/pkg/e6b471c4-b9a6-4c73-a2e4-7c3176308daf.png'; +export const TITLE_ARROW_DARK = + 'https://s.thsi.cn/staticS3/mobileweb-upload-static-server.img/offline/pkg/f90c3c71-143d-45a2-af8f-d4b4c5d8dd1d.png'; +export const INFO_IMAGE_LIGHT = + 'https://u.thsi.cn/imgsrc/bbs/e1e9b6d81b9f4f6fa817cbeb71e98ff1.png'; +export const INFO_IMAGE_DARK = + 'https://u.thsi.cn/imgsrc/bbs/19f7f20bcf800ec2b8e9fc0fa0d73369.png'; +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'; + // 字段黑名单:过滤 field_list 中不应展示的字段 export const BLACK_LIST: string[] = [ '合约代码', diff --git a/entry/src/main/ets/ai-pick/AiPickDataFetcher.ets b/entry/src/main/ets/ai-pick/AiPickDataFetcher.ets new file mode 100644 index 0000000..0fb4297 --- /dev/null +++ b/entry/src/main/ets/ai-pick/AiPickDataFetcher.ets @@ -0,0 +1,278 @@ +// AI选期数据层:负责网络请求、行情订阅及原始数据到视图数据的转换。 +import http from '@ohos.net.http'; +import allCardsMock from '../common/mock/AllCardsMock.json'; +import { + AiPickQuoteData, + AiPickQuoteRequestParams, + CardData, + PickContractItem, + PickStrategy, + PickTabData, + RawPickTabItem, + RawStrategyDetail, + RawStrategyItem, +} from './AiPickModels'; +import { + AllCardsCard, + AllCardsFloor, + AllCardsModData, + AllCardsResponse, +} from '../common/AllCardsModels'; +import { getResData, getTableList, ResData } from './AiPickUtils'; +import { MOCK_RAW_TABS } from './AiPickMock'; +import { MAX_ITEMS, MAX_TABS, MAX_TAGS } from './AiPickConstant'; + +// 模拟网络延迟(毫秒),验证 loading 状态和异步流程用;接入真实接口后删除 +const MOCK_DELAY_MS = 300; +const MOCK_QUOTE_INTERVAL_MS = 2500; +const EMPTY_RAW_STRATEGY_ITEM: RawStrategyItem = {}; +const EMPTY_RAW_STRATEGY_DETAIL: RawStrategyDetail = { + id: '', + strategy_name: '', + strategy_type: '', +}; + +export type AiPickCardDataListener = (cardData: CardData) => void; + +// 对应 Vue 端 index.vue 中 formatTabData 的单项处理(新版 isCustomVersion=true) +function parseRawTab(item: RawPickTabItem): PickTabData { + const isSystemPeriod = 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); + // count 取自 strategyItem 层级(与 detail_list/field_list 同级), + // 对应 Vue 端 strategyItem['detail_count'],不是嵌套的策略详情对象里的字段 + const count = strategyItem.detail_count ?? 0; + + // 图片:系统策略有 white_img/black_img,自定义策略为空 + // 保留两个位置(哪怕为空字符串),对应 Vue 端 [white_img, black_img] 不做过滤 + const imgs: string[] = isSystemPeriod + ? [strategyDetail.white_img ?? '', strategyDetail.black_img ?? ''] + : []; + + 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 ?? ['自编']).slice(0, MAX_TAGS), + imgs, + count, + }; + + const tabData: PickTabData = { + strategy, + items: allItems.slice(0, MAX_ITEMS), + keysList: resData.showKeyList, + fieldList: resData.fieldList, + }; + return tabData; +} + +// 把 RawPickTabItem[] 解析成 PickTabData[](上限 MAX_TABS) +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; + } + + // 从公共全部卡片 Mock 中取得 AI 选期卡片配置。 + fetchCardConfig(): AllCardsCard | undefined { + const response = allCardsMock as AllCardsResponse; + for (const floor of response.data) { + const card = this.findAiPickCard(floor); + if (card !== undefined) { + return card; + } + } + return undefined; + } + + // 模拟 HTTP 请求并返回最新的卡片数据。 + fetchCardData(): Promise { + const cardConfig = this.fetchCardConfig(); + if (cardConfig === undefined) { + return Promise.resolve([]); + } + const modData: AllCardsModData | undefined = cardConfig.mod_data[0]; + const configuredUrl = modData?.url ?? ''; + if (configuredUrl === '') { + return Promise.resolve([]); + } + const requestUrl = this.buildRequestUrl(configuredUrl, Date.now()); + const requestHeader: Record = { + 'Content-Type': 'application/json', + 'Cookie': '', + }; + const requestOptions: http.HttpRequestOptions = { + method: http.RequestMethod.GET, + header: requestHeader, + connectTimeout: 60000, + readTimeout: 60000, + }; + const httpRequest = http.createHttp(); + console.info( + `[AiPick] request: url=${requestUrl}, options=${JSON.stringify(requestOptions)}`, + ); + // 当前只模拟真实请求构造,不调用 httpRequest.request()。 + httpRequest.destroy(); + return new Promise((resolve: (value: PickTabData[]) => void) => { + setTimeout(() => { + const tabs = parseRawTabs(MOCK_RAW_TABS); + resolve(tabs); + }, MOCK_DELAY_MS); + }); + } + + // 模拟 Vue 的共享 4106 行情订阅:立即推送一次,之后每 2.5 秒整体更新 CardData。 + subscribeQuotes(cardData: CardData, listener: AiPickCardDataListener): () => void { + const requestParams = this.buildQuoteRequestParams(cardData); + console.info(`[AiPick] subscribeQuotes params: ${JSON.stringify(requestParams)}`); + const contracts = this.getUniqueContracts(cardData); + let tick = 0; + const emit = (): void => { + tick += 1; + const quoteData = this.createMockQuoteData(contracts, tick); + listener(this.mergeQuoteData(cardData, quoteData)); + }; + emit(); + const timer = setInterval(emit, MOCK_QUOTE_INTERVAL_MS); + return (): void => { + clearInterval(timer); + }; + } + + // 对齐 Vue buildHqParams,生成 AI 选期共享行情连接的完整请求参数。 + private buildQuoteRequestParams(cardData: CardData): AiPickQuoteRequestParams { + const dataItems: string[] = ['55', '10', '4', '36103', '34818', '6', '7', '66', '34877']; + const requestDic = `sortid=-1\r\n` + + `sortorder=0\r\n` + + `push=1\r\n` + + `dataitem=${dataItems.map((item: string): string => `${item},`).join('')}\r\n` + + `codelist=${this.formatCodeList(cardData)}\r\n` + + `scenario=qht_qihuo_sort\r\n` + + `precision=1\r\n` + + `pushtime=2.5\r\n`; + return { + protocolId: '4106', + pageId: '2201', + onlineId: 'contractHQData', + columnOrder: dataItems.join('|'), + requestDic: requestDic, + }; + } + + private getUniqueContracts(cardData: CardData): PickContractItem[] { + const contracts: PickContractItem[] = []; + const contractKeys: string[] = []; + cardData.tabs.forEach((tab: PickTabData): void => { + tab.items.forEach((item: PickContractItem): void => { + const contractKey = `${item.contract}_${item.market}`; + if (!contractKeys.includes(contractKey)) { + contractKeys.push(contractKey); + contracts.push(item); + } + }); + }); + return contracts; + } + + private formatCodeList(cardData: CardData): string { + const contracts = this.getUniqueContracts(cardData); + const markets: string[] = []; + const codeGroups: string[][] = []; + contracts.forEach((contract: PickContractItem): void => { + const marketIndex = 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(''); + } + + private createMockQuoteData(contracts: PickContractItem[], tick: number): AiPickQuoteData { + const data: AiPickQuoteData = { + code: [], + market: [], + price: [], + priceChg: [], + }; + contracts.forEach((contract: PickContractItem, index: number): void => { + data.code.push(contract.contract); + data.market.push(contract.market); + data.price.push(100 + index * 10 + tick * 0.1); + data.priceChg.push(((tick + index) % 5 - 2) * 0.35); + }); + return data; + } + + private mergeQuoteData(cardData: CardData, quoteData: AiPickQuoteData): CardData { + const tabs = cardData.tabs.map((tab: PickTabData): PickTabData => { + const items = tab.items.map((item: PickContractItem): PickContractItem => { + let quoteIndex = -1; + for (let index = 0; index < quoteData.code.length; index += 1) { + if (quoteData.code[index] === item.contract && quoteData.market[index] === item.market) { + quoteIndex = index; + break; + } + } + const nextItem: PickContractItem = { + contract: item.contract, + market: item.market, + name: item.name, + price: quoteIndex >= 0 ? quoteData.price[quoteIndex] : undefined, + priceChg: quoteIndex >= 0 ? quoteData.priceChg[quoteIndex] : undefined, + 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 buildRequestUrl(configuredUrl: string, timestamp: number): string { + const separator = configuredUrl.includes('?') ? '&' : '?'; + return `${configuredUrl}${separator}_=${timestamp}`; + } + + private findAiPickCard(floor: AllCardsFloor): AllCardsCard | undefined { + return floor.card_list.find( + (card: AllCardsCard): boolean => card.render_key === 'ai-pick', + ); + } +} diff --git a/entry/src/main/ets/ai-pick/AiPickMock.ets b/entry/src/main/ets/ai-pick/AiPickMock.ets index 4cc8882..a21ac5e 100644 --- a/entry/src/main/ets/ai-pick/AiPickMock.ets +++ b/entry/src/main/ets/ai-pick/AiPickMock.ets @@ -1,4 +1,4 @@ -import { RawPickTabItem } from './AiPickTypes'; +import { RawPickTabItem } from './AiPickModels'; // 完全模拟 user_data 接口响应(新版,isCustomVersion=true) // 数组每项对应一个策略 Tab,最多 5 项,这里给 3 项 diff --git a/entry/src/main/ets/ai-pick/AiPickModel.ets b/entry/src/main/ets/ai-pick/AiPickModel.ets deleted file mode 100644 index 648da2e..0000000 --- a/entry/src/main/ets/ai-pick/AiPickModel.ets +++ /dev/null @@ -1,135 +0,0 @@ -import router from '@ohos.router'; -import { CardData, CardsDataCache, PickTabData } from './AiPickTypes'; -import { fetchAiPickTabs, parseRawTabs } from './AiPickApi'; - -// 顶层缓存 key:对应 Vue 端 __GLOBAL__.cardsDataCache(跨组件/跨页面共享) -const APP_STORAGE_CACHE_KEY = 'cardsDataCache'; - -// 空 CardData 占位(仅用于锺型调用场景,实际使用需设 cardData 后重新构造 Model), 后续接入后会删除 -const EMPTY_CARD_DATA: CardData = { - card_id: 0, - card_key: '', - card_title: { value: '' }, - bury_point: '', -}; - -@Observed -export class AiPickModel { - cardData: CardData; - cardKey: string; - - tabs: PickTabData[] = []; - // 数据是否已就绪(对应 Vue 端 dataReady),View 层可据此展示 loading/骨架屏 - dataReady: boolean = false; - // 请求是否失败(对应 Vue 端 dataError) - dataError: boolean = false; - - // 构造时即同步读出 showData,对应 Vue 端 useCardHandler.showData 三态: - // 1. data_completed=true 且 mod_data[0].data 内嵌 → 直接解析内嵌数据(无需请求) - // 2. 否则从 AppStorage 卡片级缓存读出(对应 __GLOBAL__.cardsDataCache) - // 3. 都没有 → 空数组,View 层展示 loading;后续 loadData() 异步拉取 - // cardData 可选:不传则使用空占位,Model 处于“未绑定”状态, - // 需在 View 层 aboutToAppear 后赋值真实的 cardData(参见 AiPick.ets 的 @Watch 逻辑) - constructor(cardData?: CardData) { - this.cardData = cardData ?? EMPTY_CARD_DATA; - this.cardKey = this.cardData.card_key; - this.tabs = this.computeShowData(); - this.dataReady = this.cardData.data_completed === true && this.tabs.length > 0; - } - - // 同步决策:内嵌 > 缓存 > 空 - private computeShowData(): PickTabData[] { - if (this.cardData.data_completed === true) { - const embedded = this.cardData.mod_data?.[0]?.data; - if (embedded && embedded.length > 0) { - return parseRawTabs(embedded); - } - } - const cache = AppStorage.get(APP_STORAGE_CACHE_KEY); - return cache?.[this.cardKey] ?? []; - } - - // 拉取数据: - // - data_completed=true:直接完成,不再发请求 - // - 未绑定 cardData:不动作 - // - 其他:调用 fetchAiPickTabs,请求成功后写入 AppStorage - async loadData(): Promise { - if (this.cardKey === '') { - // 未绑定 cardData,不动作 - return; - } - if (this.cardData.data_completed === true) { - this.dataReady = true; - this.dataError = false; - return; - } - try { - const data = await fetchAiPickTabs(this.cardData); - this.tabs = data; - // 写入 AppStorage:合并式更新(AppStorage 不支持原地改 Map) - const current: CardsDataCache = AppStorage.get(APP_STORAGE_CACHE_KEY) ?? {}; - const next: CardsDataCache = {}; - Object.keys(current).forEach((k: string) => { - next[k] = current[k]; - }); - next[this.cardKey] = data; - AppStorage.setOrCreate(APP_STORAGE_CACHE_KEY, next); - this.dataError = false; - } catch (e) { - // 失败时保留原本的 showData(如有缓存命中) - this.dataError = true; - } finally { - this.dataReady = true; - } - } - - /** - * Vue 版 AI 选期跳转协议备忘: - * - * domain:release 使用 fupage,dev/test 使用 futures-test。 - * Web 页面统一包装为: - * client.html?action=ymtz^ishiddenbar=1^mode=new^webid=2804^url={qUrl}^title=AI选期 - * - * 1. 点击合约: - * client://client.html?action=ymtz^webid=2205^stockcode={contract}^marketid={market} - * 2. 点击策略描述/图片: - * https://{domain}.10jqka.com.cn/ai-web/ai-diagnosis-detail.html - * ?id={strategyId}&type={periodType}&name={encodeURIComponent(name)} - * 3. 点击策略标签: - * https://{domain}.10jqka.com.cn/ai-web/ai-diagnosis-label.html - * ?id={strategyId}&type={periodType}&label={encodeURIComponent(label)} - * 4. 点击“一句话定制策略”: - * https://{domain}.10jqka.com.cn/ai-web/ai-diagnosis-home.html?sync=1 - * 5. 旧版“查看详情”: - * https://{domain}.10jqka.com.cn/ai-web/futures-ai-diagnosis.html - * ?id={strategyId}&name={encodeURIComponent(strategyDetail)} - * 6. 点击卡片标题:使用 cardData.card_url.ios/android;当前 AI 选期 Mock 均为空。 - */ - jumpToDetail(contract: string, market: string): void { - router.pushUrl({ - url: 'pages/Detail', - params: { code: contract, market: market }, - }); - } - - jumpToNewHome(strategyId: string, type: string): void { - router.pushUrl({ - url: 'pages/StrategyDetail', - params: { type: 'home', id: strategyId, periodType: type }, - }); - } - - jumpToDetail2(strategyId: string, periodType: string, name: string): void { - router.pushUrl({ - url: 'pages/StrategyDetail', - params: { type: 'detail', id: strategyId, periodType, name }, - }); - } - - jumpToLabel(strategyId: string, periodType: string, label: string): void { - router.pushUrl({ - url: 'pages/StrategyDetail', - params: { type: 'label', id: strategyId, periodType, label }, - }); - } -} diff --git a/entry/src/main/ets/ai-pick/AiPickTypes.ets b/entry/src/main/ets/ai-pick/AiPickModels.ets similarity index 74% rename from entry/src/main/ets/ai-pick/AiPickTypes.ets rename to entry/src/main/ets/ai-pick/AiPickModels.ets index 3d8195d..ca1186d 100644 --- a/entry/src/main/ets/ai-pick/AiPickTypes.ets +++ b/entry/src/main/ets/ai-pick/AiPickModels.ets @@ -23,8 +23,6 @@ export interface PickFieldItem { // 单个 Tab 的完整数据 export interface PickTabData { - // Tab 标题(策略名,含"(N个)"后缀) - title: string; // 问句信息(用于 desc-text 区域) strategy: PickStrategy; // 合约行列表(最多 3 条) @@ -36,6 +34,26 @@ export interface PickTabData { fieldList: PickFieldItem[]; } +// 当前 AI 选期卡片的完整展示数据;请求完成后整体替换以触发 UI 刷新。 +export interface CardData { + tabs: PickTabData[]; +} + +export interface AiPickQuoteData { + code: string[]; + market: string[]; + price: number[]; + priceChg: number[]; +} + +export interface AiPickQuoteRequestParams { + protocolId: string; + pageId: string; + onlineId: string; + columnOrder: string; + requestDic: string; +} + // 策略摘要(问句 + 标签 + 图片,对应 desc-text.vue) export interface PickStrategy { id: string; @@ -55,15 +73,14 @@ export interface PickStrategy { } // ============ 后端接口原始响应类型 ============ -// 对应接口:futgwapi/api/ai_diagnosis/home_strategy/v1/user_data(新版自定义) -// futgwapi/api/ai_diagnosis/system_strategy/v1/home_data(旧版系统) +// 对应接口:futgwapi/api/ai_diagnosis/home_strategy/v1/user_data export interface RawStrategyDetail { id: string; strategy_name: string; // 新版自定义策略 user_question?: string; - // 旧版系统策略 + // 系统策略 content?: string; strategy_type: string; tag_list?: string[]; @@ -107,32 +124,3 @@ export interface RawPickTabItem { system_strategy?: RawStrategyItem; custom_strategy?: RawStrategyItem; } - -// ============ 第二层:卡片级配置 ============ -// 对应 Vue 端 floorList.card_list 中 ai-pick 那一项,从第一层透传到卡片 -export interface CardTitle { - value: string; -} - -export interface ModDataItem { - url: string; - param_list?: string[]; - req_desc?: string; - // data_completed=true 时,直接把数据内嵌在这里(不再发请求) - data?: RawPickTabItem[]; -} - -export interface CardData { - card_id: number; - card_key: string; - card_title: CardTitle; - // 对应 Vue 端 mod_data[0] - mod_data?: ModDataItem[]; - // 对应 Vue 端 mod_data[0].data:当后端已经在第一层完成数据下发时为 true - data_completed?: boolean; - bury_point: string; -} - -// 对应 Vue 端 __GLOBAL__.cardsDataCache[cardKey], -// second 层缓存,记录上一次请求成功的视图数据 -export type CardsDataCache = Record; diff --git a/entry/src/main/ets/ai-pick/AiPickNodeComponent.ets b/entry/src/main/ets/ai-pick/AiPickNodeComponent.ets new file mode 100644 index 0000000..80d0ad0 --- /dev/null +++ b/entry/src/main/ets/ai-pick/AiPickNodeComponent.ets @@ -0,0 +1,561 @@ +import router from '@ohos.router'; +import { + CardData, + PickContractItem, + PickStrategy, + PickTabData, +} from './AiPickModels'; +import { AllCardsCard } from '../common/AllCardsModels'; +import { AiPickDataFetcher } from './AiPickDataFetcher'; +import { cellColor, cellValue, formatColName } from './AiPickUtils'; +import { + EMPTY_IMAGE_DARK, + EMPTY_IMAGE_LIGHT, + INFO_IMAGE_DARK, + INFO_IMAGE_LIGHT, + RIGHT_ARROW_DARK, + RIGHT_ARROW_LIGHT, + TITLE_ARROW_DARK, + TITLE_ARROW_LIGHT, +} from './AiPickConstant'; + +interface StrategyDescParams { + tabIdx: number; +} + +@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; + private quoteUnsubscribe: (() => void) | undefined; + + private showTabGradient(): boolean { + const maxOffset = this.tabContentWidth - this.tabViewportWidth; + return maxOffset > 0 && this.tabScrollOffset < maxOffset; + } + + aboutToAppear(): void { + this.updateCardConfig(); + setTimeout(() => { + this.updateCardData(); + }, 600); + } + + aboutToDisappear(): void { + this.stopSubscribeQuotes(); + } + + // 只负责从公共 Mock 获取并更新卡片配置。 + private updateCardConfig(): void { + this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig(); + } + + // 只负责请求并更新卡片展示数据。 + private async updateCardData(): Promise { + this.stopSubscribeQuotes(); + this.cardData = { tabs: [] }; + const fetcher = AiPickDataFetcher.getInstance(); + try { + const tabs = await fetcher.fetchCardData(); + this.cardData = { tabs }; + if (tabs.length > 0) { + this.subscribeQuotes(); + } + } catch { + this.stopSubscribeQuotes(); + this.cardData = { tabs: [] }; + } + } + + // 使用当前 CardData 的全部 Tab 合约订阅行情,推送后整体更新 CardData。 + private subscribeQuotes(): void { + this.stopSubscribeQuotes(); + this.quoteUnsubscribe = AiPickDataFetcher.getInstance().subscribeQuotes( + this.cardData, + (cardData: CardData): void => { + this.cardData = cardData; + }, + ); + } + + private stopSubscribeQuotes(): void { + if (this.quoteUnsubscribe !== undefined) { + this.quoteUnsubscribe(); + this.quoteUnsubscribe = undefined; + } + } + + @Builder + CardTitle() { + Row({ space: 4 }) { + Text(this.cardConfig?.card_title.value || $r('app.string.ai_pick_title')) + .fontSize(18) + .fontWeight(FontWeight.Bold) + .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(12) + .height(12) + } + + if ((this.cardConfig?.explain_message ?? '') !== '') { + Image(this.isDarkMode ? INFO_IMAGE_DARK : INFO_IMAGE_LIGHT) + .width(16) + .height(16) + .onClick((event: ClickEvent) => { + this.showExplainSheet = true; + }) + } + } + .width('100%') + .height(48) + .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) => { + Text(tag) + .fontSize(11) + .lineHeight(18) + .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') }) + .onClick(() => { + this.jumpToLabel( + this.cardData.tabs[$$.tabIdx].strategy, + tag, + ); + }) + }, (tag: string) => tag) + } + .width('100%') + } + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + .margin({ bottom: 10 }) + + // 右侧:策略图片(仅系统策略有图,右侧 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%') + .margin({ top: 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(16) + .fontColor($r('app.color.text_grey')) + } + .width('100%') + .height(240) + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + } else { + this.MainContent() + } + } + .width('100%') + .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.Bold : FontWeight.Normal) + .fontColor(this.tabIdx === index + ? $r('app.color.text_blue') + : $r('app.color.text_secondary')) + .constraintSize({ minWidth: 63 }) + .maxLines(1) + .textAlign(TextAlign.Center) + .padding({ top: 6, bottom: 6, left: 12, right: 12 }) + .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(30) + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .align(Alignment.Start) + .onScroll((xOffset: number, _yOffset: number) => { + this.tabScrollOffset = xOffset; + }) + + if (this.showTabGradient()) { + Row() + .width(24) + .height(30) + .linearGradient({ + angle: 90, + colors: [['#00FFFFFF', 0], [$r('app.color.card_bg'), 1]], + }) + .hitTestBehavior(HitTestMode.None) + } + } + .width('100%') + .height(30) + .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 { + // ── 表头 ────────────────────────────────────────────── + Row() { + Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[0])) + .fontSize(12) + .fontColor($r('app.color.text_tertiary')) + .width('35%') + Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[1])) + .fontSize(12) + .fontColor($r('app.color.text_tertiary')) + .layoutWeight(1) + .margin({ left: 8 }) + .textAlign(TextAlign.End) + Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[2])) + .fontSize(12) + .fontColor($r('app.color.text_tertiary')) + .layoutWeight(1) + .margin({ left: 8 }) + .textAlign(TextAlign.End) + } + .width('100%') + .height(24) + .margin({ top: 8 }) + .padding({ bottom: 2 }) + .alignItems(VerticalAlign.Center) + .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) + + // ── 合约列表 ────────────────────────────────────────── + ForEach( + this.cardData.tabs[this.tabIdx].items, + (item: PickContractItem) => { + Row() { + Row({ space: 5 }) { + Text(item.name) + .fontSize(16) + .fontColor($r('app.color.text_primary')) + .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) + .fontFamily('monospace') + .fontColor(cellColor(this.cardData.tabs[this.tabIdx].keysList[1], item)) + .layoutWeight(1) + .margin({ left: 8 }) + .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) + .fontFamily('monospace') + .fontColor(cellColor(this.cardData.tabs[this.tabIdx].keysList[2], item)) + .layoutWeight(1) + .margin({ left: 8 }) + .textAlign(TextAlign.End) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .width('100%') + .height(44) + .alignItems(VerticalAlign.Center) + .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) + .onClick(() => { + this.jumpToDetail(item); + }) + }, + // 行情字段参与 key,确保 price/priceChg 变化后对应列表行重新渲染。 + (item: PickContractItem) => + `${item.contract}_${item.market}_${item.price ?? ''}_${item.priceChg ?? ''}`, + ) + } + + // ── 一句话定制策略 ────────────────────────────────────── + 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%') + .justifyContent(FlexAlign.Center) + .alignItems(VerticalAlign.Center) + .margin({ top: 6 }) + .onClick(() => { + this.jumpToNewHome(); + }) + } + .width('100%') + } + + @Builder + EmptyState(message: ResourceStr) { + Column() { + Image(this.isDarkMode ? EMPTY_IMAGE_DARK : EMPTY_IMAGE_LIGHT) + .width(120) + .height(120) + .margin({ bottom: 8 }) + Text(message) + .fontSize(16) + .fontColor($r('app.color.text_grey')) + } + .width('100%') + .height(240) + .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, + }, + }); + } +} diff --git a/entry/src/main/ets/ai-pick/AiPickUtils.ets b/entry/src/main/ets/ai-pick/AiPickUtils.ets index 3c151c0..98a3a22 100644 --- a/entry/src/main/ets/ai-pick/AiPickUtils.ets +++ b/entry/src/main/ets/ai-pick/AiPickUtils.ets @@ -6,13 +6,13 @@ import { RawFieldItem, RawResultItem, RawStrategyItem, -} from './AiPickTypes'; +} from './AiPickModels'; import { BLACK_LIST, MAX_DOUBLE_KEYS, MAX_SHOW_KEYS } from './AiPickConstant'; import { formatPercent, riseFallColor } from '../common/NumberFormat'; // 对应 filterFieldKey:过滤黑名单 + 期货@前缀黑名单 + 涨跌幅[正则 export function filterFieldKey(key: string): boolean { - const extBlackList = BLACK_LIST.map(v => `期货@${v}`); + const extBlackList = BLACK_LIST.map((value: string): string => `期货@${value}`); const inBlack = BLACK_LIST.includes(key) || extBlackList.includes(key); const matchRegExp = key.startsWith('涨跌幅[') || key.startsWith('期货@涨跌幅['); return !inBlack && !matchRegExp; @@ -65,11 +65,11 @@ export function getResultValueList( resultList: RawResultItem[], doubleListKey: string[], ): Record[] { - return resultList.map(v => { + return resultList.map((value: RawResultItem): Record => { const res: Record = {}; for (const key of doubleListKey) { - if (v[key] !== undefined) { - res[key] = formatBigData(v[key] as number | string); + if (value[key] !== undefined) { + res[key] = formatBigData(value[key] as number | string); } } return res; @@ -94,13 +94,13 @@ export function getResData(strategyItem: RawStrategyItem): ResData { (v: RawFieldItem) => v.type === 'DOUBLE' && filterFieldKey(v.key), ); // 全部 DOUBLE key 用于格式化 resultValueList(对应 Vue 端逻辑) - const doubleListKey: string[] = doubleList.map((v: RawFieldItem) => v.key); + const doubleListKey: string[] = doubleList.map((v: RawFieldItem): string => v.key); // 只取前 2 个给 showKeyList(对应 Vue 端 showKey = doubleListKey.slice(0, 2)) const showKey: string[] = doubleListKey.slice(0, MAX_DOUBLE_KEYS); const showKeyList: string[] = getShowKeyList(showKey); // PickFieldItem 只保留 View 层需要的 key + unit - const fieldList: PickFieldItem[] = doubleList.map((v: RawFieldItem) => { + const fieldList: PickFieldItem[] = doubleList.map((v: RawFieldItem): PickFieldItem => { const fi: PickFieldItem = { key: v.key, unit: v.unit ?? '' }; return fi; }); @@ -116,7 +116,7 @@ export function getTableList( list: RawDetailItem[], resultValueList: Record[], ): PickContractItem[] { - return list.map((item: RawDetailItem, index: number) => { + return list.map((item: RawDetailItem, index: number): PickContractItem => { const extraFields: Record = resultValueList[index] ?? {}; const ci: PickContractItem = { contract: item.contract, @@ -133,12 +133,14 @@ export function getTableList( // 对应 formatName:去除"期货@"前缀和"[...]"括号内容 export function formatColName(name: string): string { - return name.replace(/期货@/g, '').replace(/\[.*?\]/g, ''); + const prefixPattern = new RegExp('期货@', 'g'); + const bracketPattern = new RegExp('\\[.*?\\]', 'g'); + return name.replace(prefixPattern, '').replace(bracketPattern, ''); } // 对应 getUnit:从 fieldList 里按 key 查单位 export function getUnit(key: string, fieldList: PickFieldItem[]): string { - const item = fieldList.find(f => f.key === key); + const item = fieldList.find((field: PickFieldItem): boolean => field.key === key); return item ? item.unit : ''; } diff --git a/entry/src/main/ets/common/AllCardsModels.ets b/entry/src/main/ets/common/AllCardsModels.ets new file mode 100644 index 0000000..59b9288 --- /dev/null +++ b/entry/src/main/ets/common/AllCardsModels.ets @@ -0,0 +1,48 @@ +// 首页全部卡片接口中的标题配置。 +export interface AllCardsCardTitle { + value: string; + type: string; +} + +// 首页全部卡片接口中的平台跳转地址。 +export interface AllCardsCardUrl { + ios: string; + android: string; +} + +// HTTP 类卡片使用的模块请求配置。 +export interface AllCardsModData { + url?: string; + param_list?: string[]; + req_desc?: string; +} + +export interface AllCardsFloor { + card_list: AllCardsCard[]; +} + +export interface AllCardsResponse { + code: number; + msg: string; + data: AllCardsFloor[]; +} + +// 首页全部卡片接口中的单张卡片配置。 +export interface AllCardsCard { + card_id: number; + card_key: string; + title_type: number; + is_toggle: number | null; + render_key: string; + card_size: number; + explain_title: string | null; + explain_message: string | null; + card_title: AllCardsCardTitle; + card_url: AllCardsCardUrl; + background_gradient: Object | null; + data_completed: boolean | null; + mod_data: AllCardsModData[]; + config: Object | null; + bury_point: string; + check_block: number; +} diff --git a/entry/src/main/ets/common/NumberFormat.ets b/entry/src/main/ets/common/NumberFormat.ets index b03724f..339239a 100644 --- a/entry/src/main/ets/common/NumberFormat.ets +++ b/entry/src/main/ets/common/NumberFormat.ets @@ -30,5 +30,14 @@ export function formatGreatNumber(value?: number): string { // 涨红跌绿:按数值正负取色,未处理无效值时的灰色分支 export function riseFallColor(value?: number): Resource { - return (value ?? 0) >= 0 ? $r('app.color.color_rise') : $r('app.color.color_fall'); + if (value === undefined) { + return $r('app.color.text_grey'); + } + if (value > 0) { + return $r('app.color.color_rise'); + } + if (value < 0) { + return $r('app.color.color_fall'); + } + return $r('app.color.text_primary'); } diff --git a/entry/src/main/ets/common/mock/AllCardsMock.json b/entry/src/main/ets/common/mock/AllCardsMock.json new file mode 100644 index 0000000..4f39beb --- /dev/null +++ b/entry/src/main/ets/common/mock/AllCardsMock.json @@ -0,0 +1,1149 @@ +{ + "code": 0, + "msg": "success", + "data": [ + { + "id": 5395, + "key": "futuresHomepageHotMarketFloor", + "title": "热点行情", + "introduce": "期货市场热点行情监测", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1738916878_9913.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1738916885_7954.png" + }, + "layout_type": 2, + "bury_point": "hotMarket", + "show_title": 1, + "is_new": 0, + "check_block": 1, + "rec_info": null, + "card_list": [ + { + "card_id": 5380, + "card_key": "futuresHomepageHotMarketMainCardCn", + "title_type": 1, + "is_toggle": null, + "render_key": "vertical-content", + "card_size": 3, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "热点行情", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": null, + "data_completed": true, + "mod_data": [ + { + "contract": { + "name": "原油2609", + "code": "sc2609", + "market": "65" + }, + "message": { + "message": "海峡关闭!据央视新闻报道,伊朗武装部队哈塔姆安比亚中央总部当地时间22日发布声明称,美方再次威胁要打击伊朗的国家基础设施。继昨晚发出警告后,现再次宣布,霍尔木兹海峡仍处于关闭状态。若有船只确需通过该海峡,必须按照此前公布的指定航线和程序通行。声明说,如果美国将其威胁付诸行动,伊朗武装力量将不允许本地区出口一滴石油,地区内的石油、天然气、电力及经济基础设施都将成为打击目标。声明表示,美国反复发出威胁,只会导致战争在本地区进一步扩大,甚至蔓延至地区之外。", + "url": "https://fupage.10jqka.com.cn/ai-web/anomaly-analysis.html?code=sc2609&marketId=65&varietyName=原油2609&changeRate=--", + "color": "gray" + }, + "contract_tag": { + "message": "热门", + "color": "red" + }, + "message_tag": { + "message": "AI异动分析", + "color": "blue" + } + } + ], + "config": [ + { + "type": "a-rich-text", + "list": [ + { + "type": "contract", + "data_key": "contract" + }, + { + "type": "tag", + "data_key": "contract_tag" + } + ], + "props": null, + "class_name": null + }, + { + "type": "a-rich-text", + "list": [ + { + "type": "tag", + "data_key": "message_tag" + }, + { + "type": "text", + "data_key": "message" + } + ], + "props": { + "lines": 2, + "gap": 4, + "font_size": 14 + }, + "class_name": "mt-20" + } + ], + "bury_point": "hotMarket", + "check_block": 0 + }, + { + "card_id": 5385, + "card_key": "futuresHomepageHotMarketRelateCardRelateOption", + "title_type": 2, + "is_toggle": null, + "render_key": "relative-option", + "card_size": 2, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "关联期权", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": { + "light": "", + "dark": "" + }, + "data_completed": true, + "mod_data": [ + { + "contract": { + "name": "原油2609购650", + "code": "mNz2BB", + "market": "70" + }, + "expire_day": 21 + } + ], + "config": [], + "bury_point": "relateOption", + "check_block": 0 + }, + { + "card_id": 5392, + "card_key": "futuresHomepageHotMarketRelateCardAccountOpen", + "title_type": 2, + "is_toggle": null, + "render_key": "vertical-content", + "card_size": 2, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "期货开户", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": { + "light": "linear-gradient(180deg #ff24360f #ff243605)", + "dark": "linear-gradient(180deg #ff24360f #ff243605)" + }, + "data_completed": true, + "mod_data": [ + { + "main_message": { + "message": "参与模拟,即领188现金红包>>", + "url": "https://mams.10jqka.com.cn/new/server/html/109224.html", + "color": null + }, + "sub_message": { + "message": "去参与→", + "url": "https://mams.10jqka.com.cn/new/server/html/109224.html", + "color": "red" + } + } + ], + "config": [ + { + "type": "a-rich-text", + "list": [ + { + "type": "text", + "data_key": "main_message" + } + ], + "props": { + "lines": 2, + "gap": null, + "font_size": 16 + }, + "class_name": "mb-12" + }, + { + "type": "a-footer-info", + "list": [ + { + "type": "text", + "data_key": "sub_message" + } + ], + "props": null, + "class_name": "mt-auto" + } + ], + "bury_point": "accountOpen", + "check_block": 0 + } + ] + }, + { + "id": 3955, + "key": "futuresHomepageSelfQuoteFloor", + "title": "自选盯盘", + "introduce": "快速掌握您自选的行情变化", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1721649888_4387.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1721649895_9949.png" + }, + "layout_type": 1, + "bury_point": "marketTrac", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 3951, + "card_key": "futuresHomepageSelfQuoteSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "self-quote", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "自选盯盘", + "type": "text" + }, + "card_url": { + "ios": "client.html?action=ymtz^webid=2201", + "android": "client.html?action=ymtz^webid=2201" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [], + "config": null, + "bury_point": "marketTrac", + "check_block": 0 + } + ] + }, + { + "id": 4939, + "key": "futuresHomepageAIViewFloor", + "title": "AI看涨跌", + "introduce": "AI预测合约次日涨跌方向", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1730364478_5138.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1730364493_7994.png" + }, + "layout_type": 1, + "bury_point": "aiView", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 4938, + "card_key": "futuresHomepageAIViewSingleCard", + "title_type": 1, + "is_toggle": 0, + "render_key": "ai-view", + "card_size": 4, + "explain_title": "AI看涨跌说明", + "explain_message": "1、【风险揭示及免责声明】\n 本功能看涨、看跌的观点由AI推理,仅供参考。在任何情况下,本功能中的信息或所表述的意见,不构成对任何人的期货交易咨询建议。过去正确率不代表未来正确率,同花顺不对未来结果做承诺,交易者需自主决策,自行承担交易风险。\n 2、【功能逻辑】\n 通过AI能力,分析活跃的主力合约,AI会推理出具有看涨、看跌方向的合约进行展示。\n 3、【是否正确的判别规则】\n 方向看涨时,实际涨幅>=0%为正确;方向看跌时,实际涨幅<=0%为正确;其余均为错误。\n 4、【实际涨幅】\n 是固定以昨结算价为基准价计算的实时涨跌幅,并非预测的上涨幅度,也不随“涨跌计算基准价设置”功能而变化。\n 5、【正确率】\n (1)【近20日正确率】:有最新涨跌方向的交易日的前20个交易日,“方向正确的合约”占“有方向的合约”的占比。\n (2)【昨日正确率】:有最新涨跌方向的交易日的前一个交易日,“方向正确的合约”占“有方向的合约”的占比。\n (3)【品种正确率】:同一品种,近30次出现涨跌方向的正确率。\n", + "card_title": { + "value": "AI看涨跌", + "type": "text" + }, + "card_url": { + "ios": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/futures-frontend-kamis-renderer/index.0.3.6.html?token=KF9MTExODcB5^title=AI看涨跌", + "android": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/futures-frontend-kamis-renderer/index.0.3.6.html?token=KF9MTExODcB5^title=AI看涨跌" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/om/homepage/v2/component/trend_forecast", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "aiView", + "check_block": 0 + } + ] + }, + { + "id": 3956, + "key": "futuresHomepageHotNewsFloor", + "title": "热点资讯", + "introduce": "期货热门消息实时速递", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1721649875_4642.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1721649868_4635.png" + }, + "layout_type": 1, + "bury_point": "hotNews", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 3948, + "card_key": "futuresHomepageHotNewsSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "hot-list", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "热点资讯", + "type": "text" + }, + "card_url": { + "ios": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/find-page/hot.html^ishiddenbar=1^mode=new", + "android": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/find-page/hot.html^ishiddenbar=1^mode=new" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/domain/news/content/hot_list/v1/list", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "hotNews", + "check_block": 0 + } + ] + }, + { + "id": 4271, + "key": "futuresHomepageWarehouseFloor", + "title": "仓单异动", + "introduce": "期货品种的仓单异动变化", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1723195171_2158.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1723195292_2085.png" + }, + "layout_type": 1, + "bury_point": "warehouse", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 4270, + "card_key": "futuresHomepageWarehouseSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "market-data", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "仓单异动", + "type": "text" + }, + "card_url": { + "ios": "client.html?webid=2804^url=https://fupage.10jqka.com.cn/projects/m/inventory/list.html", + "android": "client.html?webid=2804^url=https://fupage.10jqka.com.cn/projects/m/inventory/list.html" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/om/homepage/v2/component/market_data?type=warehouse", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "warehouse", + "check_block": 0 + } + ] + }, + { + "id": 4269, + "key": "futuresHomepageBasisFloor", + "title": "期现基差", + "introduce": "期货品种的基差数据异动", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1723194418_7442.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1723194422_8558.png" + }, + "layout_type": 1, + "bury_point": "basis", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 4268, + "card_key": "futuresHomepageBasisSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "market-data", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "期现基差", + "type": "text" + }, + "card_url": { + "ios": "client.html?webid=2804^url=https://fupage.10jqka.com.cn/projects/m/basisanalysis/index.html^title=基差数据", + "android": "client.html?webid=2804^url=https://fupage.10jqka.com.cn/projects/m/basisanalysis/index.html^title=基差数据" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/om/homepage/v2/component/market_data?type=basis", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "basis", + "check_block": 0 + } + ] + }, + { + "id": 5521, + "key": "futuresHomepageImportantEventFloor", + "title": "重要事件", + "introduce": "重要事件", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1741166334_9108.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1741166339_2946.png" + }, + "layout_type": 3, + "bury_point": "importantEvent", + "show_title": 1, + "is_new": 0, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 5501, + "card_key": "futuresHomepageImportantEventMainCardCalendar", + "title_type": 1, + "is_toggle": null, + "render_key": "financial-calendar", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "财经日历", + "type": "text" + }, + "card_url": { + "ios": "client.html?webid=2804^ishiddenbar=1^url=https://fupage.10jqka.com.cn/financialcalendar/index.html", + "android": "client.html?webid=2804^ishiddenbar=1^url=https://fupage.10jqka.com.cn/financialcalendar/index.html" + }, + "background_gradient": null, + "data_completed": true, + "mod_data": [ + [ + { + "id": 60099146, + "data_id": 1171953, + "title": "中国至5月29日全国重点地区豆油商业库存(万吨)", + "country": "中国", + "weightiness": 3, + "previous": "103.22", + "public_time": 1780300800, + "type": 1 + }, + { + "id": 60099147, + "data_id": 1171967, + "title": "中国至5月29日全国重点地区棕榈油商业库存(万吨)", + "country": "中国", + "weightiness": 3, + "previous": "76.066", + "public_time": 1780300800, + "type": 1 + }, + { + "id": 60099148, + "data_id": 1171981, + "title": "中国至6月1日进口大豆港口库存(万吨)", + "country": "中国", + "weightiness": 3, + "previous": "860.178", + "public_time": 1780300800, + "type": 1 + } + ] + ], + "config": [], + "bury_point": "calendar", + "check_block": 0 + } + ] + }, + { + "id": 3950, + "key": "futuresHomepageMarketRankFloor", + "title": "市场排名", + "introduce": "关注国内商品市场行情异动机会", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1721649934_6963.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1721649928_4151.png" + }, + "layout_type": 1, + "bury_point": "marketRank", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 3952, + "card_key": "futuresHomepageMarketRankSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "market-ranking", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "市场排名", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [], + "config": null, + "bury_point": "marketRank", + "check_block": 0 + } + ] + }, + { + "id": 3949, + "key": "futuresHomepageCommodityOptionsFloor", + "title": "商品期权", + "introduce": "以小博大捕捉临期期权波动行情", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1721649948_1690.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1721649959_9986.png" + }, + "layout_type": 1, + "bury_point": "options", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 3954, + "card_key": "futuresHomepageCommodityOptionsSingleCard", + "title_type": 1, + "is_toggle": 0, + "render_key": "commodity-options", + "card_size": 4, + "explain_title": "商品期权说明", + "explain_message": "热门:成交量最大的期权合约涨跌幅排行\n 临期期权:到期日10天内的成交量最大的期权合约涨跌幅排行\n 涨跌幅(昨收):以昨收价为基准价计算涨跌幅\n", + "card_title": { + "value": "商品期权", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [], + "config": null, + "bury_point": "options", + "check_block": 0 + } + ] + }, + { + "id": 4520, + "key": "futuresHomepageFedChanceFloor", + "title": "美联储降息机会", + "introduce": "历次降息周期中的资产表现", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1726662193_2814.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1726662197_6627.png" + }, + "layout_type": 1, + "bury_point": "fedInvestOpp", + "show_title": 0, + "is_new": 1, + "check_block": 1, + "rec_info": null, + "card_list": [ + { + "card_id": 4518, + "card_key": "futuresHomepageFedChanceSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "market-data", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "美联储降息机会", + "type": "text" + }, + "card_url": { + "ios": "client.html?action=ymtz^webid=2804^mode=new^ishiddenbar=1^url=https://fupage.10jqka.com.cn/futures-frontend-kamis-renderer/index.0.3.0.html?token=KCENjQwNAB5", + "android": "client.html?action=ymtz^webid=2804^mode=new^ishiddenbar=1^url=https://fupage.10jqka.com.cn/futures-frontend-kamis-renderer/index.0.3.0.html?token=KCENjQwNAB5" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/fedapi/fed_chance/v1/futures_home_page", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "fedInvestOpp", + "check_block": 0 + } + ] + }, + { + "id": 4291, + "key": "futuresHomepageInventoryFloor", + "title": "商品库存", + "introduce": "品种现货的库存变化", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1723703986_3139.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1723703996_3840.png" + }, + "layout_type": 1, + "bury_point": "inventory", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 4290, + "card_key": "futuresHomepageInventorySingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "market-data", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "商品库存", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/om/homepage/v2/component/market_data?type=inventory", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "inventory", + "check_block": 0 + } + ] + }, + { + "id": 6459, + "key": "futuresHomepageOptionAnomalyFloor", + "title": "期权异动", + "introduce": "快人一步精准捕捉异动机会", + "preview": { + "light": "https://i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_18/yyztpic/302bd8c6-4980-4d01-8080-80712b7b9483.png", + "dark": "https://i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_18/yyztpic/f0e44c0c-9384-43f4-9187-bbb20e10dfd4.png" + }, + "layout_type": 1, + "bury_point": "optionAnomaly", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 6457, + "card_key": "futuresHomepageOptionAnomalySingleCard", + "title_type": 1, + "is_toggle": 0, + "render_key": "options-radar", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "期权异动", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/om/homepage/v2/component/option_anomaly", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "optionAnomaly", + "check_block": 0 + } + ] + }, + { + "id": 6458, + "key": "futuresHomepageOptionRankFloor", + "title": "期权榜单", + "introduce": "动态筛选榜单实时追踪行情", + "preview": { + "light": "https://i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_18/yyztpic/9879179d-13f8-4c97-9009-fab59b9b685f.png", + "dark": "https://i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_18/yyztpic/c5d2db41-f019-4024-a278-7b9f8e94916d.png" + }, + "layout_type": 1, + "bury_point": "optionRank", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 6456, + "card_key": "futuresHomepageOptionRankSingleCard", + "title_type": 1, + "is_toggle": 0, + "render_key": "options-radar", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "期权榜单", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/om/homepage/v2/component/option_rank", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "optionRank", + "check_block": 0 + } + ] + }, + { + "id": 3957, + "key": "futuresHomepageMainTrendFloor", + "title": "主力趋势", + "introduce": "跟踪机构主力持仓动向", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1721649813_5174.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1721649839_6586.png" + }, + "layout_type": 1, + "bury_point": "mainTrend", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 3953, + "card_key": "futuresHomepageMainTrendSingleCard", + "title_type": 1, + "is_toggle": 0, + "render_key": "main-trend", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "主力趋势", + "type": "text" + }, + "card_url": { + "ios": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/projects/m/position-analysis/index.html", + "android": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/projects/m/position-analysis/index.html" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [], + "config": null, + "bury_point": "mainTrend", + "check_block": 0 + } + ] + }, + { + "id": 6876, + "key": "futuresHomepageFundamentalAIDiagnosisFloor", + "title": "AI诊期", + "introduce": "基本面趋势AI诊断", + "preview": { + "light": "https://i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_18/yyztpic/64be2ef7-9801-4b1f-b79f-a03deddc3d7b.png", + "dark": "https://i.thsi.cn/staticS3/mobileweb-upload-static-server.img/appid_18/yyztpic/af1d6f2f-29e1-4a75-8385-068172fa4cb5.png" + }, + "layout_type": 1, + "bury_point": "AIDiagnosis", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 6875, + "card_key": "futuresHomepageFundamentalAIDiagnosisCard", + "title_type": 1, + "is_toggle": 0, + "render_key": "fundamental-ai-diagnosis", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "AI诊期", + "type": "text" + }, + "card_url": { + "ios": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/industry-chain/diagnose.html^title=AI品种诊断", + "android": "client.html?action=ymtz^webid=2804^url=https://fupage.10jqka.com.cn/industry-chain/diagnose.html^title=AI品种诊断" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://dq.10jqka.com.cn/fuyao/futures_homepage_comps/f10/v1/ai_explain", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "AIDiagnosis", + "check_block": 0 + } + ] + }, + { + "id": 5105, + "key": "futuresHomepageAIPickFloor", + "title": "AI选期", + "introduce": "AI多条件组合实时选期", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1732878141_2376.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1732878152_9381.png" + }, + "layout_type": 1, + "bury_point": "aiPick", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 5106, + "card_key": "futuresHomepageAIPickCard", + "title_type": 1, + "is_toggle": 0, + "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": "" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/api/ai_diagnosis/home_strategy/v1/user_data", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "aiPick", + "check_block": 0 + } + ] + }, + { + "id": 4202, + "key": "futuresHomepageInstitutionalStrategyFloor", + "title": "机构策略", + "introduce": "不会选品种?来看看机构观点", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1721828556_3492.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1721828547_6904.png" + }, + "layout_type": 1, + "bury_point": "InStrate", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 4200, + "card_key": "futuresHomepageInstitutionalStrategySingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "hot-list", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "机构策略", + "type": "text" + }, + "card_url": { + "ios": "https://news.10jqka.com.cn/tapp/theme/TZ-11357?stat_collect_sign=%7B%22enter_sign%22%3A%22nrgcyeka%22%2C%22activity_sign%22%3A%22null%22%2C%22adid_sign%22%3A%22296385%22%2C%22adlogid_sign%22%3A%22412686%22%7D", + "android": "https://news.10jqka.com.cn/tapp/theme/TZ-11357?stat_collect_sign=%7B%22enter_sign%22%3A%22nrgcyeka%22%2C%22activity_sign%22%3A%22null%22%2C%22adid_sign%22%3A%22296385%22%2C%22adlogid_sign%22%3A%22412686%22%7D" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://fupage.10jqka.com.cn/futgwapi/domain/news/homepage/v1/common_list?cota_id=2940", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "InStrate", + "check_block": 0 + } + ] + }, + { + "id": 4201, + "key": "futuresHomepageVarietyNewsFloor", + "title": "品种资讯", + "introduce": "实时掌握关注品种的最新消息", + "preview": { + "light": "https://s.thsi.cn/js/m/upload/yyzt/1721828523_4267.png", + "dark": "https://s.thsi.cn/js/m/upload/yyzt/1721828506_5952.png" + }, + "layout_type": 1, + "bury_point": "SelfNews", + "show_title": 0, + "is_new": 1, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 4199, + "card_key": "futuresHomepageVartieyNewsSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "hot-list", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "品种资讯", + "type": "text" + }, + "card_url": { + "ios": "https://fupage.10jqka.com.cn/find-page/recommend.html", + "android": "https://fupage.10jqka.com.cn/find-page/recommend.html" + }, + "background_gradient": null, + "data_completed": null, + "mod_data": [ + { + "url": "https://ftapi.10jqka.com.cn/futgwapi/domain/news/homepage/v1/recommend_list", + "param_list": [ + "" + ], + "req_desc": "http_get" + } + ], + "config": null, + "bury_point": "SelfNews", + "check_block": 0 + } + ] + }, + { + "id": 9999, + "key": "futuresHomepageArbitragePairFloor", + "title": "套利对", + "introduce": "套利对监控", + "preview": { + "light": "", + "dark": "" + }, + "layout_type": 1, + "bury_point": "arbitragePair", + "show_title": 0, + "is_new": 0, + "check_block": 0, + "rec_info": null, + "card_list": [ + { + "card_id": 9998, + "card_key": "futuresHomepageArbitragePairSingleCard", + "title_type": 1, + "is_toggle": 1, + "render_key": "arbitrage-pair", + "card_size": 4, + "explain_title": null, + "explain_message": null, + "card_title": { + "value": "套利对", + "type": "text" + }, + "card_url": { + "ios": "", + "android": "" + }, + "background_gradient": null, + "data_completed": true, + "mod_data": [ + [ + { + "key": "hot", + "name": "热门", + "data": [ + { + "arbitrage_type": 3, + "change": 0.28, + "quantile": 21.58, + "name": "豆一2607&2609", + "id": 4, + "ratio_unit": "元/吨", + "ratio_price": 54.17 + }, + { + "arbitrage_type": 1, + "change": 1, + "quantile": 12.39, + "name": "沪金&沪银2608", + "id": 5, + "ratio_unit": "元/克", + "ratio_price": -33 + } + ] + }, + { + "key": "crossVariety", + "name": "跨品种", + "data": [ + { + "arbitrage_type": 1, + "change": 1, + "quantile": 12.39, + "name": "沪金&沪银2608", + "id": 5, + "ratio_unit": "元/克", + "ratio_price": -33 + } + ] + }, + { + "key": "crossPeriod", + "name": "跨期", + "data": [ + { + "arbitrage_type": 3, + "change": 0.28, + "quantile": 21.58, + "name": "豆一2607&2609", + "id": 4, + "ratio_unit": "元/吨", + "ratio_price": 54.17 + } + ] + } + ] + ], + "config": null, + "bury_point": "arbitragePair", + "check_block": 0 + } + ] + } + ] +} diff --git a/entry/src/main/ets/market-ranking/MarketRankingConstant.ets b/entry/src/main/ets/market-ranking/MarketRankingConstant.ets index d245831..2c902af 100644 --- a/entry/src/main/ets/market-ranking/MarketRankingConstant.ets +++ b/entry/src/main/ets/market-ranking/MarketRankingConstant.ets @@ -1,5 +1,14 @@ import { PeriodRange, TabMetric } from './MarketRankingModels'; +export const TITLE_ARROW_LIGHT = + 'https://s.thsi.cn/staticS3/mobileweb-upload-static-server.img/offline/pkg/e6b471c4-b9a6-4c73-a2e4-7c3176308daf.png'; +export const TITLE_ARROW_DARK = + 'https://s.thsi.cn/staticS3/mobileweb-upload-static-server.img/offline/pkg/f90c3c71-143d-45a2-af8f-d4b4c5d8dd1d.png'; +export const INFO_IMAGE_LIGHT = + 'https://u.thsi.cn/imgsrc/bbs/e1e9b6d81b9f4f6fa817cbeb71e98ff1.png'; +export const INFO_IMAGE_DARK = + 'https://u.thsi.cn/imgsrc/bbs/19f7f20bcf800ec2b8e9fc0fa0d73369.png'; + // 一级 Tab 配置:涨幅/跌幅/涨速/跌速/成交额/日增仓,hqId/hqName 用于后续接入行情推送 export const TAB_METRICS: TabMetric[] = [ { id: 'rise', label: $r('app.string.market_ranking_tab_rise'), stat: 'rise', api: 'rise_percent', hqId: '34818', hqName: 'price_chg', index: 0, hasChildren: false }, diff --git a/entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets b/entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets index e83044c..9c3a6c3 100644 --- a/entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets +++ b/entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets @@ -1,5 +1,11 @@ import http from '@ohos.net.http'; import BuildProfile from 'BuildProfile'; +import allCardsMock from '../common/mock/AllCardsMock.json'; +import { + AllCardsCard, + AllCardsFloor, + AllCardsResponse, +} from '../common/AllCardsModels'; import { CardData, MarketRankingQuoteData, @@ -29,6 +35,24 @@ export class MarketRankingDataFetcher { return MarketRankingDataFetcher.instance; } + // 从公共全部卡片 Mock 中取得市场排名卡片配置。 + fetchCardConfig(): AllCardsCard | undefined { + const response = allCardsMock as AllCardsResponse; + for (const floor of response.data) { + const card = this.findMarketRankingCard(floor); + if (card !== undefined) { + return card; + } + } + return undefined; + } + + private findMarketRankingCard(floor: AllCardsFloor): AllCardsCard | undefined { + return floor.card_list.find((card: AllCardsCard): boolean => + card.render_key === 'market-ranking' + ); + } + /** * 市场排名合约列表接口(当前暂用 Mock,后续只替换本方法内部实现): * GET {API_HOST}futgwapi/api/market/homepage/v1/recommend_futures diff --git a/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets b/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets index 0941504..6cefc41 100644 --- a/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets +++ b/entry/src/main/ets/market-ranking/MarketRankingNodeComponent.ets @@ -6,13 +6,22 @@ import { } from './MarketRankingModels'; import { MarketRankingDataFetcher } from './MarketRankingDataFetcher'; import { formatGreatNumber, formatPercent, riseFallColor } from '../common/NumberFormat'; -import { PERIOD_RANGES, TAB_METRICS } from './MarketRankingConstant'; +import { AllCardsCard } from '../common/AllCardsModels'; +import { + INFO_IMAGE_DARK, + INFO_IMAGE_LIGHT, + PERIOD_RANGES, + TAB_METRICS, + TITLE_ARROW_DARK, + TITLE_ARROW_LIGHT, +} from './MarketRankingConstant'; const NORMAL_SIZE = 3; const MAX_SIZE = 5; @Component export struct MarketRankingNodeComponent { + @State cardConfig: AllCardsCard | undefined = undefined; @State cardData: CardData = { tableList: [] }; // Tab 选中/展开状态是纯 UI 交互状态,留在 View 里 @State primaryIdx: number = 0; @@ -21,45 +30,119 @@ export struct MarketRankingNodeComponent { @State tabViewportWidth: number = 0; @State tabContentWidth: number = 0; @State tabScrollOffset: number = 0; + @State showExplainSheet: boolean = false; + @StorageLink('enableDarkMode') isDarkMode: boolean = false; private quoteUnsubscribe: (() => void) | undefined; aboutToAppear(): void { + this.updateCardConfig(); // Mock 网络延迟:模拟真实接口返回前的等待过程,接入真实请求后移除该 setTimeout。 setTimeout(() => { this.updateCardData(this.primaryIdx, this.secondaryIdx); - }, 600) + }, 600); } aboutToDisappear(): void { this.stopSubscribeQuotes(); } - build() { - Column() { - // 标题栏:标题 + 展开/收起胶囊紧挨着,都在左侧 - Row() { - Text($r('app.string.market_ranking_title')) - .fontSize(18) - .fontWeight(FontWeight.Bold) - .fontColor($r('app.color.text_primary')) - Text(this.unfolded - ? $r('app.string.market_ranking_collapse') - : $r('app.string.market_ranking_expand')) - .fontSize(12) - .fontColor($r('app.color.text_tertiary')) - .height(20) - .padding({ left: 8, right: 8 }) - .borderRadius(11) - .backgroundColor($r('app.color.pill_inactive_bg')) - .textAlign(TextAlign.Center) - .margin({ left: 8 }) - .onClick(() => { - this.unfolded = !this.unfolded; + // 只负责从公共 Mock 获取并更新卡片配置。 + private updateCardConfig(): void { + this.cardConfig = MarketRankingDataFetcher.getInstance().fetchCardConfig(); + } + + @Builder + CardTitle() { + Row({ space: 4 }) { + Text(this.cardConfig?.card_title.value || $r('app.string.market_ranking_title')) + .fontSize(18) + .fontWeight(FontWeight.Bold) + .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(12) + .height(12) + } + + if ((this.cardConfig?.explain_message ?? '') !== '') { + Image(this.isDarkMode ? INFO_IMAGE_DARK : INFO_IMAGE_LIGHT) + .width(16) + .height(16) + .onClick((event: ClickEvent) => { + this.showExplainSheet = true; }) } + + Text(this.unfolded + ? $r('app.string.market_ranking_collapse') + : $r('app.string.market_ranking_expand')) + .fontSize(12) + .fontColor($r('app.color.text_tertiary')) + .height(20) + .padding({ left: 8, right: 8 }) + .borderRadius(11) + .backgroundColor($r('app.color.pill_inactive_bg')) + .textAlign(TextAlign.Center) + .margin({ left: 4 }) + .onClick((event: ClickEvent) => { + this.unfolded = !this.unfolded; + }) + } + .width('100%') + .height(48) + .alignItems(VerticalAlign.Center) + .onClick(() => { + this.jumpToCardTitle(); + }) + } + + @Builder + ExplainSheet() { + Column() { + Text(this.cardConfig?.explain_title || + this.cardConfig?.card_title.value || + $r('app.string.market_ranking_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%') - .height(48) - .alignItems(VerticalAlign.Center) + .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 }) + } + + build() { + Column() { + this.CardTitle() // 一级 Tab(胶囊按钮,可横向滚动) Stack({ alignContent: Alignment.End }) { @@ -246,6 +329,19 @@ export struct MarketRankingNodeComponent { } } .width('100%') + .bindSheet(this.showExplainSheet, this.ExplainSheet, { + height: SheetSize.FIT_CONTENT, + dragBar: true, + showClose: false, + maskColor: '#99000000', + onDisappear: () => { + this.showExplainSheet = false; + }, + radius: { + topLeft: 10, + topRight: 10, + }, + }) } // 先请求合约榜单填充列表,再根据当前 CardData 订阅行情。 @@ -309,6 +405,24 @@ export struct MarketRankingNodeComponent { }); } + // 跳转到卡片配置中的标题地址。 + // 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(`[MarketRanking] card title url: ${clientUrl}`); + router.pushUrl({ + url: 'pages/StrategyDetail', + params: { + type: 'home', + clientUrl: clientUrl, + }, + }); + } + // 以下是UI相关 private showTabGradient(): boolean { const maxOffset = this.tabContentWidth - this.tabViewportWidth; diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 402565a..31f7e13 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -1,23 +1,7 @@ import { Card } from '../common/Card'; import { AIView } from '../ai-view/AIView'; import { MarketRankingNodeComponent } from '../market-ranking/MarketRankingNodeComponent'; -import { AiPick } from '../ai-pick/AiPick'; -import { CardData } from '../ai-pick/AiPickTypes'; - -// 模拟 floorList 中 ai-pick 那一项 card_list 配置 -// 对应 src/src/pages/card-preview/mockFloorData.ts 第 950-975 行 -const aiPickCardData: CardData = { - card_id: 5106, - card_key: 'futuresHomepageAIPickCard', - card_title: { value: 'AI选期' }, - bury_point: 'aiPick', - data_completed: false, - mod_data: [{ - url: 'https://ftapi.10jqka.com.cn/futgwapi/api/ai_diagnosis/home_strategy/v1/user_data', - param_list: [''], - req_desc: 'http_get', - }], -}; +import { AiPickNodeComponent } from '../ai-pick/AiPickNodeComponent'; @Entry @Component @@ -32,7 +16,7 @@ struct Index { MarketRankingNodeComponent() } Card() { - AiPick({ cardData: aiPickCardData }) + AiPickNodeComponent() } } .width('100%') diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index 64c665b..5b172b1 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -111,6 +111,30 @@ { "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": "card_explain_confirm", + "value": "我知道了" } ] }