From b16036038578a29e305696b285d9963977add09d Mon Sep 17 00:00:00 2001 From: clz Date: Tue, 21 Jul 2026 21:27:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai-pick):=20=E7=AC=AC=E4=BA=8C=E5=B1=82=20?= =?UTF-8?q?data=5Fcompleted=20+=20=E7=BC=93=E5=AD=98=E4=B8=A4=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entry/src/main/ets/ai-pick/AiPick.ets | 11 ++++- entry/src/main/ets/ai-pick/AiPickApi.ets | 29 ++++++++++--- entry/src/main/ets/ai-pick/AiPickModel.ets | 45 +++++++++++++++++--- entry/src/main/ets/ai-pick/AiPickStorage.ets | 20 +++++++++ entry/src/main/ets/ai-pick/AiPickTypes.ets | 25 +++++++++++ entry/src/main/ets/pages/Index.ets | 18 +++++++- 6 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 entry/src/main/ets/ai-pick/AiPickStorage.ets diff --git a/entry/src/main/ets/ai-pick/AiPick.ets b/entry/src/main/ets/ai-pick/AiPick.ets index dd1d9a5..fc192e4 100644 --- a/entry/src/main/ets/ai-pick/AiPick.ets +++ b/entry/src/main/ets/ai-pick/AiPick.ets @@ -1,4 +1,4 @@ -import { PickContractItem, PickTabData } from './AiPickTypes'; +import { CardData, PickContractItem, PickTabData } from './AiPickTypes'; import { AiPickModel } from './AiPickModel'; import { cellColor, cellValue, formatColName } from './AiPickUtils'; import { MAX_ITEMS } from './AiPickConstant'; @@ -10,7 +10,14 @@ interface StrategyDescParams { @Component export struct AiPick { - @State vm: AiPickModel = new AiPickModel(); + // 对应 Vue 端 cardData prop:从第一层 floorList 透传下来的单卡片配置 + @Prop cardData: CardData = { + card_id: 0, + card_key: 'placeholder', + card_title: { value: '' }, + bury_point: '', + }; + @State vm: AiPickModel = new AiPickModel(this.cardData); @State tabIdx: number = 0; aboutToAppear(): void { diff --git a/entry/src/main/ets/ai-pick/AiPickApi.ets b/entry/src/main/ets/ai-pick/AiPickApi.ets index 0be5f59..27a918d 100644 --- a/entry/src/main/ets/ai-pick/AiPickApi.ets +++ b/entry/src/main/ets/ai-pick/AiPickApi.ets @@ -2,6 +2,7 @@ // 对应 Vue 端 index.vue 里的 getData()(http() 封装 + 解析流程) // 目前网络请求未接入,直接返回 Mock 数据;接入时替换 fetchAiPickTabs 内部实现即可。 import { + CardData, PickContractItem, PickStrategy, PickTabData, @@ -22,7 +23,7 @@ import { MAX_ITEMS, MAX_TABS, MAX_TAGS } from './AiPickConstant'; const MOCK_DELAY_MS = 300; // 对应 Vue 端 index.vue 中 formatTabData 的单项处理(新版 isCustomVersion=true) -function parseRawTab(item: RawPickTabItem): PickTabData { +export function parseRawTab(item: RawPickTabItem): PickTabData { const isSystemPeriod = item.type === 'system_period'; const strategyItem: RawStrategyItem = ( isSystemPeriod ? item.system_strategy : item.custom_strategy @@ -73,13 +74,29 @@ function parseRawTab(item: RawPickTabItem): PickTabData { return tabData; } -// 获取 AI 选期视图数据(原始网络数据 + 解析为 PickTabData[]) -// TODO: 接入真实接口后,改为调用 @ohos.net.http 请求上述 URL, -// 解析 JSON 响应体为 RawPickTabItem[] 后传入 parseRawTab -export function fetchAiPickTabs(): Promise { +// 把 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(() => { - resolve(MOCK_RAW_TABS.slice(0, MAX_TABS).map(parseRawTab)); + // 第一档: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/AiPickModel.ets b/entry/src/main/ets/ai-pick/AiPickModel.ets index f54f4bd..b14cf5b 100644 --- a/entry/src/main/ets/ai-pick/AiPickModel.ets +++ b/entry/src/main/ets/ai-pick/AiPickModel.ets @@ -1,22 +1,57 @@ import router from '@ohos.router'; -import { PickTabData } from './AiPickTypes'; -import { fetchAiPickTabs } from './AiPickApi'; +import { CardData, PickTabData } from './AiPickTypes'; +import { fetchAiPickTabs, parseRawTabs } from './AiPickApi'; +import { AiPickStorage } from './AiPickStorage'; @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. 否则从卡片级缓存 AiPickStorage 读出(对应 __GLOBAL__.cardsDataCache) + // 3. 都没有 → 空数组,View 层展示 loading;后续 loadData() 异步拉取 + constructor(cardData: CardData) { + this.cardData = cardData; + this.cardKey = 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); + } + } + return AiPickStorage.get(this.cardKey) ?? []; + } + + // 拉取数据: + // - data_completed=true:直接完成,不再发请求 + // - 其他:调用 fetchAiPickTabs,请求成功后写入 AiPickStorage async loadData(): Promise { + if (this.cardData.data_completed === true) { + this.dataReady = true; + this.dataError = false; + return; + } try { - this.tabs = await fetchAiPickTabs(); + const data = await fetchAiPickTabs(this.cardData); + this.tabs = data; + AiPickStorage.set(this.cardKey, data); this.dataError = false; } catch (e) { - this.tabs = []; + // 失败时保留原本的 showData(如有缓存命中) this.dataError = true; } finally { this.dataReady = true; diff --git a/entry/src/main/ets/ai-pick/AiPickStorage.ets b/entry/src/main/ets/ai-pick/AiPickStorage.ets new file mode 100644 index 0000000..7f65bb8 --- /dev/null +++ b/entry/src/main/ets/ai-pick/AiPickStorage.ets @@ -0,0 +1,20 @@ +// AI选期第二层卡片级 session 缓存 +// 对应 Vue 端 __GLOBAL__.cardsDataCache[cardKey] +// 鸿蒙侧不需要 Memory/AppStorage 桥接,直接用 Map;session 级,不持久化 +import { PickTabData } from './AiPickTypes'; + +export class AiPickStorage { + private static cache: Map = new Map(); + + static get(cardKey: string): PickTabData[] | undefined { + return this.cache.get(cardKey); + } + + static set(cardKey: string, data: PickTabData[]): void { + this.cache.set(cardKey, data); + } + + static has(cardKey: string): boolean { + return this.cache.has(cardKey); + } +} diff --git a/entry/src/main/ets/ai-pick/AiPickTypes.ets b/entry/src/main/ets/ai-pick/AiPickTypes.ets index 902f7a1..7be092a 100644 --- a/entry/src/main/ets/ai-pick/AiPickTypes.ets +++ b/entry/src/main/ets/ai-pick/AiPickTypes.ets @@ -107,3 +107,28 @@ export interface RawPickTabItem { system_strategy?: RawStrategyItem; custom_strategy?: RawStrategyItem; } + +// ============ 第二层:卡片级配置 ============ +// 对应 Vue 端 floorList.card_list 中 ai-pick 那一项,从第一层透传到卡片 +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: { value: string }; + // 对应 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/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 4f9a0d8..ab11361 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -2,6 +2,22 @@ import { Card } from '../common/Card'; import { AIView } from '../ai-view/AIView'; import { MarketRanking } from '../market-ranking/MarketRanking'; 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', + }], +}; @Entry @Component @@ -16,7 +32,7 @@ struct Index { MarketRanking() } Card() { - AiPick() + AiPick({ cardData: aiPickCardData }) } } .width('100%')