feat(ai-pick): 第二层 data_completed + 缓存两态

This commit is contained in:
clz
2026-07-21 21:27:42 +08:00
parent dc9cb595d0
commit b160360385
6 changed files with 134 additions and 14 deletions
+9 -2
View File
@@ -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 {
+23 -6
View File
@@ -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<PickTabData[]> {
// 把 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<PickTabData[]> {
return new Promise<PickTabData[]>((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);
});
}
+40 -5
View File
@@ -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<void> {
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;
@@ -0,0 +1,20 @@
// AI选期第二层卡片级 session 缓存
// 对应 Vue 端 __GLOBAL__.cardsDataCache[cardKey]
// 鸿蒙侧不需要 Memory/AppStorage 桥接,直接用 Mapsession 级,不持久化
import { PickTabData } from './AiPickTypes';
export class AiPickStorage {
private static cache: Map<string, PickTabData[]> = 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);
}
}
@@ -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<string, PickTabData[]>;
+17 -1
View File
@@ -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%')