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:
clz
2026-07-26 01:10:20 +08:00
commit af0025ce38
137 changed files with 13358 additions and 0 deletions
@@ -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
// 合约行情数据 Mapkey: 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)
}
}