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,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