Compare commits
13
Commits
888f7e316c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dd4c077af | ||
|
|
fe3a2e6cc0 | ||
|
|
adc8a1cfda | ||
|
|
bed2e9a75b | ||
|
|
aacb90d355 | ||
|
|
59999031eb | ||
|
|
d25d18d029 | ||
|
|
6fbdf3150d | ||
|
|
32d066db69 | ||
|
|
e2b98bc5c9 | ||
|
|
34beb5397b | ||
|
|
2a3b09236e | ||
|
|
7f38efa220 |
@@ -32,5 +32,6 @@ HTTP 只负责返回 contracts;最新价、涨跌幅、成交额等动态字
|
||||
|
||||
当前 HTTP 和行情连接均为 Mock。接入真实环境时,主要替换 `MarketRankingDataFetcher`
|
||||
和 `MarketRankingHqRequestClient` 的内部实现,视图层数据结构和生命周期保持不变。
|
||||
真实 HTTP 接入后还应增加请求版本校验或取消机制,避免快速切换 Tab 时较早的响应覆盖当前状态。
|
||||
|
||||
具体 HTTP 与 4106 参数见 [市场排名接口文档](market-ranking-interfaces.md)。
|
||||
|
||||
@@ -27,11 +27,6 @@ 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[] = [
|
||||
'合约代码',
|
||||
|
||||
@@ -25,47 +25,45 @@ export const MOCK_RAW_TABS: RawPickTabItem[] = [
|
||||
{ contract: 'ZN2506', market: '70', contract_name: '沪锌2506', main_contract_type: '1' },
|
||||
],
|
||||
field_list: [
|
||||
{ key: '仓单单日增减量', type: 'DOUBLE', unit: '张' },
|
||||
{ key: '成交量', type: 'DOUBLE', unit: '手' },
|
||||
{ key: '持仓量', type: 'DOUBLE', unit: '手' },
|
||||
{ key: '合约代码', type: 'STRING', unit: '' }, // 黑名单,不展示
|
||||
{ key: '最新涨跌幅', type: 'DOUBLE', unit: '%' }, // 黑名单,不展示
|
||||
],
|
||||
origin_result_list: [
|
||||
{ '成交量': 123000, '持仓量': 456000, '合约代码': 'CU2506', '最新涨跌幅': 1.85 },
|
||||
{ '成交量': 31000, '持仓量': 182000, '合约代码': 'CU2507', '最新涨跌幅': 1.62 },
|
||||
{ '成交量': 57000, '持仓量': 224000, '合约代码': 'NI2506', '最新涨跌幅': 0.94 },
|
||||
{ '成交量': 44000, '持仓量': 198000, '合约代码': 'AL2506', '最新涨跌幅': 0.76 },
|
||||
{ '成交量': 28000, '持仓量': 143000, '合约代码': 'ZN2506', '最新涨跌幅': 0.61 },
|
||||
{ '仓单单日增减量': 1250, '成交量': 123000, '持仓量': 456000, '合约代码': 'CU2506', '最新涨跌幅': 1.85 },
|
||||
{ '仓单单日增减量': -860, '成交量': 31000, '持仓量': 182000, '合约代码': 'CU2507', '最新涨跌幅': 1.62 },
|
||||
{ '仓单单日增减量': 420, '成交量': 57000, '持仓量': 224000, '合约代码': 'NI2506', '最新涨跌幅': 0.94 },
|
||||
{ '仓单单日增减量': -315, '成交量': 44000, '持仓量': 198000, '合约代码': 'AL2506', '最新涨跌幅': 0.76 },
|
||||
{ '仓单单日增减量': 980, '成交量': 28000, '持仓量': 143000, '合约代码': 'ZN2506', '最新涨跌幅': 0.61 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ── Tab 2:系统周期策略(type = 'system_period')──────────────────
|
||||
// ── Tab 2:只有1条数据的系统策略 ──────────────────
|
||||
{
|
||||
type: 'system_period',
|
||||
system_strategy: {
|
||||
system_strategy: {
|
||||
id: 'strat_002',
|
||||
strategy_name: '黑色系超跌反弹',
|
||||
content: '铁矿石、螺纹钢跌幅已达技术支撑位,RSI 超卖信号显现',
|
||||
content: '铁矿石跌幅已达技术支撑位,RSI 超卖信号显现',
|
||||
strategy_type: 'system_period',
|
||||
tag_list: ['反弹', '黑色系', '超卖'],
|
||||
white_img: 'https://s.thsi.cn/js/m/upload/yyzt/1721828556_3492.png',
|
||||
black_img: 'https://s.thsi.cn/js/m/upload/yyzt/1721828547_6904.png',
|
||||
},
|
||||
detail_count: 3,
|
||||
detail_count: 1,
|
||||
detail_list: [
|
||||
{ contract: 'RB2510', market: '70', contract_name: '螺纹钢2510', main_contract_type: '1' },
|
||||
{ contract: 'I2509', market: '70', contract_name: '铁矿石2509', main_contract_type: '1' },
|
||||
{ contract: 'HC2510', market: '70', contract_name: '热卷2510', main_contract_type: '0' },
|
||||
],
|
||||
// 系统策略无自定义 DOUBLE 字段
|
||||
field_list: [],
|
||||
origin_result_list: [],
|
||||
},
|
||||
},
|
||||
|
||||
// ── Tab 3:自定义策略(type = 'custom_strategy')──────────────────
|
||||
// ── Tab 3:只有2条数据的自定义策略 ──────────────────
|
||||
{
|
||||
type: 'custom_strategy',
|
||||
custom_strategy: {
|
||||
@@ -76,12 +74,10 @@ export const MOCK_RAW_TABS: RawPickTabItem[] = [
|
||||
strategy_type: 'custom_strategy',
|
||||
tag_list: ['季节性', '农产品'],
|
||||
},
|
||||
detail_count: 4,
|
||||
detail_count: 2,
|
||||
detail_list: [
|
||||
{ contract: 'M2509', market: '70', contract_name: '豆粕2509', main_contract_type: '1' },
|
||||
{ contract: 'RM2509', market: '70', contract_name: '菜粕2509', main_contract_type: '1' },
|
||||
{ contract: 'Y2509', market: '70', contract_name: '豆油2509', main_contract_type: '1' },
|
||||
{ contract: 'OI2509', market: '70', contract_name: '菜油2509', main_contract_type: '1' },
|
||||
{ contract: 'RM2509', market: '70', contract_name: '菜粕2509', main_contract_type: '0' },
|
||||
],
|
||||
field_list: [
|
||||
{ key: '成交量', type: 'DOUBLE', unit: '手' },
|
||||
@@ -90,8 +86,6 @@ export const MOCK_RAW_TABS: RawPickTabItem[] = [
|
||||
origin_result_list: [
|
||||
{ '成交量': 986000, '持仓量': 3102000 },
|
||||
{ '成交量': 324000, '持仓量': 1258000 },
|
||||
{ '成交量': 189000, '持仓量': 683000 },
|
||||
{ '成交量': 142000, '持仓量': 521000 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
EventListener,
|
||||
} from '../common/EventEmitter';
|
||||
import {
|
||||
EMPTY_IMAGE_DARK,
|
||||
EMPTY_IMAGE_LIGHT,
|
||||
INFO_IMAGE_DARK,
|
||||
INFO_IMAGE_LIGHT,
|
||||
RIGHT_ARROW_DARK,
|
||||
@@ -31,27 +29,36 @@ interface StrategyDescParams {
|
||||
|
||||
@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;
|
||||
// 外部状态
|
||||
@Link @Watch('onRefreshTriggerChange') refreshTrigger: number;
|
||||
@Consume('firstPageVisible') @Watch('onFirstPageVisibleChange')
|
||||
firstPageVisible: boolean = true;
|
||||
@StorageLink('enableDarkMode') isDarkMode: boolean = false;
|
||||
|
||||
// 卡片业务数据
|
||||
@State cardConfig: AllCardsCard | undefined = undefined;
|
||||
@State cardData: CardData = { tabs: [] };
|
||||
|
||||
// Tab 状态及滚动区域
|
||||
@State tabIdx: number = 0;
|
||||
|
||||
private getUrl(resource: Resource): string {
|
||||
return getContext(this).resourceManager.getStringSync(resource);
|
||||
}
|
||||
@State tabViewportWidth: number = 0;
|
||||
@State tabContentWidth: number = 0;
|
||||
@State tabScrollOffset: number = 0;
|
||||
|
||||
// 弹层状态
|
||||
@State showExplainSheet: boolean = false;
|
||||
|
||||
// 行情订阅与事件状态
|
||||
private hqUnsubscribe: (() => void) | undefined;
|
||||
private tcpReconnectListener: EventListener = (): void => {
|
||||
this.onTcpReconnect();
|
||||
};
|
||||
|
||||
private showTabGradient(): boolean {
|
||||
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
|
||||
return maxOffset > 0 && this.tabScrollOffset < maxOffset;
|
||||
}
|
||||
// ==================== 组件生命周期 ====================
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.updateCardConfig();
|
||||
@@ -69,7 +76,9 @@ export struct AiPickNodeComponent {
|
||||
this.stopSubscribeHq();
|
||||
}
|
||||
|
||||
// 只负责从公共 Mock 获取并更新卡片配置。
|
||||
// ==================== 配置与刷新 ====================
|
||||
|
||||
// 从首页统一 Mock 配置中读取 AI Pick 配置。
|
||||
private updateCardConfig(): void {
|
||||
this.cardConfig = AiPickDataFetcher.getInstance().fetchCardConfig();
|
||||
}
|
||||
@@ -79,6 +88,8 @@ export struct AiPickNodeComponent {
|
||||
this.updateCardData();
|
||||
}
|
||||
|
||||
// ==================== 可见性与 TCP 重连 ====================
|
||||
|
||||
// 页面不可见时停止行情;恢复可见后使用已有策略数据重建订阅。
|
||||
onFirstPageVisibleChange(): void {
|
||||
if (!this.firstPageVisible) {
|
||||
@@ -105,7 +116,9 @@ export struct AiPickNodeComponent {
|
||||
this.updateCardData();
|
||||
}
|
||||
|
||||
// 只负责请求并更新卡片展示数据。
|
||||
// ==================== 数据请求与行情订阅 ====================
|
||||
|
||||
// 请求策略和合约数据,成功后重建行情订阅。
|
||||
private async updateCardData(): Promise<void> {
|
||||
this.stopSubscribeHq();
|
||||
this.cardData = { tabs: [] };
|
||||
@@ -140,26 +153,131 @@ export struct AiPickNodeComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 页面跳转 ====================
|
||||
|
||||
// 跳转到分时详情页。
|
||||
// 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: string = this.getUrl($r('app.string.ai_pick_home_url'));
|
||||
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: string = this.getUrl($r('app.string.ai_pick_strategy_detail_url')) +
|
||||
`?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: string = this.getUrl($r('app.string.ai_pick_strategy_label_url')) +
|
||||
`?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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== UI 构建 ====================
|
||||
|
||||
private showTabGradient(): boolean {
|
||||
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
|
||||
return maxOffset > 0 && this.tabScrollOffset < maxOffset;
|
||||
}
|
||||
|
||||
@Builder
|
||||
CardTitle() {
|
||||
Row({ space: 4 }) {
|
||||
Row() {
|
||||
Text(this.cardConfig?.card_title.value || $r('app.string.ai_pick_title'))
|
||||
.fontSize(18)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontWeight(500)
|
||||
.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)
|
||||
.width(20)
|
||||
.height(20)
|
||||
}
|
||||
|
||||
if ((this.cardConfig?.explain_message ?? '') !== '') {
|
||||
Image(this.isDarkMode ? INFO_IMAGE_DARK : INFO_IMAGE_LIGHT)
|
||||
.width(16)
|
||||
.height(16)
|
||||
.margin({ left: 8 })
|
||||
.onClick((event: ClickEvent) => {
|
||||
this.showExplainSheet = true;
|
||||
})
|
||||
@@ -232,16 +350,18 @@ export struct AiPickNodeComponent {
|
||||
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 })
|
||||
Row() {
|
||||
Text(tag)
|
||||
.fontSize(11)
|
||||
.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') })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
this.jumpToLabel(
|
||||
this.cardData.tabs[$$.tabIdx].strategy,
|
||||
@@ -255,7 +375,6 @@ export struct AiPickNodeComponent {
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ bottom: 10 })
|
||||
|
||||
// 右侧:策略图片(仅系统策略有图,右侧 image)
|
||||
// imgs 长度>0 且当前选中位(imgs[0],暂无主题切换)非空才渲染
|
||||
@@ -268,7 +387,8 @@ export struct AiPickNodeComponent {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 10 })
|
||||
.height(86)
|
||||
.padding({ top: 10, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Top)
|
||||
.onClick(() => {
|
||||
this.jumpToStrategyDetail(
|
||||
@@ -285,11 +405,11 @@ export struct AiPickNodeComponent {
|
||||
|
||||
Column() {
|
||||
Text($r('app.string.ai_pick_loading'))
|
||||
.fontSize(16)
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_grey'))
|
||||
}
|
||||
.width('100%')
|
||||
.height(240)
|
||||
.height(321)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
@@ -297,6 +417,9 @@ export struct AiPickNodeComponent {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.borderRadius(4)
|
||||
.padding({ left: 10, right: 10, bottom: 10 })
|
||||
.bindSheet(this.showExplainSheet, this.ExplainSheet, {
|
||||
height: SheetSize.FIT_CONTENT,
|
||||
dragBar: true,
|
||||
@@ -325,14 +448,15 @@ export struct AiPickNodeComponent {
|
||||
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)
|
||||
.fontWeight(this.tabIdx === index ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(this.tabIdx === index
|
||||
? $r('app.color.text_blue')
|
||||
: $r('app.color.text_secondary'))
|
||||
.constraintSize({ minWidth: 63 })
|
||||
.constraintSize({ minWidth: 66 })
|
||||
.maxLines(1)
|
||||
.textAlign(TextAlign.Center)
|
||||
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
|
||||
.height(30)
|
||||
.padding({ left: 8, right: 8 })
|
||||
.borderRadius(4)
|
||||
.backgroundColor(this.tabIdx === index
|
||||
? $r('app.color.pill_active_bg')
|
||||
@@ -348,6 +472,7 @@ export struct AiPickNodeComponent {
|
||||
}
|
||||
.width('100%')
|
||||
.height(30)
|
||||
.padding({ right: 10 })
|
||||
.scrollable(ScrollDirection.Horizontal)
|
||||
.scrollBar(BarState.Off)
|
||||
.align(Alignment.Start)
|
||||
@@ -361,7 +486,8 @@ export struct AiPickNodeComponent {
|
||||
.height(30)
|
||||
.linearGradient({
|
||||
angle: 90,
|
||||
colors: [['#00FFFFFF', 0], [$r('app.color.card_bg'), 1]],
|
||||
colors: [[$r('app.color.tab_gradient_start'), 0],
|
||||
[$r('app.color.card_bg'), 1]],
|
||||
})
|
||||
.hitTestBehavior(HitTestMode.None)
|
||||
}
|
||||
@@ -380,97 +506,117 @@ export struct AiPickNodeComponent {
|
||||
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') })
|
||||
Column() {
|
||||
// ── 表头 ────────────────────────────────────────────
|
||||
Row({ space: 8 }) {
|
||||
Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[0]))
|
||||
.fontSize(14)
|
||||
.maxFontSize(14)
|
||||
.minFontSize(9)
|
||||
.maxLines(1)
|
||||
.heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.width('35%')
|
||||
Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[1]))
|
||||
.fontSize(14)
|
||||
.maxFontSize(14)
|
||||
.minFontSize(9)
|
||||
.maxLines(1)
|
||||
.heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
Text(formatColName(this.cardData.tabs[this.tabIdx].keysList[2]))
|
||||
.fontSize(14)
|
||||
.maxFontSize(14)
|
||||
.minFontSize(9)
|
||||
.maxLines(1)
|
||||
.heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
}
|
||||
.width('100%')
|
||||
.height(40)
|
||||
|
||||
// ── 合约列表 ──────────────────────────────────────────
|
||||
ForEach(
|
||||
this.cardData.tabs[this.tabIdx].items,
|
||||
(item: PickContractItem) => {
|
||||
Row() {
|
||||
Row({ space: 5 }) {
|
||||
Text(item.name)
|
||||
// ── 合约列表 ────────────────────────────────────────
|
||||
ForEach(
|
||||
this.cardData.tabs[this.tabIdx].items,
|
||||
(item: PickContractItem) => {
|
||||
Row({ space: 8 }) {
|
||||
Row({ space: 5 }) {
|
||||
Text(item.name)
|
||||
.fontSize(16)
|
||||
.maxFontSize(16)
|
||||
.minFontSize(9)
|
||||
.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)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.maxFontSize(16)
|
||||
.minFontSize(9)
|
||||
.fontWeight(500)
|
||||
.fontFamily('monospace')
|
||||
.fontColor(cellColor(this.cardData.tabs[this.tabIdx].keysList[1], item))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
Text(cellValue(
|
||||
this.cardData.tabs[this.tabIdx].keysList[2],
|
||||
item,
|
||||
this.cardData.tabs[this.tabIdx].fieldList,
|
||||
))
|
||||
.fontSize(16)
|
||||
.maxFontSize(16)
|
||||
.minFontSize(9)
|
||||
.fontWeight(500)
|
||||
.fontFamily('monospace')
|
||||
.fontColor(cellColor(this.cardData.tabs[this.tabIdx].keysList[2], item))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
.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%')
|
||||
.width('100%')
|
||||
.height(45)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.border({ width: { top: 1 }, color: $r('app.color.divider_color') })
|
||||
.onClick(() => {
|
||||
this.jumpToDetail(item);
|
||||
})
|
||||
},
|
||||
// 行情字段参与 key,确保整体替换 CardData 后列表行同步刷新。
|
||||
(item: PickContractItem) =>
|
||||
`${item.contract}_${item.market}_${item.price ?? ''}_${item.priceChg ?? ''}`,
|
||||
)
|
||||
|
||||
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 })
|
||||
}
|
||||
Divider()
|
||||
.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 ?? ''}`,
|
||||
)
|
||||
.strokeWidth(1)
|
||||
.color($r('app.color.divider_color'))
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
// ── 一句话定制策略 ──────────────────────────────────────
|
||||
@@ -484,13 +630,13 @@ export struct AiPickNodeComponent {
|
||||
.height(14)
|
||||
.margin({ top: 2 })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.margin({ top: 6 })
|
||||
.onClick(() => {
|
||||
this.jumpToNewHome();
|
||||
})
|
||||
.width('100%')
|
||||
.height(30)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
this.jumpToNewHome();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
@@ -498,112 +644,19 @@ export struct AiPickNodeComponent {
|
||||
@Builder
|
||||
EmptyState(message: ResourceStr) {
|
||||
Column() {
|
||||
Image(this.isDarkMode ? EMPTY_IMAGE_DARK : EMPTY_IMAGE_LIGHT)
|
||||
.width(120)
|
||||
.height(120)
|
||||
.margin({ bottom: 8 })
|
||||
Image(this.isDarkMode ?
|
||||
$r('app.string.ai_pick_empty_image_dark_url') :
|
||||
$r('app.string.ai_pick_empty_image_light_url'))
|
||||
.width(100)
|
||||
.height(100)
|
||||
Text(message)
|
||||
.fontSize(16)
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_grey'))
|
||||
}
|
||||
.width('100%')
|
||||
.height(240)
|
||||
.height(261)
|
||||
.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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
|
||||
import { AbilityConstant, Configuration, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
|
||||
import { resourceManager } from '@kit.LocalizationKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
|
||||
export default class EntryAbility extends UIAbility {
|
||||
private updateDarkMode(isDarkMode: boolean): void {
|
||||
AppStorage.setOrCreate<boolean>('enableDarkMode', isDarkMode);
|
||||
}
|
||||
|
||||
private syncDarkModeFromResources(): void {
|
||||
const configuration: resourceManager.Configuration =
|
||||
this.context.resourceManager.getConfigurationSync();
|
||||
this.updateDarkMode(configuration.colorMode === resourceManager.ColorMode.DARK);
|
||||
}
|
||||
|
||||
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
|
||||
try {
|
||||
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
|
||||
} catch (err) {
|
||||
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
|
||||
}
|
||||
this.syncDarkModeFromResources();
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
|
||||
}
|
||||
|
||||
onConfigurationUpdate(newConfig: Configuration): void {
|
||||
if (newConfig.colorMode !== undefined) {
|
||||
this.updateDarkMode(newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
|
||||
}
|
||||
@@ -37,7 +55,7 @@ export default class EntryAbility extends UIAbility {
|
||||
}
|
||||
|
||||
onForeground(): void {
|
||||
// Ability has brought to foreground
|
||||
this.syncDarkModeFromResources();
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
|
||||
}
|
||||
|
||||
@@ -45,4 +63,4 @@ export default class EntryAbility extends UIAbility {
|
||||
// Ability has back to background
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
CardData,
|
||||
RankItem,
|
||||
RecommendFuturesItem,
|
||||
} from './MarketRankingModels';
|
||||
|
||||
export function cardDataToContracts(cardData: CardData): RecommendFuturesItem[] {
|
||||
return cardData.tableList.map((item: RankItem): RecommendFuturesItem => ({
|
||||
contract_code: item.code,
|
||||
contract_name: item.name,
|
||||
market: item.market,
|
||||
}));
|
||||
}
|
||||
|
||||
export function contractsToPlaceholderCardData(contracts: RecommendFuturesItem[]): CardData {
|
||||
const cardData: CardData = {
|
||||
tableList: contracts.map((contract: RecommendFuturesItem): RankItem => ({
|
||||
code: contract.contract_code,
|
||||
market: contract.market,
|
||||
name: contract.contract_name,
|
||||
})),
|
||||
};
|
||||
return cardData;
|
||||
}
|
||||
@@ -202,7 +202,7 @@ export class MarketRankingHqRequestClient {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value !== '') {
|
||||
const result = Number(value);
|
||||
const result = parseFloat(value);
|
||||
return Number.isNaN(result) ? undefined : result;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import router from '@ohos.router';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { preferences } from '@kit.ArkData';
|
||||
import {
|
||||
CardData,
|
||||
PeriodRange,
|
||||
RankItem, RecommendFuturesItem, TabMetric,
|
||||
} from './MarketRankingModels';
|
||||
import { MarketRankingDataFetcher } from './MarketRankingDataFetcher';
|
||||
import {
|
||||
cardDataToContracts,
|
||||
contractsToPlaceholderCardData,
|
||||
} from './MarketRankingDataMapper';
|
||||
import { MarketRankingHqRequestClient } from './MarketRankingHqRequestClient';
|
||||
import { formatGreatNumber, formatPercent, riseFallColor } from '../common/NumberFormat';
|
||||
import { AllCardsCard } from '../common/AllCardsModels';
|
||||
@@ -24,11 +30,16 @@ import {
|
||||
|
||||
const NORMAL_SIZE = 3;
|
||||
const MAX_SIZE = 5;
|
||||
const SP_FILE_NAME_FIRST_PAGE_MARKET_RANKING = 'sp_file_name_first_page_market_ranking';
|
||||
const SP_KEY_FIRST_PAGE_MARKET_RANKING_EXPAND = 'sp_key_first_page_market_ranking_expand';
|
||||
|
||||
@Component
|
||||
export struct MarketRankingNodeComponent {
|
||||
@State cardConfig: AllCardsCard | undefined = undefined;
|
||||
@State cardData: CardData = { tableList: [] };
|
||||
private contracts: RecommendFuturesItem[] = [];
|
||||
private tabDataCache: Map<string, CardData> = new Map<string, CardData>();
|
||||
private dataRequestVersion: number = 0;
|
||||
// Tab 选中/展开状态是纯 UI 交互状态,留在 View 里
|
||||
@State primaryIdx: number = 0;
|
||||
@State secondaryIdx: number = 0;
|
||||
@@ -46,6 +57,7 @@ export struct MarketRankingNodeComponent {
|
||||
};
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.initUnfoldedState();
|
||||
this.updateCardConfig();
|
||||
emitter.on(EVENT_TCP_RECONNECT, this.tcpReconnectListener);
|
||||
if (!this.firstPageVisible) {
|
||||
@@ -58,6 +70,7 @@ export struct MarketRankingNodeComponent {
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.dataRequestVersion += 1;
|
||||
emitter.off(EVENT_TCP_RECONNECT, this.tcpReconnectListener);
|
||||
this.stopSubscribeHq();
|
||||
}
|
||||
@@ -100,50 +113,61 @@ export struct MarketRankingNodeComponent {
|
||||
|
||||
@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)
|
||||
Row() {
|
||||
Row() {
|
||||
Text(this.cardConfig?.card_title.value || $r('app.string.market_ranking_title'))
|
||||
.fontSize(18)
|
||||
.fontWeight(500)
|
||||
.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?.card_url.android ?? '') !== '' ||
|
||||
(this.cardConfig?.card_url.ios ?? '') !== '') {
|
||||
Image(this.isDarkMode ? TITLE_ARROW_DARK : TITLE_ARROW_LIGHT)
|
||||
.width(20)
|
||||
.height(20)
|
||||
}
|
||||
|
||||
if ((this.cardConfig?.explain_message ?? '') !== '') {
|
||||
Image(this.isDarkMode ? INFO_IMAGE_DARK : INFO_IMAGE_LIGHT)
|
||||
.width(16)
|
||||
.height(16)
|
||||
.margin({ left: 8 })
|
||||
.onClick(() => {
|
||||
this.showExplainSheet = true;
|
||||
})
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.height('100%')
|
||||
.onClick(() => {
|
||||
this.jumpToCardTitle();
|
||||
})
|
||||
|
||||
if ((this.cardConfig?.explain_message ?? '') !== '') {
|
||||
Image(this.isDarkMode ? INFO_IMAGE_DARK : INFO_IMAGE_LIGHT)
|
||||
.width(16)
|
||||
Row() {
|
||||
Text(this.unfolded
|
||||
? $r('app.string.market_ranking_collapse')
|
||||
: $r('app.string.market_ranking_expand'))
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.height(16)
|
||||
.onClick((event: ClickEvent) => {
|
||||
this.showExplainSheet = true;
|
||||
})
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Center)
|
||||
}
|
||||
.width(40)
|
||||
.height(20)
|
||||
.borderRadius(10)
|
||||
.backgroundColor($r('app.color.pill_inactive_bg'))
|
||||
.margin({ left: 8 })
|
||||
.onClick(() => {
|
||||
this.toggleUnfolded();
|
||||
})
|
||||
|
||||
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;
|
||||
})
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
this.jumpToCardTitle();
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
@@ -197,18 +221,19 @@ export struct MarketRankingNodeComponent {
|
||||
Row({ space: 8 }) {
|
||||
ForEach(TAB_METRICS, (metric: TabMetric, index: number) => {
|
||||
Text(metric.label)
|
||||
.fontSize(14)
|
||||
.fontWeight(this.primaryIdx === index ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(this.primaryIdx === index
|
||||
? $r('app.color.text_blue')
|
||||
: $r('app.color.text_secondary'))
|
||||
.constraintSize({ minWidth: 63 })
|
||||
.fontSize(14)
|
||||
.fontWeight(this.primaryIdx === index ? FontWeight.Medium : FontWeight.Normal)
|
||||
.padding({ left: 8, right: 8 })
|
||||
.height(30)
|
||||
.constraintSize({ minWidth: 66 })
|
||||
.textAlign(TextAlign.Center)
|
||||
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
|
||||
.borderRadius(4)
|
||||
.backgroundColor(this.primaryIdx === index
|
||||
? $r('app.color.pill_active_bg')
|
||||
: $r('app.color.pill_inactive_bg'))
|
||||
.borderRadius(4)
|
||||
.onClick(() => {
|
||||
this.primaryIdx = index;
|
||||
this.secondaryIdx = 0;
|
||||
@@ -235,7 +260,7 @@ export struct MarketRankingNodeComponent {
|
||||
.height(30)
|
||||
.linearGradient({
|
||||
angle: 90,
|
||||
colors: [['#00FFFFFF', 0], [$r('app.color.card_bg'), 1]],
|
||||
colors: [[$r('app.color.tab_gradient_start'), 0], [$r('app.color.card_bg'), 1]],
|
||||
})
|
||||
.hitTestBehavior(HitTestMode.None)
|
||||
}
|
||||
@@ -259,7 +284,7 @@ export struct MarketRankingNodeComponent {
|
||||
}
|
||||
Text(period.label)
|
||||
.fontSize(12)
|
||||
.fontWeight(this.secondaryIdx === index ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontWeight(this.secondaryIdx === index ? FontWeight.Medium : FontWeight.Normal)
|
||||
.fontColor(this.secondaryIdx === index
|
||||
? $r('app.color.text_primary')
|
||||
: $r('app.color.text_tertiary'))
|
||||
@@ -272,8 +297,7 @@ export struct MarketRankingNodeComponent {
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.End)
|
||||
.padding({ left: 10, right: 10 })
|
||||
.margin({ top: 8, left: -10, right: -10 })
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
|
||||
if (this.cardData.tableList.length === 0) {
|
||||
@@ -284,98 +308,89 @@ export struct MarketRankingNodeComponent {
|
||||
}
|
||||
.width('100%')
|
||||
.height(this.emptyHeight())
|
||||
.margin({ top: 10 })
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
// 表头
|
||||
Row() {
|
||||
Text($r('app.string.market_ranking_contract_name'))
|
||||
.fontSize(12)
|
||||
.lineHeight(21)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.width('35%')
|
||||
Text($r('app.string.market_ranking_latest_price'))
|
||||
.fontSize(12)
|
||||
.lineHeight(21)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.layoutWeight(1)
|
||||
.margin({ left: 8 })
|
||||
.textAlign(TextAlign.End)
|
||||
Text(this.thirdColumnTitle())
|
||||
.fontSize(12)
|
||||
.lineHeight(21)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.layoutWeight(1)
|
||||
.margin({ left: 8 })
|
||||
.textAlign(TextAlign.End)
|
||||
Column() {
|
||||
// 表头
|
||||
Row({ space: 8 }) {
|
||||
Text($r('app.string.market_ranking_contract_name'))
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.width('35%')
|
||||
Text($r('app.string.market_ranking_latest_price'))
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
Text(this.thirdColumnTitle())
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
}
|
||||
.width('100%')
|
||||
.height(40)
|
||||
|
||||
// 表格列表
|
||||
ForEach(
|
||||
this.cardData.tableList.slice(0, this.displaySize()),
|
||||
(item: RankItem, _index: number) => {
|
||||
Row({ space: 8 }) {
|
||||
Text(item.name)
|
||||
.fontSize(16)
|
||||
.maxFontSize(16)
|
||||
.minFontSize(9)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('35%')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(item.price === undefined ? '--' : `${item.price}`)
|
||||
.fontSize(16)
|
||||
.fontWeight(500)
|
||||
.maxFontSize(16)
|
||||
.minFontSize(9)
|
||||
.fontColor(this.priceColor(item))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(this.thirdColumnValue(item))
|
||||
.fontSize(16)
|
||||
.fontWeight(500)
|
||||
.maxFontSize(16)
|
||||
.minFontSize(9)
|
||||
.fontColor(this.thirdColumnColor(item))
|
||||
.layoutWeight(1)
|
||||
.textAlign(TextAlign.End)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
.height(45)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.border({ width: { top: 1 }, color: $r('app.color.divider_color') })
|
||||
.onClick(() => {
|
||||
if (item.market !== undefined) {
|
||||
this.jumpToDetail(item);
|
||||
}
|
||||
})
|
||||
},
|
||||
// 行情更新时字段参与 key,确保整体替换 CardData 后列表行同步刷新。
|
||||
(item: RankItem) =>
|
||||
`${item.code}_${item.market}_${item.price ?? ''}_${item.price_chg ?? ''}_` +
|
||||
`${item.chg_speed ?? ''}_${item.turnover ?? ''}_${item.incre_posit ?? ''}`,
|
||||
)
|
||||
}
|
||||
.width('100%')
|
||||
.height(23)
|
||||
.margin({ top: 10 })
|
||||
.padding({ bottom: 2 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') })
|
||||
|
||||
// 表格列表
|
||||
ForEach(
|
||||
this.cardData.tableList.slice(0, this.displaySize()),
|
||||
(item: RankItem, index: number) => {
|
||||
Row() {
|
||||
Text(item.name)
|
||||
.fontSize(16)
|
||||
.minFontSize(10)
|
||||
.lineHeight(21)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('35%')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(item.price === undefined ? '--' : `${item.price}`)
|
||||
.fontSize(16)
|
||||
.minFontSize(10)
|
||||
.lineHeight(21)
|
||||
.fontFamily('monospace')
|
||||
.fontColor(this.priceColor(item))
|
||||
.layoutWeight(1)
|
||||
.margin({ left: 8 })
|
||||
.textAlign(TextAlign.End)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(this.thirdColumnValue(item))
|
||||
.fontSize(16)
|
||||
.minFontSize(10)
|
||||
.lineHeight(21)
|
||||
.fontFamily('monospace')
|
||||
.fontColor(this.thirdColumnColor(item))
|
||||
.layoutWeight(1)
|
||||
.margin({ left: 8 })
|
||||
.textAlign(TextAlign.End)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
.height(45)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.border(index === Math.min(
|
||||
this.displaySize(),
|
||||
this.cardData.tableList.length,
|
||||
) - 1
|
||||
? undefined
|
||||
: { width: { bottom: 0.5 }, color: $r('app.color.divider_color') })
|
||||
.onClick(() => {
|
||||
if (item.market !== undefined) {
|
||||
this.jumpToDetail(item);
|
||||
}
|
||||
})
|
||||
},
|
||||
// 行情更新时字段参与 key,确保整体替换 CardData 后列表行同步刷新。
|
||||
(item: RankItem) =>
|
||||
`${item.code}_${item.market}_${item.price ?? ''}_${item.price_chg ?? ''}_` +
|
||||
`${item.chg_speed ?? ''}_${item.turnover ?? ''}_${item.incre_posit ?? ''}`,
|
||||
)
|
||||
.height(this.emptyHeight())
|
||||
.animation({ duration: 180, curve: Curve.EaseInOut })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.borderRadius(4)
|
||||
.padding({ left: 10, right: 10, bottom: 10 })
|
||||
.bindSheet(this.showExplainSheet, this.ExplainSheet, {
|
||||
height: SheetSize.FIT_CONTENT,
|
||||
dragBar: true,
|
||||
@@ -393,40 +408,61 @@ export struct MarketRankingNodeComponent {
|
||||
|
||||
// 先请求合约榜单填充列表,再根据当前 CardData 订阅行情。
|
||||
private async updateCardData(primaryIdx: number, secondaryIdx: number): Promise<void> {
|
||||
// 切换 Tab 后立即清理旧列表,避免新请求返回前闪现上一 Tab 的数据。
|
||||
const requestVersion: number = this.dataRequestVersion + 1;
|
||||
this.dataRequestVersion = requestVersion;
|
||||
const cacheKey: string = this.getTabCacheKey(primaryIdx, secondaryIdx);
|
||||
const cachedData: CardData | undefined = this.tabDataCache.get(cacheKey);
|
||||
this.stopSubscribeHq();
|
||||
this.cardData = { tableList: [] };
|
||||
if (cachedData !== undefined) {
|
||||
this.cardData = cachedData;
|
||||
this.contracts = cardDataToContracts(cachedData);
|
||||
if (this.firstPageVisible) {
|
||||
this.subscribeHq(primaryIdx, secondaryIdx);
|
||||
}
|
||||
} else {
|
||||
this.contracts = [];
|
||||
this.cardData = { tableList: [] };
|
||||
}
|
||||
try {
|
||||
const contracts = await MarketRankingDataFetcher
|
||||
.getInstance()
|
||||
.fetchRecommendFutures(primaryIdx, secondaryIdx);
|
||||
this.cardData = {
|
||||
tableList: contracts.map((contract: RecommendFuturesItem): RankItem => ({
|
||||
code: contract.contract_code,
|
||||
market: contract.market,
|
||||
name: contract.contract_name,
|
||||
})),
|
||||
};
|
||||
if (!this.isCurrentDataRequest(requestVersion, primaryIdx, secondaryIdx)) {
|
||||
return;
|
||||
}
|
||||
this.contracts = contracts;
|
||||
if (cachedData === undefined) {
|
||||
this.cardData = contractsToPlaceholderCardData(contracts);
|
||||
}
|
||||
this.tabDataCache.set(cacheKey, this.cardData);
|
||||
if (this.firstPageVisible) {
|
||||
this.subscribeHq(primaryIdx, secondaryIdx);
|
||||
}
|
||||
} catch {
|
||||
this.cardData = { tableList: [] };
|
||||
if (!this.isCurrentDataRequest(requestVersion, primaryIdx, secondaryIdx)) {
|
||||
return;
|
||||
}
|
||||
if (cachedData === undefined) {
|
||||
this.contracts = [];
|
||||
this.cardData = { tableList: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用当前 CardData 中的合约列表订阅行情,推送后整体更新 CardData。
|
||||
// 使用 HTTP 保存的合约列表订阅行情,推送后整体更新当前 Tab 缓存。
|
||||
private subscribeHq(primaryIdx: number, secondaryIdx: number): void {
|
||||
const contracts = this.cardData.tableList.map((item: RankItem): RecommendFuturesItem => ({
|
||||
contract_code: item.code,
|
||||
contract_name: item.name,
|
||||
market: item.market,
|
||||
}));
|
||||
this.stopSubscribeHq();
|
||||
if (this.contracts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hqUnsubscribe = MarketRankingHqRequestClient
|
||||
.getInstance()
|
||||
.subscribeHq(contracts, primaryIdx, secondaryIdx, (cardData: CardData): void => {
|
||||
this.cardData = cardData;
|
||||
.subscribeHq(this.contracts, primaryIdx, secondaryIdx, (cardData: CardData): void => {
|
||||
if (primaryIdx === this.primaryIdx && secondaryIdx === this.secondaryIdx) {
|
||||
this.cardData = cardData;
|
||||
this.tabDataCache.set(this.getTabCacheKey(primaryIdx, secondaryIdx), cardData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -437,6 +473,24 @@ export struct MarketRankingNodeComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private isCurrentDataRequest(
|
||||
requestVersion: number,
|
||||
primaryIdx: number,
|
||||
secondaryIdx: number,
|
||||
): boolean {
|
||||
return requestVersion === this.dataRequestVersion &&
|
||||
primaryIdx === this.primaryIdx &&
|
||||
secondaryIdx === this.secondaryIdx;
|
||||
}
|
||||
|
||||
private getTabCacheKey(primaryIdx: number, secondaryIdx: number): string {
|
||||
const metric: TabMetric = TAB_METRICS[primaryIdx];
|
||||
if (metric.hasChildren) {
|
||||
return `${metric.id}_${PERIOD_RANGES[secondaryIdx].id}`;
|
||||
}
|
||||
return metric.id;
|
||||
}
|
||||
|
||||
// 跳转到分时详情页
|
||||
// todo: 后续跳转为真实的页面
|
||||
private jumpToDetail(item: RankItem): void {
|
||||
@@ -473,6 +527,50 @@ export struct MarketRankingNodeComponent {
|
||||
}
|
||||
|
||||
// 以下是UI相关
|
||||
private toggleUnfolded(): void {
|
||||
this.unfolded = !this.unfolded;
|
||||
try {
|
||||
const store: preferences.Preferences | undefined = this.getPreferenceStore();
|
||||
if (store === undefined) {
|
||||
return;
|
||||
}
|
||||
store.putSync(SP_KEY_FIRST_PAGE_MARKET_RANKING_EXPAND, this.unfolded);
|
||||
store.flush().catch((error: Error) => {
|
||||
console.error(`[MarketRanking] persist unfolded state failed: ${error.message}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[MarketRanking] persist unfolded state failed: ${JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private initUnfoldedState(): void {
|
||||
try {
|
||||
const store: preferences.Preferences | undefined = this.getPreferenceStore();
|
||||
if (store === undefined) {
|
||||
this.unfolded = false;
|
||||
return;
|
||||
}
|
||||
this.unfolded =
|
||||
store.getSync(SP_KEY_FIRST_PAGE_MARKET_RANKING_EXPAND, false) as boolean;
|
||||
} catch (error) {
|
||||
console.error(`[MarketRanking] read unfolded state failed: ${JSON.stringify(error)}`);
|
||||
this.unfolded = false;
|
||||
}
|
||||
}
|
||||
|
||||
private getPreferenceStore(): preferences.Preferences | undefined {
|
||||
try {
|
||||
const context: common.UIAbilityContext =
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext;
|
||||
return preferences.getPreferencesSync(context, {
|
||||
name: SP_FILE_NAME_FIRST_PAGE_MARKET_RANKING,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[MarketRanking] get preference store failed: ${JSON.stringify(error)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private showTabGradient(): boolean {
|
||||
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
|
||||
return maxOffset > 0 && this.tabScrollOffset < maxOffset;
|
||||
@@ -480,7 +578,7 @@ export struct MarketRankingNodeComponent {
|
||||
|
||||
private emptyHeight(): number {
|
||||
const rowHeight = 45;
|
||||
const headerHeight = 24;
|
||||
const headerHeight = 40;
|
||||
return this.displaySize() * rowHeight + headerHeight;
|
||||
}
|
||||
|
||||
@@ -556,9 +654,14 @@ export struct MarketRankingNodeComponent {
|
||||
}
|
||||
|
||||
private priceColor(item: RankItem): Resource {
|
||||
if (item.price === undefined || item.price_chg === undefined) {
|
||||
if (item.price_chg === undefined) {
|
||||
return $r('app.color.text_primary');
|
||||
}
|
||||
return riseFallColor(item.price_chg);
|
||||
if (item.price_chg > 0) {
|
||||
return $r('app.color.color_rise');
|
||||
} else if (item.price_chg < 0) {
|
||||
return $r('app.color.color_fall');
|
||||
}
|
||||
return $r('app.color.text_primary');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
{ "name": "color_rise", "value": "#FF2436" },
|
||||
{ "name": "color_fall", "value": "#07AB4B" },
|
||||
{ "name": "color_orange", "value": "#FF661A" },
|
||||
{ "name": "bg_orange", "value": "#1AFF661A" }
|
||||
{ "name": "bg_orange", "value": "#1AFF661A" },
|
||||
{ "name": "tab_gradient_start", "value": "#00FFFFFF" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "ai_pick_empty_image_light_url",
|
||||
"value": "https://u.thsi.cn/imgsrc/bbs/8eee9cd4fa7e359032de11a3f7c2a70b_300_230.png"
|
||||
},
|
||||
{
|
||||
"name": "ai_pick_empty_image_dark_url",
|
||||
"value": "https://u.thsi.cn/imgsrc/bbs/1c4e679244ed87231befad2b04cddfe2_300_230.png"
|
||||
},
|
||||
{
|
||||
"name": "ai_pick_home_url",
|
||||
"value": "https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-home.html?sync=1"
|
||||
},
|
||||
{
|
||||
"name": "ai_pick_strategy_detail_url",
|
||||
"value": "https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-detail.html"
|
||||
},
|
||||
{
|
||||
"name": "ai_pick_strategy_label_url",
|
||||
"value": "https://fupage.10jqka.com.cn/ai-web/ai-diagnosis-label.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
{ "name": "pill_active_bg", "value": "#1A527DFF" },
|
||||
{ "name": "pill_inactive_bg", "value": "#0FFFFFFF" },
|
||||
{ "name": "color_rise", "value": "#FF2436" },
|
||||
{ "name": "color_fall", "value": "#07AB4B" }
|
||||
{ "name": "color_fall", "value": "#07AB4B" },
|
||||
{ "name": "tab_gradient_start", "value": "#002B2B2B" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user