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,109 @@
/**
* getAllCards 接口响应
*/
export interface AllCardsResponse {
code: number
msg: string
data: AllCardsFloor[]
}
/**
* 楼层数据
*/
export interface AllCardsFloor {
id: number
key: string
title: string
card_list: AllCardsCard[]
}
/**
* 卡片配置
*/
export interface AllCardsCard {
card_id: number
card_key: string
render_key: string
card_title: CardTitle
card_url: CardUrl
mod_data: ModDataItem[]
bury_point: string
explain_title?: string
explain_message?: string
}
/**
* 卡片标题
*/
export interface CardTitle {
value: string
type: string
}
/**
* 卡片跳转链接
*/
export interface CardUrl {
ios: string
android: string
}
/**
* 数据请求配置
*/
export interface ModDataItem {
url: string
param_list: string[]
req_desc: string
}
/**
* AI 诊断接口响应
*/
export interface AiDiagnosisResponse {
status_code: number
status_msg: string
data: AiDiagnosisData
}
/**
* AI 诊断数据
*/
export class AiDiagnosisData {
market: string = ''
score: string = ''
code: string = ''
factor_list: FactorItem[] = []
name: string = ''
recommend: RecommendItem[] = []
message: string = ''
}
/**
* 因子项
*/
export class FactorItem {
score: string = ''
judge: string = ''
factor: string = ''
}
/**
* 推荐合约项
*/
export class RecommendItem {
market: string = ''
score: string = ''
code: string = ''
name: string = ''
}
/**
* 推荐合约(用于点击跳转)
*/
export interface RecommendContract {
name: string
score: string
code: string
market: string
}
@@ -0,0 +1,50 @@
/**
* AI 看涨跌接口响应
*/
export interface AiRiseFallResponse {
code: number
msg: string
data: AiRiseFallData
}
/**
* AI 看涨跌数据
*/
export interface AiRiseFallData {
date: string // 日期 "2026-06-01"
last_trade_date_accuracy: string // 昨日正确率 "66.7"
last_twenty_trade_date_accuracy: string // 近20日正确率 "66.4"
rise: RiseFallListData // 看涨列表
fall: RiseFallListData // 看跌列表
}
/**
* 看涨/看跌列表数据
*/
export interface RiseFallListData {
count: number
data: RiseFallItem[]
}
/**
* 看涨/看跌项
*/
export interface RiseFallItem {
variety: string // 品种代码 "SA"
name: string // 合约名称 "纯碱2609"
contract: string // 合约代码 "SA2609"
market: string // 市场代码 "67"
forecast: number // 预测方向 0=看涨, 1=看跌
accuracy: string // 品种正确率 "80.0"
show_quote: number // 是否显示行情 1=显示
}
/**
* 合约行情数据(用于行情订阅)
*/
export interface ContractHqData {
contract: string
market: string
name: string
priceChg: number | null // 涨跌幅,null 表示待开盘
}
@@ -0,0 +1,9 @@
export type BannerNodeModel = {
id: number,
title: string
imgUrl: string
jumpUrl: string
startTime: number
endTime: number
}
@@ -0,0 +1,56 @@
/**
* 商品期权卡片数据模型
*
* Author: YS
* Date: 2026-06-03
*/
/**
* Tab 配置项
*/
export interface CommodityOptionsTab {
label: ResourceStr // Tab 标签
api: string // API 标识
stat: string // 统计标识
defaultMsg: ResourceStr // 空数据提示信息
index: number // Tab 索引
}
/**
* Tab 配置
*/
export const COMMODITY_OPTIONS_TABS: CommodityOptionsTab[] = [
{
label: $r('app.string.tab_hot'),
api: 'rise_percent',
stat: 'hot',
defaultMsg: $r('app.string.commodity_options_hot_default_msg'),
index: 0,
},
{
label: $r('app.string.tab_near_expired'),
api: 'near_expired',
stat: 'impend',
defaultMsg: $r('app.string.commodity_options_near_expired_default_msg'),
index: 1,
},
]
/**
* 推荐期权合约项
*/
export class CommodityOptionsContract {
contract_code: string = '' // 合约代码
contract_name: string = '' // 合约名称
market: string = '' // 市场代码
expired_date: string = '' // 到期日(格式:20250628
}
/**
* API 响应
*/
export interface CommodityOptionsResponse {
code: number
msg: string
data: CommodityOptionsContract[]
}
@@ -0,0 +1,13 @@
export class CustomEntryListNodeModel {
id: number = -1
title: string = ''
imgUrl: string | Resource = ''
jumpUrl: string = ''
position: number = -1
startVersion: number = 0
endVersion: number = 0
buriedPoint: string = ''
buriedPointSource: string = ''
gridTag: string = ''
gridTagEffect: string = ''
}
@@ -0,0 +1,14 @@
export type EntryListNodeModel = {
id: number,
title: string,
imgUrl: string,
jumpUrl: string,
startVersion: string,
endVersion: string,
startTime: number,
endTime: number,
buriedPoint: string,
buriedPointSource: string,
gridTag: string
}
@@ -0,0 +1,55 @@
/**
* 热点排行数据模型
* @author YS
* @date 2026-06-03
*/
/**
* 热点排行数据项
* API 返回字段: title, url, tag, color, sorts, hot_value
*/
export interface HotListItemModel {
url: string // 跳转地址
title: string // 资讯标题
tag: string | null // 标签名称(可能为 null)
color: string | null // 颜色名称("red" | "orange",可能为 null
sorts: number // 排序值
hot_value: string // 热度值
}
/**
* 热点排行接口返回结果
*/
export interface HotListResultModel {
code: number // 接口返回码
msg: string // 返回消息
data: HotListItemModel[] // 热点数据列表
}
/**
* 颜色名称到资源ID的映射
*/
export const COLOR_NAME_MAP: Record<string, Resource> = {
'purple': $r('app.color.hotlist_purple'),
'blue': $r('app.color.hotlist_blue'),
'acidblue': $r('app.color.hotlist_acid_blue'),
'green': $r('app.color.hotlist_green'),
'orange': $r('app.color.hotlist_orange'),
'red': $r('app.color.hotlist_red'),
}
/**
* 默认颜色资源
*/
const DEFAULT_COLOR: Resource = $r('app.color.hotlist_blue')
/**
* 根据颜色名称获取颜色值
*/
export function getColorValue(colorName: string | null): ResourceStr {
if (!colorName) {
return DEFAULT_COLOR
}
const color = COLOR_NAME_MAP[colorName]
return color ?? DEFAULT_COLOR
}
@@ -0,0 +1,27 @@
export type HotNewsNodeModelResult = {
code: string,
mes: string,
data: HotNewsNodeModel[]
}
export type HotNewsNodeModel = {
seq: string, // 资讯唯一标识
title: string, // 资讯标题
source: string, // 来源
url: string, // 资讯跳转地址
img_url: string, // 图片地址
create_time: string, // 资讯发布时间
copyright: string, // 资讯版权
type: string, // 资讯分类
tags: HotNewsTagNodeModel[], // 资讯标签
}
export type HotNewsTagNodeModel = {
seq: string, // 资讯唯一标识
name: string, // 标签名称
weight: string, // 资讯权重
code: string, // 品种代码
market: string, // 市场id
}
+83
View File
@@ -0,0 +1,83 @@
import { BaseAd, FatigueDegree } from "@b2c-f/fuhm-ads"
/**
* author : liuqingliang@myhexin.com
* time : created on 2026/2/27
* desc : 弹窗广告数据模型
*/
export class PopupAdMode {
adid: string = ''
startTime: number = 0
endTime: number = 0
imgUrl: string | null = null
jumpUrl: string | null = null
isOpenInnerWebView: boolean = false
frequency: AdFrequency = AdFrequency.FREQUENCY_EVERY_LOGIN
position: number = 0
/**
* 上次显示时间
*/
showTime: number = 0
/**
* 上次是否显示过
*/
hasShow: boolean = false
fatigueDegree: FatigueDegree | null = null
toString(): string {
return `PopupAdMode (adid='${this.adid}', frequency=${this.frequency}, position=${this.position}, hasShow=${this.hasShow}, fatigueDegree=${this.fatigueDegree})`
}
toBaseAd() {
let newAd = new BaseAd()
newAd.adId = this.adid
newAd.startTime = this.startTime
newAd.endTime = this.endTime
newAd.imgUrl = this.imgUrl ?? ""
newAd.jumpUrl = this.jumpUrl ?? ""
newAd.isOpenInnerWebView = this.isOpenInnerWebView
newAd.fatigueDegree = this.fatigueDegree
newAd.position = this.position
return newAd
}
}
/**
* 首页弹窗广告显示频率
*/
export enum AdFrequency {
/**
* 有效期间内,每次登录都显示
*/
FREQUENCY_EVERY_LOGIN = 0,
/**
* 有效期间内,每天显示一次
*/
FREQUENCY_EVERY_DAY = 1,
/**
* 有效期间内,只显示一次
*/
FREQUENCY_ONLY_ONCE = 2
}
/**
* 首页cpyyy适配器
* */
export function adapterForIndexPopupActivity(newAdModels: BaseAd[]) {
let oldAdModels: PopupAdMode[] = []
newAdModels.forEach((it) => {
let oldPopupAdMode = new PopupAdMode()
oldPopupAdMode.adid = it.adId ?? ""
oldPopupAdMode.startTime = it.startTime
oldPopupAdMode.endTime = it.endTime
oldPopupAdMode.imgUrl = it.imgUrl
oldPopupAdMode.jumpUrl = it.jumpUrl
oldPopupAdMode.isOpenInnerWebView = it.isOpenInnerWebView
oldPopupAdMode.frequency =
it.fatigueDegree?.isDailyShowOnce == 1 ? AdFrequency.FREQUENCY_EVERY_DAY : AdFrequency.FREQUENCY_EVERY_LOGIN
oldPopupAdMode.fatigueDegree = it.fatigueDegree
oldPopupAdMode.position = it.position
oldAdModels.push(oldPopupAdMode)
})
return oldAdModels
}
@@ -0,0 +1,152 @@
import { HXUtils, StockMarketUtil } from "@b2c/lib_baseui";
import { NumberUtils } from "hxutil";
export interface SelfSelectedStockModel {
id: string;
stockCode: string;
stockName: string;
currentPrice: number;
changeAmount: number;
changePercent: number;
updateTime?: number;
}
export interface SelfSelectedStockModelMock {
code: string;
name: string;
price: number;
change: number;
percent: number;
}
export interface StockGroupModel {
id: string;
groupName: string;
stocks: SelfSelectedStockModel[];
isSelected: boolean;
}
const HQ_DATA_INVALID = "--"
@Observed
export class SwiperHqDataItemModel extends Array<GridHqDataItemModel>{}
@Observed
export class GridHqDataItemModel extends Array<HQDataItemModel>{}
/**
* 行情自定义数据结构
*/
@Observed
export class HQDataItemModel{
name:string = HQ_DATA_INVALID
code:string = HQ_DATA_INVALID
showCode:string = HQ_DATA_INVALID
market:string = "0"
price:string = HQ_DATA_INVALID
riseFail:string = HQ_DATA_INVALID //涨跌幅
priceRise:string = HQ_DATA_INVALID
nameColor:ResourceColor = 0
priceColor:ResourceColor = 0
riseFailColor:ResourceColor = 0
priceRiseColor:ResourceColor = 0
showAddImage:boolean = false
/**
* -1:跌,1:涨,0:不变
*/
riseFailFlag:number = 0
priceChange:boolean = false
constructor(stockCode?:string,stockMarket?:string,stockName?:string) {
this.code = stockCode??HQ_DATA_INVALID
this.market = stockMarket??HQ_DATA_INVALID
this.name = stockName??HQ_DATA_INVALID
this.showCode = stockCode??HQ_DATA_INVALID
}
copyFrom(dataItemModel:HQDataItemModel){
this.name = dataItemModel.name
const market = dataItemModel.market
const showCodeTemp = dataItemModel.showCode
if ((StockMarketUtil.isOption(market+"") || StockMarketUtil.isFutureTL(market+"")) && HXUtils.isValidContractData(showCodeTemp+"")) {
this.showCode = showCodeTemp
}else{
this.showCode = this.code
}
this.riseFailFlag = this.getRiseFailFlag(dataItemModel.price,this.price)
this.priceChange = this.riseFailFlag != 0
this.price = dataItemModel.price
this.riseFail = dataItemModel.riseFail
this.priceRise = dataItemModel.priceRise
this.riseFailColor = dataItemModel.riseFailColor
this.riseFailColor = dataItemModel.riseFailColor
}
getMapKey():string{
return this.code+"_"+this.market
}
private getRiseFailFlag(newPrice:string,lastPrice:string){
let ret = 0
if (newPrice == lastPrice){
return ret
}
if (NumberUtils.isNumeric(newPrice) && NumberUtils.isNumeric(lastPrice)) {
const newPriceNumber = parseFloat(newPrice)
const lastPriceNumber = parseFloat(lastPrice)
if (newPriceNumber == lastPriceNumber) {
ret = 0
}else {
ret = newPriceNumber > lastPriceNumber ? 1:-1
}
}
return ret
}
copy():HQDataItemModel{
let copyDataItemModel = new HQDataItemModel(this.code,this.market,this.name)
copyDataItemModel.showCode = this.showCode
copyDataItemModel.price = this.price
copyDataItemModel.riseFail = this.riseFail
copyDataItemModel.priceRise = this.priceRise
copyDataItemModel.nameColor = this.nameColor
copyDataItemModel.riseFailColor = this.riseFailColor
copyDataItemModel.priceRiseColor = this.priceRiseColor
copyDataItemModel.showAddImage = this.showAddImage
copyDataItemModel.priceChange = this.priceChange
copyDataItemModel.riseFailFlag = this.riseFailFlag
return copyDataItemModel
}
}
export class GridDataSource implements IDataSource{
private dataArray:HQDataItemModel[] = []
constructor(data:HQDataItemModel[]) {
this.dataArray = data
}
totalCount(): number {
return this.dataArray.length
}
getData(index: number): HQDataItemModel {
return this.dataArray[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}