Initial commit: @b2c/first_page HarmonyOS stage-mode HAR
## Repository - HarmonyOS stage-mode HAR module, consumed by host app - No standalone build — hvigorw, oh_modules, and ../biz_quote absent here ## Architecture ### Feed Cards - FirstPage.createNodeView dispatches cards by FirstPageConstant.KEY_* - Card config from first_page_cards_config.json loaded by FirstPageCardsConfigLoader - Public entrypoint: Index.ets ### Market Ranking (src/main/ets/market-ranking/) - HTTP: MarketRankingDataFetcher fetches recommend_futures contracts - HQ: MarketRankingHqRequestClient subscribes via 4106/frame 2201/scenario qht_qihuo_sort - TCP reconnect rebuilds subscription from original HTTP contract list - Async callbacks guarded by request/subscription versions ### Other Cards - HotList, CommodityOptions, AiDiagnosis, AiRiseFall, SelfSelectedStock, CustomEntryList, FourEntryList - Each has DataFetcher + HQ request client + NodeComponent ## Key Changes (from CardHarmonyOS reference) - Added MetricId, PeriodId, SortOrder enums (MarketRankingModels.ets) - Replaced string comparisons with enum switches in thirdColumnTitle/Value/Color - Removed redundant undefined guards in thirdColumnColor/priceColor, delegated to riseFallColor - Inlined priceColor(), use riseFallColor directly - Tab bar Stack.height(30), Alignment.End (reverted from 46/TopEnd) - Name column fontWeight removed (default regular)
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { SortItem, TableRequestClient, TableConstants } from '@b2c/lib_baseui';
|
||||
import { HeaderItem, TableData } from '@b2b/hq_table';
|
||||
import { StandardPriceTypeHelper } from 'biz_quote';
|
||||
import { RequestHelper } from '@kernel/lib_communication';
|
||||
|
||||
/**
|
||||
* AI诊断卡片行情请求客户端
|
||||
* 参考 FirstPageSelfStockRequestClient 实现,支持动态 computemode 和基准价
|
||||
*/
|
||||
export class AiDiagnosisRequestClient extends TableRequestClient {
|
||||
// 合约代码
|
||||
private code: string = ''
|
||||
// 市场代码
|
||||
private market: string = ''
|
||||
// 扩展字段ID列表(用于基准价计算)
|
||||
private extFieldIds: number[] = []
|
||||
|
||||
constructor(frameId: number, pageId: number, code: string, market: string,
|
||||
headerConfig: Array<HeaderItem>, sortItem?: SortItem | null,
|
||||
dataReceiveListener?: (tableData: TableData | string) => void) {
|
||||
super(frameId, pageId, headerConfig, sortItem, dataReceiveListener, undefined)
|
||||
|
||||
this.code = code
|
||||
this.market = market
|
||||
|
||||
// 设置订阅字段 ID(包含扩展字段用于基准价计算)
|
||||
let ids: number[] = []
|
||||
headerConfig.forEach((item: HeaderItem) => {
|
||||
this.addId(ids, item.mobileid)
|
||||
})
|
||||
// 添加基准价计算所需的扩展字段
|
||||
this.extFieldIds = [
|
||||
TableConstants.DATA_ID_CODE_4,
|
||||
TableConstants.DATA_ID_PRICE,
|
||||
TableConstants.DATA_ID_ZF,
|
||||
TableConstants.DATA_ID_ZD,
|
||||
TableConstants.DATA_ID_MARKET,
|
||||
TableConstants.DATA_ID_MARKET_OLD,
|
||||
StandardPriceTypeHelper.FIELD_ZUO_SHOU,
|
||||
StandardPriceTypeHelper.FIELD_JIN_KAI,
|
||||
StandardPriceTypeHelper.FIELD_ZUO_JIE
|
||||
]
|
||||
this.extFieldIds.forEach((id: number) => {
|
||||
this.addId(ids, id)
|
||||
})
|
||||
this.setSubscribeIds(ids)
|
||||
}
|
||||
|
||||
private addId(ids: number[], id: number): void {
|
||||
if (!ids.includes(id)) {
|
||||
ids.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写请求参数构建
|
||||
* 与 hqHelper.js buildHqParams 保持一致的参数格式
|
||||
*/
|
||||
protected createRequestParam(): string {
|
||||
let param = "\r\nsortid=-1"
|
||||
param += "\r\nsortorder=0"
|
||||
param += "\r\npush=1"
|
||||
param += "\r\n" + this.createHeaderParam()
|
||||
param += this.createStockListParam() // 合约列表参数(format4106Data 格式)
|
||||
param += "\r\nscenario=qht_qihuo_sort"
|
||||
param += "\r\nprecision=1"
|
||||
param += "\r\npushtime=2.5"
|
||||
return param
|
||||
}
|
||||
|
||||
protected doRequest() {
|
||||
let requestParam = this.createRequestParam()
|
||||
RequestHelper.getHqManager().addRequestStructToBuff(this.frameId, this.pageId, this.getInstanceId(), requestParam)
|
||||
RequestHelper.build().base(this.frameId, this.pageId, this, requestParam).request();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建合约列表参数
|
||||
* 与 hqHelper.js format4106Data 保持一致格式: market1(code1,code2,);market2(code3,);
|
||||
*/
|
||||
private createStockListParam(): string {
|
||||
if (!this.code || !this.market) {
|
||||
return ''
|
||||
}
|
||||
// 使用 format4106Data 格式
|
||||
return "\r\ncodelist=" + this.market + "(" + this.code + ",);"
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 dataitem 参数
|
||||
* 与 hqHelper.js dataitem 保持一致格式:字段ID用逗号分隔
|
||||
*/
|
||||
private createHeaderParam(): string {
|
||||
let param = "dataitem="
|
||||
|
||||
this.headerConfig.forEach((item: HeaderItem) => {
|
||||
param += item.mobileid + ","
|
||||
if (item.extDataIds) {
|
||||
item.extDataIds.forEach((id: number) => {
|
||||
param += id + ","
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return param
|
||||
}
|
||||
|
||||
release(isDestory: boolean = false): void {
|
||||
super.release(isDestory)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { TableConstants, TableRequestClient } from '@b2c/lib_baseui';
|
||||
import { AbsRowData, HeaderItem, RowData, TableData } from '@b2b/hq_table';
|
||||
import { TableConstants as HXTableConstants } from '@b2c/lib_baseui/src/main/ets/constant/ConstantsTable';
|
||||
import { HXLog } from 'biz_common';
|
||||
import { StandardPriceTypeHelper, applyStandardPriceToTableData } from 'biz_quote';
|
||||
import { RequestHelper, StuffTableStruct } from '@kernel/lib_communication';
|
||||
|
||||
/**
|
||||
* AI看涨跌卡片行情请求客户端
|
||||
* 参考 FirstPageSelfStockRequestClient 实现,支持动态 computemode 和基准价
|
||||
*/
|
||||
export class AiRiseFallHqRequestClient extends TableRequestClient {
|
||||
// 行情数据回调
|
||||
private hqDataCallback?: (hqs: ContractHqItem[]) => void
|
||||
// 合约代码列表
|
||||
private codeList: string[] = []
|
||||
// 市场代码列表
|
||||
private marketList: string[] = []
|
||||
// 扩展字段ID列表(用于基准价计算)
|
||||
private extFieldIds: number[] = []
|
||||
|
||||
constructor(frameId: number, pageId: number, headerConfig: Array<HeaderItem>,
|
||||
dataReceiveListener?: (tableData: TableData | string) => void) {
|
||||
super(frameId, pageId, headerConfig, null, dataReceiveListener, undefined)
|
||||
this.initSubscribeIds(headerConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化订阅字段ID(包含扩展字段用于基准价计算)
|
||||
*/
|
||||
private initSubscribeIds(headerConfig: Array<HeaderItem>): void {
|
||||
let ids: number[] = []
|
||||
headerConfig.forEach((item: HeaderItem) => {
|
||||
this.addId(ids, item.mobileid)
|
||||
})
|
||||
// 添加基准价计算所需的扩展字段
|
||||
this.extFieldIds = [
|
||||
TableConstants.DATA_ID_CODE_4,
|
||||
TableConstants.DATA_ID_PRICE,
|
||||
TableConstants.DATA_ID_ZF,
|
||||
TableConstants.DATA_ID_ZD,
|
||||
TableConstants.DATA_ID_MARKET,
|
||||
TableConstants.DATA_ID_MARKET_OLD,
|
||||
StandardPriceTypeHelper.FIELD_ZUO_SHOU,
|
||||
StandardPriceTypeHelper.FIELD_JIN_KAI,
|
||||
StandardPriceTypeHelper.FIELD_ZUO_JIE
|
||||
]
|
||||
this.extFieldIds.forEach((id: number) => {
|
||||
this.addId(ids, id)
|
||||
})
|
||||
this.setSubscribeIds(ids)
|
||||
}
|
||||
|
||||
private addId(ids: number[], id: number): void {
|
||||
if (!ids.includes(id)) {
|
||||
ids.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置合约列表并请求数据
|
||||
* @param codes 合约代码数组
|
||||
* @param markets 市场代码数组
|
||||
*/
|
||||
setStockListAndRequest(codes: string[], markets: string[]): void {
|
||||
if (codes.length !== markets.length) {
|
||||
HXLog.e('AiRiseFallHqRequestClient', 'codes and markets length mismatch')
|
||||
return
|
||||
}
|
||||
this.codeList = codes
|
||||
this.marketList = markets
|
||||
this.request(0, codes.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置行情数据回调
|
||||
*/
|
||||
setHqDataCallback(callback: (hqs: ContractHqItem[]) => void): void {
|
||||
this.hqDataCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写 release 方法
|
||||
*/
|
||||
release(isDestory: boolean = false): void {
|
||||
super.release(isDestory)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写请求参数构建
|
||||
* 与 hqHelper.js buildHqParams 保持一致的参数格式
|
||||
*/
|
||||
protected createRequestParam(): string {
|
||||
let param = "\r\nsortid=-1"
|
||||
param += "\r\nsortorder=0"
|
||||
param += "\r\npush=1"
|
||||
param += "\r\n" + this.createHeaderParam()
|
||||
param += this.createStockListParam() // 合约列表参数(format4106Data 格式)
|
||||
param += "\r\nscenario=qht_qihuo_sort"
|
||||
param += "\r\nprecision=1"
|
||||
param += "\r\npushtime=2.5"
|
||||
return param
|
||||
}
|
||||
|
||||
protected doRequest() {
|
||||
let requestParam = this.createRequestParam()
|
||||
RequestHelper.getHqManager().addRequestStructToBuff(this.frameId, this.pageId, this.getInstanceId(), requestParam)
|
||||
RequestHelper.build().base(this.frameId, this.pageId, this, requestParam).request();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建合约列表参数
|
||||
* 与 hqHelper.js format4106Data 保持一致格式: market1(code1,code2,);market2(code3,);
|
||||
*/
|
||||
private createStockListParam(): string {
|
||||
if (this.codeList.length === 0 || this.marketList.length === 0) {
|
||||
return ''
|
||||
}
|
||||
// 按市场分组,生成 format4106Data 格式
|
||||
const marketMap: Map<string, string[]> = new Map()
|
||||
for (let i = 0; i < this.codeList.length; i++) {
|
||||
const market = this.marketList[i]
|
||||
const code = this.codeList[i]
|
||||
if (marketMap.has(market)) {
|
||||
marketMap.get(market)!.push(code + ',')
|
||||
} else {
|
||||
marketMap.set(market, [code + ','])
|
||||
}
|
||||
}
|
||||
let codelist = ''
|
||||
marketMap.forEach((codes, market) => {
|
||||
codelist += market + '(' + codes.join('') + ');'
|
||||
})
|
||||
return "\r\ncodelist=" + codelist
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建表头参数
|
||||
*/
|
||||
private createHeaderParam(): string {
|
||||
let param = "dataitem="
|
||||
let addedIds: number[] = []
|
||||
|
||||
this.headerConfig.forEach((item: HeaderItem) => {
|
||||
param += item.mobileid + ","
|
||||
addedIds.push(item.mobileid)
|
||||
if (item.extDataIds) {
|
||||
item.extDataIds.forEach((id: number) => {
|
||||
param += id + ","
|
||||
addedIds.push(id)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return param
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据解析完成回调
|
||||
*/
|
||||
protected afterParserModel(tableData: TableData, structData: StuffTableStruct): void {
|
||||
// 先按当前基准价类型补齐 originalData 并重算 ZF/ZD(首次与实时推送都会走到这里),
|
||||
// 保证下面读取的 priceChg 是按用户所选基准价计算的口径。
|
||||
applyStandardPriceToTableData(tableData, structData, this.extFieldIds)
|
||||
|
||||
// 解析行情数据
|
||||
const hqs: ContractHqItem[] = []
|
||||
if (tableData.tableRows) {
|
||||
tableData.tableRows.forEach((row: AbsRowData) => {
|
||||
if (row instanceof RowData) {
|
||||
const data = row.originalData
|
||||
const code = data.get(HXTableConstants.DATA_ID_CODE_4)
|
||||
const market = data.get(HXTableConstants.DATA_ID_MARKET)
|
||||
const name = data.get(HXTableConstants.DATA_ID_NAME)
|
||||
const price = data.get(HXTableConstants.DATA_ID_PRICE)
|
||||
const priceChg = data.get(HXTableConstants.DATA_ID_ZF) // 涨跌幅字段
|
||||
|
||||
if (typeof code === 'string' && typeof market === 'string') {
|
||||
let priceChgValue: number | null = null
|
||||
if (priceChg && typeof priceChg === 'string' && priceChg !== '--') {
|
||||
priceChgValue = parseFloat(priceChg)
|
||||
}
|
||||
|
||||
hqs.push({
|
||||
code: code,
|
||||
market: market,
|
||||
name: typeof name === 'string' ? name : '',
|
||||
price: typeof price === 'string' ? price : '--',
|
||||
priceChg: priceChgValue
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 回调通知
|
||||
if (this.hqDataCallback && hqs.length > 0) {
|
||||
this.hqDataCallback(hqs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合约行情数据项
|
||||
*/
|
||||
export interface ContractHqItem {
|
||||
code: string
|
||||
market: string
|
||||
name: string
|
||||
price: string
|
||||
priceChg: number | null
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { TableConstants, TableRequestClient } from '@b2c/lib_baseui';
|
||||
import { AbsRowData, HeaderItem, RowData, TableData } from '@b2b/hq_table';
|
||||
import { HXLog } from 'biz_common';
|
||||
import { RequestHelper } from '@kernel/lib_communication';
|
||||
/**
|
||||
* 商品期权卡片行情请求客户端
|
||||
* 参考 AiRiseFallHqRequestClient 实现,使用 codelist 格式
|
||||
*
|
||||
* Author: YS
|
||||
* Date: 2026-06-03
|
||||
*/
|
||||
export class CommodityOptionsHqRequestClient extends TableRequestClient {
|
||||
private codeList: string[] = []
|
||||
private marketList: string[] = []
|
||||
private hqDataCallback?: (hqs: CommodityOptionHqItem[]) => void
|
||||
|
||||
constructor(frameId: number, pageId: number, headerConfig: Array<HeaderItem>,
|
||||
dataReceiveListener?: (tableData: TableData | string) => void) {
|
||||
super(frameId, pageId, headerConfig, null, dataReceiveListener, undefined)
|
||||
this.initSubscribeIds(headerConfig)
|
||||
}
|
||||
|
||||
private initSubscribeIds(headerConfig: Array<HeaderItem>): void {
|
||||
let ids: number[] = []
|
||||
headerConfig.forEach((item: HeaderItem) => {
|
||||
if (!ids.includes(item.mobileid)) {
|
||||
ids.push(item.mobileid)
|
||||
}
|
||||
})
|
||||
// 订阅行情所需字段
|
||||
const extFieldIds = [
|
||||
TableConstants.DATA_ID_CODE_4,
|
||||
TableConstants.DATA_ID_PRICE,
|
||||
TableConstants.DATA_ID_MARKET,
|
||||
TableConstants.DATA_ID_ZF_YTD,
|
||||
]
|
||||
extFieldIds.forEach((id: number) => {
|
||||
if (!ids.includes(id)) {
|
||||
ids.push(id)
|
||||
}
|
||||
})
|
||||
this.setSubscribeIds(ids)
|
||||
}
|
||||
|
||||
setStockListAndRequest(codes: string[], markets: string[]): void {
|
||||
if (codes.length !== markets.length) {
|
||||
HXLog.e('CommodityOptionsHqRequestClient', 'codes and markets length mismatch')
|
||||
return
|
||||
}
|
||||
this.codeList = codes
|
||||
this.marketList = markets
|
||||
this.request(0, codes.length)
|
||||
}
|
||||
|
||||
setHqDataCallback(callback: (hqs: CommodityOptionHqItem[]) => void): void {
|
||||
this.hqDataCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写请求参数构建
|
||||
*/
|
||||
protected createRequestParam(): string {
|
||||
let param = "\r\nsortid=" + TableConstants.DATA_ID_ZF_YTD
|
||||
param += "\r\nsortorder=0"
|
||||
param += "\r\npush=1"
|
||||
param += "\r\n" + this.createHeaderParam()
|
||||
param += this.createStockListParam()
|
||||
param += "\r\nscenario=qht_qiquan_sort"
|
||||
param += "\r\nprecision=1"
|
||||
param += "\r\npushtime=2.5"
|
||||
return param
|
||||
}
|
||||
|
||||
protected doRequest() {
|
||||
let requestParam = this.createRequestParam()
|
||||
RequestHelper.getHqManager().addRequestStructToBuff(this.frameId, this.pageId, this.getInstanceId(), requestParam)
|
||||
RequestHelper.build().base(this.frameId, this.pageId, this, requestParam).request();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建合约列表参数
|
||||
* 与 hqHelper.js format4106Data 保持一致格式: market1(code1,code2,);market2(code3,);
|
||||
*/
|
||||
private createStockListParam(): string {
|
||||
if (this.codeList.length === 0 || this.marketList.length === 0) {
|
||||
return ''
|
||||
}
|
||||
// 按市场分组,生成 format4106Data 格式
|
||||
const marketMap: Map<string, string[]> = new Map()
|
||||
for (let i = 0; i < this.codeList.length; i++) {
|
||||
const market = this.marketList[i]
|
||||
const code = this.codeList[i]
|
||||
if (marketMap.has(market)) {
|
||||
marketMap.get(market)!.push(code + ',')
|
||||
} else {
|
||||
marketMap.set(market, [code + ','])
|
||||
}
|
||||
}
|
||||
let codelist = ''
|
||||
marketMap.forEach((codes, market) => {
|
||||
codelist += market + '(' + codes.join('') + ');'
|
||||
})
|
||||
return "\r\ncodelist=" + codelist
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 dataitem 参数
|
||||
* 与 hqHelper.js dataitem 保持一致格式:字段ID用逗号分隔
|
||||
*/
|
||||
private createHeaderParam(): string {
|
||||
let param = "dataitem="
|
||||
let addedIds: number[] = []
|
||||
|
||||
this.headerConfig.forEach((item: HeaderItem) => {
|
||||
param += item.mobileid + ","
|
||||
addedIds.push(item.mobileid)
|
||||
if (item.extDataIds) {
|
||||
item.extDataIds.forEach((id: number) => {
|
||||
param += id + ","
|
||||
addedIds.push(id)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return param
|
||||
}
|
||||
|
||||
protected afterParserModel(tableData: TableData): void {
|
||||
const hqs: CommodityOptionHqItem[] = []
|
||||
if (tableData.tableRows) {
|
||||
tableData.tableRows.forEach((row: AbsRowData) => {
|
||||
if (row instanceof RowData) {
|
||||
const data = row.originalData
|
||||
const code = data.get(TableConstants.DATA_ID_CODE_4)
|
||||
const market = data.get(TableConstants.DATA_ID_MARKET)
|
||||
const name = data.get(TableConstants.DATA_ID_NAME)
|
||||
const price = data.get(TableConstants.DATA_ID_PRICE)
|
||||
const priceChg = data.get(TableConstants.DATA_ID_ZF_YTD)
|
||||
|
||||
if (typeof code === 'string' && typeof market === 'string') {
|
||||
let priceChgValue: number | null = null
|
||||
if (priceChg && typeof priceChg === 'string' && priceChg !== '--') {
|
||||
priceChgValue = parseFloat(priceChg)
|
||||
}
|
||||
|
||||
hqs.push({
|
||||
code: code,
|
||||
market: market,
|
||||
name: typeof name === 'string' ? name : '',
|
||||
price: typeof price === 'string' ? price : '--',
|
||||
priceChg: priceChgValue,
|
||||
expiredDate: '' // 到期日稍后从 contractList 中补充
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hqDataCallback && hqs.length > 0) {
|
||||
this.hqDataCallback(hqs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品期权行情数据项
|
||||
*/
|
||||
export interface CommodityOptionHqItem {
|
||||
code: string
|
||||
market: string
|
||||
name: string
|
||||
price: string
|
||||
priceChg: number | null
|
||||
expiredDate: string // 到期日(格式:20250628)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { SortItem, TableConstants, TableRequestClient } from '@b2c/lib_baseui';
|
||||
import { HeaderItem, TableData } from '@b2b/hq_table';
|
||||
import {
|
||||
QuoteCustomSettingManager,
|
||||
StandardPriceTypeHelper,
|
||||
applyStandardPriceToTableData
|
||||
} from 'biz_quote';
|
||||
import { StuffTableStruct } from '@kernel/lib_communication';
|
||||
|
||||
const REQUIRED_IDS_FOR_STANDARD_PRICE: number[] = [
|
||||
TableConstants.DATA_ID_CODE_4,
|
||||
TableConstants.DATA_ID_PRICE,
|
||||
TableConstants.DATA_ID_ZF,
|
||||
TableConstants.DATA_ID_ZD,
|
||||
TableConstants.DATA_ID_MARKET,
|
||||
TableConstants.DATA_ID_MARKET_OLD,
|
||||
StandardPriceTypeHelper.FIELD_ZUO_SHOU,
|
||||
StandardPriceTypeHelper.FIELD_JIN_KAI,
|
||||
StandardPriceTypeHelper.FIELD_ZUO_JIE
|
||||
]
|
||||
|
||||
export class FirstPageSelfStockRequestClient extends TableRequestClient {
|
||||
|
||||
constructor(frameId: number, pageId: number, headerConfig: Array<HeaderItem>, sortItem?: SortItem | null,
|
||||
dataReceiveListener?: (tableData: TableData | string) => void, extRequestText?: string) {
|
||||
super(frameId, pageId, headerConfig, sortItem, dataReceiveListener, extRequestText)
|
||||
|
||||
let ids: number[] = []
|
||||
this.headerConfig.forEach((item: HeaderItem) => {
|
||||
this.addId(ids, item.mobileid)
|
||||
})
|
||||
REQUIRED_IDS_FOR_STANDARD_PRICE.forEach((id: number) => {
|
||||
this.addId(ids, id)
|
||||
})
|
||||
|
||||
this.setSubscribeIds(ids)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 增加自选股列表请求的定制参数
|
||||
* @returns
|
||||
*/
|
||||
protected createRequestParam(): string {
|
||||
let param = super.createRequestParam()
|
||||
param += "\r\n" + this.createHeaderParam()
|
||||
param += "\r\nupdate=1"
|
||||
param += "\r\ncomputemode=" + QuoteCustomSettingManager.getInstance().getStandardPriceType()
|
||||
return param;
|
||||
}
|
||||
|
||||
protected doRequest(){
|
||||
super.doRequest()
|
||||
}
|
||||
|
||||
protected afterParserModel(tableData: TableData, structData: StuffTableStruct): void {
|
||||
applyStandardPriceToTableData(tableData, structData, REQUIRED_IDS_FOR_STANDARD_PRICE)
|
||||
}
|
||||
|
||||
release(isDestory:boolean = false): void {
|
||||
super.release(isDestory)
|
||||
}
|
||||
|
||||
/**
|
||||
* 目前直接请求全部字段的数据,暂不考虑横向分页
|
||||
* @returns
|
||||
*/
|
||||
private createHeaderParam(): string {
|
||||
let param = "columnorder="
|
||||
let addedIds: number[] = []
|
||||
this.headerConfig.forEach((item: HeaderItem) => {
|
||||
if (!addedIds.includes(item.mobileid)) {
|
||||
param += item.mobileid + "|"
|
||||
addedIds.push(item.mobileid)
|
||||
}
|
||||
if (item.extDataIds) {
|
||||
item.extDataIds.forEach((id: number) => {
|
||||
if (!addedIds.includes(id)) {
|
||||
param += id + "|"
|
||||
addedIds.push(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
REQUIRED_IDS_FOR_STANDARD_PRICE.forEach((id: number) => {
|
||||
if (!addedIds.includes(id)) {
|
||||
param += id + "|"
|
||||
}
|
||||
})
|
||||
return param
|
||||
}
|
||||
|
||||
private addId(ids: number[], id: number): void {
|
||||
if (!ids.includes(id)) {
|
||||
ids.push(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { AbsRowData, RowData, TableData } from '@b2b/hq_table';
|
||||
import { TableRequestClient } from '@b2c/lib_baseui';
|
||||
import { RequestHelper, StuffTableStruct } from '@kernel/lib_communication';
|
||||
import { applyStandardPriceToTableData } from 'biz_quote';
|
||||
import {
|
||||
CardData,
|
||||
RankItem,
|
||||
RecommendFuturesItem,
|
||||
TabMetric,
|
||||
} from '../../market-ranking/MarketRankingModels';
|
||||
import {
|
||||
PERIOD_RANGES,
|
||||
TAB_METRICS,
|
||||
} from '../../market-ranking/MarketRankingConstant';
|
||||
import { TableConstants } from '../../market-ranking/TableConstants';
|
||||
|
||||
const FRAME_ID = 2201;
|
||||
const PAGE_ID = 4106;
|
||||
|
||||
export type MarketRankingCardDataListener = (cardData: CardData) => void;
|
||||
|
||||
/**
|
||||
* 单次市场排名行情请求。
|
||||
*/
|
||||
class MarketRankingTableRequestClient extends TableRequestClient {
|
||||
private requestParams: string;
|
||||
private fieldIds: number[];
|
||||
private sortId: number;
|
||||
private listener: MarketRankingCardDataListener;
|
||||
|
||||
constructor(
|
||||
requestParams: string,
|
||||
fieldIds: number[],
|
||||
sortId: number,
|
||||
listener: MarketRankingCardDataListener,
|
||||
) {
|
||||
super(FRAME_ID, PAGE_ID, [], null, undefined);
|
||||
this.requestParams = requestParams;
|
||||
this.fieldIds = fieldIds;
|
||||
this.sortId = sortId;
|
||||
this.listener = listener;
|
||||
this.setSubscribeIds(fieldIds);
|
||||
}
|
||||
|
||||
protected doRequest(): void {
|
||||
RequestHelper.getHqManager()
|
||||
.addRequestStructToBuff(this.frameId, this.pageId, this.getInstanceId(), this.requestParams);
|
||||
RequestHelper.build().base(this.frameId, this.pageId, this, this.requestParams).request();
|
||||
}
|
||||
|
||||
protected afterParserModel(tableData: TableData, structData: StuffTableStruct): void {
|
||||
applyStandardPriceToTableData(tableData, structData, this.fieldIds);
|
||||
this.listener(this.tableDataToCardData(tableData));
|
||||
}
|
||||
|
||||
private tableDataToCardData(tableData: TableData): CardData {
|
||||
const tableList: RankItem[] = [];
|
||||
if (tableData.tableRows) {
|
||||
tableData.tableRows.forEach((row: AbsRowData): void => {
|
||||
if (row instanceof RowData) {
|
||||
const codeValue = row.originalData.get(TableConstants.DATA_ID_CODE_4);
|
||||
const marketValue = row.originalData.get(TableConstants.DATA_ID_MARKET);
|
||||
if (codeValue === undefined || marketValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: RankItem = {
|
||||
code: `${codeValue}`,
|
||||
market: `${marketValue}`,
|
||||
name: this.getString(row, TableConstants.DATA_ID_NAME),
|
||||
};
|
||||
item.price = this.getNumber(row, TableConstants.DATA_ID_PRICE);
|
||||
item.price_chg = this.getNumber(row, TableConstants.DATA_ID_ZF);
|
||||
if (this.sortId === TableConstants.DATA_ID_TURNOVER) {
|
||||
item.turnover = this.getNumber(row, TableConstants.DATA_ID_TURNOVER);
|
||||
} else if (this.sortId === TableConstants.DATA_ID_INCRE_POSIT) {
|
||||
item.incre_posit = this.getNumber(row, TableConstants.DATA_ID_INCRE_POSIT);
|
||||
} else if (this.sortId !== TableConstants.DATA_ID_ZF) {
|
||||
item.chg_speed = this.getNumber(row, this.sortId);
|
||||
}
|
||||
tableList.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
return { tableList: tableList };
|
||||
}
|
||||
|
||||
private getString(row: RowData, fieldId: number): string {
|
||||
const value = row.originalData.get(fieldId);
|
||||
return value === undefined ? '' : `${value}`;
|
||||
}
|
||||
|
||||
private getNumber(row: RowData, fieldId: number): number | undefined {
|
||||
const value = row.originalData.get(fieldId);
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value !== '' && value !== '--') {
|
||||
const result: number = Number(value);
|
||||
return Number.isNaN(result) ? undefined : result;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 市场排名行情入口:根据当前 Tab 生成字段和请求参数,并管理真实行情订阅。
|
||||
*/
|
||||
export class MarketRankingHqRequestClient {
|
||||
private static readonly instance: MarketRankingHqRequestClient =
|
||||
new MarketRankingHqRequestClient();
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
static getInstance(): MarketRankingHqRequestClient {
|
||||
return MarketRankingHqRequestClient.instance;
|
||||
}
|
||||
|
||||
subscribeHq(
|
||||
contracts: RecommendFuturesItem[],
|
||||
primaryIdx: number,
|
||||
secondaryIdx: number,
|
||||
listener: MarketRankingCardDataListener,
|
||||
): () => void {
|
||||
if (contracts.length === 0) {
|
||||
return (): void => {
|
||||
};
|
||||
}
|
||||
|
||||
const sortId: number = this.getSortId(primaryIdx, secondaryIdx);
|
||||
const fieldIds: number[] = [
|
||||
TableConstants.DATA_ID_NAME,
|
||||
TableConstants.DATA_ID_PRICE,
|
||||
TableConstants.DATA_ID_CODE_4,
|
||||
TableConstants.DATA_ID_MARKET,
|
||||
TableConstants.DATA_ID_ZF,
|
||||
TableConstants.DATA_ID_PRE_PRICE,
|
||||
TableConstants.DATA_ID_OPEN_PRICE,
|
||||
TableConstants.DATA_ID_JSJ,
|
||||
];
|
||||
if (!fieldIds.includes(sortId)) {
|
||||
fieldIds.push(sortId);
|
||||
}
|
||||
|
||||
const requestParams: string =
|
||||
this.buildRequestParams(contracts, primaryIdx, sortId, fieldIds);
|
||||
const requestClient = new MarketRankingTableRequestClient(
|
||||
requestParams,
|
||||
fieldIds,
|
||||
sortId,
|
||||
listener,
|
||||
);
|
||||
requestClient.request(0, contracts.length);
|
||||
|
||||
return (): void => {
|
||||
requestClient.release(true);
|
||||
};
|
||||
}
|
||||
|
||||
private buildRequestParams(
|
||||
contracts: RecommendFuturesItem[],
|
||||
primaryIdx: number,
|
||||
sortId: number,
|
||||
fieldIds: number[],
|
||||
): string {
|
||||
const metric: TabMetric = TAB_METRICS[primaryIdx];
|
||||
const sortOrder: string = metric.sortOrder;
|
||||
return `\r\nsortid=${sortId}\r\n` +
|
||||
`sortorder=${sortOrder}\r\n` +
|
||||
`push=1\r\n` +
|
||||
`dataitem=${fieldIds.map((fieldId: number): string => `${fieldId},`).join('')}\r\n` +
|
||||
`codelist=${this.formatCodeList(contracts)}\r\n` +
|
||||
`scenario=qht_qihuo_sort\r\n` +
|
||||
`precision=1\r\n` +
|
||||
`pushtime=2.5\r\n`;
|
||||
}
|
||||
|
||||
private getSortId(primaryIdx: number, secondaryIdx: number): number {
|
||||
const metric: TabMetric = TAB_METRICS[primaryIdx];
|
||||
return metric.hasChildren ? PERIOD_RANGES[secondaryIdx].hqId : metric.hqId;
|
||||
}
|
||||
|
||||
private formatCodeList(contracts: RecommendFuturesItem[]): string {
|
||||
const markets: string[] = [];
|
||||
const codeGroups: string[][] = [];
|
||||
contracts.forEach((contract: RecommendFuturesItem): void => {
|
||||
const marketIndex: number = markets.indexOf(contract.market);
|
||||
if (marketIndex >= 0) {
|
||||
codeGroups[marketIndex].push(contract.contract_code);
|
||||
} else {
|
||||
markets.push(contract.market);
|
||||
codeGroups.push([contract.contract_code]);
|
||||
}
|
||||
});
|
||||
return markets.map((market: string, index: number): string =>
|
||||
`${market}(${codeGroups[index].map((code: string): string => `${code},`).join('')});`
|
||||
).join('');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { buildHeader } from '../../util/Header';
|
||||
import { AiDiagnosisData, AiDiagnosisResponse, AllCardsCard } from '../model/AiDiagnosisNodeModel';
|
||||
import { HXLog } from 'biz_common';
|
||||
import { FirstPageCardsConfigLoader } from './FirstPageCardsConfigLoader';
|
||||
|
||||
/**
|
||||
* AI 诊断数据获取器
|
||||
* 职责:
|
||||
* 1. 从本地 first_page_cards_config.json 读取卡片配置
|
||||
* 2. 调用 mod_data[0].url 获取卡片数据
|
||||
*/
|
||||
|
||||
export class AiDiagnosisDataFetcher {
|
||||
private static instance: AiDiagnosisDataFetcher
|
||||
|
||||
// 卡片配置回调
|
||||
onCardConfigChange?: (config: AllCardsCard | null) => void
|
||||
// 卡片数据回调
|
||||
onCardDataChange?: (data: AiDiagnosisData | null) => void
|
||||
// 错误回调
|
||||
onError?: (errMsg: string) => void
|
||||
|
||||
// 当前卡片配置
|
||||
private cardConfig: AllCardsCard | null = null
|
||||
// 当前卡片数据
|
||||
private cardData: AiDiagnosisData | null = null
|
||||
|
||||
static getInstance(): AiDiagnosisDataFetcher {
|
||||
if (!AiDiagnosisDataFetcher.instance) {
|
||||
AiDiagnosisDataFetcher.instance = new AiDiagnosisDataFetcher()
|
||||
}
|
||||
return AiDiagnosisDataFetcher.instance
|
||||
}
|
||||
|
||||
getCardConfig(): AllCardsCard | null {
|
||||
return this.cardConfig
|
||||
}
|
||||
|
||||
getCardData(): AiDiagnosisData | null {
|
||||
return this.cardData
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求卡片配置(从本地 first_page_cards_config.json 读取)
|
||||
*/
|
||||
fetchCardConfig(): void {
|
||||
const aiCardsConfig = FirstPageCardsConfigLoader.getConfig()
|
||||
|
||||
if (aiCardsConfig?.aiDiagnosis) {
|
||||
this.cardConfig = aiCardsConfig.aiDiagnosis
|
||||
if (this.onCardConfigChange) {
|
||||
this.onCardConfigChange(this.cardConfig)
|
||||
}
|
||||
this.fetchCardData()
|
||||
} else {
|
||||
if (this.onCardConfigChange) {
|
||||
this.onCardConfigChange(null)
|
||||
}
|
||||
HXLog.e('AiDiagnosisDataFetcher', 'fetchCardConfig: aiDiagnosis config not found')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求卡片数据(调用 mod_data[0].url)
|
||||
*/
|
||||
fetchCardData(): void {
|
||||
if (!this.cardConfig?.mod_data?.[0]?.url) {
|
||||
return
|
||||
}
|
||||
|
||||
let httpRequest = http.createHttp()
|
||||
const url = this.cardConfig.mod_data[0].url
|
||||
|
||||
httpRequest.request(url, {
|
||||
method: http.RequestMethod.GET,
|
||||
header: buildHeader(),
|
||||
}, (err: BusinessError, data: http.HttpResponse) => {
|
||||
if (err) {
|
||||
HXLog.e('AiDiagnosisDataFetcher', 'fetchCardData error: ' + err.message)
|
||||
if (this.onError) {
|
||||
this.onError(err.message)
|
||||
}
|
||||
httpRequest.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.responseCode === http.ResponseCode.OK) {
|
||||
try {
|
||||
const resultStr = data.result as string
|
||||
const result = JSON.parse(resultStr) as AiDiagnosisResponse
|
||||
if (result.status_code === 0 && result.data) {
|
||||
this.cardData = result.data
|
||||
}
|
||||
if (this.onCardDataChange) {
|
||||
this.onCardDataChange(this.cardData)
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as BusinessError
|
||||
HXLog.e('AiDiagnosisDataFetcher', `parse data error ${error.code} ${error.message}`)
|
||||
if (this.onError) {
|
||||
this.onError('数据解析失败')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HXLog.e('AiDiagnosisDataFetcher', 'fetchCardData responseCode: ' + data.responseCode)
|
||||
if (this.onError) {
|
||||
this.onError('请求失败: ' + data.responseCode)
|
||||
}
|
||||
}
|
||||
httpRequest.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { buildHeader } from '../../util/Header';
|
||||
import { AiRiseFallData, AiRiseFallResponse } from '../model/AiRiseFallNodeModel';
|
||||
import { AllCardsCard } from '../model/AiDiagnosisNodeModel';
|
||||
import { HXLog } from 'biz_common';
|
||||
import { FirstPageCardsConfigLoader } from './FirstPageCardsConfigLoader';
|
||||
|
||||
/**
|
||||
* AI 看涨跌数据获取器
|
||||
* 职责:
|
||||
* 1. 从本地 first_page_cards_config.json 读取卡片配置
|
||||
* 2. 调用 mod_data[0].url 获取卡片数据
|
||||
*/
|
||||
|
||||
export class AiRiseFallDataFetcher {
|
||||
private static instance: AiRiseFallDataFetcher
|
||||
|
||||
// 卡片配置回调
|
||||
onCardConfigChange?: (config: AllCardsCard | null) => void
|
||||
// 卡片数据回调
|
||||
onCardDataChange?: (data: AiRiseFallData | null) => void
|
||||
// 错误回调
|
||||
onError?: (errMsg: string) => void
|
||||
|
||||
// 当前卡片配置
|
||||
private cardConfig: AllCardsCard | null = null
|
||||
// 当前卡片数据
|
||||
private cardData: AiRiseFallData | null = null
|
||||
|
||||
static getInstance(): AiRiseFallDataFetcher {
|
||||
if (!AiRiseFallDataFetcher.instance) {
|
||||
AiRiseFallDataFetcher.instance = new AiRiseFallDataFetcher()
|
||||
}
|
||||
return AiRiseFallDataFetcher.instance
|
||||
}
|
||||
|
||||
getCardConfig(): AllCardsCard | null {
|
||||
return this.cardConfig
|
||||
}
|
||||
|
||||
getCardData(): AiRiseFallData | null {
|
||||
return this.cardData
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求卡片配置(从本地 first_page_cards_config.json 读取)
|
||||
*/
|
||||
fetchCardConfig(): void {
|
||||
const aiCardsConfig = FirstPageCardsConfigLoader.getConfig()
|
||||
|
||||
if (aiCardsConfig?.aiRiseFall) {
|
||||
this.cardConfig = aiCardsConfig.aiRiseFall
|
||||
if (this.onCardConfigChange) {
|
||||
this.onCardConfigChange(this.cardConfig)
|
||||
}
|
||||
this.fetchCardData()
|
||||
} else {
|
||||
if (this.onCardConfigChange) {
|
||||
this.onCardConfigChange(null)
|
||||
}
|
||||
HXLog.e('AiRiseFallDataFetcher', 'fetchCardConfig: aiRiseFall config not found')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求卡片数据
|
||||
*/
|
||||
fetchCardData(): void {
|
||||
if (!this.cardConfig?.mod_data?.[0]?.url) {
|
||||
return
|
||||
}
|
||||
|
||||
let httpRequest = http.createHttp()
|
||||
const url = this.cardConfig.mod_data[0].url
|
||||
|
||||
httpRequest.request(url, {
|
||||
method: http.RequestMethod.GET,
|
||||
header: buildHeader(),
|
||||
}, (err: BusinessError, data: http.HttpResponse) => {
|
||||
if (err) {
|
||||
HXLog.e('AiRiseFallDataFetcher', 'fetchCardData error: ' + err.message)
|
||||
if (this.onError) {
|
||||
this.onError(err.message)
|
||||
}
|
||||
httpRequest.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.responseCode === http.ResponseCode.OK) {
|
||||
try {
|
||||
const resultStr = data.result as string
|
||||
const result = JSON.parse(resultStr) as AiRiseFallResponse
|
||||
if (result.code === 0 && result.data) {
|
||||
this.cardData = result.data
|
||||
}
|
||||
if (this.onCardDataChange) {
|
||||
this.onCardDataChange(this.cardData)
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as BusinessError
|
||||
HXLog.e('AiRiseFallDataFetcher', `parse data error ${error.code} ${error.message}`)
|
||||
if (this.onError) {
|
||||
this.onError('数据解析失败')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HXLog.e('AiRiseFallDataFetcher', 'fetchCardData responseCode: ' + data.responseCode)
|
||||
if (this.onError) {
|
||||
this.onError('请求失败: ' + data.responseCode)
|
||||
}
|
||||
}
|
||||
httpRequest.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { buildHeader } from '../../util/Header';
|
||||
import { CommodityOptionsContract, CommodityOptionsResponse } from '../model/CommodityOptionsNodeModel';
|
||||
import { COMMODITY_OPTIONS_TABS, CommodityOptionsTab } from '../model/CommodityOptionsNodeModel';
|
||||
import { HXLog } from 'biz_common';
|
||||
import { FirstPageCardsConfigLoader, CommodityOptionsCardConfig } from './FirstPageCardsConfigLoader';
|
||||
import { AppUtil } from '@pura/harmony-utils';
|
||||
|
||||
/**
|
||||
* 商品期权数据获取器
|
||||
* 职责:
|
||||
* 1. 请求推荐期权数据(热门/临期期权)
|
||||
* 2. 支持 Tab 切换
|
||||
*
|
||||
* Author: YS
|
||||
* Date: 2026-06-03
|
||||
*/
|
||||
|
||||
export class CommodityOptionsDataFetcher {
|
||||
private static instance: CommodityOptionsDataFetcher
|
||||
|
||||
// 数据回调
|
||||
onCardDataChange?: (data: CommodityOptionsContract[], tabIndex: number) => void
|
||||
// 卡片配置回调
|
||||
onCardConfigChange?: (config: CommodityOptionsCardConfig | null) => void
|
||||
|
||||
// 卡片配置
|
||||
private cardConfig: CommodityOptionsCardConfig | null = null
|
||||
|
||||
// 当前选中的 Tab 索引
|
||||
private currentTabIndex: number = 0
|
||||
// 缓存的数据(按 Tab 索引存储)
|
||||
private cachedData: Map<number, CommodityOptionsContract[]> = new Map()
|
||||
|
||||
static getInstance(): CommodityOptionsDataFetcher {
|
||||
if (!CommodityOptionsDataFetcher.instance) {
|
||||
CommodityOptionsDataFetcher.instance = new CommodityOptionsDataFetcher()
|
||||
}
|
||||
return CommodityOptionsDataFetcher.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取卡片配置
|
||||
*/
|
||||
getCardConfig(): CommodityOptionsCardConfig | null {
|
||||
return this.cardConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求卡片配置(从 FirstPageCardsConfigLoader 读取)
|
||||
*/
|
||||
fetchCardConfig(): void {
|
||||
const config = FirstPageCardsConfigLoader.getConfig()
|
||||
|
||||
if (config?.commodityOptions) {
|
||||
this.cardConfig = config.commodityOptions
|
||||
if (this.onCardConfigChange) {
|
||||
this.onCardConfigChange(this.cardConfig)
|
||||
}
|
||||
// 同时请求所有tab的数据
|
||||
this.fetchAllTabData()
|
||||
} else {
|
||||
if (this.onCardConfigChange) {
|
||||
this.onCardConfigChange(null)
|
||||
}
|
||||
HXLog.e('CommodityOptionsDataFetcher', 'fetchCardConfig: commodityOptions config not found')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同时请求所有Tab的数据并缓存
|
||||
*/
|
||||
fetchAllTabData(): void {
|
||||
COMMODITY_OPTIONS_TABS.forEach((tab, index) => {
|
||||
this.requestOptions(index)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 Tab
|
||||
*/
|
||||
getCurrentTab(): CommodityOptionsTab {
|
||||
return COMMODITY_OPTIONS_TABS[this.currentTabIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 Tab
|
||||
* @param index Tab 索引
|
||||
*/
|
||||
switchTab(index: number): void {
|
||||
if (index < 0 || index >= COMMODITY_OPTIONS_TABS.length) {
|
||||
return
|
||||
}
|
||||
this.currentTabIndex = index
|
||||
// 触发数据获取
|
||||
this.fetchCardData(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定Tab的数据
|
||||
*/
|
||||
getDataByTab(tabIndex: number): CommodityOptionsContract[] {
|
||||
return this.cachedData.get(tabIndex) || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求数据
|
||||
*/
|
||||
fetchCardData(index: number): void {
|
||||
this.requestOptions(index)
|
||||
}
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
destroy(): void {
|
||||
this.cachedData.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求期权数据
|
||||
* @param quoteType API类型
|
||||
* @param tabIndex Tab索引(可选,用于同时请求多个tab)
|
||||
*/
|
||||
private requestOptions(tabIndex: number): void {
|
||||
if (tabIndex >= COMMODITY_OPTIONS_TABS.length) {
|
||||
return
|
||||
}
|
||||
const tabConfig = COMMODITY_OPTIONS_TABS[tabIndex]
|
||||
const url = this.buildRequestUrl(tabConfig.api)
|
||||
|
||||
let httpRequest = http.createHttp()
|
||||
httpRequest.request(url, {
|
||||
method: http.RequestMethod.GET,
|
||||
header: buildHeader(),
|
||||
}, (err: BusinessError, data: http.HttpResponse) => {
|
||||
if (err) {
|
||||
HXLog.e('CommodityOptionsDataFetcher', 'requestOptions error: ' + err.message)
|
||||
httpRequest.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.responseCode === http.ResponseCode.OK) {
|
||||
try {
|
||||
const resultStr = data.result as string
|
||||
const result = JSON.parse(resultStr) as CommodityOptionsResponse
|
||||
if (result.code === 0 && result.data && result.data.length > 0) {
|
||||
// 缓存数据到指定tab
|
||||
this.cachedData.set(tabIndex, result.data)
|
||||
|
||||
// 如果是当前tab,触发回调
|
||||
if (this.onCardDataChange) {
|
||||
this.onCardDataChange(result.data, tabIndex)
|
||||
}
|
||||
} else {
|
||||
HXLog.e('CommodityOptionsDataFetcher', 'requestOptions: data is empty')
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as BusinessError
|
||||
HXLog.e('CommodityOptionsDataFetcher', 'parse data error: ' + error.message)
|
||||
}
|
||||
} else {
|
||||
HXLog.e('CommodityOptionsDataFetcher', 'requestOptions responseCode: ' + data.responseCode)
|
||||
}
|
||||
httpRequest.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求 URL
|
||||
*/
|
||||
private buildRequestUrl(quoteType: string): string {
|
||||
const urlTemplate: string = AppUtil.getContext().resourceManager.getStringSync($r('app.string.commodity_options_recommend_url'))
|
||||
return urlTemplate.replace('%s', quoteType)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { GlobalContext, HXLog } from 'biz_common';
|
||||
import util from '@ohos.util';
|
||||
import { AllCardsCard, CardTitle, CardUrl, ModDataItem } from '../model/AiDiagnosisNodeModel';
|
||||
|
||||
/**
|
||||
* 首页卡片配置加载器
|
||||
* 职责:
|
||||
* 1. 从本地 first_page_cards_config.json 读取卡片配置
|
||||
* 2. 提供公共方法供各 Fetcher 使用
|
||||
*
|
||||
* Author: yansong@myhexin.com
|
||||
* Date: 2026/06/04
|
||||
*/
|
||||
|
||||
// 配置文件名
|
||||
const FIRST_PAGE_CARDS_CONFIG_FILE = 'first_page_cards_config.json'
|
||||
|
||||
/**
|
||||
* 热点资讯卡片配置
|
||||
*/
|
||||
export interface HotListCardConfig {
|
||||
card_id: number
|
||||
card_key: string
|
||||
card_title: CardTitle
|
||||
card_url: CardUrl
|
||||
mod_data: ModDataItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品期权卡片配置
|
||||
*/
|
||||
export interface CommodityOptionsCardConfig {
|
||||
card_id: number
|
||||
card_key: string
|
||||
card_title: CardTitle
|
||||
card_url: CardUrl
|
||||
mod_data: ModDataItem[]
|
||||
explain_title: string | null
|
||||
explain_message: string | null
|
||||
}
|
||||
|
||||
// AI卡片配置接口
|
||||
export interface FirstPageCardsConfig {
|
||||
hotList: HotListCardConfig
|
||||
aiDiagnosis: AllCardsCard
|
||||
aiRiseFall: AllCardsCard
|
||||
commodityOptions: CommodityOptionsCardConfig
|
||||
marketRanking: AllCardsCard
|
||||
}
|
||||
|
||||
export class FirstPageCardsConfigLoader {
|
||||
private static cardsConfig: FirstPageCardsConfig | null = null
|
||||
|
||||
static getConfig(): FirstPageCardsConfig | null {
|
||||
if (FirstPageCardsConfigLoader.cardsConfig) {
|
||||
return FirstPageCardsConfigLoader.cardsConfig
|
||||
}
|
||||
|
||||
const rawData = FirstPageCardsConfigLoader.readRawFile(FIRST_PAGE_CARDS_CONFIG_FILE)
|
||||
if (!rawData) {
|
||||
HXLog.e('AiCardsConfigLoader', 'read first_page_cards_config.json failed')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
FirstPageCardsConfigLoader.cardsConfig = JSON.parse(rawData) as FirstPageCardsConfig
|
||||
HXLog.d('AiCardsConfigLoader', 'load success')
|
||||
} catch (e) {
|
||||
const error = e as BusinessError
|
||||
HXLog.e('AiCardsConfigLoader', 'parse error ' + error.message)
|
||||
}
|
||||
return FirstPageCardsConfigLoader.cardsConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 rawfile 读取配置文件
|
||||
* @param fileName rawfile 文件名
|
||||
* @returns 读取的字符串内容,失败返回空字符串
|
||||
*/
|
||||
private static readRawFile(fileName: string): string {
|
||||
try {
|
||||
const context = GlobalContext.get()
|
||||
const mgr = context.resourceManager
|
||||
const rawFile = mgr.getRawFileContentSync(fileName)
|
||||
let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
|
||||
return textDecoder.decodeToString(rawFile, { stream: false })
|
||||
} catch (e) {
|
||||
const error = e as BusinessError
|
||||
HXLog.e('AiCardsConfigLoader', `readRawFile ${fileName} error: ${error.code} ${error.message}`)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { HotListItemModel, HotListResultModel } from '../model/HotListNodeModel';
|
||||
import { FileManager } from '../../util/FileManager';
|
||||
import { buildHeader } from '../../util/Header';
|
||||
import { HXLog } from 'biz_common';
|
||||
import { FirstPageCardsConfigLoader } from './FirstPageCardsConfigLoader';
|
||||
|
||||
/**
|
||||
* 热点排行数据获取器
|
||||
* @author YS
|
||||
* @date 2026-06-03
|
||||
*/
|
||||
export class HotListDataFetcher {
|
||||
// 单例实例
|
||||
private static instance: HotListDataFetcher
|
||||
|
||||
// 数据变更回调
|
||||
hotListChange?: (data: HotListItemModel[] | null) => void
|
||||
// 配置变更回调
|
||||
cardConfigChange?: (cardUrl: string) => void
|
||||
|
||||
// API 地址
|
||||
private apiUrl: string = ''
|
||||
// 跳转列表页 URL
|
||||
private cardUrl: string = ''
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): HotListDataFetcher {
|
||||
if (!HotListDataFetcher.instance) {
|
||||
HotListDataFetcher.instance = new HotListDataFetcher()
|
||||
}
|
||||
return HotListDataFetcher.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取跳转列表页 URL
|
||||
*/
|
||||
getCardUrl(): string {
|
||||
return this.cardUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置(供外部调用)
|
||||
*/
|
||||
loadConfig(): void {
|
||||
const config = FirstPageCardsConfigLoader.getConfig()
|
||||
if (config?.hotList) {
|
||||
// 获取 API 地址
|
||||
if (config.hotList.mod_data?.[0]?.url) {
|
||||
this.apiUrl = config.hotList.mod_data[0].url
|
||||
}
|
||||
// 获取跳转 URL(使用 android 平台的 URL,鸿蒙类似 Android)
|
||||
if (config.hotList.card_url) {
|
||||
this.cardUrl = config.hotList.card_url.android || config.hotList.card_url.ios || ''
|
||||
}
|
||||
HXLog.d('HotListDataFetcher', `loadConfig: success, apiUrl=${this.apiUrl}, cardUrl=${this.cardUrl}`)
|
||||
} else {
|
||||
HXLog.e('HotListDataFetcher', 'loadConfig: hotList config not found')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程获取数据
|
||||
*/
|
||||
remoteFetchData(): void {
|
||||
// 加载配置
|
||||
this.loadConfig()
|
||||
|
||||
if (!this.apiUrl) {
|
||||
HXLog.e('HotListDataFetcher', 'remoteFetchData: apiUrl is empty')
|
||||
if (this.hotListChange) {
|
||||
this.hotListChange(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let httpRequest = http.createHttp()
|
||||
httpRequest.request(this.apiUrl, {
|
||||
method: http.RequestMethod.GET,
|
||||
header: buildHeader()
|
||||
}, (err: BusinessError, data: http.HttpResponse) => {
|
||||
if (err) {
|
||||
HXLog.e('HotListDataFetcher', `remoteFetchData: request error ${err.code} ${err.message}`)
|
||||
if (this.hotListChange) {
|
||||
this.hotListChange(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (data.responseCode === http.ResponseCode.OK) {
|
||||
let dataStr = data.result as string
|
||||
const dataObj = this.parseData(dataStr)
|
||||
if (dataObj) {
|
||||
if (this.hotListChange) {
|
||||
this.hotListChange(dataObj)
|
||||
}
|
||||
} else {
|
||||
// 数据解析失败
|
||||
if (this.hotListChange) {
|
||||
this.hotListChange(null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非 200 响应
|
||||
if (this.hotListChange) {
|
||||
this.hotListChange(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数据
|
||||
*/
|
||||
private parseData(dataStr: string): HotListItemModel[] | null {
|
||||
try {
|
||||
const result = JSON.parse(dataStr) as HotListResultModel
|
||||
if (result && result.code === 0 && result.data && result.data.length > 0) {
|
||||
return result.data
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
HXLog.e('HotListDataFetcher', `parseData: ${e}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { parseInt } from '@wolfx/lodash';
|
||||
import { GlobalContext, HXLog } from 'biz_common';
|
||||
import { StringUtils } from 'hxutil';
|
||||
import { DateUtils } from '../../util/DateUtils';
|
||||
import { FileManager } from '../../util/FileManager';
|
||||
import { HotNewsNodeModel, HotNewsNodeModelResult } from '../model/HotNewsNodeModel';
|
||||
|
||||
export class NewsDataExecutor {
|
||||
|
||||
private static ONE_SECOND = 1000; //1秒
|
||||
static ONE_MINUTE = 60 * 1000; //1分
|
||||
static ONE_HOUR = 60 * 60 * 1000; // 1小时
|
||||
static ONE_DAY = 24 * 60 * 60 * 1000; //1天
|
||||
static TWO_DAY = 2 * 24 * 60 * 60 * 1000; //2天
|
||||
static MINUTE_BEFORE = "分钟前";
|
||||
static HOUR_BEFORE = "小时前";
|
||||
static YESTERDAY = "昨天";
|
||||
|
||||
static parseData(data : string) : HotNewsNodeModel[] | null {
|
||||
let resultData : HotNewsNodeModel[] | null = null
|
||||
if (StringUtils.isNotEmpty(data)) {
|
||||
try {
|
||||
const firstPageNodeResult = JSON.parse(data) as HotNewsNodeModelResult
|
||||
if (firstPageNodeResult) {
|
||||
resultData = NewsDataExecutor.handlerNewsModel(firstPageNodeResult.data)
|
||||
}
|
||||
} catch (e) {
|
||||
HXLog.e("FirstpageDataExecutor.parseData", "error " + JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
return resultData;
|
||||
}
|
||||
|
||||
static handlerNewsModel(models: HotNewsNodeModel[]): HotNewsNodeModel[] {
|
||||
models.forEach((item) => {
|
||||
item.create_time = NewsDataExecutor.transferTime(item.create_time)
|
||||
if (item.tags.length > 2) {
|
||||
item.tags = item.tags.slice(0, 2)
|
||||
}
|
||||
})
|
||||
return models
|
||||
}
|
||||
|
||||
static transferTime(time: string): string {
|
||||
let showTime = ''
|
||||
let timeTmp = parseInt(time)
|
||||
if (timeTmp) {
|
||||
timeTmp = timeTmp * NewsDataExecutor.ONE_SECOND
|
||||
let currentTime = new Date().getTime()
|
||||
let timeSpace = currentTime - timeTmp
|
||||
|
||||
if (timeSpace < NewsDataExecutor.ONE_MINUTE) {
|
||||
showTime = '1' + NewsDataExecutor.MINUTE_BEFORE
|
||||
} else if (timeSpace < NewsDataExecutor.ONE_HOUR) {
|
||||
showTime = Math.floor(timeSpace / NewsDataExecutor.ONE_MINUTE) + NewsDataExecutor.MINUTE_BEFORE
|
||||
} else if (timeSpace < NewsDataExecutor.ONE_DAY) {
|
||||
showTime = Math.floor(timeSpace / NewsDataExecutor.ONE_HOUR) + NewsDataExecutor.HOUR_BEFORE
|
||||
} else if (timeSpace < NewsDataExecutor.TWO_DAY) {
|
||||
showTime = NewsDataExecutor.YESTERDAY
|
||||
} else {
|
||||
showTime = DateUtils.formatDate(new Date(timeTmp), 'MM月dd日')
|
||||
}
|
||||
}
|
||||
return showTime
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { buildHeader } from '../../util/Header';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { NewsDataExecutor } from './HotNewsDataExecutor';
|
||||
import { HotNewsNodeModel } from '../model/HotNewsNodeModel';
|
||||
import { FileManager } from '../../util/FileManager';
|
||||
|
||||
export class NewsDataFetcher {
|
||||
|
||||
private static CACHE_FILE_NAME = '/firstpage/firstpage_news.txt'
|
||||
|
||||
private static instance: NewsDataFetcher
|
||||
newsChange?: (news : HotNewsNodeModel[] | null) => void
|
||||
|
||||
static getInstance() {
|
||||
if (!NewsDataFetcher.instance) {
|
||||
NewsDataFetcher.instance = new NewsDataFetcher
|
||||
}
|
||||
return NewsDataFetcher.instance
|
||||
}
|
||||
|
||||
fetchDataLocal() {
|
||||
let cacheData = FileManager.getInstance().getCachePathSync(NewsDataFetcher.CACHE_FILE_NAME)
|
||||
if (cacheData) {
|
||||
const dataObj = NewsDataExecutor.parseData(cacheData)
|
||||
if (dataObj && this.newsChange) {
|
||||
this.newsChange(dataObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remoteFetchData(url: string) {
|
||||
let httpRequest = http.createHttp();
|
||||
httpRequest.request(url, {
|
||||
method: http.RequestMethod.GET,
|
||||
header: buildHeader(),
|
||||
}, (err: BusinessError, data: http.HttpResponse) => {
|
||||
httpRequest.destroy()
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
if (data.responseCode === http.ResponseCode.OK) {
|
||||
let daraStr = data.result as string
|
||||
const dataObj = NewsDataExecutor.parseData(daraStr)
|
||||
if (dataObj) {
|
||||
FileManager.getInstance().saveCacheSync(daraStr, NewsDataFetcher.CACHE_FILE_NAME)
|
||||
if (this.newsChange) {
|
||||
this.newsChange(dataObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import { BaseAd, AdsManager, ADS_TYPE_INDEX_POPUP } from "@b2c-f/fuhm-ads"
|
||||
import { InfDialog, DialogHub } from "@hadss/dialoghub"
|
||||
import { BusinessError, systemDateTime } from "@kit.BasicServicesKit"
|
||||
import { AppUtil } from "@pura/harmony-utils"
|
||||
import { HXLog, FrameId, AdsPageConfig } from "biz_common"
|
||||
import { DateManager } from "biz_common/src/main/ets/datePicker/DateManager"
|
||||
import { HXUserService } from "biz_hxservice"
|
||||
import { getCurrentFrameId } from "biz_quote/src/main/ets/util/FrameId"
|
||||
import { HexinVersionControl } from "biz_trade/src/main/ets/utils/HexinVersionControl"
|
||||
import { ELK_LEVEL, sendElk } from "service_monitor"
|
||||
import {
|
||||
CBAS_FIRSTPAGE_TANGCHUANGAD_CLOSE,
|
||||
CBAS_FIRSTPAGE_TANGCHUANGAD_BAOGUANG,
|
||||
CBAS_FIRSTPAGE_PREFIX,
|
||||
CBAS_FIRSTPAGE_TANGCHUANGAD_DIANJI
|
||||
} from "../../constants/FirstPageConstants"
|
||||
import { AdsPopupHost } from "../../popup/AdsPopupHost"
|
||||
import { FileManager } from "../../util/FileManager"
|
||||
import { ImageLoader } from "../../util/ImageLoader"
|
||||
import { RouterUtil } from "../../util/RouterUtil"
|
||||
import { PopupAdMode, adapterForIndexPopupActivity, AdFrequency } from "../model/PopupAdMode"
|
||||
import { FirstPageDialogParam, FirstPageDialogBuilder } from "../view/FirstPageDialog"
|
||||
import { JSON } from "@kit.ArkTS"
|
||||
|
||||
|
||||
/**
|
||||
* author : liuqingliang@myhexin.com
|
||||
* time : created on 2026/5/28
|
||||
* desc : 首页弹窗广告帮助类
|
||||
*/
|
||||
const TAG = 'FirstPagePopupAdHelper'
|
||||
|
||||
export class FirstPagePopupAdHelper {
|
||||
private mPopupAdModel: PopupAdMode | null = null
|
||||
private static instance: FirstPagePopupAdHelper | null = null
|
||||
firstPageVisible: boolean = true
|
||||
/**
|
||||
* 本次启动是否显示过弹窗广告
|
||||
*/
|
||||
isShowedPopupAds = false
|
||||
private adDialog: InfDialog | null = null
|
||||
/**
|
||||
* 是否有弹窗广告未显示(用于投教冲突时缓存)
|
||||
*/
|
||||
private isHasTanChuangAdNotShow = false
|
||||
private popAdRecordFilePath = 'record/popAdRecordNew.txt'
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
|
||||
static getInstance(): FirstPagePopupAdHelper {
|
||||
if (!FirstPagePopupAdHelper.instance) {
|
||||
FirstPagePopupAdHelper.instance = new FirstPagePopupAdHelper()
|
||||
}
|
||||
return FirstPagePopupAdHelper.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页 OnResume 时调用
|
||||
* 场景:投教弹窗遮盖期间收到广告数据,返回首页后从此方法恢复
|
||||
*/
|
||||
showPopupAdWhenOnResume(): void {
|
||||
if (!this.isHasTanChuangAdNotShow) {
|
||||
return
|
||||
}
|
||||
this.isHasTanChuangAdNotShow = false
|
||||
const showResult = this.showFirstPagePopupAd()
|
||||
const elkMsg =
|
||||
`showPopupAd showPopupAdModel:${this.mPopupAdModel?.toString()} showFirstPagePopupAd:${showResult}`
|
||||
this.uploadLog(ELK_LEVEL.ERROR, elkMsg)
|
||||
}
|
||||
|
||||
/**
|
||||
* 广告数据更新入口
|
||||
* FirstPage 通过 AdsManager.addAdsDataUpdateListener 收到数据后调用此方法
|
||||
*/
|
||||
onAdsDataUpdate(adsType: string, adsDataModels: BaseAd[]): void {
|
||||
//如果隐藏交易直接不展示
|
||||
if (!this.isCanShowIndexPopupAd() || HexinVersionControl.hideTrade()) {
|
||||
return
|
||||
}
|
||||
if (this.isShowedPopupAds) {
|
||||
return
|
||||
}
|
||||
let elkMsg = `onAdsDataUpdate adType:${adsType} userId:${this.getNotNullUserId()}`
|
||||
let popupAdModelList: PopupAdMode[] = adapterForIndexPopupActivity(adsDataModels)
|
||||
elkMsg += ` popupAdModelList size:${popupAdModelList.length}`
|
||||
// 找到本地弹窗记录
|
||||
let popAdRecordMap: Record<string, PopupAdMode> = this.readPopAdRecord()
|
||||
// 找到需要弹出的广告
|
||||
this.mPopupAdModel = this.findNeedPopAd(popAdRecordMap, popupAdModelList)
|
||||
elkMsg += ` findNeedPopupAd:${this.mPopupAdModel?.toString()}`
|
||||
if (this.mPopupAdModel === null && popupAdModelList.length !== 0) {
|
||||
// 重置
|
||||
this.resetRecordFlag(popupAdModelList, popAdRecordMap)
|
||||
// 找到需要弹出的广告
|
||||
this.mPopupAdModel = this.findNeedPopAd(popAdRecordMap, popupAdModelList)
|
||||
elkMsg += ` resetRecordFlag:${this.mPopupAdModel?.toString()}`
|
||||
}
|
||||
// show ad
|
||||
let showResult = this.showFirstPagePopupAd()
|
||||
elkMsg += ` showResult:${showResult}`
|
||||
if (!showResult) {
|
||||
// 首页弹窗显隐确定时,通知广告组件
|
||||
AdsPopupHost.getInstance().notifyFirstPageMatch(false)
|
||||
}
|
||||
this.uploadLog(ELK_LEVEL.ERROR, elkMsg)
|
||||
// 记录弹窗信息
|
||||
this.recordAdModel(popAdRecordMap, this.mPopupAdModel)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录弹窗广告的弹出记录
|
||||
*
|
||||
* @param popAdRecordMap 当前用户的所有弹出记录
|
||||
* @param showAdModel 当前弹出的弹窗广告
|
||||
*/
|
||||
private recordAdModel(popAdRecordMap: Record<string, PopupAdMode>, showAdModel: PopupAdMode | null) {
|
||||
if (showAdModel == null) {
|
||||
return
|
||||
}
|
||||
let userId = this.getNotNullUserId()
|
||||
let adId = showAdModel.adid
|
||||
showAdModel.hasShow = true
|
||||
showAdModel.showTime = systemDateTime.getTime()
|
||||
popAdRecordMap[adId] = showAdModel
|
||||
let allUserRecord = this.readPopAdRecordFromFile()
|
||||
allUserRecord[userId] = popAdRecordMap
|
||||
let record = JSON.stringify(allUserRecord)
|
||||
FileManager.getInstance().saveFilesSync(record, this.popAdRecordFilePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件中读取当前用户的广告弹窗的弹出记录,如果没有记录就自动生成一个记录Map
|
||||
* @return 返回当前用户的广告记录
|
||||
*/
|
||||
private readPopAdRecord(): Record<string, PopupAdMode> {
|
||||
const popAdRecord: Record<string, Record<string, PopupAdMode>> = this.readPopAdRecordFromFile()
|
||||
const userId = this.getNotNullUserId()
|
||||
const adModels = popAdRecord[userId]
|
||||
return adModels ?? {}
|
||||
}
|
||||
|
||||
private getNotNullUserId(): string {
|
||||
return HXUserService.getInstance().getUserIdFromLocal()
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到本地的弹窗纪录文件并取出所有的弹窗记录
|
||||
* @return 返回本机器所有用户的记录内容
|
||||
*/
|
||||
private readPopAdRecordFromFile(): Record<string, Record<string, PopupAdMode>> {
|
||||
let record: Record<string, Record<string, PopupAdMode>> = {}
|
||||
const recordText = FileManager.getInstance().loadFilesSync(this.popAdRecordFilePath)
|
||||
try {
|
||||
record = JSON.parse(recordText) as Record<string, Record<string, PopupAdMode>>
|
||||
} catch (e) {
|
||||
HXLog.e(TAG, `readPopAdRecordFromFile parse failed. recordText:${recordText}`)
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 BaseAd 列表中查找需要弹出的广告
|
||||
* @param adsDataModels 网络返回的 BaseAd 列表
|
||||
* @return 需要弹出的 BaseAd,若无则返回 null
|
||||
*/
|
||||
private findNeedPopAd(popAdRecord: Record<string, PopupAdMode>, adList: PopupAdMode[]): PopupAdMode | null {
|
||||
if (adList.length === 0) {
|
||||
return null
|
||||
}
|
||||
for (let index = 0; index < adList.length; index++) {
|
||||
//先获取adList中索引位置的adModel
|
||||
const adModel = adList[index]
|
||||
if (!this.isNeedShowPopupAd(adModel)) {
|
||||
continue
|
||||
}
|
||||
let adId = adModel.adid
|
||||
//判断本地纪录中是否含有该条广告
|
||||
if (popAdRecord[adId]) {
|
||||
let recordModel = popAdRecord[adId]
|
||||
//判断如果该条adModel是否显示过,true为显示过,false直接返回
|
||||
if (!this.isHasShowRecord(recordModel)) {
|
||||
//将adList中的广告弹窗返回
|
||||
return adModel
|
||||
} else {
|
||||
if (index === adList.length - 1) {
|
||||
//第一遍未查找到则直接返回null,在adlist长度不为0的情况下会有第二次查找
|
||||
return null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//将没有记录过的广告计入map记录
|
||||
adModel.hasShow = false
|
||||
popAdRecord[adId] = adModel
|
||||
return adModel
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private isNeedShowPopupAd(popupAdModel: PopupAdMode) {
|
||||
return !(!this.isAdModelValid(popupAdModel) || this.isShowedPopupAds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已经显示过
|
||||
* @param recordModel
|
||||
* @return
|
||||
*/
|
||||
private isHasShowRecord(recordModel: PopupAdMode) {
|
||||
return recordModel != null && recordModel.hasShow
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置本地纪录中的弹窗广告记录标志为false
|
||||
* @param adList 本次请求的弹窗广告
|
||||
* @param popAdRecordMap 本地的弹窗广告记录
|
||||
*/
|
||||
private resetRecordFlag(adList: PopupAdMode[], popAdRecordMap: Record<string, PopupAdMode>) {
|
||||
//先判断每次广告弹窗的显示频率,重置所有每次登录都显示的显示标志为false
|
||||
//每日一次的广告是否需要重置根据上一次显示时间确定
|
||||
for (let adIndex = 0; adIndex < adList.length; adIndex++) {
|
||||
let id = adList[adIndex].adid
|
||||
if (!popAdRecordMap[id]) {
|
||||
continue
|
||||
}
|
||||
let recordAdModel = popAdRecordMap[id]
|
||||
if (!recordAdModel) {
|
||||
continue
|
||||
}
|
||||
let frequency = recordAdModel.frequency
|
||||
//每天只弹一次的弹窗
|
||||
if (frequency == AdFrequency.FREQUENCY_EVERY_DAY &&
|
||||
!DateManager.isSameDay(new Date(), new Date(recordAdModel.showTime))
|
||||
) {
|
||||
//如果记录的显示时间和今天不是同一天
|
||||
recordAdModel.hasShow = false
|
||||
}
|
||||
//每次都要弹出的弹窗
|
||||
if (frequency == AdFrequency.FREQUENCY_EVERY_LOGIN) {
|
||||
recordAdModel.hasShow = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 去获取广告图片,展示首页弹窗广告
|
||||
* @return false:不弹首页弹窗广告
|
||||
*/
|
||||
private showFirstPagePopupAd(): boolean {
|
||||
if (this.mPopupAdModel == null) {
|
||||
return false
|
||||
}
|
||||
if (!this.isSupportDialogAd()) {
|
||||
return false
|
||||
}
|
||||
this.sendMessage(this.mPopupAdModel)
|
||||
return true
|
||||
}
|
||||
|
||||
private sendMessage(showAdModel: PopupAdMode): void {
|
||||
let isShow = this.showPopupAdDialog(showAdModel)
|
||||
let elkMsg =
|
||||
`handle popup index ad userId:${this.getNotNullUserId()} poPupAdModel:${showAdModel.toString()} showPopupAdDialog:${isShow}`
|
||||
if (isShow) {
|
||||
//发送弹窗广告曝光埋点
|
||||
this.sendPopupAndStat(showAdModel)
|
||||
elkMsg += ` isShowedPopupAds:${this.isShowedPopupAds}`
|
||||
this.isShowedPopupAds = true
|
||||
// 首页弹窗显隐确定时,通知广告组件
|
||||
AdsPopupHost.getInstance().notifyFirstPageMatch(true)
|
||||
} else {
|
||||
// 首页弹窗显隐确定时,通知广告组件
|
||||
AdsPopupHost.getInstance().notifyFirstPageMatch(false)
|
||||
}
|
||||
this.uploadLog(ELK_LEVEL.ERROR, elkMsg)
|
||||
}
|
||||
|
||||
private showPopupAdDialog(showAdModel: PopupAdMode): boolean {
|
||||
if (!this.isAdModelValid(showAdModel)) {
|
||||
return false
|
||||
}
|
||||
//显示投教就先不显示弹窗广告,并计入有弹窗广告没显示
|
||||
if (this.isShowToujiaoViewed()) {
|
||||
this.isHasTanChuangAdNotShow = true
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
let dialogParam: FirstPageDialogParam = new FirstPageDialogParam(showAdModel.toBaseAd(), () => {
|
||||
// 全域运营位广告的5分钟防抖也被首页弹窗影响
|
||||
AdsPopupHost.getInstance().notifyFirstPageClose()
|
||||
// 发送关闭埋点
|
||||
this.sendPopupAdCbas(CBAS_FIRSTPAGE_TANGCHUANGAD_CLOSE, showAdModel)
|
||||
HXLog.d(TAG,
|
||||
`[埋点] sendClickStat page:AD_FIRST_PAGE_DIALOG, adid:${showAdModel.adid}, type:AD_CLICK_TYPE_UNINTEREST`)
|
||||
this.adDialog?.dismiss()
|
||||
this.adDialog = null
|
||||
dialogParam.releaseAdImg()
|
||||
}, () => {
|
||||
this.adDialog?.dismiss()
|
||||
this.adDialog = null
|
||||
dialogParam.releaseAdImg()
|
||||
// 发送点击埋点
|
||||
this.sendPopupAdEnterCbas(showAdModel)
|
||||
HXLog.d(TAG,
|
||||
`[埋点] sendClickStat page:AD_FIRST_PAGE_DIALOG, adid:${showAdModel.adid}, type:AD_CLICK_TYPE_INTEREST`)
|
||||
AdsManager.clickAd(ADS_TYPE_INDEX_POPUP, showAdModel.toBaseAd())
|
||||
if (showAdModel.isOpenInnerWebView) {
|
||||
RouterUtil.jumpPage(showAdModel.jumpUrl)
|
||||
} else {
|
||||
// 外部浏览器打开
|
||||
this.openWithExternalWebView(showAdModel.jumpUrl ?? '')
|
||||
}
|
||||
AdsPopupHost.getInstance().onPageJump(AdsPageConfig.INVALID)
|
||||
AdsPopupHost.getInstance().notifyFirstPageMatch(false)
|
||||
})
|
||||
this.adDialog = DialogHub.getCustomDialog()
|
||||
.setContent(wrapBuilder(FirstPageDialogBuilder), dialogParam)
|
||||
.setStyle({ backgroundColor: Color.Transparent, maskColor: $r('app.color.color_80000000') })
|
||||
.setConfig({ dialogBehavior: { autoDismiss: false } })
|
||||
.build()
|
||||
//加载弹窗图片
|
||||
ImageLoader.load(showAdModel.imgUrl ?? "", (errMsg) => {
|
||||
let elkMsg = `onLoadFailed showAdModel:${showAdModel.toString()} Exception:${errMsg}`
|
||||
this.uploadLog(ELK_LEVEL.ERROR, elkMsg)
|
||||
}, (img) => {
|
||||
if (this.firstPageVisible) {
|
||||
AdsManager.showAd(ADS_TYPE_INDEX_POPUP, showAdModel.toBaseAd())
|
||||
dialogParam.releaseAdImg()
|
||||
dialogParam.adImg = img
|
||||
this.adDialog?.updateContent(dialogParam)
|
||||
// 首页弹窗显隐确定时,通知广告组件
|
||||
AdsPopupHost.getInstance().notifyFirstPageMatch(true)
|
||||
this.adDialog?.show()
|
||||
return
|
||||
}
|
||||
img.release()
|
||||
let elkMsg = `onResourceReady showAdModel:${showAdModel.toString()} frameId:${getCurrentFrameId()}`
|
||||
this.uploadLog(ELK_LEVEL.ERROR, elkMsg)
|
||||
})
|
||||
return true
|
||||
} catch (e) {
|
||||
const err = e as BusinessError
|
||||
let elkMsg = `BadTokenException showAdModel:${showAdModel.toString()} Exception:${err.message}`
|
||||
this.uploadLog(ELK_LEVEL.ERROR, elkMsg)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部浏览器打开
|
||||
*/
|
||||
private openWithExternalWebView(url: string): void {
|
||||
const context = AppUtil.getContext()
|
||||
context.openLink(url).then(() => {
|
||||
HXLog.i(TAG, `openWithExternalWebView success`)
|
||||
}).catch((e: BusinessError) => {
|
||||
HXLog.e(TAG, `openWithExternalWebView failed. code:${e.code} msg:${e.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否支持弹框广告,true,公版默认支持,false,不支持
|
||||
*/
|
||||
isSupportDialogAd(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 对于首页弹窗可能会与投承广告跳转冲突问题进行特殊解决
|
||||
* 投承限定于新用户,对于新用户来说在第一次到首页可能会有投承,所以对新用户第二次回到首页才重新进行显示弹窗
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
isCanShowIndexPopupAd(): boolean {
|
||||
// !NewUserManager.isNewUser() || isHasJumpOtherPage || isHasQhJump
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
isAdModelValid(popupAdMode: PopupAdMode) {
|
||||
return !(!popupAdMode.imgUrl || !popupAdMode.jumpUrl)
|
||||
}
|
||||
|
||||
isShowToujiaoViewed() {
|
||||
return false
|
||||
}
|
||||
|
||||
// ==================== 埋点 ====================
|
||||
private sendPopupAndStat(popupAdModel: PopupAdMode) {
|
||||
// 发送曝光埋点
|
||||
this.sendPopupAdCbas(CBAS_FIRSTPAGE_TANGCHUANGAD_BAOGUANG, popupAdModel)
|
||||
// 发送 HTTPStat 曝光
|
||||
HXLog.d(TAG,
|
||||
`[埋点] sendNonClickStat operationType:AD_OPERATION_SHOW, page:AD_FIRST_PAGE_DIALOG, adid:${popupAdModel.adid}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送非跳转页面类的埋点
|
||||
*/
|
||||
private sendPopupAdCbas(cbas: string, popupAdModel: PopupAdMode) {
|
||||
const cbasObj = CBAS_FIRSTPAGE_PREFIX + cbas
|
||||
const formatCbas = cbasObj.replace('%s', popupAdModel.adid)
|
||||
HXLog.d(TAG, `[埋点] sendPopupAdCbas: ${formatCbas}`)
|
||||
}
|
||||
|
||||
|
||||
private sendPopupAdEnterCbas(popupAdModel: PopupAdMode) {
|
||||
const cbasObj = CBAS_FIRSTPAGE_PREFIX + CBAS_FIRSTPAGE_TANGCHUANGAD_DIANJI
|
||||
const formatCbas = cbasObj.replace('%s', popupAdModel.adid)
|
||||
HXLog.d(TAG, `[埋点] sendPopupAdEnterCbas: ${formatCbas}`)
|
||||
}
|
||||
|
||||
private uploadLog(biz_level: ELK_LEVEL, msg: string) {
|
||||
sendElk('biz_firstpage', msg, biz_level, true, TAG)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* author : liuqingliang@myhexin.com
|
||||
* time : created on 2026/6/2
|
||||
* desc : 广告埋点过滤器 -- 防止同一 adId 在同一轮播周期内重复发送曝光埋点。
|
||||
*/
|
||||
export class StatLogFilter {
|
||||
/** 已发送过的 adId 集合 */
|
||||
private adIdSet: Set<string> = new Set()
|
||||
|
||||
/**
|
||||
* 判断 adId 是否已在过滤列表中,是则跳过埋点。
|
||||
* @param adId 广告 id
|
||||
* @returns true=已存在需过滤,false=新广告需发送
|
||||
*/
|
||||
filterAdId(adId: string): boolean {
|
||||
if (this.adIdSet.has(adId)) {
|
||||
return true
|
||||
} else {
|
||||
this.adIdSet.add(adId)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 adId 添加到过滤列表。
|
||||
* @param adId 广告 id
|
||||
*/
|
||||
addAdId(adId: string): void {
|
||||
this.adIdSet.add(adId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置过滤器,清空所有记录。
|
||||
*/
|
||||
resetFilter(): void {
|
||||
this.adIdSet.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* getAllCards 接口响应
|
||||
*/
|
||||
export interface AllCardsResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: AllCardsFloor[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 楼层数据
|
||||
*/
|
||||
export interface AllCardsFloor {
|
||||
id: number
|
||||
key: string
|
||||
title: string
|
||||
card_list: AllCardsCard[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片配置
|
||||
*/
|
||||
export interface AllCardsCard {
|
||||
card_id: number
|
||||
card_key: string
|
||||
render_key: string
|
||||
card_title: CardTitle
|
||||
card_url: CardUrl
|
||||
mod_data: ModDataItem[]
|
||||
bury_point: string
|
||||
explain_title?: string
|
||||
explain_message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片标题
|
||||
*/
|
||||
export interface CardTitle {
|
||||
value: string
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片跳转链接
|
||||
*/
|
||||
export interface CardUrl {
|
||||
ios: string
|
||||
android: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据请求配置
|
||||
*/
|
||||
export interface ModDataItem {
|
||||
url: string
|
||||
param_list: string[]
|
||||
req_desc: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 诊断接口响应
|
||||
*/
|
||||
export interface AiDiagnosisResponse {
|
||||
status_code: number
|
||||
status_msg: string
|
||||
data: AiDiagnosisData
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 诊断数据
|
||||
*/
|
||||
export class AiDiagnosisData {
|
||||
market: string = ''
|
||||
score: string = ''
|
||||
code: string = ''
|
||||
factor_list: FactorItem[] = []
|
||||
name: string = ''
|
||||
recommend: RecommendItem[] = []
|
||||
message: string = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 因子项
|
||||
*/
|
||||
export class FactorItem {
|
||||
score: string = ''
|
||||
judge: string = ''
|
||||
factor: string = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 推荐合约项
|
||||
*/
|
||||
export class RecommendItem {
|
||||
market: string = ''
|
||||
score: string = ''
|
||||
code: string = ''
|
||||
name: string = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 推荐合约(用于点击跳转)
|
||||
*/
|
||||
export interface RecommendContract {
|
||||
name: string
|
||||
score: string
|
||||
code: string
|
||||
market: string
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* AI 看涨跌接口响应
|
||||
*/
|
||||
export interface AiRiseFallResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: AiRiseFallData
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 看涨跌数据
|
||||
*/
|
||||
export interface AiRiseFallData {
|
||||
date: string // 日期 "2026-06-01"
|
||||
last_trade_date_accuracy: string // 昨日正确率 "66.7"
|
||||
last_twenty_trade_date_accuracy: string // 近20日正确率 "66.4"
|
||||
rise: RiseFallListData // 看涨列表
|
||||
fall: RiseFallListData // 看跌列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 看涨/看跌列表数据
|
||||
*/
|
||||
export interface RiseFallListData {
|
||||
count: number
|
||||
data: RiseFallItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 看涨/看跌项
|
||||
*/
|
||||
export interface RiseFallItem {
|
||||
variety: string // 品种代码 "SA"
|
||||
name: string // 合约名称 "纯碱2609"
|
||||
contract: string // 合约代码 "SA2609"
|
||||
market: string // 市场代码 "67"
|
||||
forecast: number // 预测方向 0=看涨, 1=看跌
|
||||
accuracy: string // 品种正确率 "80.0"
|
||||
show_quote: number // 是否显示行情 1=显示
|
||||
}
|
||||
|
||||
/**
|
||||
* 合约行情数据(用于行情订阅)
|
||||
*/
|
||||
export interface ContractHqData {
|
||||
contract: string
|
||||
market: string
|
||||
name: string
|
||||
priceChg: number | null // 涨跌幅,null 表示待开盘
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
export type BannerNodeModel = {
|
||||
id: number,
|
||||
title: string
|
||||
imgUrl: string
|
||||
jumpUrl: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 商品期权卡片数据模型
|
||||
*
|
||||
* Author: YS
|
||||
* Date: 2026-06-03
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tab 配置项
|
||||
*/
|
||||
export interface CommodityOptionsTab {
|
||||
label: ResourceStr // Tab 标签
|
||||
api: string // API 标识
|
||||
stat: string // 统计标识
|
||||
defaultMsg: ResourceStr // 空数据提示信息
|
||||
index: number // Tab 索引
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab 配置
|
||||
*/
|
||||
export const COMMODITY_OPTIONS_TABS: CommodityOptionsTab[] = [
|
||||
{
|
||||
label: $r('app.string.tab_hot'),
|
||||
api: 'rise_percent',
|
||||
stat: 'hot',
|
||||
defaultMsg: $r('app.string.commodity_options_hot_default_msg'),
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
label: $r('app.string.tab_near_expired'),
|
||||
api: 'near_expired',
|
||||
stat: 'impend',
|
||||
defaultMsg: $r('app.string.commodity_options_near_expired_default_msg'),
|
||||
index: 1,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 推荐期权合约项
|
||||
*/
|
||||
export class CommodityOptionsContract {
|
||||
contract_code: string = '' // 合约代码
|
||||
contract_name: string = '' // 合约名称
|
||||
market: string = '' // 市场代码
|
||||
expired_date: string = '' // 到期日(格式:20250628)
|
||||
}
|
||||
|
||||
/**
|
||||
* API 响应
|
||||
*/
|
||||
export interface CommodityOptionsResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: CommodityOptionsContract[]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export class CustomEntryListNodeModel {
|
||||
id: number = -1
|
||||
title: string = ''
|
||||
imgUrl: string | Resource = ''
|
||||
jumpUrl: string = ''
|
||||
position: number = -1
|
||||
startVersion: number = 0
|
||||
endVersion: number = 0
|
||||
buriedPoint: string = ''
|
||||
buriedPointSource: string = ''
|
||||
gridTag: string = ''
|
||||
gridTagEffect: string = ''
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
export type EntryListNodeModel = {
|
||||
id: number,
|
||||
title: string,
|
||||
imgUrl: string,
|
||||
jumpUrl: string,
|
||||
startVersion: string,
|
||||
endVersion: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
buriedPoint: string,
|
||||
buriedPointSource: string,
|
||||
gridTag: string
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 热点排行数据模型
|
||||
* @author YS
|
||||
* @date 2026-06-03
|
||||
*/
|
||||
|
||||
/**
|
||||
* 热点排行数据项
|
||||
* API 返回字段: title, url, tag, color, sorts, hot_value
|
||||
*/
|
||||
export interface HotListItemModel {
|
||||
url: string // 跳转地址
|
||||
title: string // 资讯标题
|
||||
tag: string | null // 标签名称(可能为 null)
|
||||
color: string | null // 颜色名称("red" | "orange",可能为 null)
|
||||
sorts: number // 排序值
|
||||
hot_value: string // 热度值
|
||||
}
|
||||
|
||||
/**
|
||||
* 热点排行接口返回结果
|
||||
*/
|
||||
export interface HotListResultModel {
|
||||
code: number // 接口返回码
|
||||
msg: string // 返回消息
|
||||
data: HotListItemModel[] // 热点数据列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色名称到资源ID的映射
|
||||
*/
|
||||
export const COLOR_NAME_MAP: Record<string, Resource> = {
|
||||
'purple': $r('app.color.hotlist_purple'),
|
||||
'blue': $r('app.color.hotlist_blue'),
|
||||
'acidblue': $r('app.color.hotlist_acid_blue'),
|
||||
'green': $r('app.color.hotlist_green'),
|
||||
'orange': $r('app.color.hotlist_orange'),
|
||||
'red': $r('app.color.hotlist_red'),
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认颜色资源
|
||||
*/
|
||||
const DEFAULT_COLOR: Resource = $r('app.color.hotlist_blue')
|
||||
|
||||
/**
|
||||
* 根据颜色名称获取颜色值
|
||||
*/
|
||||
export function getColorValue(colorName: string | null): ResourceStr {
|
||||
if (!colorName) {
|
||||
return DEFAULT_COLOR
|
||||
}
|
||||
const color = COLOR_NAME_MAP[colorName]
|
||||
return color ?? DEFAULT_COLOR
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
export type HotNewsNodeModelResult = {
|
||||
code: string,
|
||||
mes: string,
|
||||
data: HotNewsNodeModel[]
|
||||
}
|
||||
|
||||
export type HotNewsNodeModel = {
|
||||
seq: string, // 资讯唯一标识
|
||||
title: string, // 资讯标题
|
||||
source: string, // 来源
|
||||
url: string, // 资讯跳转地址
|
||||
img_url: string, // 图片地址
|
||||
create_time: string, // 资讯发布时间
|
||||
copyright: string, // 资讯版权
|
||||
type: string, // 资讯分类
|
||||
tags: HotNewsTagNodeModel[], // 资讯标签
|
||||
}
|
||||
|
||||
export type HotNewsTagNodeModel = {
|
||||
seq: string, // 资讯唯一标识
|
||||
name: string, // 标签名称
|
||||
weight: string, // 资讯权重
|
||||
code: string, // 品种代码
|
||||
market: string, // 市场id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { BaseAd, FatigueDegree } from "@b2c-f/fuhm-ads"
|
||||
|
||||
/**
|
||||
* author : liuqingliang@myhexin.com
|
||||
* time : created on 2026/2/27
|
||||
* desc : 弹窗广告数据模型
|
||||
*/
|
||||
export class PopupAdMode {
|
||||
adid: string = ''
|
||||
startTime: number = 0
|
||||
endTime: number = 0
|
||||
imgUrl: string | null = null
|
||||
jumpUrl: string | null = null
|
||||
isOpenInnerWebView: boolean = false
|
||||
frequency: AdFrequency = AdFrequency.FREQUENCY_EVERY_LOGIN
|
||||
position: number = 0
|
||||
/**
|
||||
* 上次显示时间
|
||||
*/
|
||||
showTime: number = 0
|
||||
/**
|
||||
* 上次是否显示过
|
||||
*/
|
||||
hasShow: boolean = false
|
||||
fatigueDegree: FatigueDegree | null = null
|
||||
|
||||
toString(): string {
|
||||
return `PopupAdMode (adid='${this.adid}', frequency=${this.frequency}, position=${this.position}, hasShow=${this.hasShow}, fatigueDegree=${this.fatigueDegree})`
|
||||
}
|
||||
|
||||
toBaseAd() {
|
||||
let newAd = new BaseAd()
|
||||
newAd.adId = this.adid
|
||||
newAd.startTime = this.startTime
|
||||
newAd.endTime = this.endTime
|
||||
newAd.imgUrl = this.imgUrl ?? ""
|
||||
newAd.jumpUrl = this.jumpUrl ?? ""
|
||||
newAd.isOpenInnerWebView = this.isOpenInnerWebView
|
||||
newAd.fatigueDegree = this.fatigueDegree
|
||||
newAd.position = this.position
|
||||
return newAd
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页弹窗广告显示频率
|
||||
*/
|
||||
export enum AdFrequency {
|
||||
/**
|
||||
* 有效期间内,每次登录都显示
|
||||
*/
|
||||
FREQUENCY_EVERY_LOGIN = 0,
|
||||
/**
|
||||
* 有效期间内,每天显示一次
|
||||
*/
|
||||
FREQUENCY_EVERY_DAY = 1,
|
||||
/**
|
||||
* 有效期间内,只显示一次
|
||||
*/
|
||||
FREQUENCY_ONLY_ONCE = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页cpyyy适配器
|
||||
* */
|
||||
export function adapterForIndexPopupActivity(newAdModels: BaseAd[]) {
|
||||
let oldAdModels: PopupAdMode[] = []
|
||||
newAdModels.forEach((it) => {
|
||||
let oldPopupAdMode = new PopupAdMode()
|
||||
oldPopupAdMode.adid = it.adId ?? ""
|
||||
oldPopupAdMode.startTime = it.startTime
|
||||
oldPopupAdMode.endTime = it.endTime
|
||||
oldPopupAdMode.imgUrl = it.imgUrl
|
||||
oldPopupAdMode.jumpUrl = it.jumpUrl
|
||||
oldPopupAdMode.isOpenInnerWebView = it.isOpenInnerWebView
|
||||
oldPopupAdMode.frequency =
|
||||
it.fatigueDegree?.isDailyShowOnce == 1 ? AdFrequency.FREQUENCY_EVERY_DAY : AdFrequency.FREQUENCY_EVERY_LOGIN
|
||||
oldPopupAdMode.fatigueDegree = it.fatigueDegree
|
||||
oldPopupAdMode.position = it.position
|
||||
oldAdModels.push(oldPopupAdMode)
|
||||
})
|
||||
return oldAdModels
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { HXUtils, StockMarketUtil } from "@b2c/lib_baseui";
|
||||
import { NumberUtils } from "hxutil";
|
||||
|
||||
export interface SelfSelectedStockModel {
|
||||
id: string;
|
||||
stockCode: string;
|
||||
stockName: string;
|
||||
currentPrice: number;
|
||||
changeAmount: number;
|
||||
changePercent: number;
|
||||
updateTime?: number;
|
||||
}
|
||||
|
||||
export interface SelfSelectedStockModelMock {
|
||||
code: string;
|
||||
name: string;
|
||||
price: number;
|
||||
change: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
export interface StockGroupModel {
|
||||
id: string;
|
||||
groupName: string;
|
||||
stocks: SelfSelectedStockModel[];
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
|
||||
const HQ_DATA_INVALID = "--"
|
||||
|
||||
@Observed
|
||||
export class SwiperHqDataItemModel extends Array<GridHqDataItemModel>{}
|
||||
|
||||
@Observed
|
||||
export class GridHqDataItemModel extends Array<HQDataItemModel>{}
|
||||
|
||||
/**
|
||||
* 行情自定义数据结构
|
||||
*/
|
||||
@Observed
|
||||
export class HQDataItemModel{
|
||||
name:string = HQ_DATA_INVALID
|
||||
code:string = HQ_DATA_INVALID
|
||||
showCode:string = HQ_DATA_INVALID
|
||||
market:string = "0"
|
||||
price:string = HQ_DATA_INVALID
|
||||
riseFail:string = HQ_DATA_INVALID //涨跌幅
|
||||
priceRise:string = HQ_DATA_INVALID
|
||||
nameColor:ResourceColor = 0
|
||||
priceColor:ResourceColor = 0
|
||||
riseFailColor:ResourceColor = 0
|
||||
priceRiseColor:ResourceColor = 0
|
||||
showAddImage:boolean = false
|
||||
/**
|
||||
* -1:跌,1:涨,0:不变
|
||||
*/
|
||||
riseFailFlag:number = 0
|
||||
|
||||
priceChange:boolean = false
|
||||
|
||||
|
||||
|
||||
constructor(stockCode?:string,stockMarket?:string,stockName?:string) {
|
||||
this.code = stockCode??HQ_DATA_INVALID
|
||||
this.market = stockMarket??HQ_DATA_INVALID
|
||||
this.name = stockName??HQ_DATA_INVALID
|
||||
this.showCode = stockCode??HQ_DATA_INVALID
|
||||
}
|
||||
|
||||
copyFrom(dataItemModel:HQDataItemModel){
|
||||
this.name = dataItemModel.name
|
||||
const market = dataItemModel.market
|
||||
const showCodeTemp = dataItemModel.showCode
|
||||
if ((StockMarketUtil.isOption(market+"") || StockMarketUtil.isFutureTL(market+"")) && HXUtils.isValidContractData(showCodeTemp+"")) {
|
||||
this.showCode = showCodeTemp
|
||||
}else{
|
||||
this.showCode = this.code
|
||||
}
|
||||
|
||||
this.riseFailFlag = this.getRiseFailFlag(dataItemModel.price,this.price)
|
||||
this.priceChange = this.riseFailFlag != 0
|
||||
this.price = dataItemModel.price
|
||||
this.riseFail = dataItemModel.riseFail
|
||||
this.priceRise = dataItemModel.priceRise
|
||||
this.riseFailColor = dataItemModel.riseFailColor
|
||||
this.riseFailColor = dataItemModel.riseFailColor
|
||||
}
|
||||
|
||||
getMapKey():string{
|
||||
return this.code+"_"+this.market
|
||||
}
|
||||
|
||||
|
||||
|
||||
private getRiseFailFlag(newPrice:string,lastPrice:string){
|
||||
let ret = 0
|
||||
if (newPrice == lastPrice){
|
||||
return ret
|
||||
}
|
||||
if (NumberUtils.isNumeric(newPrice) && NumberUtils.isNumeric(lastPrice)) {
|
||||
const newPriceNumber = parseFloat(newPrice)
|
||||
const lastPriceNumber = parseFloat(lastPrice)
|
||||
if (newPriceNumber == lastPriceNumber) {
|
||||
ret = 0
|
||||
}else {
|
||||
ret = newPriceNumber > lastPriceNumber ? 1:-1
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
copy():HQDataItemModel{
|
||||
let copyDataItemModel = new HQDataItemModel(this.code,this.market,this.name)
|
||||
copyDataItemModel.showCode = this.showCode
|
||||
copyDataItemModel.price = this.price
|
||||
copyDataItemModel.riseFail = this.riseFail
|
||||
copyDataItemModel.priceRise = this.priceRise
|
||||
copyDataItemModel.nameColor = this.nameColor
|
||||
copyDataItemModel.riseFailColor = this.riseFailColor
|
||||
copyDataItemModel.priceRiseColor = this.priceRiseColor
|
||||
copyDataItemModel.showAddImage = this.showAddImage
|
||||
copyDataItemModel.priceChange = this.priceChange
|
||||
copyDataItemModel.riseFailFlag = this.riseFailFlag
|
||||
return copyDataItemModel
|
||||
}
|
||||
}
|
||||
|
||||
export class GridDataSource implements IDataSource{
|
||||
private dataArray:HQDataItemModel[] = []
|
||||
|
||||
constructor(data:HQDataItemModel[]) {
|
||||
this.dataArray = data
|
||||
}
|
||||
totalCount(): number {
|
||||
return this.dataArray.length
|
||||
}
|
||||
|
||||
getData(index: number): HQDataItemModel {
|
||||
return this.dataArray[index]
|
||||
}
|
||||
|
||||
registerDataChangeListener(listener: DataChangeListener): void {
|
||||
|
||||
}
|
||||
|
||||
unregisterDataChangeListener(listener: DataChangeListener): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,782 @@
|
||||
import { LengthMetrics, router } from '@kit.ArkUI';
|
||||
import { emitter } from '@kit.BasicServicesKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { AiDiagnosisDataFetcher } from '../datacenter/AiDiagnosisDataFetcher';
|
||||
import { AiDiagnosisData, AllCardsCard, FactorItem, RecommendItem } from '../model/AiDiagnosisNodeModel';
|
||||
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel';
|
||||
import { AiDiagnosisRequestClient } from '../clients/AiDiagnosisRequestClient';
|
||||
import { HeaderItem, RowData, CellArray, TableData } from '@b2b/hq_table';
|
||||
import { EmitterConstants, GlobalContext, HXLog } from 'biz_common';
|
||||
import { RouterUtil } from '../../util/RouterUtil';
|
||||
import { enableDarkMode } from '@kernel/theme_manager';
|
||||
import { TableConstants } from '@b2c/lib_baseui';
|
||||
import { QuoteCustomSettingManager, StandardPriceType } from 'biz_quote';
|
||||
import { TabType } from 'biz_quote/src/main/ets/config/tabConfig';
|
||||
import util from '@ohos.util';
|
||||
|
||||
// 评分背景图片配置文件
|
||||
const AI_SCORE_BG_IMAGES_FILE = 'ai_score_background_images.json'
|
||||
|
||||
// 评分背景图片配置数据结构
|
||||
interface ScoreBgImagesConfig {
|
||||
light: Record<string, string>
|
||||
dark: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 诊断节点组件
|
||||
* @author yansong@myhexin.com
|
||||
*/
|
||||
|
||||
// AI诊断卡片行情请求 FrameId 和 PageId
|
||||
const FRAME_ID_AI_DIAGNOSIS = 2201
|
||||
const PAGE_ID_AI_DIAGNOSIS = 4106
|
||||
// 因子列表最大显示数量
|
||||
const FACTOR_LIST_MAX = 3
|
||||
|
||||
@Component
|
||||
export struct AiDiagnosisNodeComponent {
|
||||
nodeModel: FirstPageNodeModel | null = null
|
||||
|
||||
// 刷新触发器(父组件通过 @Link 传入,每次递增触发刷新)
|
||||
@Link @Watch('onRefreshTriggerChange') refreshTrigger: number
|
||||
|
||||
// Tab 可见性(用于控制行情请求的 request/release)
|
||||
@Watch('onTabVisibilityChange') @Consume firstPageVisible: boolean = true
|
||||
|
||||
// 深色模式状态
|
||||
@StorageProp(enableDarkMode) isDarkMode: boolean = false
|
||||
|
||||
@State private cardConfig: AllCardsCard | null = null
|
||||
@State private cardData: AiDiagnosisData | null = null
|
||||
|
||||
// 行情数据
|
||||
@State private priceChg: string = '--' // 涨跌幅
|
||||
@State private priceChgColor: ResourceStr = $r('app.color.price_even') // 涨跌幅颜色
|
||||
|
||||
// 行情订阅客户端
|
||||
private hqRequestClient: AiDiagnosisRequestClient | null = null
|
||||
|
||||
// 评分背景图片配置
|
||||
private scoreBgImagesLight: Record<string, string> = {}
|
||||
private scoreBgImagesDark: Record<string, string> = {}
|
||||
|
||||
// TCP 重连监听
|
||||
private mIsEventSubscribed: boolean = false
|
||||
private netRelinkCallBack = () => {
|
||||
if (this.firstPageVisible) {
|
||||
setTimeout(() => {
|
||||
if (this.cardData && this.cardData.code && this.cardData.market) {
|
||||
this.subscribeHqData(this.cardData.code, this.cardData.market)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 上一次刷新时使用的基准价类型,用于在变可见时判断是否需要按新基准价重新请求
|
||||
private lastStandardPriceType: StandardPriceType =
|
||||
QuoteCustomSettingManager.getInstance().getStandardPriceType()
|
||||
|
||||
aboutToAppear(): void {
|
||||
// 加载评分背景图片配置
|
||||
this.loadScoreBgImages()
|
||||
|
||||
// 初始化数据获取器回调
|
||||
AiDiagnosisDataFetcher.getInstance().onCardConfigChange = (config) => {
|
||||
this.cardConfig = config
|
||||
}
|
||||
|
||||
AiDiagnosisDataFetcher.getInstance().onCardDataChange = (data) => {
|
||||
this.cardData = data
|
||||
// 当卡片数据更新时,重新订阅行情
|
||||
if (data && data.code && data.market) {
|
||||
this.subscribeHqData(data.code, data.market)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始请求数据
|
||||
this.refreshCardData()
|
||||
|
||||
// 订阅 TCP 重连事件
|
||||
if (this.firstPageVisible) {
|
||||
this.subscribeTcpNetStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载评分背景图片配置
|
||||
*/
|
||||
private loadScoreBgImages(): void {
|
||||
const rawData = this.readRawFile(AI_SCORE_BG_IMAGES_FILE)
|
||||
if (!rawData) {
|
||||
HXLog.e('AiDiagnosisNodeComponent', 'loadScoreBgImages: read file failed')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const config = JSON.parse(rawData) as ScoreBgImagesConfig
|
||||
this.scoreBgImagesLight = config.light || {}
|
||||
this.scoreBgImagesDark = config.dark || {}
|
||||
HXLog.d('AiDiagnosisNodeComponent', 'loadScoreBgImages: success')
|
||||
} catch (e) {
|
||||
const error = e as BusinessError
|
||||
HXLog.e('AiDiagnosisNodeComponent', 'loadScoreBgImages: parse error ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 rawfile 读取配置文件
|
||||
*/
|
||||
private readRawFile(fileName: string): string {
|
||||
try {
|
||||
const context = GlobalContext.get()
|
||||
const mgr = context.resourceManager
|
||||
const rawFile = mgr.getRawFileContentSync(fileName)
|
||||
let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
|
||||
return textDecoder.decodeToString(rawFile, { stream: false })
|
||||
} catch (e) {
|
||||
const error = e as BusinessError
|
||||
HXLog.e('AiDiagnosisNodeComponent', `readRawFile ${fileName} error: ${error.code} ${error.message}`)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新触发器变化时的回调
|
||||
* 首页下拉刷新完成后会触发此方法
|
||||
*/
|
||||
onRefreshTriggerChange(): void {
|
||||
HXLog.d('AiDiagnosisNodeComponent', 'onRefreshTriggerChange triggered, refreshTrigger: ' + this.refreshTrigger)
|
||||
this.refreshCardData()
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab 可见性变化时的回调
|
||||
* 可见时请求行情数据,不可见时释放行情请求
|
||||
*/
|
||||
onTabVisibilityChange(): void {
|
||||
HXLog.d('AiDiagnosisNodeComponent', 'onTabVisibilityChange: ' + this.firstPageVisible)
|
||||
if (this.firstPageVisible) {
|
||||
// 可见时重新请求数据
|
||||
if (this.cardData && this.cardData.code && this.cardData.market) {
|
||||
this.subscribeHqData(this.cardData.code, this.cardData.market)
|
||||
}
|
||||
// 订阅 TCP 重连事件
|
||||
this.subscribeTcpNetStatus()
|
||||
// 检测基准价是否切换,切换后重新订阅
|
||||
const currentStandardPriceType = QuoteCustomSettingManager.getInstance().getStandardPriceType()
|
||||
if (this.lastStandardPriceType !== currentStandardPriceType) {
|
||||
HXLog.d('AiDiagnosisNodeComponent', 'Standard price type changed, resubscribe')
|
||||
this.lastStandardPriceType = currentStandardPriceType
|
||||
if (this.cardData && this.cardData.code && this.cardData.market) {
|
||||
this.subscribeHqData(this.cardData.code, this.cardData.market)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不可见时释放行情订阅
|
||||
this.releaseHqClient()
|
||||
// 取消订阅 TCP 重连事件
|
||||
this.unSubscribeTcpNetStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新卡片数据(诊断数据 + 重新订阅行情)
|
||||
*/
|
||||
refreshCardData(): void {
|
||||
AiDiagnosisDataFetcher.getInstance().fetchCardConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放行情订阅客户端
|
||||
*/
|
||||
private releaseHqClient(): void {
|
||||
if (this.hqRequestClient) {
|
||||
this.hqRequestClient.release(true)
|
||||
this.hqRequestClient = null
|
||||
}
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.releaseHqClient()
|
||||
this.unSubscribeTcpNetStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅 TCP 重连事件
|
||||
*/
|
||||
private subscribeTcpNetStatus(): void {
|
||||
if (this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = true
|
||||
emitter.on({ eventId: EmitterConstants.NETWORK_TCP_RECONNECT }, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅 TCP 重连事件
|
||||
*/
|
||||
private unSubscribeTcpNetStatus(): void {
|
||||
if (!this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = false
|
||||
emitter.off(EmitterConstants.NETWORK_TCP_RECONNECT, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅行情数据(涨跌幅)
|
||||
* 参考自选股模块实现,使用 AiDiagnosisRequestClient
|
||||
*/
|
||||
private subscribeHqData(code: string, market: string): void {
|
||||
// 释放之前的订阅
|
||||
if (this.hqRequestClient) {
|
||||
this.hqRequestClient.release(true)
|
||||
this.hqRequestClient = null
|
||||
}
|
||||
|
||||
// 创建行情请求头配置
|
||||
const headerConfig: HeaderItem[] = [
|
||||
new HeaderItem(TableConstants.DATA_ID_NAME, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_PRICE, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_ZF, '')
|
||||
]
|
||||
|
||||
// 创建请求客户端(使用 AiDiagnosisRequestClient,参考自选股实现)
|
||||
this.hqRequestClient = new AiDiagnosisRequestClient(
|
||||
FRAME_ID_AI_DIAGNOSIS,
|
||||
PAGE_ID_AI_DIAGNOSIS,
|
||||
code,
|
||||
market,
|
||||
headerConfig,
|
||||
null,
|
||||
(tableData: TableData | string) => {
|
||||
this.onHqDataReceive(tableData)
|
||||
}
|
||||
)
|
||||
|
||||
// 发送请求
|
||||
this.hqRequestClient.request(0, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理行情数据回调
|
||||
*/
|
||||
private onHqDataReceive(tableData: TableData | string): void {
|
||||
if (tableData instanceof TableData) {
|
||||
const rows = tableData?.tableRows
|
||||
if (rows && rows.length > 0) {
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i]
|
||||
if (row instanceof RowData) {
|
||||
const cols: CellArray = row.scrollableCols
|
||||
if (cols && cols.length > 0) {
|
||||
// 解析涨跌幅数据
|
||||
for (let j = 0; j < cols.length; j++) {
|
||||
const cellData = cols[j]
|
||||
if (cellData.dataId === TableConstants.DATA_ID_ZF) {
|
||||
this.priceChg = cellData.data || '--'
|
||||
// 根据涨跌幅设置颜色
|
||||
const numValue = parseFloat(this.priceChg)
|
||||
if (!isNaN(numValue)) {
|
||||
if (numValue > 0) {
|
||||
this.priceChgColor = $r('app.color.price_up_100') // 红色
|
||||
} else if (numValue < 0) {
|
||||
this.priceChgColor = $r('app.color.price_down_100') // 绿色
|
||||
} else {
|
||||
this.priceChgColor = $r('app.color.price_even') // 灰色
|
||||
}
|
||||
} else {
|
||||
this.priceChgColor = $r('app.color.price_even')
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化涨跌幅,与AI看涨跌保持一致
|
||||
*/
|
||||
formatPriceChg(rawValue: string): string {
|
||||
if (!rawValue || rawValue === '--') {
|
||||
return '--'
|
||||
}
|
||||
const numValue = parseFloat(rawValue)
|
||||
if (isNaN(numValue)) {
|
||||
return '--'
|
||||
}
|
||||
if (numValue > 0) {
|
||||
return `+${numValue.toFixed(2)}%`
|
||||
} else if (numValue < 0) {
|
||||
return `${numValue.toFixed(2)}%`
|
||||
} else {
|
||||
return '0.00%'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评分文案
|
||||
*/
|
||||
getScoreText(score: string | number): ResourceStr {
|
||||
const numScore = parseInt(String(score))
|
||||
if (isNaN(numScore)) {
|
||||
return '--'
|
||||
}
|
||||
if (numScore === 0) {
|
||||
return $r('app.string.score_neutral')
|
||||
}
|
||||
if (numScore > 0) {
|
||||
if (numScore < 40) return $r('app.string.score_weak_bull')
|
||||
if (numScore < 80) return $r('app.string.score_medium_bull')
|
||||
return $r('app.string.score_strong_bull')
|
||||
} else {
|
||||
const absScore = Math.abs(numScore)
|
||||
if (absScore < 40) return $r('app.string.score_weak_bear')
|
||||
if (absScore < 80) return $r('app.string.score_medium_bear')
|
||||
return $r('app.string.score_strong_bear')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评分颜色
|
||||
*/
|
||||
getScoreColor(score: string | number): ResourceStr {
|
||||
const numScore = parseInt(String(score))
|
||||
if (isNaN(numScore) || numScore === 0) {
|
||||
return $r('app.color.ai_score_neutral')
|
||||
}
|
||||
return numScore > 0 ? $r('app.color.price_up_100') : $r('app.color.price_down_100') // 红涨绿跌
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取因子方向颜色
|
||||
*/
|
||||
getFactorColor(judge: string): ResourceStr {
|
||||
if (judge.includes('利多') || judge.includes('多')) {
|
||||
return $r('app.color.price_up_100')
|
||||
}
|
||||
if (judge.includes('利空') || judge.includes('空')) {
|
||||
return $r('app.color.price_down_100')
|
||||
}
|
||||
return $r('app.color.ai_score_neutral')
|
||||
}
|
||||
|
||||
handleTitleClick() {
|
||||
// 如果没有合约信息,则跳转到 WebView 详情页
|
||||
if (!this.cardConfig?.card_url) {
|
||||
return
|
||||
}
|
||||
const url = this.cardConfig.card_url.ios || this.cardConfig.card_url.android || ''
|
||||
if (url.length === 0) {
|
||||
return
|
||||
}
|
||||
// 使用 RouterUtil 处理 client.html 协议格式的 URL
|
||||
RouterUtil.jumpPage(url, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理卡片点击
|
||||
*/
|
||||
handleCardClick(): void {
|
||||
if (this.cardData && this.cardData.code && this.cardData.market) {
|
||||
const codeList = this.cardData.code
|
||||
const marketList = this.cardData.market
|
||||
const nameList = this.cardData.name
|
||||
this.jumpToQuote(codeList, marketList, codeList, marketList, nameList)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理推荐合约点击
|
||||
*/
|
||||
handleRecommendClick(item: RecommendItem): void {
|
||||
if (!item.code || !item.market) {
|
||||
return
|
||||
}
|
||||
// 构建推荐合约列表(包括当前推荐合约)
|
||||
const codeList = this.cardData?.recommend?.map(r => r.code).join(',') || item.code
|
||||
const marketList = this.cardData?.recommend?.map(r => r.market).join(',') || item.market
|
||||
const nameList = this.cardData?.recommend?.map(r => r.name).join(',') || item.name
|
||||
|
||||
// 跳转到行情详情页(分时页面)
|
||||
this.jumpToQuote(item.code, item.market, codeList, marketList, nameList)
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到行情详情页
|
||||
*/
|
||||
private jumpToQuote(code: string, market: string, codeList: string, marketList: string, nameList: string): void {
|
||||
router.pushUrl({
|
||||
url: 'pages/QuoteDetailPage',
|
||||
params: {
|
||||
marketId: market,
|
||||
code: code,
|
||||
name: nameList,
|
||||
codeList: codeList,
|
||||
marketIdList: marketList,
|
||||
nameList: nameList,
|
||||
periodIndex: 0, // 默认显示分时
|
||||
defaultTabType: TabType.F10, // 切换到基本面(F10) Tab,
|
||||
enableScrollToTargetTab: false // 禁止跳转到目标tab,保持在分时页顶部
|
||||
}
|
||||
}, router.RouterMode.Standard, (err) => {
|
||||
if (err) {
|
||||
HXLog.e('AiDiagnosisNodeComponent', `jumpToQuote failed: ${err.code}, ${err.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 标题栏
|
||||
this.buildTitleBar()
|
||||
|
||||
// 卡片内容
|
||||
if (this.cardData) {
|
||||
this.buildCardContent()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.surface_layer1_foreground'))
|
||||
.borderRadius(4)
|
||||
.padding({ left: 10, right: 10, bottom: 10 })
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildTitleBar() {
|
||||
Row() {
|
||||
Row() {
|
||||
Text(this.cardConfig?.card_title?.value || $r('app.string.ai_diagnosis_title'))
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(18)
|
||||
.fontWeight(500)
|
||||
|
||||
Image($r('app.media.icon_arrow_forward_20'))
|
||||
.width(20)
|
||||
.height(20)
|
||||
.fillColor($r('app.color.elements_icon_primary_02'))
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.height('100%')
|
||||
.onClick(() => this.handleTitleClick())
|
||||
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildCardContent() {
|
||||
Column() {
|
||||
Column({ space: 12 }) {
|
||||
// 合约信息
|
||||
this.buildContractInfo()
|
||||
|
||||
// 评分区域
|
||||
this.buildScoreArea()
|
||||
}
|
||||
.borderRadius(4)
|
||||
.padding(8)
|
||||
.linearGradient(this.getScoreAreaGradientConfig())
|
||||
.onClick(() => this.handleCardClick())
|
||||
|
||||
// AI诊断说明
|
||||
if (this.cardData && this.cardData.message && this.cardData.message.length > 0) {
|
||||
this.buildDiagnosisText()
|
||||
}
|
||||
|
||||
// 推荐合约
|
||||
if (this.cardData && this.cardData.recommend && this.cardData.recommend.length > 0) {
|
||||
this.buildRecommendContracts()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.borderRadius(8)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评分区域渐变配置
|
||||
* 从上到下渐变:从不透明到透明
|
||||
*/
|
||||
getScoreAreaGradientConfig(): LinearGradientOptions {
|
||||
const score = this.cardData ? parseInt(this.cardData.score) : 0
|
||||
if (isNaN(score) || score === 0) {
|
||||
// 橙色渐变
|
||||
return {
|
||||
direction: GradientDirection.Top,
|
||||
colors: [[$r('app.color.ai_score_gradient_neutral_start'), 0.0], [$r('app.color.ai_score_gradient_neutral_end'), 1.0]]
|
||||
}
|
||||
}
|
||||
if (score > 0) {
|
||||
// 红色渐变
|
||||
return {
|
||||
direction: GradientDirection.Top,
|
||||
colors: [[$r('app.color.ai_score_gradient_bull_start'), 0.0], [$r('app.color.ai_score_gradient_bull_end'), 1.0]]
|
||||
}
|
||||
}
|
||||
// 绿色渐变
|
||||
return {
|
||||
direction: GradientDirection.Top,
|
||||
colors: [[$r('app.color.ai_score_gradient_bear_start'), 0.0], [$r('app.color.ai_score_gradient_bear_end'), 1.0]]
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildContractInfo() {
|
||||
Row() {
|
||||
Text(this.cardData ? this.cardData.name : '--')
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(16)
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(20)
|
||||
.fontWeight(500)
|
||||
|
||||
Blank().width(10)
|
||||
|
||||
// 涨跌幅
|
||||
Text(this.formatPriceChg(this.priceChg))
|
||||
.fontColor(this.priceChgColor)
|
||||
.fontSize(14)
|
||||
.fontWeight(500)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildScoreArea() {
|
||||
Row() {
|
||||
// 评分圆盘
|
||||
this.buildScoreDial()
|
||||
|
||||
// 因子表格
|
||||
this.buildFactorTable()
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildScoreDial() {
|
||||
Stack({ alignContent: Alignment.Bottom }) {
|
||||
// 背景图片
|
||||
Image(this.getScoreBackgroundImage(this.cardData ? this.cardData.score : '0'))
|
||||
.borderRadius(12)
|
||||
.objectFit(ImageFit.Contain)
|
||||
|
||||
// 文字叠加层
|
||||
Column() {
|
||||
Text(this.cardData ? this.cardData.score : '--')
|
||||
.fontColor(this.getScoreTextColor())
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
|
||||
Text(this.getScoreText(this.cardData ? this.cardData.score : 0))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ bottom: 10 })
|
||||
.justifyContent(FlexAlign.End)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.margin({ right: 12 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评分文字颜色
|
||||
* 根据评分正负返回:红涨绿跌
|
||||
*/
|
||||
getScoreTextColor(): ResourceStr {
|
||||
const score = this.cardData ? parseInt(this.cardData.score) : 0
|
||||
if (isNaN(score) || score === 0) {
|
||||
// 0或无效值:深色(黑色)
|
||||
return $r('app.color.price_even')
|
||||
}
|
||||
// 正数红色,负数绿色
|
||||
return score > 0 ? $r('app.color.price_up_100') : $r('app.color.price_down_100')
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据评分和主题获取背景图片 URL
|
||||
*/
|
||||
getScoreBackgroundImage(score: string): string {
|
||||
const images = this.isDarkMode ? this.scoreBgImagesDark : this.scoreBgImagesLight
|
||||
const numScore = parseInt(score)
|
||||
if (isNaN(numScore)) {
|
||||
return images['neutral']
|
||||
}
|
||||
// 根据评分区间返回对应图片
|
||||
if (numScore <= -60) {
|
||||
return images['strong_bear']
|
||||
} else if (numScore <= -40) {
|
||||
return images['medium_bear']
|
||||
} else if (numScore <= -20) {
|
||||
return images['weak_bear']
|
||||
} else if (numScore < 0) {
|
||||
return images['slight_bear']
|
||||
} else if (numScore === 0) {
|
||||
return images['neutral']
|
||||
} else if (numScore < 20) {
|
||||
return images['slight_bull']
|
||||
} else if (numScore < 40) {
|
||||
return images['weak_bull']
|
||||
} else if (numScore < 60) {
|
||||
return images['medium_bull']
|
||||
} else {
|
||||
return images['strong_bull']
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildFactorTable() {
|
||||
Column() {
|
||||
// 表头
|
||||
Row() {
|
||||
// 因子列 - 有右边框
|
||||
Column() {
|
||||
Text($r('app.string.table_header_factor'))
|
||||
.fontColor($r('app.color.elements_text_secondary'))
|
||||
.fontSize(12)
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.border({
|
||||
width: { right: 1 },
|
||||
color: $r('app.color.elements_others_divider_primary')
|
||||
})
|
||||
|
||||
// 方向列
|
||||
Column() {
|
||||
Text($r('app.string.table_header_direction'))
|
||||
.fontColor($r('app.color.elements_text_secondary'))
|
||||
.fontSize(12)
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.End)
|
||||
}
|
||||
.width(50)
|
||||
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
|
||||
.alignItems(HorizontalAlign.End)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// 因子列表
|
||||
ForEach((this.cardData ? this.cardData.factor_list.slice(0, FACTOR_LIST_MAX) : []), (item: FactorItem, index: number) => {
|
||||
Row() {
|
||||
// 因子列 - 有右边框
|
||||
Column() {
|
||||
Text(item.factor)
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(12)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.border({
|
||||
width: { right: 1 },
|
||||
color: $r('app.color.elements_others_divider_primary')
|
||||
})
|
||||
|
||||
// 方向列
|
||||
Column() {
|
||||
Text(item.judge)
|
||||
.fontColor(this.getFactorColor(item.judge))
|
||||
.fontSize(12)
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.End)
|
||||
.textVerticalAlign(TextVerticalAlign.CENTER)
|
||||
}
|
||||
.width(50)
|
||||
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
|
||||
.alignItems(HorizontalAlign.End)
|
||||
}
|
||||
.border({
|
||||
width: { top: 1 },
|
||||
color: $r('app.color.elements_others_divider_primary')
|
||||
})
|
||||
.width('100%')
|
||||
}, (item: FactorItem) => item.factor)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.borderRadius(8)
|
||||
.border({
|
||||
width: 1,
|
||||
color: $r('app.color.elements_others_divider_primary'),
|
||||
radius: 4
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildDiagnosisText() {
|
||||
Column() {
|
||||
Text() {
|
||||
ImageSpan($r('app.media.ai_diagnosis_txt_icon'))
|
||||
.width(41)
|
||||
.height(18)
|
||||
.objectFit(ImageFit.Contain)
|
||||
.baselineOffset(LengthMetrics.vp(0))
|
||||
.margin({right: 8})
|
||||
|
||||
Span(this.cardData ? this.cardData.message : '')
|
||||
.fontColor($r('app.color.elements_text_secondary'))
|
||||
.fontSize(14)
|
||||
.lineHeight(20)
|
||||
}
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
}
|
||||
.padding(8)
|
||||
.borderRadius(4)
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.elements_others_bg_layer1'))
|
||||
.onClick(() => this.handleCardClick())
|
||||
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildRecommendContracts() {
|
||||
Row({ space: 8 }) {
|
||||
Text($r('app.string.recommend_contracts'))
|
||||
.fontColor($r('app.color.elements_text_secondary'))
|
||||
.fontSize(12)
|
||||
|
||||
|
||||
ForEach(this.cardData ? this.cardData.recommend : [], (item: RecommendItem, index: number) => {
|
||||
Row({ space: 8 }) {
|
||||
Text(item.name)
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(11)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
Text(this.getScoreText(item.score))
|
||||
.fontColor(this.getScoreColor(item.score))
|
||||
.fontSize(11)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
}
|
||||
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
|
||||
.backgroundColor($r('app.color.elements_others_bg_layer1'))
|
||||
.borderRadius(4)
|
||||
.margin({ right: index < (this.cardData ? this.cardData.recommend.length : 0) - 1 ? 8 : 0 })
|
||||
.onClick(() => this.handleRecommendClick(item))
|
||||
}, (item: RecommendItem) => item.code + item.name)
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.margin({ top: 12 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,738 @@
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { emitter } from '@kit.BasicServicesKit';
|
||||
import { AiRiseFallDataFetcher } from '../datacenter/AiRiseFallDataFetcher';
|
||||
import { AiRiseFallData, RiseFallListData, RiseFallItem } from '../model/AiRiseFallNodeModel';
|
||||
import { AllCardsCard } from '../model/AiDiagnosisNodeModel';
|
||||
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel';
|
||||
import { AiRiseFallHqRequestClient, ContractHqItem } from '../clients/AiRiseFallHqRequestClient';
|
||||
import { HeaderItem, TableData } from '@b2b/hq_table';
|
||||
import { EmitterConstants, GlobalContext, HXLog } from 'biz_common';
|
||||
import { RouterUtil } from '../../util/RouterUtil';
|
||||
import { enableDarkMode } from '@kernel/theme_manager';
|
||||
import { TableConstants } from '@b2c/lib_baseui';
|
||||
import { getResourceString } from '../../util/ResourceUtil';
|
||||
import { QuoteCustomSettingManager, StandardPriceType } from 'biz_quote';
|
||||
|
||||
// FrameId 和 PageId
|
||||
const FRAME_ID_AI_RISEFALL = 2201
|
||||
const PAGE_ID_AI_RISEFALL = 4106
|
||||
|
||||
// 显示数量限制
|
||||
const MAX_CONTRACT_COUNT = 3
|
||||
|
||||
@Component
|
||||
export struct AiRiseFallNodeComponent {
|
||||
nodeModel: FirstPageNodeModel | null = null
|
||||
@Link @Watch('onRefreshTriggerChange') refreshTrigger: number
|
||||
|
||||
// Tab 可见性(用于控制行情请求的 request/release)
|
||||
@Watch('onTabVisibilityChange') @Consume firstPageVisible: boolean = true
|
||||
|
||||
@StorageProp(enableDarkMode) isDarkMode: boolean = false
|
||||
|
||||
@State private cardConfig: AllCardsCard | null = null
|
||||
@State private cardData: AiRiseFallData | null = null
|
||||
|
||||
// Tab 状态
|
||||
@State private currentTabIndex: number = 0
|
||||
|
||||
// 合约行情数据 Map(key: contract_market)
|
||||
@State private contractHqMap: Map<string, ContractHqItem> = new Map()
|
||||
|
||||
// 行情请求客户端
|
||||
private hqRequestClient: AiRiseFallHqRequestClient | null = null
|
||||
|
||||
// TCP 重连监听
|
||||
private mIsEventSubscribed: boolean = false
|
||||
private netRelinkCallBack = () => {
|
||||
if (this.firstPageVisible) {
|
||||
setTimeout(() => {
|
||||
if (this.cardData) {
|
||||
this.subscribeAllContracts(this.cardData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 上一次刷新时使用的基准价类型,用于在变可见时判断是否需要按新基准价重新请求
|
||||
private lastStandardPriceType: StandardPriceType =
|
||||
QuoteCustomSettingManager.getInstance().getStandardPriceType()
|
||||
|
||||
// 表头配置
|
||||
private headerConfig: HeaderItem[] = [
|
||||
new HeaderItem(TableConstants.DATA_ID_NAME, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_CODE_4, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_MARKET, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_PRICE, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_ZF, '')
|
||||
]
|
||||
|
||||
aboutToAppear(): void {
|
||||
AiRiseFallDataFetcher.getInstance().onCardConfigChange = (config) => {
|
||||
this.cardConfig = config
|
||||
}
|
||||
|
||||
AiRiseFallDataFetcher.getInstance().onCardDataChange = (data) => {
|
||||
this.cardData = data
|
||||
if (data) {
|
||||
// 订阅所有合约的行情
|
||||
this.subscribeAllContracts(data)
|
||||
}
|
||||
}
|
||||
|
||||
this.initHqRequestClient()
|
||||
this.refreshCardData()
|
||||
|
||||
// 订阅 TCP 重连事件
|
||||
if (this.firstPageVisible) {
|
||||
this.subscribeTcpNetStatus()
|
||||
}
|
||||
}
|
||||
|
||||
onRefreshTriggerChange(): void {
|
||||
this.refreshCardData()
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab 可见性变化时的回调
|
||||
* 可见时请求行情数据,不可见时释放行情请求
|
||||
*/
|
||||
onTabVisibilityChange(): void {
|
||||
HXLog.d('AiRiseFallNodeComponent', 'onTabVisibilityChange: ' + this.firstPageVisible)
|
||||
if (this.firstPageVisible) {
|
||||
// 可见时重新请求数据
|
||||
if (this.cardData) {
|
||||
this.subscribeAllContracts(this.cardData)
|
||||
}
|
||||
// 订阅 TCP 重连事件
|
||||
this.subscribeTcpNetStatus()
|
||||
// 检测基准价是否切换,切换后重新订阅
|
||||
const currentStandardPriceType = QuoteCustomSettingManager.getInstance().getStandardPriceType()
|
||||
if (this.lastStandardPriceType !== currentStandardPriceType) {
|
||||
HXLog.d('AiRiseFallNodeComponent', 'Standard price type changed, resubscribe')
|
||||
this.lastStandardPriceType = currentStandardPriceType
|
||||
if (this.cardData) {
|
||||
this.subscribeAllContracts(this.cardData)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不可见时释放行情订阅
|
||||
this.releaseHqClient()
|
||||
// 取消订阅 TCP 重连事件
|
||||
this.unSubscribeTcpNetStatus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化行情请求客户端
|
||||
*/
|
||||
private initHqRequestClient(): void {
|
||||
this.hqRequestClient = new AiRiseFallHqRequestClient(
|
||||
FRAME_ID_AI_RISEFALL,
|
||||
PAGE_ID_AI_RISEFALL,
|
||||
this.headerConfig,
|
||||
(tableData: TableData | string) => {}
|
||||
)
|
||||
|
||||
// 设置行情数据回调
|
||||
this.hqRequestClient.setHqDataCallback((hqs: ContractHqItem[]) => {
|
||||
this.onHqDataReceive(hqs)
|
||||
})
|
||||
}
|
||||
|
||||
refreshCardData(): void {
|
||||
AiRiseFallDataFetcher.getInstance().fetchCardConfig()
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.releaseHqClient()
|
||||
this.unSubscribeTcpNetStatus()
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅 TCP 重连事件
|
||||
*/
|
||||
private subscribeTcpNetStatus(): void {
|
||||
if (this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = true
|
||||
emitter.on({ eventId: EmitterConstants.NETWORK_TCP_RECONNECT }, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅 TCP 重连事件
|
||||
*/
|
||||
private unSubscribeTcpNetStatus(): void {
|
||||
if (!this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = false
|
||||
emitter.off(EmitterConstants.NETWORK_TCP_RECONNECT, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅所有合约的行情数据
|
||||
*/
|
||||
private subscribeAllContracts(data: AiRiseFallData): void {
|
||||
const codes: string[] = []
|
||||
const markets: string[] = []
|
||||
|
||||
const collectContracts = (items: RiseFallItem[]) => {
|
||||
if (items) {
|
||||
items.slice(0, MAX_CONTRACT_COUNT).forEach((item: RiseFallItem) => {
|
||||
if (item.show_quote === 1 && item.contract && item.market) {
|
||||
codes.push(item.contract)
|
||||
markets.push(item.market)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 收集看涨和看跌列表中的合约
|
||||
collectContracts(data.rise?.data || [])
|
||||
collectContracts(data.fall?.data || [])
|
||||
|
||||
if (codes.length > 0) {
|
||||
// 先清理上次的订阅,避免重复
|
||||
this.releaseHqClient()
|
||||
// 重新初始化客户端
|
||||
this.initHqRequestClient()
|
||||
this.hqRequestClient?.setStockListAndRequest(codes, markets)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理行情数据接收
|
||||
*/
|
||||
private onHqDataReceive(hqs: ContractHqItem[]): void {
|
||||
hqs.forEach((hq: ContractHqItem) => {
|
||||
const key = `${hq.code}_${hq.market}`
|
||||
this.contractHqMap.set(key, hq)
|
||||
})
|
||||
}
|
||||
|
||||
private releaseHqClient(): void {
|
||||
if (this.hqRequestClient) {
|
||||
this.hqRequestClient.release(true)
|
||||
this.hqRequestClient = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化涨跌幅
|
||||
*/
|
||||
formatPriceChg(priceChg: number | null): ResourceStr {
|
||||
if (priceChg === null) {
|
||||
return $r('app.string.wait_open')
|
||||
}
|
||||
const sign = priceChg > 0 ? '+' : ''
|
||||
return `${sign}${priceChg.toFixed(2)}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取涨跌幅颜色
|
||||
*/
|
||||
getPriceChgColor(priceChg: number | null): ResourceStr {
|
||||
if (priceChg === null) {
|
||||
return $r('app.color.price_even')
|
||||
}
|
||||
if (priceChg > 0) {
|
||||
return $r('app.color.price_up_100')
|
||||
} else if (priceChg < 0) {
|
||||
return $r('app.color.price_down_100')
|
||||
}
|
||||
return $r('app.color.price_even')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取方向文案
|
||||
*/
|
||||
getDirectionText(forecast: number): ResourceStr {
|
||||
return forecast === 0 ? $r('app.string.tab_fall') : $r('app.string.tab_rise')
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理标题点击
|
||||
*/
|
||||
handleTitleClick() {
|
||||
if (!this.cardConfig?.card_url) {
|
||||
return
|
||||
}
|
||||
const url = this.cardConfig.card_url.ios || this.cardConfig.card_url.android || ''
|
||||
if (url.length === 0) {
|
||||
return
|
||||
}
|
||||
RouterUtil.jumpPage(url, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理正确率点击(跳转历史页面)
|
||||
*/
|
||||
handleAccuracyClick(label: string) {
|
||||
if (label === '昨日正确率') {
|
||||
const url = getResourceString(getContext(this), $r('app.string.ai_rise_fall_history_url'))
|
||||
RouterUtil.jumpPage(url, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理合约点击
|
||||
*/
|
||||
handleContractClick(item: RiseFallItem) {
|
||||
if (item.contract && item.market) {
|
||||
router.pushUrl({
|
||||
url: 'pages/QuoteDetailPage',
|
||||
params: {
|
||||
marketId: item.market,
|
||||
code: item.contract,
|
||||
name: item.name,
|
||||
codeList: item.contract,
|
||||
marketIdList: item.market,
|
||||
nameList: item.name,
|
||||
periodIndex: 0,
|
||||
defaultTabType: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 Tab 数据
|
||||
*/
|
||||
getCurrentTabData(): RiseFallListData | null {
|
||||
if (!this.cardData) {
|
||||
return null
|
||||
}
|
||||
return this.currentTabIndex === 0 ? this.cardData.rise : this.cardData.fall
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
this.buildTitleBar()
|
||||
|
||||
if (this.cardData) {
|
||||
this.buildCardContent()
|
||||
}
|
||||
}
|
||||
.padding({ left: 10, right: 10, bottom: 10 })
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.surface_layer1_foreground'))
|
||||
.borderRadius(4)
|
||||
.bindSheet($$this.isSheetShow, this.buildExplainSheetContent, {
|
||||
height: SheetSize.FIT_CONTENT, // 固定高度
|
||||
dragBar: true,
|
||||
showClose: false,
|
||||
maskColor: $r('app.color.mask_level2'),
|
||||
onDisappear: () => {
|
||||
// 关闭时重置状态
|
||||
},
|
||||
radius: {
|
||||
topLeft: 10,
|
||||
topRight: 10
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildTitleBar() {
|
||||
Row() {
|
||||
Row() {
|
||||
Text(this.cardConfig?.card_title?.value || 'AI看涨跌')
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(18)
|
||||
.fontWeight(500)
|
||||
|
||||
Image($r('app.media.icon_arrow_forward_20'))
|
||||
.width(20)
|
||||
.height(20)
|
||||
.fillColor($r('app.color.elements_icon_primary_02'))
|
||||
|
||||
// 功能提醒图标
|
||||
if (this.cardConfig?.explain_message) {
|
||||
Image($r('app.media.icon_info_24'))
|
||||
.width(16)
|
||||
.height(16)
|
||||
.fillColor($r('app.color.elements_icon_tertiary'))
|
||||
.margin({ left: 8 })
|
||||
.onClick(() => this.showExplainDialog())
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.height('100%')
|
||||
.onClick(() => this.handleTitleClick())
|
||||
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示功能说明弹窗(底部弹出,占屏幕80%高度,宽度撑满)
|
||||
*/
|
||||
showExplainDialog(): void {
|
||||
const title = this.cardConfig?.explain_title ?? $r('app.string.ai_rise_fall_explain_title')
|
||||
const message = this.cardConfig?.explain_message ?? ''
|
||||
|
||||
// 使用 bindSheet 实现底部弹出效果
|
||||
this.sheetTitle = title
|
||||
this.sheetMessage = message
|
||||
this.isSheetShow = true
|
||||
}
|
||||
|
||||
@State private sheetTitle: ResourceStr = ''
|
||||
@State private sheetMessage: string = ''
|
||||
@State private isSheetShow: boolean = false
|
||||
|
||||
@Builder
|
||||
buildExplainSheetContent() {
|
||||
Column() {
|
||||
Row() {
|
||||
Blank().layoutWeight(1)
|
||||
|
||||
Text(this.sheetTitle)
|
||||
.fontSize(18)
|
||||
.fontWeight(500)
|
||||
.layoutWeight(1)
|
||||
|
||||
Row() {
|
||||
Image($r('app.media.icon_close_24'))
|
||||
.width(24)
|
||||
.height(24)
|
||||
.fillColor($r('app.color.elements_icon_primary_02'))
|
||||
.onClick(() => {
|
||||
this.isSheetShow = false
|
||||
})
|
||||
}
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.End)
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.height(64)
|
||||
.width('100%')
|
||||
|
||||
|
||||
Text(this.sheetMessage.replace(/\\n/g, '\n'))
|
||||
.fontSize(14)
|
||||
.lineHeight(22)
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.width('100%')
|
||||
|
||||
|
||||
Button($r('app.string.confirm_button'))
|
||||
.type(ButtonType.Normal)
|
||||
.backgroundColor($r('app.color.elements_button_bg_primary'))
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.borderRadius(4)
|
||||
.onClick(() => {
|
||||
this.isSheetShow = false
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.surface_layer3_background'))
|
||||
.padding({
|
||||
bottom: px2vp(GlobalContext.navigationBarHeight),
|
||||
left: 16,
|
||||
right: 16
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildCardContent() {
|
||||
Column({ space: 10 }) {
|
||||
Column({ space: 10 }) {
|
||||
// 正确率展示
|
||||
this.buildDataDisplay()
|
||||
|
||||
// 风险提示(在正确率下方)
|
||||
this.buildRiskTip()
|
||||
}
|
||||
.padding({
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
left: 4,
|
||||
right: 4
|
||||
})
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.elements_others_bg_layer2'))
|
||||
.borderRadius(4)
|
||||
|
||||
// Tab 切换
|
||||
this.buildTabBar()
|
||||
|
||||
// 合约表格
|
||||
this.buildContractTable()
|
||||
|
||||
// 显示更多
|
||||
this.buildSeeMore()
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildDataDisplay() {
|
||||
Row() {
|
||||
// 近20日正确率
|
||||
Column() {
|
||||
Text($r('app.string.accuracy_recent_twenty'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(13)
|
||||
.margin({ bottom: 4 })
|
||||
|
||||
Text(this.cardData ? this.cardData.last_twenty_trade_date_accuracy + '%' : '--')
|
||||
.fontColor(this.getAccuracyColor(this.cardData?.last_twenty_trade_date_accuracy))
|
||||
.fontSize(16)
|
||||
.fontWeight(500)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
|
||||
// 分割线
|
||||
Column()
|
||||
.width(0.5)
|
||||
.height(32)
|
||||
.backgroundColor($r('app.color.elements_others_divider_primary'))
|
||||
|
||||
// 昨日正确率(可点击)
|
||||
Column() {
|
||||
Row() {
|
||||
Text($r('app.string.accuracy_yesterday'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(13)
|
||||
.margin({ bottom: 4 })
|
||||
|
||||
Image($r('app.media.icon_arrow_forward_20'))
|
||||
.width(16)
|
||||
.height(16)
|
||||
.fillColor($r('app.color.elements_icon_primary_02'))
|
||||
.margin({ left: 4, bottom: 4 })
|
||||
}
|
||||
|
||||
Text(this.cardData ? this.cardData.last_trade_date_accuracy + '%' : '--')
|
||||
.fontColor(this.getAccuracyColor(this.cardData?.last_trade_date_accuracy))
|
||||
.fontSize(16)
|
||||
.fontWeight(500)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.onClick(() => this.handleAccuracyClick('昨日正确率'))
|
||||
}
|
||||
.width('100%')
|
||||
.borderRadius(8)
|
||||
}
|
||||
|
||||
getAccuracyColor(value: string | undefined): ResourceStr {
|
||||
if (!value) {
|
||||
return $r('app.color.price_even')
|
||||
}
|
||||
const num = parseFloat(value)
|
||||
if (num > 0) {
|
||||
return $r('app.color.price_up_100')
|
||||
} else if (num < 0) {
|
||||
return $r('app.color.price_down_100')
|
||||
}
|
||||
return $r('app.color.price_even')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildTabBar() {
|
||||
Row() {
|
||||
// 看涨 Tab
|
||||
Row({ space: 8 }) {
|
||||
Text($r('app.string.tab_rise'))
|
||||
.fontColor(this.currentTabIndex === 0 ? $r('app.color.elements_text_primary_01') : $r('app.color.elements_text_secondary'))
|
||||
.fontSize(14)
|
||||
.fontWeight(this.currentTabIndex === 0 ? FontWeight.Medium : FontWeight.Normal)
|
||||
|
||||
Text(`(${this.cardData?.rise?.count || 0}个)`)
|
||||
.fontColor(this.currentTabIndex === 0 ? $r('app.color.elements_text_primary_01') : $r('app.color.elements_text_secondary'))
|
||||
.fontSize(14)
|
||||
.fontWeight(this.currentTabIndex === 0 ? FontWeight.Medium : FontWeight.Normal)
|
||||
}
|
||||
.height(30)
|
||||
.constraintSize({
|
||||
minWidth: 66
|
||||
})
|
||||
.padding({ left: 8, right: 8, top: 8, bottom: 8 })
|
||||
.backgroundColor(this.currentTabIndex === 0 ? $r('app.color.elements_others_bg_primary') : $r('app.color.elements_button_bg_tertiary'))
|
||||
.borderRadius(4)
|
||||
.onClick(() => {
|
||||
if (this.currentTabIndex !== 0) {
|
||||
this.currentTabIndex = 0
|
||||
}
|
||||
})
|
||||
|
||||
Blank().width(12)
|
||||
|
||||
// 看跌 Tab
|
||||
Row({ space: 8 }) {
|
||||
Text($r('app.string.tab_fall'))
|
||||
.fontColor(this.currentTabIndex === 1 ? $r('app.color.elements_text_primary_01') : $r('app.color.elements_text_secondary'))
|
||||
.fontSize(14)
|
||||
.fontWeight(this.currentTabIndex === 1 ? FontWeight.Medium : FontWeight.Normal)
|
||||
|
||||
Text(`(${this.cardData?.fall?.count || 0}个)`)
|
||||
.fontColor(this.currentTabIndex === 1 ? $r('app.color.elements_text_primary_01') : $r('app.color.elements_text_secondary'))
|
||||
.fontSize(14)
|
||||
.fontWeight(this.currentTabIndex === 1 ? FontWeight.Medium : FontWeight.Normal)
|
||||
}
|
||||
.height(30)
|
||||
.constraintSize({
|
||||
minWidth: 66
|
||||
})
|
||||
.padding({ left: 8, right: 8, top: 8, bottom: 8 })
|
||||
.backgroundColor(this.currentTabIndex === 1 ? $r('app.color.elements_others_bg_primary') : $r('app.color.elements_button_bg_tertiary'))
|
||||
.borderRadius(4)
|
||||
.onClick(() => {
|
||||
if (this.currentTabIndex !== 1) {
|
||||
this.currentTabIndex = 1
|
||||
}
|
||||
})
|
||||
|
||||
Blank()
|
||||
|
||||
// 更新提示
|
||||
Text($r('app.string.update_time_tip'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(12)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildContractTable() {
|
||||
Column() {
|
||||
// 表头
|
||||
Row({ space: 8 }) {
|
||||
Text($r('app.string.table_header_main_contract'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.layoutWeight(1)
|
||||
|
||||
Text($r('app.string.table_header_direction'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.width(72)
|
||||
.textAlign(TextAlign.End)
|
||||
|
||||
Text($r('app.string.table_header_actual_change'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.width(72)
|
||||
.textAlign(TextAlign.End)
|
||||
|
||||
Text($r('app.string.table_header_variety_accuracy'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.width(72)
|
||||
.textAlign(TextAlign.End)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 8, bottom: 8 })
|
||||
.border({
|
||||
width: { bottom: 1 },
|
||||
color: $r('app.color.elements_others_divider_primary'),
|
||||
})
|
||||
|
||||
// 数据行
|
||||
ForEach(this.getCurrentTabData()?.data?.slice(0, MAX_CONTRACT_COUNT) || [], (item: RiseFallItem, index: number) => {
|
||||
this.buildContractRow(item, index)
|
||||
}, (item: RiseFallItem) => `${item.contract}_${item.market}`)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildContractRow(item: RiseFallItem, index: number) {
|
||||
Row({space: 8}) {
|
||||
// 主力合约
|
||||
|
||||
Row() {
|
||||
Text(item.name || item.contract)
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(16)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 方向
|
||||
Text(this.getDirectionText(item.forecast))
|
||||
.fontColor(this.currentTabIndex === 0 ? $r('app.color.price_up_100') : $r('app.color.price_down_100'))
|
||||
.fontSize(16)
|
||||
.width(40)
|
||||
.fontWeight(500)
|
||||
.textAlign(TextAlign.End)
|
||||
}
|
||||
.height(20)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 实际涨幅
|
||||
Text(this.formatPriceChg(this.getContractHqPriceChg(item)))
|
||||
.fontColor(this.getPriceChgColor(this.getContractHqPriceChg(item)))
|
||||
.fontSize(16)
|
||||
.width(72)
|
||||
.height(20)
|
||||
.textAlign(TextAlign.End)
|
||||
.fontWeight(500)
|
||||
|
||||
// 正确率
|
||||
Text(item.accuracy + '%')
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(16)
|
||||
.width(72)
|
||||
.height(20)
|
||||
.textAlign(TextAlign.End)
|
||||
.fontWeight(500)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 12, bottom: 8 })
|
||||
.border({
|
||||
width: { bottom: 1 },
|
||||
color: $r('app.color.elements_others_divider_primary')
|
||||
})
|
||||
.onClick(() => this.handleContractClick(item))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取合约的行情涨跌幅
|
||||
*/
|
||||
private getContractHqPriceChg(item: RiseFallItem): number | null {
|
||||
const key = `${item.contract}_${item.market}`
|
||||
return this.contractHqMap.get(key)?.priceChg ?? null
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildRiskTip() {
|
||||
Text($r('app.string.risk_tip_content'))
|
||||
.fontColor($r('app.color.elements_text_quaternary'))
|
||||
.maxFontSize(10)
|
||||
.minFontSize(9)
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildSeeMore() {
|
||||
Row() {
|
||||
Text($r('app.string.see_more'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(16)
|
||||
.height(20)
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.padding({ top: 2, bottom: 6 })
|
||||
.onClick(() => this.handleSeeMore())
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理显示更多点击
|
||||
*/
|
||||
handleSeeMore() {
|
||||
// 跳转到 AI 看涨跌详情页面
|
||||
if (this.cardConfig?.card_url) {
|
||||
const url = this.cardConfig.card_url.ios || this.cardConfig.card_url.android || ''
|
||||
RouterUtil.jumpPage(url, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { OnAdsDataUpdateListener, BaseAd, AdsManager, ADS_TYPE_INDEX_BANAER } from '@b2c-f/fuhm-ads'
|
||||
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel'
|
||||
import { RouterUtil } from '../../util/RouterUtil'
|
||||
import { HXLog } from 'biz_common'
|
||||
import { systemDateTime } from '@kit.BasicServicesKit'
|
||||
import { LengthMetrics } from '@kit.ArkUI'
|
||||
import {
|
||||
AD_CLICK_TYPE_INTEREST,
|
||||
AD_CLICK_TYPE_INVALIDE,
|
||||
AD_LOCATION_YUNYING_SHOUYE,
|
||||
AD_OPERATION_CLICK,
|
||||
AD_OPERATION_SHOW,
|
||||
AD_OPERATION_SWITCH,
|
||||
AD_TYPE_INVALID
|
||||
} from '../../constants/FirstPageConstants'
|
||||
import { StatLogFilter } from '../helper/StatLogFilter'
|
||||
import { Action, HxCBASAgentProvider, logMapValue } from '@b2c-spi/hx_cbas_api'
|
||||
import { FirstPageFiveCBASConstants } from '../../constants/FirstPageFiveCBASConstants'
|
||||
|
||||
const TAG = 'BannerNodeComponent'
|
||||
|
||||
/**
|
||||
* 首页轮播图节点
|
||||
* @author wubingsong@myhexin.com
|
||||
*/
|
||||
@Component
|
||||
export struct BannerNodeComponent {
|
||||
nodeModel: FirstPageNodeModel | null = null
|
||||
@State bannerHeight: number = 135
|
||||
@State private isNodeShow: boolean = false
|
||||
@State private bannerNodeModelArr: BaseAd[] = []
|
||||
@State private isNeedIndicator: boolean = false
|
||||
@State private currentIndex: number = 0
|
||||
@Consume @Watch('onShowStatusChange') firstPageVisible: boolean = true;
|
||||
private CYCLE_GAP = 3000
|
||||
/** 上一次手动触摸时间,用于区分手动切换和自动轮播 */
|
||||
private lastTouchTime: number = 0
|
||||
/** 广告数据监听器 */
|
||||
private adsListener: OnAdsDataUpdateListener = {
|
||||
onAdsDataUpdate: (adsType: string, adsDataModels: BaseAd[]) => {
|
||||
if (this.firstPageVisible) {
|
||||
this.handleAdsDataUpdate(adsDataModels)
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 广告埋点过滤器 */
|
||||
private statLogFilter: StatLogFilter = new StatLogFilter()
|
||||
/** 当前轮播真实广告数量 */
|
||||
private realCount: number = 0
|
||||
|
||||
aboutToAppear(): void {
|
||||
// 注册广告数据监听
|
||||
AdsManager.addAdsDataUpdateListener(ADS_TYPE_INDEX_BANAER, this.adsListener)
|
||||
if (this.firstPageVisible) {
|
||||
// 请求广告数据
|
||||
AdsManager.refreshNotifyAds(ADS_TYPE_INDEX_BANAER)
|
||||
}
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
// 移除广告数据监听
|
||||
AdsManager.removeAdsDataUpdateListener(ADS_TYPE_INDEX_BANAER, this.adsListener)
|
||||
this.statLogFilter.resetFilter()
|
||||
}
|
||||
|
||||
onShowStatusChange() {
|
||||
if (this.firstPageVisible) {
|
||||
this.statLogFilter.resetFilter()
|
||||
AdsManager.refreshNotifyAds(ADS_TYPE_INDEX_BANAER)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理广告数据更新
|
||||
*/
|
||||
private handleAdsDataUpdate(adsDataModels: BaseAd[]): void {
|
||||
HXLog.i(TAG, `${TAG} onAdsDataUpdate, size = ${adsDataModels.length}`)
|
||||
// 按 position 排序
|
||||
adsDataModels.sort((a, b) => a.position - b.position)
|
||||
this.bannerNodeModelArr = adsDataModels
|
||||
this.isNeedIndicator = adsDataModels.length > 1
|
||||
this.isNodeShow = adsDataModels.length > 0
|
||||
this.realCount = adsDataModels.length
|
||||
// 数据到达且当前可见时,发曝光 stat
|
||||
if (adsDataModels.length > 0 && this.firstPageVisible) {
|
||||
this.sendAdStat(AD_OPERATION_SHOW, AD_CLICK_TYPE_INVALIDE, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送广告统计日志
|
||||
*
|
||||
* @param operationType 操作类型
|
||||
* @param clickType 点击类型
|
||||
* @param isNeedFilter 是否过滤
|
||||
*/
|
||||
private sendAdStat(operationType: number, clickType: number, isNeedFilter: boolean): void {
|
||||
const adItem = this.bannerNodeModelArr[this.currentIndex]
|
||||
const adId = adItem?.adId
|
||||
if (!adId) {
|
||||
return
|
||||
}
|
||||
// 需要过滤且 adId 已存在则跳过
|
||||
if (isNeedFilter && this.statLogFilter.filterAdId(adId)) {
|
||||
return
|
||||
}
|
||||
const adModule: string = AD_LOCATION_YUNYING_SHOUYE
|
||||
if (operationType === AD_OPERATION_SWITCH && !this.statLogFilter.filterAdId(adId)) {
|
||||
// 手动切换到广告且未在过滤列表时,先发一次曝光埋点,再发切换埋点
|
||||
this.doSendNonClickStat(AD_OPERATION_SHOW, adModule, adId, AD_TYPE_INVALID)
|
||||
}
|
||||
|
||||
if (operationType === AD_OPERATION_CLICK) {
|
||||
this.doSendClickStat(adModule, adId, AD_TYPE_INVALID, clickType)
|
||||
AdsManager.clickAd(ADS_TYPE_INDEX_BANAER, adItem)
|
||||
} else {
|
||||
this.sendCbas(Action.SHOW, adItem)
|
||||
this.doSendNonClickStat(operationType, adModule, adId, AD_TYPE_INVALID)
|
||||
AdsManager.showAd(ADS_TYPE_INDEX_BANAER, adItem)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送带广告类型的非点击STAT日志
|
||||
private doSendNonClickStat(operationType: number, adModule?: string, adId?: string, adType?: string) {
|
||||
HXLog.i(TAG, `doSendNonClickStat operationType=${operationType} adModule=${adModule} adId=${adId} adType=${adType}`)
|
||||
}
|
||||
|
||||
// 发送带广告类型的点击STAT日志
|
||||
private doSendClickStat(adModule?: string, adId?: string, adType?: string, clickType?: number) {
|
||||
HXLog.i(TAG, `doSendClickStat adModule=${adModule} adId=${adId} adType=${adType} clickType=${clickType}`)
|
||||
}
|
||||
|
||||
private sendCbas(action: Action, item: BaseAd) {
|
||||
const oid = FirstPageFiveCBASConstants.CBAS_SHOUYE_BANNER
|
||||
let logMap: Map<string, logMapValue> = new Map<string, logMapValue>()
|
||||
logMap.set(FirstPageFiveCBASConstants.CBAS_BANNER_LOGMAP_ADID, item.adId)
|
||||
HxCBASAgentProvider.getInstance().doEvent(oid, action, logMap)
|
||||
HXLog.i(TAG, `sendCbas action:${action} adId=${item.adId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 广告点击处理
|
||||
*/
|
||||
private handleAdClick(model: BaseAd): void {
|
||||
// 发送点击埋点
|
||||
this.sendCbas(Action.CLICK, model)
|
||||
// 处理网页埋点的初始化
|
||||
let jumUrl = model.jumpUrl
|
||||
if (jumUrl && jumUrl.length !== 0) {
|
||||
const adModule: string = AD_LOCATION_YUNYING_SHOUYE
|
||||
this.doSendClickStat(adModule, model.adId, AD_TYPE_INVALID, AD_CLICK_TYPE_INTEREST)
|
||||
AdsManager.clickAd(ADS_TYPE_INDEX_BANAER, model)
|
||||
RouterUtil.jumpPage(model.jumpUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户触摸时间,用于区分手动切换和自动轮播
|
||||
*/
|
||||
private handleTouch(): void {
|
||||
this.lastTouchTime = systemDateTime.getTime(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Swiper 页面切换回调
|
||||
* 3 秒内切换视为手动切换,发 SWITCH 埋点;否则视为自动轮播,发 SHOW 埋点
|
||||
*/
|
||||
private handlePageSelected(index: number): void {
|
||||
this.currentIndex = index
|
||||
const currentTime = systemDateTime.getTime(false)
|
||||
if (currentTime - this.lastTouchTime < this.CYCLE_GAP) {
|
||||
this.lastTouchTime = 0
|
||||
this.sendAdStat(AD_OPERATION_SWITCH, AD_CLICK_TYPE_INVALIDE, false)
|
||||
} else {
|
||||
this.sendAdStat(AD_OPERATION_SHOW, AD_CLICK_TYPE_INVALIDE, true)
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Swiper() {
|
||||
ForEach(this.bannerNodeModelArr, (model: BaseAd) => {
|
||||
Image(model.imgUrl)
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.objectFit(ImageFit.Fill)
|
||||
.onClick(() => {
|
||||
this.handleAdClick(model)
|
||||
})
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height(this.bannerHeight)
|
||||
.loop(true)
|
||||
.autoPlay(this.isNeedIndicator ? true : false)
|
||||
.interval(this.CYCLE_GAP)
|
||||
.indicator(this.isNeedIndicator ? Indicator.dot()
|
||||
.itemWidth(6)
|
||||
.itemHeight(3)
|
||||
.selectedItemHeight(3)
|
||||
.selectedItemWidth(8)
|
||||
.space(LengthMetrics.vp(4))
|
||||
.bottom(LengthMetrics.vp(5), true)
|
||||
.color($r('app.color.color_ffffff'))
|
||||
.selectedColor($r('app.color.color_3d3d42')) : false
|
||||
)
|
||||
.effectMode(EdgeEffect.None)
|
||||
.onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
|
||||
if (newValue.width) {
|
||||
let width = newValue.width as number
|
||||
this.bannerHeight = width * 0.25
|
||||
}
|
||||
})
|
||||
.borderRadius(6)
|
||||
.visibility(this.isNodeShow ? Visibility.Visible : Visibility.None)
|
||||
.onChange((index: number) => {
|
||||
this.handlePageSelected(index)
|
||||
})
|
||||
.onTouch((event: TouchEvent) => {
|
||||
if (event.type === TouchType.Down) {
|
||||
this.handleTouch()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { emitter } from '@kit.BasicServicesKit';
|
||||
import { CommodityOptionsDataFetcher } from '../datacenter/CommodityOptionsDataFetcher';
|
||||
import { CommodityOptionsContract, COMMODITY_OPTIONS_TABS, CommodityOptionsTab } from '../model/CommodityOptionsNodeModel';
|
||||
import { CommodityOptionsHqRequestClient, CommodityOptionHqItem } from '../clients/CommodityOptionsHqRequestClient';
|
||||
import { HeaderItem, TableData } from '@b2b/hq_table';
|
||||
import { EmitterConstants, GlobalContext, HXLog } from 'biz_common';
|
||||
import { TableConstants } from '@b2c/lib_baseui';
|
||||
|
||||
// FrameId 和 PageId
|
||||
const FRAME_ID_COMMODITY_OPTIONS = 2201
|
||||
const PAGE_ID_COMMODITY_OPTIONS = 4106
|
||||
|
||||
// 显示数量限制
|
||||
const MAX_SHOW_COUNT = 5
|
||||
|
||||
@Component
|
||||
export struct CommodityOptionsNodeComponent {
|
||||
// 刷新触发器(首页下拉刷新时会递增)
|
||||
@Link @Watch('onRefreshTriggerChange') refreshTrigger: number
|
||||
|
||||
// Tab 可见性
|
||||
@Watch('onTabVisibilityChange') @Consume firstPageVisible: boolean = true
|
||||
|
||||
// 当前 Tab 索引
|
||||
@State private currentTabIndex: number = 0
|
||||
|
||||
// 合约数据(50条,后端按涨跌幅降序排列)
|
||||
@State private contractList: CommodityOptionsContract[] = []
|
||||
|
||||
// 合约列表(5条)
|
||||
@State private displayHqList: CommodityOptionHqItem[] = []
|
||||
// 行情数据缓存(按 tabIndex 存储)
|
||||
private displayHqCache: Map<number, CommodityOptionHqItem[]> = new Map()
|
||||
|
||||
// 功能提示弹窗状态
|
||||
@State private isSheetShow: boolean = false
|
||||
@State private explainTitle: ResourceStr = $r('app.string.commodity_options_explain_title')
|
||||
@State private explainMessage: string = ''
|
||||
|
||||
// 行情请求客户端
|
||||
private hqRequestClient: CommodityOptionsHqRequestClient | null = null
|
||||
|
||||
// TCP 重连监听
|
||||
private mIsEventSubscribed: boolean = false
|
||||
private netRelinkCallBack = () => {
|
||||
if (this.firstPageVisible && this.contractList.length > 0) {
|
||||
setTimeout(() => {
|
||||
this.subscribeContracts()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 表头配置
|
||||
private headerConfig: HeaderItem[] = [
|
||||
new HeaderItem(TableConstants.DATA_ID_NAME, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_CODE_4, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_MARKET, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_PRICE, ''),
|
||||
new HeaderItem(TableConstants.DATA_ID_ZF_YTD, '')
|
||||
]
|
||||
|
||||
aboutToAppear(): void {
|
||||
// 设置卡片配置回调
|
||||
CommodityOptionsDataFetcher.getInstance().onCardConfigChange = (config) => {
|
||||
if (config) {
|
||||
// 从配置中读取功能说明
|
||||
if (config.explain_message) {
|
||||
this.explainMessage = config.explain_message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置数据回调
|
||||
CommodityOptionsDataFetcher.getInstance().onCardDataChange = (data, tabIndex) => {
|
||||
if (tabIndex !== this.currentTabIndex) {
|
||||
return
|
||||
}
|
||||
this.contractList = data
|
||||
if (data.length > 0) {
|
||||
this.subscribeContracts()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始请求配置和数据
|
||||
CommodityOptionsDataFetcher.getInstance().fetchCardConfig()
|
||||
|
||||
// 初始请求数据
|
||||
this.initHqRequestClient()
|
||||
this.refreshCardData()
|
||||
|
||||
// 订阅 TCP 重连事件
|
||||
if (this.firstPageVisible) {
|
||||
this.subscribeTcpNetStatus()
|
||||
}
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.releaseHqClient()
|
||||
this.unSubscribeTcpNetStatus()
|
||||
CommodityOptionsDataFetcher.getInstance().destroy()
|
||||
}
|
||||
|
||||
onRefreshTriggerChange(): void {
|
||||
this.refreshCardData()
|
||||
}
|
||||
|
||||
onTabVisibilityChange(): void {
|
||||
if (this.firstPageVisible) {
|
||||
if (this.contractList.length > 0) {
|
||||
this.subscribeContracts()
|
||||
}
|
||||
this.subscribeTcpNetStatus()
|
||||
} else {
|
||||
this.releaseHqClient()
|
||||
this.unSubscribeTcpNetStatus()
|
||||
}
|
||||
}
|
||||
|
||||
private initHqRequestClient(): void {
|
||||
this.hqRequestClient = new CommodityOptionsHqRequestClient(
|
||||
FRAME_ID_COMMODITY_OPTIONS,
|
||||
PAGE_ID_COMMODITY_OPTIONS,
|
||||
this.headerConfig,
|
||||
(tableData: TableData | string) => {}
|
||||
)
|
||||
this.hqRequestClient.setHqDataCallback((hqs: CommodityOptionHqItem[]) => {
|
||||
this.onHqDataReceive(hqs)
|
||||
})
|
||||
}
|
||||
|
||||
refreshCardData(): void {
|
||||
CommodityOptionsDataFetcher.getInstance().fetchCardData(this.currentTabIndex)
|
||||
}
|
||||
|
||||
private subscribeTcpNetStatus(): void {
|
||||
if (this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = true
|
||||
emitter.on({ eventId: EmitterConstants.NETWORK_TCP_RECONNECT }, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
private unSubscribeTcpNetStatus(): void {
|
||||
if (!this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = false
|
||||
emitter.off(EmitterConstants.NETWORK_TCP_RECONNECT, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
private subscribeContracts(): void {
|
||||
const codes: string[] = []
|
||||
const markets: string[] = []
|
||||
|
||||
// 传入全部合约请求行情(与Web端一致)
|
||||
this.contractList.forEach((item: CommodityOptionsContract) => {
|
||||
if (item.contract_code && item.market) {
|
||||
codes.push(item.contract_code)
|
||||
markets.push(item.market)
|
||||
}
|
||||
})
|
||||
|
||||
if (codes.length > 0) {
|
||||
this.releaseHqClient()
|
||||
this.initHqRequestClient()
|
||||
this.hqRequestClient?.setStockListAndRequest(codes, markets)
|
||||
}
|
||||
}
|
||||
|
||||
private onHqDataReceive(hqs: CommodityOptionHqItem[]): void {
|
||||
// 行情接口返回的数据已按涨跌幅降序排列,直接取前5条(无需再次排序)
|
||||
const top5Hqs = hqs.slice(0, MAX_SHOW_COUNT)
|
||||
|
||||
// 为每个合约设置到期日(从 contractList 中获取)
|
||||
top5Hqs.forEach((hq: CommodityOptionHqItem) => {
|
||||
const contract = this.contractList.find(c =>
|
||||
`${c.contract_code}_${c.market}` === `${hq.code}_${hq.market}`
|
||||
)
|
||||
if (contract) {
|
||||
hq.expiredDate = String(contract.expired_date)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新当前tab的显示列表
|
||||
this.displayHqList = top5Hqs
|
||||
// 同时缓存到对应tab
|
||||
this.displayHqCache.set(this.currentTabIndex, top5Hqs)
|
||||
}
|
||||
|
||||
private releaseHqClient(): void {
|
||||
if (this.hqRequestClient) {
|
||||
this.hqRequestClient.release(true)
|
||||
this.hqRequestClient = null
|
||||
}
|
||||
}
|
||||
|
||||
formatPriceChg(priceChg: number | null): string {
|
||||
if (priceChg === null) {
|
||||
return '--'
|
||||
}
|
||||
const sign = priceChg > 0 ? '+' : ''
|
||||
return `${sign}${priceChg.toFixed(2)}%`
|
||||
}
|
||||
|
||||
getDueDays(expiredDate: string | number): number {
|
||||
if (expiredDate === null || expiredDate === undefined || expiredDate === '') {
|
||||
return -1
|
||||
}
|
||||
// 转换为字符串处理
|
||||
const dateStr = String(expiredDate)
|
||||
if (dateStr.length !== 8) {
|
||||
return -1
|
||||
}
|
||||
const year = parseInt(dateStr.substring(0, 4))
|
||||
const month = parseInt(dateStr.substring(4, 6)) - 1
|
||||
const day = parseInt(dateStr.substring(6, 8))
|
||||
const expiredTime = new Date(year, month, day).getTime()
|
||||
const now = Date.now()
|
||||
const diffDays = Math.ceil((expiredTime - now) / (24 * 60 * 60 * 1000))
|
||||
return diffDays >= 0 ? diffDays : -1
|
||||
}
|
||||
|
||||
handleContractClick(item: CommodityOptionsContract): void {
|
||||
if (item.contract_code && item.market) {
|
||||
// 构建合约列表
|
||||
const codeList = this.contractList.map(c => c.contract_code).join(',')
|
||||
const marketList = this.contractList.map(c => c.market).join(',')
|
||||
const nameList = this.contractList.map(c => c.contract_name).join(',')
|
||||
|
||||
router.pushUrl({
|
||||
url: 'pages/QuoteDetailPage',
|
||||
params: {
|
||||
marketId: item.market,
|
||||
code: item.contract_code,
|
||||
name: item.contract_name,
|
||||
codeList: codeList,
|
||||
marketIdList: marketList,
|
||||
nameList: nameList,
|
||||
periodIndex: 0 // 分时
|
||||
}
|
||||
}, router.RouterMode.Standard, (err) => {
|
||||
if (err) {
|
||||
HXLog.e('CommodityOptionsNodeComponent', `jumpToQuote failed: ${err.code}, ${err.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleTabClick(index: number): void {
|
||||
if (this.currentTabIndex !== index) {
|
||||
// 先从缓存切换到对应tab的行情数据(解决切换tab显示旧数据问题)
|
||||
this.displayHqList = this.displayHqCache.get(index) ?? []
|
||||
this.contractList = CommodityOptionsDataFetcher.getInstance().getDataByTab(index)
|
||||
if (this.contractList.length > 0) {
|
||||
this.subscribeContracts()
|
||||
}
|
||||
this.currentTabIndex = index
|
||||
// 切换到新tab,触发数据更新
|
||||
CommodityOptionsDataFetcher.getInstance().switchTab(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示功能说明弹窗
|
||||
*/
|
||||
showExplainDialog(): void {
|
||||
this.isSheetShow = true
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
this.buildTitleBar()
|
||||
this.buildTabBar()
|
||||
this.buildContent()
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.surface_layer1_foreground'))
|
||||
.borderRadius(4)
|
||||
.padding({ left: 10, right: 10, bottom: 10 })
|
||||
.bindSheet($$this.isSheetShow, this.buildExplainSheetContent, {
|
||||
height: SheetSize.FIT_CONTENT,
|
||||
dragBar: true,
|
||||
showClose: false,
|
||||
maskColor: $r('app.color.mask_level2'),
|
||||
radius: {
|
||||
topLeft: 10,
|
||||
topRight: 10
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildTitleBar() {
|
||||
Row() {
|
||||
Row({ space: 8 }) {
|
||||
Text($r('app.string.commodity_options_title'))
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(18)
|
||||
.height(22)
|
||||
.fontWeight(500)
|
||||
|
||||
// 功能提醒图标
|
||||
Image($r('app.media.icon_info_24'))
|
||||
.width(20)
|
||||
.height(20)
|
||||
.fillColor($r('app.color.elements_icon_tertiary'))
|
||||
.onClick(() => this.showExplainDialog())
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.height('100%')
|
||||
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildExplainSheetContent() {
|
||||
Column() {
|
||||
Stack({ alignContent: Alignment.End }) {
|
||||
Text(this.explainTitle)
|
||||
.fontSize(18)
|
||||
.fontWeight(500)
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
|
||||
Row() {
|
||||
Image($r('app.media.icon_close_24'))
|
||||
.width(24)
|
||||
.height(24)
|
||||
.fillColor($r('app.color.elements_icon_primary_02'))
|
||||
.onClick(() => {
|
||||
this.isSheetShow = false
|
||||
})
|
||||
}
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.End)
|
||||
}
|
||||
.height(64)
|
||||
.width('100%')
|
||||
|
||||
Text(this.explainMessage.replace(/\\n/g, '\n'))
|
||||
.fontSize(14)
|
||||
.lineHeight(22)
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.width('100%')
|
||||
|
||||
Button($r('app.string.commodity_options_confirm'))
|
||||
.type(ButtonType.Normal)
|
||||
.backgroundColor($r('app.color.elements_button_bg_primary'))
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.borderRadius(4)
|
||||
.onClick(() => {
|
||||
this.isSheetShow = false
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.surface_layer3_background'))
|
||||
.padding({
|
||||
bottom: px2vp(GlobalContext.navigationBarHeight),
|
||||
left: 16,
|
||||
right: 16
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildTabBar() {
|
||||
Row({ space: 8 }) {
|
||||
ForEach(COMMODITY_OPTIONS_TABS, (tab: CommodityOptionsTab, index: number) => {
|
||||
Text(tab.label)
|
||||
.fontColor(this.currentTabIndex === index ? $r('app.color.elements_text_primary_01') : $r('app.color.elements_text_secondary'))
|
||||
.fontSize(14)
|
||||
.fontWeight(this.currentTabIndex === index ? FontWeight.Medium : FontWeight.Normal)
|
||||
.padding({ left: 8, right: 8 })
|
||||
.height(30)
|
||||
.constraintSize({
|
||||
minWidth: 66
|
||||
})
|
||||
.textAlign(TextAlign.Center)
|
||||
.backgroundColor(this.currentTabIndex === index ? $r('app.color.elements_others_bg_primary') : $r('app.color.elements_button_bg_tertiary'))
|
||||
.borderRadius(4)
|
||||
.onClick(() => this.handleTabClick(index))
|
||||
})
|
||||
}
|
||||
.height(46)
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildContent() {
|
||||
Column() {
|
||||
// 数据列表(与Web端一致:表头包含在列表组件内,数据为空时整个组件不显示)
|
||||
if (this.displayHqList.length > 0) {
|
||||
// 表头
|
||||
Row({ space: 8 }) {
|
||||
Text($r('app.string.commodity_options_table_header_name'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.layoutWeight(1)
|
||||
|
||||
Text($r('app.string.commodity_options_table_header_price'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.width(96)
|
||||
.textAlign(TextAlign.End)
|
||||
|
||||
Text($r('app.string.commodity_options_table_header_change'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.width(96)
|
||||
.textAlign(TextAlign.End)
|
||||
}
|
||||
.height(40)
|
||||
.width('100%')
|
||||
|
||||
// 列表数据
|
||||
ForEach(this.displayHqList, (item: CommodityOptionHqItem, index: number) => {
|
||||
this.buildContractRow(item, index)
|
||||
})
|
||||
} else {
|
||||
// 空数据占位(与Web端一致:数据为空时整个区域不显示表头)
|
||||
this.buildEmptyView()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildEmptyView() {
|
||||
Column() {
|
||||
Text(this.getDefaultMsg())
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
}
|
||||
.width('100%')
|
||||
.height(120)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}
|
||||
|
||||
private getDefaultMsg(): ResourceStr {
|
||||
if (this.currentTabIndex >= COMMODITY_OPTIONS_TABS.length) {
|
||||
return $r('app.string.commodity_options_no_data')
|
||||
}
|
||||
return COMMODITY_OPTIONS_TABS[this.currentTabIndex].defaultMsg
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildContractRow(item: CommodityOptionHqItem, index: number) {
|
||||
Row({ space: 8 }) {
|
||||
Column({ space: 2 }) {
|
||||
// 合约名称
|
||||
Text(item.name)
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(16)
|
||||
.height(20)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
// 临期期权 Tab 显示"x天到期"标签
|
||||
if (this.currentTabIndex === 1 && this.getDueDays(item.expiredDate) >= 0) {
|
||||
Row() {
|
||||
Text(String(this.getDueDays(item.expiredDate)))
|
||||
.fontColor($r('app.color.commodity_options_expire_label_color'))
|
||||
.fontSize(9)
|
||||
Text($r('app.string.commodity_options_expire_suffix'))
|
||||
.fontColor($r('app.color.commodity_options_expire_label_color'))
|
||||
.fontSize(9)
|
||||
}
|
||||
.height(10)
|
||||
.padding({ left: 1, right: 1 })
|
||||
.border({
|
||||
radius: 1,
|
||||
width: 0.33,
|
||||
color: $r('app.color.commodity_options_expire_label_color')
|
||||
})
|
||||
.alignSelf(ItemAlign.Start)
|
||||
}
|
||||
}
|
||||
.margin({ top: 12, bottom: 8 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 最新价
|
||||
Text(item.price)
|
||||
.fontColor(this.getPriceColor(item.priceChg))
|
||||
.fontSize(16)
|
||||
.fontWeight(500)
|
||||
.textAlign(TextAlign.End)
|
||||
|
||||
// 涨跌幅(昨收)
|
||||
Text(this.formatPriceChg(item.priceChg))
|
||||
.fontColor(this.getPriceChgColor(item.priceChg))
|
||||
.fontSize(16)
|
||||
.fontWeight(500)
|
||||
.width(96)
|
||||
.textAlign(TextAlign.End)
|
||||
}
|
||||
.width('100%')
|
||||
.border({
|
||||
width: { top: 1 },
|
||||
color: $r('app.color.elements_others_divider_primary')
|
||||
})
|
||||
.onClick(() => this.handleContractClickByHq(item))
|
||||
}
|
||||
|
||||
private getPriceColor(priceChg: number | null): ResourceStr {
|
||||
if (priceChg === null) {
|
||||
return $r('app.color.elements_text_primary_02')
|
||||
}
|
||||
if (priceChg > 0) {
|
||||
return $r('app.color.price_up_100')
|
||||
} else if (priceChg < 0) {
|
||||
return $r('app.color.price_down_100')
|
||||
}
|
||||
return $r('app.color.elements_text_primary_02')
|
||||
}
|
||||
|
||||
private getPriceChgColor(priceChg: number | null): ResourceStr {
|
||||
return this.getPriceColor(priceChg)
|
||||
}
|
||||
|
||||
private handleContractClickByHq(item: CommodityOptionHqItem): void {
|
||||
// 从 hq 数据中获取对应的原始合约信息进行跳转
|
||||
const contract = this.contractList.find(c =>
|
||||
`${c.contract_code}_${c.market}` === `${item.code}_${item.market}`
|
||||
)
|
||||
if (contract) {
|
||||
this.handleContractClick(contract)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { StringUtils } from 'hxutil';
|
||||
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel';
|
||||
import { CustomEntryListNodeModel } from '../model/CustomEntryListNodeModel';
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { RouterUtil } from '../../util/RouterUtil';
|
||||
|
||||
/**
|
||||
* 首页自定义宫格节点
|
||||
* @author wubingsong@myhexin.com
|
||||
*/
|
||||
@Component
|
||||
export struct CustomEntryListNodeComponent {
|
||||
|
||||
nodeModel: FirstPageNodeModel | null = null
|
||||
@State
|
||||
private isNodeShow: boolean = false
|
||||
@State
|
||||
private customEntryListNodeModelArr: CustomEntryListNodeModel[] | null = null;
|
||||
private static MAX_SHOW_ITEM_COUNT = 10
|
||||
|
||||
aboutToAppear(): void {
|
||||
if (this.nodeModel != null) {
|
||||
let extraData = this.nodeModel.extraData
|
||||
if (StringUtils.isNotEmpty(extraData)) {
|
||||
let customEntryListNodes = JSON.parse(extraData) as CustomEntryListNodeModel[]
|
||||
if (customEntryListNodes && customEntryListNodes.length > 0) {
|
||||
let moreModel: CustomEntryListNodeModel = new CustomEntryListNodeModel()
|
||||
moreModel.title = '更多'
|
||||
moreModel.imgUrl = $r('app.media.firstpage_grid_item_all')
|
||||
customEntryListNodes.push(moreModel)
|
||||
if (customEntryListNodes.length > CustomEntryListNodeComponent.MAX_SHOW_ITEM_COUNT) {
|
||||
this.customEntryListNodeModelArr = customEntryListNodes.slice(0, CustomEntryListNodeComponent.MAX_SHOW_ITEM_COUNT)
|
||||
} else {
|
||||
this.customEntryListNodeModelArr = customEntryListNodes
|
||||
}
|
||||
this.isNodeShow = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
List({ initialIndex: 0 }) {
|
||||
ForEach(this.customEntryListNodeModelArr, (item: CustomEntryListNodeModel) => {
|
||||
ListItem() {
|
||||
this.createItem(item)
|
||||
}
|
||||
}, (item: CustomEntryListNodeModel) => item.id.toString())
|
||||
}
|
||||
.width("100%")
|
||||
.height("auto")
|
||||
.lanes(5)
|
||||
.alignListItem(ListItemAlign.Center)
|
||||
.scrollBar(BarState.Off)
|
||||
.backgroundColor($r('app.color.surface_layer1_foreground'))
|
||||
.borderRadius(10)
|
||||
.visibility(this.isNodeShow ? Visibility.Visible : Visibility.None)
|
||||
}
|
||||
|
||||
@Builder
|
||||
createItem(item: CustomEntryListNodeModel) {
|
||||
Column() {
|
||||
Image(item.imgUrl)
|
||||
.width(40)
|
||||
.height(40)
|
||||
Text(item.title)
|
||||
.margin( { top : 4 })
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.maxLines(1)
|
||||
.minFontSize(9)
|
||||
.maxFontSize(12)
|
||||
.fontSize(12)
|
||||
}
|
||||
.padding( { top : 16, bottom: 16 })
|
||||
.onClick(() => {
|
||||
if (item.title == '更多') {
|
||||
router.pushNamedRoute({
|
||||
name: 'GridNodeAllPage'
|
||||
})
|
||||
} else {
|
||||
let fullscreen = false
|
||||
if (item.title.includes("智能客服")) {
|
||||
fullscreen = true
|
||||
}
|
||||
RouterUtil.jumpPage(item.jumpUrl, fullscreen)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { BaseAd } from '@b2c-f/fuhm-ads'
|
||||
import { image } from '@kit.ImageKit'
|
||||
|
||||
/**
|
||||
* author : liuqingliang@myhexin.com
|
||||
* time : created on 2026/2/27
|
||||
* desc : 首页弹窗广告组件
|
||||
*/
|
||||
export class FirstPageDialogParam {
|
||||
constructor(showAd: BaseAd, onClose: () => void, onClick: () => void) {
|
||||
this.showAd = showAd
|
||||
this.onClose = onClose
|
||||
this.onClick = onClick
|
||||
}
|
||||
|
||||
/** 广告数据(从外部传入) */
|
||||
showAd: BaseAd
|
||||
/** 关闭按钮回调 */
|
||||
onClose: () => void
|
||||
/** 广告点击回调 */
|
||||
onClick: () => void
|
||||
adImg?: image.PixelMap
|
||||
|
||||
releaseAdImg(): void {
|
||||
if (!this.adImg) {
|
||||
return
|
||||
}
|
||||
this.adImg.release()
|
||||
this.adImg = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
export function FirstPageDialogBuilder(param: FirstPageDialogParam) {
|
||||
|
||||
// 广告内容
|
||||
Column() {
|
||||
// 关闭按钮
|
||||
Row() {
|
||||
Blank()
|
||||
Image($r('app.media.firstpage_dialog_tanchuang_close'))
|
||||
.width(28)
|
||||
.height(28)
|
||||
.onClick(() => {
|
||||
param.onClose()
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Image(param.adImg ?? param.showAd.imgUrl)
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.objectFit(ImageFit.Fill)
|
||||
.borderRadius(12)
|
||||
.onClick(() => {
|
||||
param.onClick()
|
||||
})
|
||||
.margin({ top: 10 })
|
||||
}
|
||||
.width(255)
|
||||
.height(378)
|
||||
.backgroundColor(Color.Transparent)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { StringUtils } from 'hxutil';
|
||||
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel';
|
||||
import { EntryListNodeModel } from '../model/EntryListNodeModel';
|
||||
import { RouterUtil } from '../../util/RouterUtil';
|
||||
|
||||
/**
|
||||
* 首页四宫格节点
|
||||
* @author wubingsong@myhexin.com
|
||||
*/
|
||||
@Component
|
||||
export struct FourEntryListNodeComponent {
|
||||
|
||||
nodeModel: FirstPageNodeModel | null = null
|
||||
@State
|
||||
private isNodeShow: boolean = false
|
||||
@State
|
||||
private entryListNodeModelArr: EntryListNodeModel[] | null = null
|
||||
private static COLUMN_COUNT = 4
|
||||
|
||||
aboutToAppear(): void {
|
||||
if (this.nodeModel != null) {
|
||||
let extraData = this.nodeModel.extraData
|
||||
if (StringUtils.isNotEmpty(extraData)) {
|
||||
let entryListNodes = JSON.parse(extraData) as EntryListNodeModel[]
|
||||
if (entryListNodes) {
|
||||
if (entryListNodes.length > FourEntryListNodeComponent.COLUMN_COUNT) {
|
||||
this.entryListNodeModelArr = entryListNodes.slice(0, FourEntryListNodeComponent.COLUMN_COUNT)
|
||||
} else {
|
||||
this.entryListNodeModelArr = entryListNodes
|
||||
}
|
||||
if (this.entryListNodeModelArr.length > 0) {
|
||||
this.isNodeShow = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
List({ initialIndex: 0 }) {
|
||||
ForEach(this.entryListNodeModelArr, (item: EntryListNodeModel) => {
|
||||
ListItem() {
|
||||
this.createItem(item)
|
||||
}
|
||||
}, (item: EntryListNodeModel) => item.id.toString())
|
||||
}
|
||||
.width("100%")
|
||||
.height('auto')
|
||||
.lanes(FourEntryListNodeComponent.COLUMN_COUNT)
|
||||
.alignListItem(ListItemAlign.Center)
|
||||
.scrollBar(BarState.Off)
|
||||
.visibility(this.isNodeShow ? Visibility.Visible : Visibility.None)
|
||||
}
|
||||
.width("100%")
|
||||
.height(80)
|
||||
.backgroundImageSize(ImageSize.FILL)
|
||||
|
||||
}
|
||||
|
||||
@Builder
|
||||
createItem(item: EntryListNodeModel) {
|
||||
Column() {
|
||||
Image(item.imgUrl)
|
||||
.width(40)
|
||||
.height(40)
|
||||
Text(item.title)
|
||||
.margin( { top : 4 })
|
||||
.fontColor($r('app.color.color_333333'))
|
||||
.maxLines(1)
|
||||
.minFontSize(9)
|
||||
.maxFontSize(14)
|
||||
.fontSize(14)
|
||||
}
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.height(80)
|
||||
.onClick(() => {
|
||||
RouterUtil.jumpPage(item.jumpUrl)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { HotListDataFetcher } from '../datacenter/HotListDataFetcher';
|
||||
import { HotListItemModel, getColorValue } from '../model/HotListNodeModel';
|
||||
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel';
|
||||
import { RouterUtil } from '../../util/RouterUtil';
|
||||
|
||||
/**
|
||||
* 热点排行卡片组件
|
||||
* @author YS
|
||||
* @date 2026-06-03
|
||||
*/
|
||||
@Component
|
||||
export struct HotListNodeComponent {
|
||||
nodeModel: FirstPageNodeModel | null = null
|
||||
|
||||
// 刷新触发器(首页下拉刷新时会递增)
|
||||
@Link @Watch('onRefreshTriggerChange') refreshTrigger: number
|
||||
|
||||
@State private newsList: HotListItemModel[] | null = null
|
||||
@State private isUnfold: boolean = false
|
||||
@State private dataReady: boolean = false // 数据加载状态(区分加载中/暂无数据)
|
||||
|
||||
// 展示数量:折叠3条,展开5条
|
||||
private readonly NORMAL_SIZE: number = 3
|
||||
private readonly MAX_SIZE: number = 5
|
||||
|
||||
// 排名背景色
|
||||
private readonly bgColors: ResourceColor[] = [
|
||||
$r('app.color.hotlist_rank_bg_1'),
|
||||
$r('app.color.hotlist_rank_bg_2'),
|
||||
$r('app.color.hotlist_rank_bg_3')
|
||||
]
|
||||
|
||||
aboutToAppear(): void {
|
||||
if (this.nodeModel != null) {
|
||||
HotListDataFetcher.getInstance().hotListChange = (data) => {
|
||||
this.newsList = data
|
||||
this.dataReady = true // 标记数据已加载
|
||||
}
|
||||
this.refreshCardData()
|
||||
}
|
||||
}
|
||||
|
||||
onRefreshTriggerChange(): void {
|
||||
this.refreshCardData()
|
||||
}
|
||||
|
||||
refreshCardData(): void {
|
||||
// 初始状态为加载中
|
||||
this.dataReady = false
|
||||
// 远程获取最新数据(从配置文件读取 API 地址)
|
||||
HotListDataFetcher.getInstance().remoteFetchData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换展开/折叠状态
|
||||
*/
|
||||
toggleUnfold(): void {
|
||||
this.isUnfold = !this.isUnfold
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到列表页(点击标题栏)
|
||||
*/
|
||||
jumpToListPage(): void {
|
||||
const cardUrl = HotListDataFetcher.getInstance().getCardUrl()
|
||||
if (cardUrl) {
|
||||
RouterUtil.jumpPage(cardUrl, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到资讯详情(点击列表项)
|
||||
*/
|
||||
jumpToDetail(item: HotListItemModel): void {
|
||||
RouterUtil.jumpPage(item.url, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取显示的数据
|
||||
*/
|
||||
private getDisplayList(): HotListItemModel[] {
|
||||
if (!this.newsList || this.newsList.length === 0) {
|
||||
return []
|
||||
}
|
||||
const size = this.isUnfold ? this.MAX_SIZE : this.NORMAL_SIZE
|
||||
return this.newsList.slice(0, size)
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 标题栏:标题 + 展开/收起 按钮
|
||||
this.buildTitleBar()
|
||||
|
||||
if (this.newsList && this.newsList.length > 0) {
|
||||
this.buildContentList()
|
||||
} else {
|
||||
// 空数据占位(与Web端一致:dataReady为false显示"加载中",否则显示"暂无数据")
|
||||
this.buildEmptyView()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.surface_layer1_foreground'))
|
||||
.borderRadius(4)
|
||||
.padding({ left: 10, right: 10, bottom: 10 })
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildEmptyView() {
|
||||
Column() {
|
||||
Text(this.dataReady ? $r('app.string.hotlist_no_data') : $r('app.string.hotlist_loading'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
}
|
||||
.width('100%')
|
||||
.height(80)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildTitleBar() {
|
||||
Row() {
|
||||
Row() {
|
||||
Text($r('app.string.hotlist_title'))
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(18)
|
||||
.fontWeight(500)
|
||||
|
||||
Image($r('app.media.icon_arrow_forward_20'))
|
||||
.width(20)
|
||||
.height(20)
|
||||
.fillColor($r('app.color.elements_icon_primary_02'))
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.height('100%')
|
||||
.onClick(() => {
|
||||
this.jumpToListPage()
|
||||
})
|
||||
|
||||
// 展开/收起 按钮
|
||||
if (this.newsList && this.newsList.length > this.NORMAL_SIZE) {
|
||||
Row() {
|
||||
Text(this.isUnfold ? $r('app.string.hotlist_collapse') : $r('app.string.hotlist_expand'))
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(12)
|
||||
.height(16)
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Center)
|
||||
}
|
||||
.width(40)
|
||||
.height(20)
|
||||
.borderRadius(10)
|
||||
.backgroundColor($r('app.color.elements_button_bg_disable_02'))
|
||||
.margin({ left: 8 })
|
||||
.onClick(() => {
|
||||
this.toggleUnfold()
|
||||
})
|
||||
}
|
||||
|
||||
Blank()
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildContentList() {
|
||||
Column({ space: 12 }) {
|
||||
ForEach(this.getDisplayList(), (item: HotListItemModel, index: number) => {
|
||||
this.buildNewsItem(item, index)
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 8, bottom: 8 })
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildNewsItem(item: HotListItemModel, index: number) {
|
||||
Row() {
|
||||
// 排名序号
|
||||
Row() {
|
||||
Text((index + 1).toString())
|
||||
.fontSize(14)
|
||||
.fontWeight(700)
|
||||
.fontColor(index < 3 ? $r('app.color.elements_text_on_color') : $r('app.color.elements_text_tertiary'))
|
||||
}
|
||||
.width(16)
|
||||
.height(16)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(index < this.bgColors.length ? this.bgColors[index] : undefined)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 标题
|
||||
Text(item.title)
|
||||
.fontSize(16)
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.margin({ left: 8 })
|
||||
.flexShrink(1)
|
||||
|
||||
// 标签(tag 存在时显示)
|
||||
if (item.tag) {
|
||||
Text(item.tag)
|
||||
.fontSize(10)
|
||||
.fontWeight(500)
|
||||
.fontColor($r('app.color.elements_text_on_color'))
|
||||
.height(14)
|
||||
.borderRadius(2)
|
||||
.padding(2)
|
||||
.backgroundColor(getColorValue(item.color))
|
||||
.margin({ left: 4 })
|
||||
.flexShrink(0)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height(22)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick(() => {
|
||||
this.jumpToDetail(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
import { GroupInfo } from 'service_watchlist';
|
||||
import { FirstPageSelfCodeGroupView } from '../../views/FirstPageSelfCodeGroupView';
|
||||
import {
|
||||
GridDataSource,
|
||||
GridHqDataItemModel,
|
||||
HQDataItemModel,
|
||||
SwiperHqDataItemModel,
|
||||
} from '../model/SelfSelectedStockModel';
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { StrUtil } from '@pura/harmony-utils';
|
||||
import { CellArray, HeaderItem, RowData, TableData } from '@b2b/hq_table';
|
||||
import { FirstPageSelfStockRequestClient } from '../clients/FirstPageSelfStockRequestClient';
|
||||
import { EmitterConstants, HQPageConstants, jumpToQuote, PreferenceService } from 'biz_common';
|
||||
import { emitter } from '@kit.BasicServicesKit';
|
||||
import { NumberUtils } from 'hxutil';
|
||||
import { FistPageSelfCodeGridItemView } from '../../views/FistPageSelfCodeGridItemView';
|
||||
import { HXUtils, TableConstants } from '@b2c/lib_baseui';
|
||||
import { QuoteCustomSettingManager, StandardPriceType } from 'biz_quote';
|
||||
|
||||
//TableRowBGView TableConstants
|
||||
|
||||
/**
|
||||
* 每页显示item数量3
|
||||
*/
|
||||
const ITEM_COUNT_PER_PAGE_3 = 3
|
||||
/**
|
||||
* 每页显示item数量5
|
||||
*/
|
||||
const ITEM_COUNT_PER_PAGE_5 = 5
|
||||
|
||||
|
||||
const DATA_ID_ZUO_SHOU = 6 //左收
|
||||
const DATA_ID_JIN_KAI = 7 //左收
|
||||
|
||||
|
||||
const STR_DEFAULT = "--"
|
||||
const SELF_CODE_DATA_IDS: number[] = [
|
||||
TableConstants.DATA_ID_NAME, //股票名称
|
||||
TableConstants.DATA_ID_PRICE, //最新价
|
||||
TableConstants.DATA_ID_ZF, //涨跌幅
|
||||
TableConstants.DATA_ID_ZD, //涨跌
|
||||
TableConstants.DATA_ID_SHOW_CODE, //showCode
|
||||
TableConstants.DATA_ID_CODE_4, //股票代码
|
||||
DATA_ID_ZUO_SHOU, //左收
|
||||
DATA_ID_JIN_KAI, //今开
|
||||
TableConstants.DATA_ID_JSJ, //昨结
|
||||
TableConstants.DATA_ID_MARKET,//市场id
|
||||
]
|
||||
/**
|
||||
* 此处frameId修改要慎重,在启动的时候执行FutureMarketDataManager.init() 发送socket请求(frameId从FrameIdStore.get()),不一致可能会导致启动的时候行情刷新失败
|
||||
*/
|
||||
const FRAME_ID_SELF_CODE = 2201 //自选股行情页面frameId
|
||||
const PAGE_ID_SELF_CODE = 1264 //自选股行情pageId
|
||||
const SP_KEY_FIRST_PAGE_SELF_CODE_EXPAND = "sp_key_first_page_self_code_expand"
|
||||
|
||||
@Component
|
||||
export struct SelfSelectedStockNodeComponent {
|
||||
@Consume @Watch("onVisibleChanged") firstPageVisible: boolean;
|
||||
//一多断点
|
||||
@StorageLink('displayBreakPoint') @Watch("displayChanged") curBp: string = 'sm'
|
||||
@StorageLink('windowWidthVp') windowWidthVp: number = 0;
|
||||
@State isExtend: boolean = false;
|
||||
@State mSwiperPagesItems: SwiperHqDataItemModel = new SwiperHqDataItemModel()
|
||||
private mDataItemModels: HQDataItemModel[] = []
|
||||
@State private swiperIndex: number = 0;
|
||||
private swiperController: SwiperController = new SwiperController();
|
||||
private mRequestClient?: FirstPageSelfStockRequestClient
|
||||
private mHeaderConfig: Array<HeaderItem> = []
|
||||
private mPageItemCount = ITEM_COUNT_PER_PAGE_3
|
||||
private mMaxTotalItemCount = 3 * ITEM_COUNT_PER_PAGE_3
|
||||
private mFrameId = FRAME_ID_SELF_CODE
|
||||
private mPageId = PAGE_ID_SELF_CODE
|
||||
private mGroupInfo?: GroupInfo
|
||||
private mIsNeedRealData = true
|
||||
private mIsHasReceivedData = false
|
||||
private mIsEventSubscribed = false
|
||||
// 上一次刷新时使用的基准价类型,用于在变可见时判断是否需要按新基准价重新请求
|
||||
private lastStandardPriceType: StandardPriceType =
|
||||
QuoteCustomSettingManager.getInstance().getStandardPriceType();
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.initHeadConfig();
|
||||
this.initPageConfig()
|
||||
if (this.firstPageVisible) {
|
||||
this.subscribeTcpNetStatus()
|
||||
}
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
this.unSubscribeTcpNetStatus()
|
||||
this.mRequestClient?.release(true)
|
||||
this.mRequestClient = undefined
|
||||
}
|
||||
|
||||
initPageConfig() {
|
||||
this.isExtend =
|
||||
PreferenceService.getBooleanValueSync(FirstPageSelfCodeGroupView.SP_FILE_NAME_FIRST_PAGE_SELF_CODE_FILE_NAME,
|
||||
SP_KEY_FIRST_PAGE_SELF_CODE_EXPAND, false)
|
||||
this.mPageItemCount = this.getPageItemCount()
|
||||
this.mMaxTotalItemCount = this.getTotalItemCount()
|
||||
}
|
||||
|
||||
displayChanged() {
|
||||
this.mPageItemCount = this.getPageItemCount()
|
||||
this.mMaxTotalItemCount = this.getTotalItemCount()
|
||||
this.updateSwiperPagesItemsWidthDataItemModels()
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
this.buildCardHeader()
|
||||
FirstPageSelfCodeGroupView({
|
||||
onForeground: this.firstPageVisible,
|
||||
onSelectGroupInfo: (index: number, groupInfo: GroupInfo) => {
|
||||
this.onSelectGroupInfo(index, groupInfo)
|
||||
}
|
||||
}).margin({ left: 11 })
|
||||
this.buildStockGrid()
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor($r('app.color.surface_layer1_foreground'))
|
||||
.borderRadius(4)
|
||||
}
|
||||
|
||||
onSelectGroupInfo(_index: number, groupInfo: GroupInfo) {
|
||||
if (this.mGroupInfo && JSON.stringify(this.mGroupInfo) == JSON.stringify(groupInfo)) {
|
||||
return
|
||||
}
|
||||
this.mGroupInfo = groupInfo
|
||||
this.initPageStocks()
|
||||
this.requestData()
|
||||
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildCardHeader() {
|
||||
Row() {
|
||||
Text('自选盯盘')
|
||||
.fontColor($r('app.color.elements_text_primary_02'))
|
||||
.fontSize(16)
|
||||
.margin({ left: 16, right: 10 })
|
||||
this.buildExpandButton()
|
||||
Blank()
|
||||
Row() {
|
||||
Text('更多')
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.fontSize(14)
|
||||
.margin({ right: 2 })
|
||||
|
||||
Image($r('app.media.icon_arrow_forward_24'))
|
||||
.fillColor($r('app.color.elements_icon_tertiary'))
|
||||
.width(17)
|
||||
.height(17)
|
||||
.margin({ right: 8 })
|
||||
}.onClick(() => {
|
||||
emitter.emit({eventId:EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX},{
|
||||
data: {
|
||||
tabUIManagerTabIndex:1,
|
||||
hqPageSubTabIndex: HQPageConstants.HQ_PAGE_INDEX_SELF_CODE_PAGE
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
.justifyContent(FlexAlign.Start)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
.height(44)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildStockGrid() {
|
||||
Column() {
|
||||
Swiper(this.swiperController) {
|
||||
ForEach(this.mSwiperPagesItems, (pageStocks: GridHqDataItemModel, pageIndex: number) => {
|
||||
Grid() {
|
||||
ForEach(pageStocks, (stock: HQDataItemModel) => {
|
||||
GridItem() {
|
||||
FistPageSelfCodeGridItemView({ hqItemData: stock,jumpToQuoteCallBack:(stockModel: HQDataItemModel)=>{
|
||||
this.jumpToQuote(stockModel)
|
||||
},jumpToMoreCallBack:()=>{
|
||||
this.jumpToMore()
|
||||
} })
|
||||
}
|
||||
})
|
||||
}
|
||||
.columnsTemplate('1fr 1fr 1fr')
|
||||
.rowsTemplate(this.isExtend ? '1fr 1fr' : '1fr')
|
||||
.rowsGap(8)
|
||||
.padding({ bottom: 30 })
|
||||
.width('100%')
|
||||
.height(this.isExtend ? 170 : 90)
|
||||
})
|
||||
}
|
||||
.index(this.swiperIndex)
|
||||
.autoPlay(false)
|
||||
.loop(false)
|
||||
.duration(0)
|
||||
.itemSpace(0)
|
||||
.displayCount(1)
|
||||
.backgroundColor($r('app.color.surface_layer1_foreground'))
|
||||
.cachedCount(1)
|
||||
|
||||
.indicatorInteractive(true)
|
||||
.indicator(new DotIndicator().bottom(0)
|
||||
.selectedItemWidth(8)
|
||||
.itemWidth(5)
|
||||
.itemHeight(5)
|
||||
.selectedItemHeight(5))
|
||||
.onChange((index: number) => {
|
||||
this.swiperIndex = index;
|
||||
})
|
||||
.onTouch((event) => {
|
||||
switch (event.type) {
|
||||
case TouchType.Down:
|
||||
this.mIsNeedRealData = false
|
||||
break;
|
||||
case TouchType.Up:
|
||||
case TouchType.Cancel:
|
||||
setTimeout(()=>{
|
||||
this.mIsNeedRealData = true
|
||||
},500)
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
buildExpandButton() {
|
||||
Row() {
|
||||
Image( $r('app.media.icon_arrow_forward_24'))
|
||||
.fillColor($r('app.color.elements_icon_primary_02'))
|
||||
.width(24)
|
||||
.height(16)
|
||||
.padding({ right: 8 })
|
||||
.onClick((event)=>{
|
||||
emitter.emit({eventId:EmitterConstants.TAB_UI_MANAGER_CHANGE_INDEX},{
|
||||
data: {
|
||||
tabUIManagerTabIndex:1,
|
||||
hqPageSubTabIndex: HQPageConstants.HQ_PAGE_INDEX_SELF_CODE_PAGE
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Text(this.isExtend ? '收起' : '展开')
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.elements_text_tertiary'))
|
||||
.padding({top:3,bottom:3,left:10,right:10})
|
||||
.backgroundColor($r('app.color.elements_others_bg_layer1'))
|
||||
.borderRadius(10)
|
||||
}
|
||||
.onClick(() => this.toggleExpand())
|
||||
}
|
||||
|
||||
onVisibleChanged() {
|
||||
if (this.firstPageVisible) {
|
||||
this.onForeground()
|
||||
} else {
|
||||
this.onBackground()
|
||||
}
|
||||
}
|
||||
|
||||
onForeground() {
|
||||
this.requestData()
|
||||
this.subscribeTcpNetStatus()
|
||||
// 切换基准价后回到前台:computemode 由 FirstPageSelfStockRequestClient 实时拼装,requestData 已会按新基准价重发,这里仅同步快照
|
||||
this.lastStandardPriceType = QuoteCustomSettingManager.getInstance().getStandardPriceType();
|
||||
}
|
||||
|
||||
onBackground() {
|
||||
this.unSubscribeTcpNetStatus()
|
||||
this.mRequestClient?.release()
|
||||
this.mRequestClient = undefined
|
||||
}
|
||||
|
||||
onReceiveData(tableData: string | TableData) {
|
||||
if (tableData instanceof TableData) {
|
||||
if (!this.mIsNeedRealData && this.mIsHasReceivedData) {
|
||||
return
|
||||
}
|
||||
const dataItems: HQDataItemModel[] = []
|
||||
tableData?.tableRows?.forEach((rowData, index) => {
|
||||
if (rowData instanceof RowData) {
|
||||
const dataItem = this.parseRowCellData2HQDataItemModel(rowData.scrollableCols)
|
||||
dataItems.push(dataItem)
|
||||
}
|
||||
})
|
||||
this.updatePageDataItemModels(dataItems)
|
||||
this.mIsHasReceivedData = true
|
||||
} else {
|
||||
console.log("firstPageSelfCodeData:" + JSON.stringify(tableData))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//socket连接成功
|
||||
private netRelinkCallBack = () => {
|
||||
if (this.firstPageVisible) {
|
||||
setTimeout(() => {
|
||||
this.requestData()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeTcpNetStatus() {
|
||||
if (this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = true
|
||||
emitter.on({ eventId: EmitterConstants.NETWORK_TCP_RECONNECT }, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
private unSubscribeTcpNetStatus() {
|
||||
if (!this.mIsEventSubscribed) {
|
||||
return
|
||||
}
|
||||
this.mIsEventSubscribed = false
|
||||
emitter.off(EmitterConstants.NETWORK_TCP_RECONNECT, this.netRelinkCallBack)
|
||||
}
|
||||
|
||||
private mockData() {
|
||||
let dataItems: HQDataItemModel[] = []
|
||||
for (let index = 0; index < this.mDataItemModels.length; index++) {
|
||||
const element = this.mDataItemModels[index];
|
||||
if (element.showAddImage) {
|
||||
const moreDataHqDataItemModel = new HQDataItemModel(STR_DEFAULT, STR_DEFAULT, STR_DEFAULT)
|
||||
moreDataHqDataItemModel.showAddImage = true;
|
||||
dataItems.push(moreDataHqDataItemModel)
|
||||
} else {
|
||||
let haDataItemModel = new HQDataItemModel(element.code, element.market, element.name)
|
||||
haDataItemModel.showCode = element.showCode
|
||||
haDataItemModel.price = this.updatePrice(element.price, index)
|
||||
haDataItemModel.riseFail = element.riseFail
|
||||
haDataItemModel.priceRise = element.priceRise
|
||||
haDataItemModel.nameColor = element.nameColor
|
||||
haDataItemModel.riseFail = element.riseFail
|
||||
haDataItemModel.priceColor = element.priceColor
|
||||
haDataItemModel.riseFailColor = element.riseFailColor
|
||||
haDataItemModel.priceRiseColor = element.priceRiseColor
|
||||
dataItems.push(haDataItemModel)
|
||||
}
|
||||
}
|
||||
this.updatePageDataItemModels(dataItems)
|
||||
}
|
||||
|
||||
private updatePrice(oldPrice: string, index: number) {
|
||||
if (NumberUtils.isNumeric(oldPrice)) {
|
||||
let newPrice = index % 2 == 0 ? (parseFloat(oldPrice) + 1).toString() : (parseFloat(oldPrice) - 1).toString()
|
||||
return newPrice
|
||||
}
|
||||
return oldPrice
|
||||
}
|
||||
|
||||
private getPageItemCount(): number {
|
||||
if (this.isFoldExpand()) {
|
||||
return this.isExtend ? 2 * ITEM_COUNT_PER_PAGE_5 : ITEM_COUNT_PER_PAGE_5
|
||||
} else {
|
||||
return this.isExtend ? 2 * ITEM_COUNT_PER_PAGE_3 : ITEM_COUNT_PER_PAGE_3
|
||||
}
|
||||
}
|
||||
|
||||
private getTotalItemCount(): number {
|
||||
if (this.isFoldExpand()) {
|
||||
return this.isExtend ? 2 * 2 * ITEM_COUNT_PER_PAGE_5 : 2 * ITEM_COUNT_PER_PAGE_5
|
||||
} else {
|
||||
return this.isExtend ? 3 * 2 * ITEM_COUNT_PER_PAGE_3 : 3 * ITEM_COUNT_PER_PAGE_3
|
||||
}
|
||||
}
|
||||
|
||||
private isFoldExpand(): boolean {
|
||||
return this.curBp === "lg"
|
||||
}
|
||||
|
||||
private initHeadConfig() {
|
||||
const headerConfig: HeaderItem[] = []
|
||||
SELF_CODE_DATA_IDS.forEach((idValue, index) => {
|
||||
headerConfig.push(new HeaderItem(idValue, ""))
|
||||
})
|
||||
this.mHeaderConfig = headerConfig
|
||||
}
|
||||
|
||||
private initPageStocks() {
|
||||
if (!this.mGroupInfo) {
|
||||
this.mDataItemModels = []
|
||||
this.mSwiperPagesItems = []
|
||||
this.swiperIndex = 0
|
||||
return
|
||||
}
|
||||
|
||||
const stocks = this.mGroupInfo.stocks
|
||||
let hqDataItems: HQDataItemModel[] = []
|
||||
stocks.forEach((security, index) => {
|
||||
if (StrUtil.isNotEmpty(security.code) && StrUtil.isNotEmpty(security.market)) {
|
||||
hqDataItems.push(new HQDataItemModel(security.code, security.market, security.name))
|
||||
}
|
||||
})
|
||||
let isNeedAddMoreItem = false
|
||||
if (hqDataItems.length < this.mMaxTotalItemCount) {
|
||||
// 数据没满,增加更多
|
||||
isNeedAddMoreItem = true;
|
||||
} else {
|
||||
// 数据满了,只返回前mMaxTotalItemCount个数据
|
||||
hqDataItems = hqDataItems.slice(0, this.mMaxTotalItemCount);
|
||||
}
|
||||
|
||||
if (isNeedAddMoreItem) {
|
||||
const moreDataHqDataItemModel = new HQDataItemModel(STR_DEFAULT, STR_DEFAULT, STR_DEFAULT)
|
||||
moreDataHqDataItemModel.showAddImage = true
|
||||
hqDataItems.push(moreDataHqDataItemModel)
|
||||
}
|
||||
this.mDataItemModels = hqDataItems
|
||||
this.swiperIndex = 0
|
||||
this.updateSwiperPagesItemsWidthDataItemModels()
|
||||
}
|
||||
|
||||
private updatePageDataItemModels(hqDataItems: HQDataItemModel[]) {
|
||||
if (this.mDataItemModels.length === 0) {
|
||||
this.mSwiperPagesItems.length = 0
|
||||
return;
|
||||
}
|
||||
if (hqDataItems.length > 0) {
|
||||
for (let currIndex = 0; currIndex < hqDataItems.length; currIndex++) {
|
||||
const currentDataItem = hqDataItems[currIndex]
|
||||
const stockCode = currentDataItem.code
|
||||
const market = currentDataItem.market
|
||||
if (!HXUtils.isValidContractData(stockCode) || !HXUtils.isValidContractData(market)) {
|
||||
continue;
|
||||
}
|
||||
for (let oldIndex = this.mDataItemModels.length - 1; oldIndex >= 0; oldIndex--) {
|
||||
const lastDataItemModel = this.mDataItemModels[oldIndex]
|
||||
if (lastDataItemModel && !lastDataItemModel.showAddImage && stockCode == lastDataItemModel.code) {
|
||||
lastDataItemModel.copyFrom(currentDataItem)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.updateSwiperPagesItemsWidthDataItemModels()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据模型构建Swiper
|
||||
*/
|
||||
private updateSwiperPagesItemsWidthDataItemModels(): void {
|
||||
const itemsPerPage = this.mPageItemCount;
|
||||
const pages: SwiperHqDataItemModel = new SwiperHqDataItemModel();
|
||||
|
||||
for (let i = 0; i < this.mDataItemModels.length; i += itemsPerPage) {
|
||||
let gridHqDataItemModel = new GridHqDataItemModel()
|
||||
gridHqDataItemModel.push(...this.mDataItemModels.slice(i,
|
||||
Math.min(i + itemsPerPage, this.mDataItemModels.length)))
|
||||
pages.push(gridHqDataItemModel);
|
||||
}
|
||||
this.mSwiperPagesItems = pages
|
||||
|
||||
}
|
||||
|
||||
private requestData() {
|
||||
this.mIsNeedRealData = true
|
||||
this.mIsHasReceivedData = false
|
||||
if (!this.firstPageVisible) {
|
||||
return
|
||||
}
|
||||
const stockListRequestText = this.buildExtRequestText();
|
||||
if (stockListRequestText.length < 0) {
|
||||
return
|
||||
}
|
||||
if (!this.mRequestClient) {
|
||||
this.mRequestClient = new FirstPageSelfStockRequestClient(this.mFrameId, this.mPageId, this.mHeaderConfig,
|
||||
null, (tableData: TableData | string) => {
|
||||
this.onReceiveData(tableData)
|
||||
}, stockListRequestText)
|
||||
}
|
||||
this.mRequestClient?.setExtRequestText(stockListRequestText)
|
||||
this.mRequestClient.request(0, 25)
|
||||
}
|
||||
|
||||
private buildExtRequestText(): string {
|
||||
if (this.mDataItemModels.length > 0) {
|
||||
let stockListText = "stocklist="
|
||||
let marketListText = "marketlist="
|
||||
let isValidStock = false;
|
||||
this.mDataItemModels.forEach((value, index) => {
|
||||
if (!value.showAddImage && HXUtils.isValidContractData(value.code) &&
|
||||
HXUtils.isValidContractData(value.market)) {
|
||||
isValidStock = true
|
||||
stockListText = stockListText + value.code + "|"
|
||||
marketListText = marketListText + value.market + "|"
|
||||
}
|
||||
})
|
||||
if (isValidStock) {
|
||||
return "selfstockcustom=1\r\n" + stockListText + "\r\n" + marketListText
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private parseRowCellData2HQDataItemModel(cellArrayData: CellArray): HQDataItemModel {
|
||||
const dataItem = new HQDataItemModel()
|
||||
cellArrayData.forEach((cellData, index) => {
|
||||
switch (cellData.dataId) {
|
||||
case TableConstants.DATA_ID_NAME:
|
||||
dataItem.name = cellData.data
|
||||
dataItem.nameColor = cellData.color
|
||||
break;
|
||||
case TableConstants.DATA_ID_MARKET:
|
||||
dataItem.market = cellData.data
|
||||
break;
|
||||
case TableConstants.DATA_ID_CODE_4:
|
||||
dataItem.code = cellData.data
|
||||
break;
|
||||
case TableConstants.DATA_ID_SHOW_CODE:
|
||||
dataItem.showCode = cellData.data
|
||||
break;
|
||||
case TableConstants.DATA_ID_PRICE:
|
||||
dataItem.price = cellData.data
|
||||
dataItem.priceColor = cellData.color
|
||||
break;
|
||||
case TableConstants.DATA_ID_ZF:
|
||||
dataItem.riseFail = cellData.data
|
||||
dataItem.riseFailColor = cellData.color
|
||||
break;
|
||||
case TableConstants.DATA_ID_ZD:
|
||||
dataItem.priceRise = cellData.data
|
||||
dataItem.priceRiseColor = cellData.color
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
return dataItem
|
||||
}
|
||||
|
||||
private toggleExpand(): void {
|
||||
animateTo({
|
||||
duration: 300,
|
||||
curve: Curve.EaseInOut
|
||||
}, () => {
|
||||
this.isExtend = !this.isExtend;
|
||||
PreferenceService.putVal(FirstPageSelfCodeGroupView.SP_FILE_NAME_FIRST_PAGE_SELF_CODE_FILE_NAME,
|
||||
SP_KEY_FIRST_PAGE_SELF_CODE_EXPAND, this.isExtend, true)
|
||||
this.swiperIndex = 0;
|
||||
this.mPageItemCount = this.getPageItemCount()
|
||||
this.mMaxTotalItemCount = this.getTotalItemCount()
|
||||
this.initPageStocks()
|
||||
this.requestData()
|
||||
});
|
||||
}
|
||||
|
||||
private jumpToMore() {
|
||||
router.pushUrl({
|
||||
url: 'pages/SearchPage' // 目标url
|
||||
}, router.RouterMode.Standard, (err) => {
|
||||
if (err) {
|
||||
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
|
||||
return;
|
||||
}
|
||||
console.info('Invoke pushUrl succeeded.');
|
||||
});
|
||||
}
|
||||
|
||||
private jumpToQuote(dataModel: HQDataItemModel) {
|
||||
let code = dataModel.code
|
||||
let market = dataModel.market
|
||||
|
||||
let codeList: Array<string> = []
|
||||
let marketList: Array<string> = []
|
||||
let nameList: Array<string> = []
|
||||
|
||||
this.mDataItemModels.forEach((dataModel, _index) => {
|
||||
if (!dataModel.showAddImage) {
|
||||
codeList.push(dataModel.code ?? '')
|
||||
marketList.push(dataModel.market ?? '')
|
||||
nameList.push(dataModel.name ?? '')
|
||||
}
|
||||
})
|
||||
try {
|
||||
jumpToQuote(code, market, codeList.join(","), marketList.join(","), nameList.join(","));
|
||||
} catch (error) {
|
||||
console.log("Invoke pushUrl from selfCode to QuoteDetailPage Failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { StringUtils } from 'hxutil'
|
||||
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel'
|
||||
import { display } from '@kit.ArkUI'
|
||||
import { RouterUtil } from '../../util/RouterUtil'
|
||||
|
||||
/**
|
||||
* 首页顶部运营图节点
|
||||
* @author wubingsong@myhexin.com
|
||||
*/
|
||||
@Component
|
||||
export struct TopImageNodeComponent {
|
||||
|
||||
nodeModel: FirstPageNodeModel | null = null
|
||||
@State
|
||||
private isNodeShow: boolean = false
|
||||
@State bgUrl: string = ''
|
||||
private jumpUrl: string = ''
|
||||
@State nodeHeight: number = 70
|
||||
|
||||
aboutToAppear(): void {
|
||||
if (this.nodeModel != null) {
|
||||
this.bgUrl = this.nodeModel.iconUrl
|
||||
this.jumpUrl = this.nodeModel.url
|
||||
if (StringUtils.isNotEmpty(this.bgUrl)) {
|
||||
this.isNodeShow = true
|
||||
this.calculateHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private calculateHeight() {
|
||||
let dis = display.getDefaultDisplaySync()
|
||||
if (dis.densityPixels > 0) {
|
||||
let screenWidth = dis.width / dis.densityPixels
|
||||
if (StringUtils.isNotEmpty(this.bgUrl)) {
|
||||
this.nodeHeight = screenWidth * 0.61
|
||||
} else {
|
||||
this.nodeHeight = screenWidth * 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
|
||||
Image(this.bgUrl)
|
||||
.width('100%')
|
||||
.height(this.nodeHeight)
|
||||
.objectFit(ImageFit.Fill)
|
||||
.onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
|
||||
if (newValue.width) {
|
||||
let width = newValue.width as number
|
||||
if (StringUtils.isNotEmpty(this.bgUrl)) {
|
||||
this.nodeHeight = width * 0.61
|
||||
} else {
|
||||
this.nodeHeight = width * 0.3
|
||||
}
|
||||
}
|
||||
})
|
||||
.onClick(() => {
|
||||
RouterUtil.jumpPage(this.jumpUrl)
|
||||
})
|
||||
.visibility(this.isNodeShow ? Visibility.Visible : Visibility.None)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user