Initial commit: @b2c/first_page HarmonyOS stage-mode HAR

## Repository
- HarmonyOS stage-mode HAR module, consumed by host app
- No standalone build — hvigorw, oh_modules, and ../biz_quote absent here

## Architecture
### Feed Cards
- FirstPage.createNodeView dispatches cards by FirstPageConstant.KEY_*
- Card config from first_page_cards_config.json loaded by FirstPageCardsConfigLoader
- Public entrypoint: Index.ets

### Market Ranking (src/main/ets/market-ranking/)
- HTTP: MarketRankingDataFetcher fetches recommend_futures contracts
- HQ: MarketRankingHqRequestClient subscribes via 4106/frame 2201/scenario qht_qihuo_sort
- TCP reconnect rebuilds subscription from original HTTP contract list
- Async callbacks guarded by request/subscription versions

### Other Cards
- HotList, CommodityOptions, AiDiagnosis, AiRiseFall, SelfSelectedStock, CustomEntryList, FourEntryList
- Each has DataFetcher + HQ request client + NodeComponent

## Key Changes (from CardHarmonyOS reference)
- Added MetricId, PeriodId, SortOrder enums (MarketRankingModels.ets)
- Replaced string comparisons with enum switches in thirdColumnTitle/Value/Color
- Removed redundant undefined guards in thirdColumnColor/priceColor, delegated to riseFallColor
- Inlined priceColor(), use riseFallColor directly
- Tab bar Stack.height(30), Alignment.End (reverted from 46/TopEnd)
- Name column fontWeight removed (default regular)
This commit is contained in:
clz
2026-07-26 01:10:20 +08:00
commit af0025ce38
137 changed files with 13358 additions and 0 deletions
@@ -0,0 +1,413 @@
import { AdsPageConfig, AppInfoConfig, FirstPageTabManager, FrameId, GlobalContext } from 'biz_common'
import { CustomEntryListNodeComponent } from '../../node/view/CustomEntryListNodeComponent'
import FirstPageTitleBar from '../../views/FirstPageTitleBar'
import { BannerNodeComponent } from '../../node/view/BannerNodeComponent'
import { FirstPageDataFetcher } from '../../datacenter/FirstPageDataFetcher'
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel'
import { FourEntryListNodeComponent } from '../../node/view/FourEntryListNodeComponent'
import { FirstPageConstant } from '../../datacenter/FirstPageConstant'
import { TopImageNodeComponent } from '../../node/view/TopImageNodeComponent'
import { TITLE_BAR_HEIGHT } from '../../util/FirstPageConstant'
import { FirstPageContentBg } from '../../views/FirstPageContentBg'
import { PreferenceUtils } from 'hxutil'
import { getResourceString } from '../../util/ResourceUtil'
import { router } from '@kit.ArkUI'
import { AppUtil } from '@pura/harmony-utils'
import { SelfSelectedStockNodeComponent } from '../../node/view/SelfSelectedStockNodeComponent'
import { FirstPagePopupAdHelper } from '../../node/helper/FirstPagePopupAdHelper'
import { OnAdsDataUpdateListener, BaseAd, AdsManager, ADS_TYPE_INDEX_POPUP, AdsPopupManager } from '@b2c-f/fuhm-ads'
import { DebugToolUtil } from 'biz_debug'
import { KaihuFlagManager, NewUserManager, UserSourceManager } from 'biz_hxservice'
import { AdsPopupHost } from '../../popup/AdsPopupHost'
import { AiDiagnosisNodeComponent } from '../../node/view/AiDiagnosisNodeComponent'
import { AiRiseFallNodeComponent } from '../../node/view/AiRiseFallNodeComponent'
import { HotListNodeComponent } from '../../node/view/HotListNodeComponent'
import { CommodityOptionsNodeComponent } from '../../node/view/CommodityOptionsNodeComponent'
import { MarketRankingNodeComponent } from '../../market-ranking/MarketRankingNodeComponent'
/**
* 首页
* @author wubingsong@myhexin.com
*/
@Component
export struct FirstPage {
//首页tab管理器
@ObjectLink @Watch('onVisibilityChange') tabManager: FirstPageTabManager;
@Provide firstPageVisible: boolean = true;
@State nodeModelArr: FirstPageNodeModel[] = []
private topBarHeight: number = 0
private scrollHeight: number = 0
@State isTopViewShow: boolean = false
@State isRefreshing: boolean = false
private refreshTimer: number = -1
@State isStartRefresh: boolean = true
@State refreshTitle: string = '下拉可以刷新'
@State refreshTime: string = ''
@State refreshRotateAngle: number = 0
@State needAnimation: boolean = false
/** 广告数据监听器 */
private adsPopupListener: OnAdsDataUpdateListener = {
onAdsDataUpdate: (adsType: string, adsDataModels: BaseAd[]) => {
if (this.firstPageVisible) {
FirstPagePopupAdHelper.getInstance().onAdsDataUpdate(adsType, adsDataModels)
}
}
}
// 卡片刷新触发器
@State refreshTrigger: number = 0
// 自动刷新定时器 ID
private autoRefreshTimer: number = -1
// 自动刷新间隔(60秒)
private readonly AUTO_REFRESH_INTERVAL = 60000
private kaihuFlagListener = (): void => {
const kaihuFlagManager = KaihuFlagManager.getInstance()
//归宿 更新了加载一遍首页和广告
if (kaihuFlagManager.isFlagChanged()) {
kaihuFlagManager.resetFlagChanged()
this.refreshFirstPageDataAndAds()
}
}
private userSourceUpdateListener = (): void => {
//来源更新后,只有当没有归属时,才重新刷新首页和广告
if (!KaihuFlagManager.getInstance().getKaihuFlag()) {
this.refreshFirstPageDataAndAds()
}
}
onVisibilityChange() {
this.firstPageVisible = this.tabManager.tabVisibility;
FirstPagePopupAdHelper.getInstance().firstPageVisible = this.firstPageVisible;
if (this.tabManager.tabVisibility) {
FirstPageDataFetcher.getInstance().remoteFetchData()
FirstPagePopupAdHelper.getInstance().showPopupAdWhenOnResume()
this.refreshAdsData()
AdsManager.refreshNotifyAds(ADS_TYPE_INDEX_POPUP)
AdsPopupHost.getInstance().onPageJump(AdsPageConfig.FIRST_PAGE_ID)
// 启动定时器
this.startAutoRefresh()
} else {
KaihuFlagManager.getInstance().removeListener(this.kaihuFlagListener)
UserSourceManager.getInstance().removeListener(this.userSourceUpdateListener)
AdsPopupHost.getInstance().onPageJump(AdsPageConfig.INVALID)
// 暂停定时器
this.stopAutoRefresh()
this.stopDelayRefresh()
}
}
private refreshAdsData() {
KaihuFlagManager.getInstance().addListener(this.kaihuFlagListener)
UserSourceManager.getInstance().addListener(this.userSourceUpdateListener)
if (KaihuFlagManager.getInstance().isFlagChanged()) {
//用户开户标志如果有更新此时发送首页请求了则将其复原重置,用来防止没有变化却重复发送请求
KaihuFlagManager.getInstance().resetFlagChanged()
}
if (NewUserManager.getInstance().needRequestAdsNow()) {
//发送请求首页广告
this.refreshFirstPageDataAndAds()
}
}
aboutToAppear(): void {
this.firstPageVisible = this.tabManager.tabVisibility;
FirstPagePopupAdHelper.getInstance().firstPageVisible = this.firstPageVisible;
this.topBarHeight = px2vp(GlobalContext.statusBarHeight) + TITLE_BAR_HEIGHT
if (DebugToolUtil.isInnerNetSimulateMode()) {
// 内网请求不到首页板块,默认只填充自选盯盘
const hqNode = new FirstPageNodeModel()
hqNode.id = 0
hqNode.targetTypeId = FirstPageConstant.KEY_HANGQING_TABLE.toString()
this.nodeModelArr = [hqNode]
}
FirstPageDataFetcher.getInstance().nodeConfigChange = (response) => {
this.finishRefresh()
if (response != null) {
for (let index = 0; index < response.length; index++) {
const model = response[index];
if (model.targetTypeId == FirstPageConstant.KEY_TOP_BG.toString()) {
if (index > 0) {
let topModel = response.splice(index, 1)
response.unshift(...topModel)
}
this.isTopViewShow = true
break
} else if (index == response.length - 1) {
this.isTopViewShow = false
}
}
let hotListModel = new FirstPageNodeModel()
hotListModel.targetTypeId = FirstPageConstant.KEY_HOT_LIST.toString()
let commodityOptionModel = new FirstPageNodeModel()
commodityOptionModel.targetTypeId = FirstPageConstant.KEY_COMMODITY_OPTIONS.toString()
let aiDiagnosisModel = new FirstPageNodeModel()
aiDiagnosisModel.targetTypeId = FirstPageConstant.KEY_AI_DIAGNOSIS.toString()
let aiRiseFallModel = new FirstPageNodeModel()
aiRiseFallModel.targetTypeId = FirstPageConstant.KEY_AI_RISEFALL.toString()
let marketRankingModel = new FirstPageNodeModel()
marketRankingModel.targetTypeId = FirstPageConstant.KEY_MARKET_RANKING.toString()
let feedBackModel = new FirstPageNodeModel()
feedBackModel.targetTypeId = FirstPageConstant.KEY_FEED_BACK.toString()
let endTipModel = new FirstPageNodeModel()
endTipModel.targetTypeId = FirstPageConstant.KEY_END_TIP.toString()
response.push(hotListModel, commodityOptionModel, aiRiseFallModel, aiDiagnosisModel, marketRankingModel, feedBackModel, endTipModel)
this.nodeModelArr = response
}
}
FirstPageDataFetcher.getInstance().onError = () => {
this.isRefreshing = false
}
FirstPageDataFetcher.getInstance().fetchDataLocal()
FirstPageDataFetcher.getInstance().remoteFetchData()
this.refreshAdsData()
AdsManager.addAdsDataUpdateListener(ADS_TYPE_INDEX_POPUP, this.adsPopupListener)
if (this.firstPageVisible) {
AdsManager.refreshNotifyAds(ADS_TYPE_INDEX_POPUP)
}
// 启动ai卡片定时器
this.startAutoRefresh()
}
/**
* 启动自动刷新定时器
*/
private startAutoRefresh(): void {
if (this.autoRefreshTimer !== -1) {
return
}
this.autoRefreshTimer = setInterval(() => {
this.refreshTrigger++
}, this.AUTO_REFRESH_INTERVAL)
}
/**
* 停止自动刷新定时器
*/
private stopAutoRefresh(): void {
if (this.autoRefreshTimer !== -1) {
clearInterval(this.autoRefreshTimer)
this.autoRefreshTimer = -1
}
}
private startDelayRefresh(): void {
this.refreshTimer = setTimeout(() => {
this.isRefreshing = false
}, 1000)
}
private stopDelayRefresh(): void {
if (this.refreshTimer !== -1) {
clearTimeout(this.refreshTimer)
this.refreshTimer = -1
}
}
aboutToDisappear(): void {
AdsManager.removeAdsDataUpdateListener(ADS_TYPE_INDEX_POPUP, this.adsPopupListener)
// 停止自动刷新定时器
this.stopAutoRefresh()
this.stopDelayRefresh()
}
private refreshFirstPageDataAndAds(): void {
AdsManager.refreshNotifyFirstPageAds(true)
}
private finishRefresh() {
this.refreshTime = PreferenceUtils.getStringSync(GlobalContext.get(), 'SpFirstPage', 'refreshTime', '')
// 刷新完成后递增卡片刷新触发器
this.refreshTrigger++
this.startDelayRefresh()
}
build() {
Refresh({ refreshing: $$this.isRefreshing, builder: this.updateRefreshHeader()}) {
Stack({ alignContent: Alignment.TopStart }) {
FirstPageTitleBar().zIndex(5)
Scroll() {
Stack({ alignContent: Alignment.TopStart }) {
FirstPageContentBg()
.visibility(this.isTopViewShow ? Visibility.None : Visibility.Visible)
Column() {
Column()
.width('100%')
.height(this.topBarHeight)
.visibility(this.isTopViewShow ? Visibility.None : Visibility.Visible)
ForEach(this.nodeModelArr, (model: FirstPageNodeModel, index: number) => {
this.createNodeView(model, index)
})
}
.margin({bottom: 10})
}
.width('100%')
}
.scrollBar(BarState.Off)
.width('100%')
.height('auto')
.backgroundColor($r('app.color.surface_layer1_background'))
.onScrollStart(()=>{
AdsPopupHost.getInstance().onScrollEvent()
})
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.refreshOffset(90)
.pullToRefresh(true)
.onStateChange((refreshStatus: RefreshStatus) => {
if (refreshStatus == RefreshStatus.Drag) {
this.refreshTitle = getResourceString(getContext(),$r('app.string.drag_refresh_pull'));
this.needAnimation = false
this.isStartRefresh = true
this.refreshRotateAngle = 0
} else if (refreshStatus == RefreshStatus.OverDrag) {
this.refreshTitle = getResourceString(getContext(),$r('app.string.drag_refresh_to_refresh'));
this.needAnimation = true
this.refreshRotateAngle = 180
} else if (refreshStatus == RefreshStatus.Refresh) {
this.refreshTitle = getResourceString(getContext(),$r('app.string.drag_refresh_loading'));
this.refreshTime = PreferenceUtils.getStringSync(GlobalContext.get(), 'SpFirstPage', 'refreshTime', '')
this.isStartRefresh = false
}
})
.onRefreshing(() => {
FirstPageDataFetcher.getInstance().remoteFetchData()
})
}
@Builder
updateRefreshHeader() {
Row() {
Image($r('app.media.firstpage_pull_status_arrow'))
.width(18)
.height(40)
.rotate({ angle: this.refreshRotateAngle })
.animation(this.needAnimation ? {
duration: 500,
} : null)
.visibility(this.isStartRefresh ? Visibility.Visible : Visibility.None)
LoadingProgress()
.width(30)
.height(30)
.visibility(this.isStartRefresh ? Visibility.None : Visibility.Visible)
Column() {
Text(this.refreshTitle)
.fontSize(15)
.fontColor($r('app.color.elements_text_primary_02'))
Row() {
Text($r('app.string.drag_refresh_last_update_time'))
.fontSize(15)
.fontColor($r('app.color.elements_text_primary_02'))
.margin({right: 10})
Text(this.refreshTime)
.fontSize(15)
.fontColor($r('app.color.elements_text_primary_02'))
}
.visibility(this.refreshTime == '' ? Visibility.None : Visibility.Visible)
}
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.margin({left: 10})
}
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.width('100%')
}
@Builder
createNodeView(model: FirstPageNodeModel, index: number) {
if (model.targetTypeId == FirstPageConstant.KEY_BANNER.toString()) {
BannerNodeComponent({
nodeModel: model
}).margin({top: index > 0 ? 10 : 0, left: 10, right: 10})
} else if (model.targetTypeId == FirstPageConstant.KEY_FOUR_ENTRY_LIST.toString()) {
FourEntryListNodeComponent({
nodeModel: model
}).margin({top: index > 0 ? 10 : 0, left: 10, right: 10})
} else if (model.targetTypeId == FirstPageConstant.KEY_CUSTOM_ENTRY_LIST.toString()) {
CustomEntryListNodeComponent({
nodeModel: model
}).margin({top: (AppInfoConfig.Official && !this.isTopViewShow || index) > 0 ? 10 : 0, left: 10, right: 10})
} else if (model.targetTypeId == FirstPageConstant.KEY_AI_DIAGNOSIS.toString()) {
AiDiagnosisNodeComponent({
nodeModel: model,
refreshTrigger: this.refreshTrigger,
}).margin({ top: index > 0 ? 10 : 0, left: 10, right: 10 })
} else if (model.targetTypeId == FirstPageConstant.KEY_AI_RISEFALL.toString()) {
AiRiseFallNodeComponent({
nodeModel: model,
refreshTrigger: this.refreshTrigger,
}).margin({ top: index > 0 ? 10 : 0, left: 10, right: 10 })
} else if (model.targetTypeId == FirstPageConstant.KEY_HOT_LIST.toString()) {
HotListNodeComponent({
nodeModel: model,
refreshTrigger: this.refreshTrigger,
}).margin({ top: index > 0 ? 10 : 0, left: 10, right: 10 })
} else if (model.targetTypeId == FirstPageConstant.KEY_COMMODITY_OPTIONS.toString()) {
CommodityOptionsNodeComponent({
refreshTrigger: this.refreshTrigger,
}).margin({ top: index > 0 ? 10 : 0, left: 10, right: 10 })
} else if (model.targetTypeId == FirstPageConstant.KEY_MARKET_RANKING.toString()) {
MarketRankingNodeComponent({
refreshTrigger: this.refreshTrigger,
}).margin({ top: index > 0 ? 10 : 0, left: 10, right: 10 })
} else if (model.targetTypeId == FirstPageConstant.KEY_TOP_BG.toString()) {
TopImageNodeComponent({
nodeModel: model
})
} else if (model.targetTypeId == FirstPageConstant.KEY_FEED_BACK.toString()) {
this.buildFeedBackView(index)
}else if (model.targetTypeId == FirstPageConstant.KEY_HANGQING_TABLE.toString()){
SelfSelectedStockNodeComponent().margin({top: index > 0 ? 10 : 0, left: 10, right: 10})
}
else if (model.targetTypeId == FirstPageConstant.KEY_END_TIP.toString()) {
Text($r('app.string.drag_refresh_to_bottom'))
.fontSize(14)
.fontColor($r('app.color.color_a3a3a3'))
.textAlign(TextAlign.Center)
.width('100%')
.margin({top: 16, bottom: 15})
}
}
@Builder
private buildFeedBackView(index:number){
Column(){
Row(){
Image($r('app.media.icon_edit_square_24')).width(16).height(16).align(Alignment.Center)
.fillColor($r('app.color.elements_icon_primary_02'))
Text($r('app.string.feed_back_help_text'))
.fontSize(15)
.fontColor($r('app.color.elements_text_primary_02'))
.textAlign(TextAlign.Center)
.align(Alignment.Center)
}.width('100%')
.height(44)
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
.backgroundColor($r('app.color.surface_layer1_foreground'))
.borderRadius(10)
.onClick((event)=>{
router.pushUrl({ url: 'pages/WebViewComponentPage', params: {
url:AppUtil.getContext().resourceManager.getStringSync($r('app.string.fu_feed_back_help_url')),
title: AppUtil.getContext().resourceManager.getStringSync($r('app.string.feed_back_help_text')),
userCookie:true,
isLoginRefreshWeb:true,
refreshOnWidthChange:true
} })
})
}.width('100%')
.padding({left:10,right:10})
.padding({top: index > 0 ? 10 : 0, left: 10, right: 10})
}
}