refactor(cards): 统一 AI 选期与市场排名卡片架构
- 将 AI Pick 重构为 NodeComponent、DataFetcher、Models 分层结构 - 新增公共 AllCardsCard 模型与卡片配置 Mock 数据 - 分离 cardConfig 与 cardData 的状态及更新职责 - 对齐两个卡片的标题、配置跳转、深色模式和说明 Sheet 设计 - 完善 AI Pick HTTP 请求、行情订阅、跳转参数及接口文档 - 保留 MarketRanking 榜单请求、行情合并和展开收起交互
This commit is contained in:
@@ -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`。
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<PickTabData[]> {
|
||||
return new Promise<PickTabData[]>((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);
|
||||
});
|
||||
}
|
||||
@@ -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[] = [
|
||||
'合约代码',
|
||||
|
||||
@@ -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<PickTabData[]> {
|
||||
const cardConfig = this.fetchCardConfig();
|
||||
if (cardConfig === undefined) {
|
||||
return Promise.resolve<PickTabData[]>([]);
|
||||
}
|
||||
const modData: AllCardsModData | undefined = cardConfig.mod_data[0];
|
||||
const configuredUrl = modData?.url ?? '';
|
||||
if (configuredUrl === '') {
|
||||
return Promise.resolve<PickTabData[]>([]);
|
||||
}
|
||||
const requestUrl = this.buildRequestUrl(configuredUrl, Date.now());
|
||||
const requestHeader: Record<string, string> = {
|
||||
'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<PickTabData[]>((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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RawPickTabItem } from './AiPickTypes';
|
||||
import { RawPickTabItem } from './AiPickModels';
|
||||
|
||||
// 完全模拟 user_data 接口响应(新版,isCustomVersion=true)
|
||||
// 数组每项对应一个策略 Tab,最多 5 项,这里给 3 项
|
||||
|
||||
@@ -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<CardsDataCache>(APP_STORAGE_CACHE_KEY);
|
||||
return cache?.[this.cardKey] ?? [];
|
||||
}
|
||||
|
||||
// 拉取数据:
|
||||
// - data_completed=true:直接完成,不再发请求
|
||||
// - 未绑定 cardData:不动作
|
||||
// - 其他:调用 fetchAiPickTabs,请求成功后写入 AppStorage
|
||||
async loadData(): Promise<void> {
|
||||
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<CardsDataCache>(APP_STORAGE_CACHE_KEY) ?? {};
|
||||
const next: CardsDataCache = {};
|
||||
Object.keys(current).forEach((k: string) => {
|
||||
next[k] = current[k];
|
||||
});
|
||||
next[this.cardKey] = data;
|
||||
AppStorage.setOrCreate<CardsDataCache>(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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
+22
-34
@@ -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<string, PickTabData[]>;
|
||||
@@ -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<void> {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<string, string>[] {
|
||||
return resultList.map(v => {
|
||||
return resultList.map((value: RawResultItem): Record<string, string> => {
|
||||
const res: Record<string, string> = {};
|
||||
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<string, string>[],
|
||||
): PickContractItem[] {
|
||||
return list.map((item: RawDetailItem, index: number) => {
|
||||
return list.map((item: RawDetailItem, index: number): PickContractItem => {
|
||||
const extraFields: Record<string, string> = 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 : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%')
|
||||
|
||||
@@ -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": "我知道了"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user