refactor(hq): 抽离卡片行情订阅客户端

This commit is contained in:
clz
2026-07-24 13:58:22 +08:00
parent 63fcb9cbaa
commit 66151d78a0
11 changed files with 469 additions and 365 deletions
+6 -5
View File
@@ -1,7 +1,7 @@
# 市场排名接口文档
本文记录 `MarketRankingNodeComponent` 卡片使用的合约榜单 HTTP 接口及 4106 行情订阅协议。当前实现位于
`entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets`
本文记录 `MarketRankingNodeComponent` 卡片使用的合约榜单 HTTP 接口及 4106 行情订阅协议。HTTP
实现位于 `MarketRankingDataFetcher.ets`,行情实现位于 `MarketRankingHqRequestClient.ets`
## 合约榜单 HTTP 接口
@@ -93,12 +93,13 @@ GET {API_HOST}futgwapi/api/market/homepage/v1/recommend_futures
| `chg_speed_10min` | 34875 | 10 分钟涨跌速 |
| `chg_speed_15min` | 34876 | 15 分钟涨跌速 |
原始推送为“字段 ID → 数组”的分列结构,相同数组下标表示同一合约。Fetcher 将其合并为
`RankItem[]`,再通过完整替换 `CardData` 刷新 UI。
鸿蒙原始推送为 `TableData` 行式结构。`tableRows` 中每个 Row 代表一份合约行情,通过
`row.get(字段 ID)` 读取对应值。`MarketRankingHqRequestClient` 将其转换为 `RankItem[]`,再通过
完整替换 `CardData` 刷新 UI。
## 当前 Mock 行为
- HTTP:使用 `@ohos.net.http` 构造 GET URL 和请求选项,但不调用 `request()`
- 行情:构造并输出完整 4106 参数,不连接真实行情桥。
- 行情:构造并输出完整 4106 参数,不连接真实行情桥Mock 使用 `tableRows + row.get(id)` 模拟回调
- Mock 行情每 `2500ms` 推送一次,与 `pushtime=2.5` 保持一致。
- 每次请求及订阅参数均使用 `[MarketRanking]` 前缀输出到控制台。
@@ -1,10 +1,7 @@
// AI选期数据层:负责网络请求、行情订阅及原始数据到视图数据的转换。
// AI 选期数据层:负责网络请求及原始数据到视图数据的转换。
import http from '@ohos.net.http';
import allCardsMock from '../common/mock/AllCardsMock.json';
import {
AiPickQuoteData,
AiPickQuoteRequestParams,
CardData,
PickContractItem,
PickStrategy,
PickTabData,
@@ -24,7 +21,6 @@ import { MAX_ITEMS, MAX_TABS, MAX_TAGS } from './AiPickConstant';
// 模拟网络延迟(毫秒),验证 loading 状态和异步流程用;接入真实接口后删除
const MOCK_DELAY_MS = 300;
const MOCK_QUOTE_INTERVAL_MS = 2500;
const EMPTY_RAW_STRATEGY_ITEM: RawStrategyItem = {};
const EMPTY_RAW_STRATEGY_DETAIL: RawStrategyDetail = {
id: '',
@@ -32,8 +28,6 @@ const EMPTY_RAW_STRATEGY_DETAIL: RawStrategyDetail = {
strategy_type: '',
};
export type AiPickCardDataListener = (cardData: CardData) => void;
// 对应 Vue 端 index.vue 中 formatTabData 的单项处理(新版 isCustomVersion=true
function parseRawTab(item: RawPickTabItem): PickTabData {
const isSystemPeriod = item.type === 'system_period';
@@ -143,128 +137,6 @@ export class AiPickDataFetcher {
});
}
// 模拟 Vue 的共享 4106 行情订阅:立即推送一次,之后每 2.5 秒整体更新 CardData。
subscribeQuotes(cardData: CardData, listener: AiPickCardDataListener): () => void {
const requestParams = this.buildQuoteRequestParams(cardData);
console.info(`[AiPick] subscribeQuotes params: ${JSON.stringify(requestParams)}`);
const contracts = this.getUniqueContracts(cardData);
let tick = 0;
const emit = (): void => {
tick += 1;
const quoteData = this.createMockQuoteData(contracts, tick);
listener(this.mergeQuoteData(cardData, quoteData));
};
emit();
const timer = setInterval(emit, MOCK_QUOTE_INTERVAL_MS);
return (): void => {
clearInterval(timer);
};
}
// 对齐 Vue buildHqParams,生成 AI 选期共享行情连接的完整请求参数。
private buildQuoteRequestParams(cardData: CardData): AiPickQuoteRequestParams {
const dataItems: string[] = ['55', '10', '4', '36103', '34818', '6', '7', '66', '34877'];
const requestDic = `sortid=-1\r\n` +
`sortorder=0\r\n` +
`push=1\r\n` +
`dataitem=${dataItems.map((item: string): string => `${item},`).join('')}\r\n` +
`codelist=${this.formatCodeList(cardData)}\r\n` +
`scenario=qht_qihuo_sort\r\n` +
`precision=1\r\n` +
`pushtime=2.5\r\n`;
return {
protocolId: '4106',
pageId: '2201',
onlineId: 'contractHQData',
columnOrder: dataItems.join('|'),
requestDic: requestDic,
};
}
private getUniqueContracts(cardData: CardData): PickContractItem[] {
const contracts: PickContractItem[] = [];
const contractKeys: string[] = [];
cardData.tabs.forEach((tab: PickTabData): void => {
tab.items.forEach((item: PickContractItem): void => {
const contractKey = `${item.contract}_${item.market}`;
if (!contractKeys.includes(contractKey)) {
contractKeys.push(contractKey);
contracts.push(item);
}
});
});
return contracts;
}
private formatCodeList(cardData: CardData): string {
const contracts = this.getUniqueContracts(cardData);
const markets: string[] = [];
const codeGroups: string[][] = [];
contracts.forEach((contract: PickContractItem): void => {
const marketIndex = markets.indexOf(contract.market);
if (marketIndex >= 0) {
codeGroups[marketIndex].push(contract.contract);
} else {
markets.push(contract.market);
codeGroups.push([contract.contract]);
}
});
return markets.map((market: string, index: number): string =>
`${market}(${codeGroups[index].map((code: string): string => `${code},`).join('')});`
).join('');
}
private createMockQuoteData(contracts: PickContractItem[], tick: number): AiPickQuoteData {
const data: AiPickQuoteData = {
code: [],
market: [],
price: [],
priceChg: [],
};
contracts.forEach((contract: PickContractItem, index: number): void => {
data.code.push(contract.contract);
data.market.push(contract.market);
data.price.push(100 + index * 10 + tick * 0.1);
data.priceChg.push(((tick + index) % 5 - 2) * 0.35);
});
return data;
}
private mergeQuoteData(cardData: CardData, quoteData: AiPickQuoteData): CardData {
const tabs = cardData.tabs.map((tab: PickTabData): PickTabData => {
const items = tab.items.map((item: PickContractItem): PickContractItem => {
let quoteIndex = -1;
for (let index = 0; index < quoteData.code.length; index += 1) {
if (quoteData.code[index] === item.contract && quoteData.market[index] === item.market) {
quoteIndex = index;
break;
}
}
const nextItem: PickContractItem = {
contract: item.contract,
market: item.market,
name: item.name,
price: quoteIndex >= 0 ? quoteData.price[quoteIndex] : undefined,
priceChg: quoteIndex >= 0 ? quoteData.priceChg[quoteIndex] : undefined,
isMain: item.isMain,
extraFields: item.extraFields,
};
return nextItem;
});
const nextTab: PickTabData = {
strategy: tab.strategy,
items: items,
keysList: tab.keysList,
fieldList: tab.fieldList,
};
return nextTab;
});
const nextCardData: CardData = {
tabs: tabs,
};
return nextCardData;
}
private buildRequestUrl(configuredUrl: string, timestamp: number): string {
const separator = configuredUrl.includes('?') ? '&' : '?';
return `${configuredUrl}${separator}_=${timestamp}`;
@@ -0,0 +1,158 @@
import {
AiPickHqData,
AiPickHqRequestParams,
CardData,
PickContractItem,
PickTabData,
} from './AiPickModels';
import { HqFieldId } from '../common/HqFieldId';
const MOCK_QUOTE_INTERVAL_MS = 2500;
export type AiPickCardDataListener = (cardData: CardData) => void;
// AI 选期行情请求客户端:负责合约去重、4106 参数、订阅和行情数据合并。
export class AiPickHqRequestClient {
private static readonly instance: AiPickHqRequestClient = new AiPickHqRequestClient();
private constructor() {
}
static getInstance(): AiPickHqRequestClient {
return AiPickHqRequestClient.instance;
}
// 模拟共享 4106 行情订阅:立即推送一次,之后每 2.5 秒更新完整卡片数据。
subscribeHq(cardData: CardData, listener: AiPickCardDataListener): () => void {
const requestParams = this.buildHqRequestParams(cardData);
console.info(`[AiPick] subscribeHq params: ${JSON.stringify(requestParams)}`);
// todo: 接入真实的行情订阅。
const contracts = this.getUniqueContracts(cardData);
let tick = 0;
const emit = (): void => {
tick += 1;
const hqData = this.createMockHqData(contracts, tick);
listener(this.mergeHqData(cardData, hqData));
};
emit();
const timer = setInterval(emit, MOCK_QUOTE_INTERVAL_MS);
return (): void => {
clearInterval(timer);
};
}
// 对齐 Vue buildHqParams,生成 AI 选期共享行情连接的完整请求参数。
buildHqRequestParams(cardData: CardData): AiPickHqRequestParams {
const dataItems: number[] = [
HqFieldId.DATA_ID_NAME,
HqFieldId.DATA_ID_PRICE,
HqFieldId.DATA_ID_CODE,
HqFieldId.DATA_ID_MARKET,
HqFieldId.DATA_ID_PRICE_CHG,
HqFieldId.DATA_ID_PRE_PRICE,
HqFieldId.DATA_ID_OPEN_PRICE,
HqFieldId.DATA_ID_PRE_SETTLE_PRICE,
HqFieldId.DATA_ID_PRICE_CHG_YTD,
];
const requestDic = `sortid=-1\r\n` +
`sortorder=0\r\n` +
`push=1\r\n` +
`dataitem=${dataItems.map((item: number): string => `${item},`).join('')}\r\n` +
`codelist=${this.formatCodeList(cardData)}\r\n` +
`scenario=qht_qihuo_sort\r\n` +
`precision=1\r\n` +
`pushtime=2.5\r\n`;
return {
protocolId: '4106',
pageId: '2201',
onlineId: 'contractHQData',
columnOrder: dataItems.join('|'),
requestDic: requestDic,
};
}
private getUniqueContracts(cardData: CardData): PickContractItem[] {
const contracts: PickContractItem[] = [];
const contractKeys: string[] = [];
cardData.tabs.forEach((tab: PickTabData): void => {
tab.items.forEach((item: PickContractItem): void => {
const contractKey = `${item.contract}_${item.market}`;
if (!contractKeys.includes(contractKey)) {
contractKeys.push(contractKey);
contracts.push(item);
}
});
});
return contracts;
}
private formatCodeList(cardData: CardData): string {
const contracts = this.getUniqueContracts(cardData);
const markets: string[] = [];
const codeGroups: string[][] = [];
contracts.forEach((contract: PickContractItem): void => {
const marketIndex = markets.indexOf(contract.market);
if (marketIndex >= 0) {
codeGroups[marketIndex].push(contract.contract);
} else {
markets.push(contract.market);
codeGroups.push([contract.contract]);
}
});
return markets.map((market: string, index: number): string =>
`${market}(${codeGroups[index].map((code: string): string => `${code},`).join('')});`
).join('');
}
private createMockHqData(contracts: PickContractItem[], tick: number): AiPickHqData {
const data: AiPickHqData = {
code: [],
market: [],
price: [],
priceChg: [],
};
contracts.forEach((contract: PickContractItem, index: number): void => {
data.code.push(contract.contract);
data.market.push(contract.market);
data.price.push(100 + index * 10 + tick * 0.1);
data.priceChg.push(((tick + index) % 5 - 2) * 0.35);
});
return data;
}
private mergeHqData(cardData: CardData, hqData: AiPickHqData): CardData {
const tabs = cardData.tabs.map((tab: PickTabData): PickTabData => {
const items = tab.items.map((item: PickContractItem): PickContractItem => {
let hqIndex = -1;
for (let index = 0; index < hqData.code.length; index += 1) {
if (hqData.code[index] === item.contract && hqData.market[index] === item.market) {
hqIndex = index;
break;
}
}
const nextItem: PickContractItem = {
contract: item.contract,
market: item.market,
name: item.name,
price: hqIndex >= 0 ? hqData.price[hqIndex] : undefined,
priceChg: hqIndex >= 0 ? hqData.priceChg[hqIndex] : undefined,
isMain: item.isMain,
extraFields: item.extraFields,
};
return nextItem;
});
const nextTab: PickTabData = {
strategy: tab.strategy,
items: items,
keysList: tab.keysList,
fieldList: tab.fieldList,
};
return nextTab;
});
const nextCardData: CardData = {
tabs: tabs,
};
return nextCardData;
}
}
+2 -2
View File
@@ -39,14 +39,14 @@ export interface CardData {
tabs: PickTabData[];
}
export interface AiPickQuoteData {
export interface AiPickHqData {
code: string[];
market: string[];
price: number[];
priceChg: number[];
}
export interface AiPickQuoteRequestParams {
export interface AiPickHqRequestParams {
protocolId: string;
pageId: string;
onlineId: string;
@@ -7,6 +7,7 @@ import {
} from './AiPickModels';
import { AllCardsCard } from '../common/AllCardsModels';
import { AiPickDataFetcher } from './AiPickDataFetcher';
import { AiPickHqRequestClient } from './AiPickHqRequestClient';
import { cellColor, cellValue, formatColName } from './AiPickUtils';
import {
EMPTY_IMAGE_DARK,
@@ -34,7 +35,7 @@ export struct AiPickNodeComponent {
@State tabContentWidth: number = 0;
@State tabScrollOffset: number = 0;
@State showExplainSheet: boolean = false;
private quoteUnsubscribe: (() => void) | undefined;
private hqUnsubscribe: (() => void) | undefined;
private showTabGradient(): boolean {
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
@@ -49,7 +50,7 @@ export struct AiPickNodeComponent {
}
aboutToDisappear(): void {
this.stopSubscribeQuotes();
this.stopSubscribeHq();
}
// 只负责从公共 Mock 获取并更新卡片配置。
@@ -59,25 +60,25 @@ export struct AiPickNodeComponent {
// 只负责请求并更新卡片展示数据。
private async updateCardData(): Promise<void> {
this.stopSubscribeQuotes();
this.stopSubscribeHq();
this.cardData = { tabs: [] };
const fetcher = AiPickDataFetcher.getInstance();
try {
const tabs = await fetcher.fetchCardData();
this.cardData = { tabs };
if (tabs.length > 0) {
this.subscribeQuotes();
this.subscribeHq();
}
} catch {
this.stopSubscribeQuotes();
this.stopSubscribeHq();
this.cardData = { tabs: [] };
}
}
// 使用当前 CardData 的全部 Tab 合约订阅行情,推送后整体更新 CardData。
private subscribeQuotes(): void {
this.stopSubscribeQuotes();
this.quoteUnsubscribe = AiPickDataFetcher.getInstance().subscribeQuotes(
private subscribeHq(): void {
this.stopSubscribeHq();
this.hqUnsubscribe = AiPickHqRequestClient.getInstance().subscribeHq(
this.cardData,
(cardData: CardData): void => {
this.cardData = cardData;
@@ -85,10 +86,10 @@ export struct AiPickNodeComponent {
);
}
private stopSubscribeQuotes(): void {
if (this.quoteUnsubscribe !== undefined) {
this.quoteUnsubscribe();
this.quoteUnsubscribe = undefined;
private stopSubscribeHq(): void {
if (this.hqUnsubscribe !== undefined) {
this.hqUnsubscribe();
this.hqUnsubscribe = undefined;
}
}
+21
View File
@@ -0,0 +1,21 @@
// 4106 行情协议通用字段 ID。
export class HqFieldId {
static readonly DATA_ID_CODE: number = 4; // 合约代码
static readonly DATA_ID_PRE_PRICE: number = 6; // 昨收价
static readonly DATA_ID_OPEN_PRICE: number = 7; // 今日开盘价
static readonly DATA_ID_PRICE: number = 10; // 最新价
static readonly DATA_ID_TURNOVER: number = 19; // 成交额
static readonly DATA_ID_NAME: number = 55; // 合约名称
static readonly DATA_ID_PRE_SETTLE_PRICE: number = 66; // 昨日结算价
static readonly DATA_ID_CHG_SPEED_5MIN: number = 34325; // 5 分钟涨跌速
static readonly DATA_ID_INCRE_POSIT: number = 34355; // 日增仓
static readonly DATA_ID_PRICE_CHG: number = 34818; // 涨跌幅
static readonly DATA_ID_CHG_SPEED_1MIN: number = 34874; // 1 分钟涨跌速
static readonly DATA_ID_CHG_SPEED_10MIN: number = 34875; // 10 分钟涨跌速
static readonly DATA_ID_CHG_SPEED_15MIN: number = 34876; // 15 分钟涨跌速
static readonly DATA_ID_PRICE_CHG_YTD: number = 34877; // 昨收涨跌幅
static readonly DATA_ID_MARKET: number = 36103; // 市场 ID
private constructor() {
}
}
@@ -1,4 +1,5 @@
import { PeriodRange, TabMetric } from './MarketRankingModels';
import { HqFieldId } from '../common/HqFieldId';
export const TITLE_ARROW_LIGHT =
'https://s.thsi.cn/staticS3/mobileweb-upload-static-server.img/offline/pkg/e6b471c4-b9a6-4c73-a2e4-7c3176308daf.png';
@@ -9,20 +10,20 @@ export const INFO_IMAGE_LIGHT =
export const INFO_IMAGE_DARK =
'https://u.thsi.cn/imgsrc/bbs/19f7f20bcf800ec2b8e9fc0fa0d73369.png';
// 一级 Tab 配置:涨幅/跌幅/涨速/跌速/成交额/日增仓hqId/hqName 用于后续接入行情推送
// 一级 Tab 配置:涨幅/跌幅/涨速/跌速/成交额/日增仓
export const TAB_METRICS: TabMetric[] = [
{ id: 'rise', label: $r('app.string.market_ranking_tab_rise'), stat: 'rise', api: 'rise_percent', hqId: '34818', hqName: 'price_chg', index: 0, hasChildren: false },
{ id: 'fall', label: $r('app.string.market_ranking_tab_fall'), stat: 'fall', api: 'fall_percent', hqId: '34818', hqName: 'price_chg', index: 1, hasChildren: false },
{ id: 'riseSpeed', label: $r('app.string.market_ranking_tab_rise_speed'), stat: 'riseSpeed', api: 'rise_percent', hqId: '34821', hqName: 'chg_speed', index: 2, hasChildren: true },
{ id: 'fallSpeed', label: $r('app.string.market_ranking_tab_fall_speed'), stat: 'fallSpeed', api: 'fall_percent', hqId: '34821', hqName: 'chg_speed', index: 3, hasChildren: true },
{ id: 'turnOver', label: $r('app.string.market_ranking_tab_turnover'), stat: 'turnOver', api: 'turnover', hqId: '19', hqName: 'turnover', index: 4, hasChildren: false },
{ id: 'increPosit', label: $r('app.string.market_ranking_tab_increase_position'), stat: 'increPosit', api: 'increase_position', hqId: '34355', hqName: 'incre_posit', index: 5, hasChildren: false },
{ id: 'rise', label: $r('app.string.market_ranking_tab_rise'), api: 'rise_percent', hqId: HqFieldId.DATA_ID_PRICE_CHG, hasChildren: false },
{ id: 'fall', label: $r('app.string.market_ranking_tab_fall'), api: 'fall_percent', hqId: HqFieldId.DATA_ID_PRICE_CHG, hasChildren: false },
{ id: 'riseSpeed', label: $r('app.string.market_ranking_tab_rise_speed'), api: 'rise_percent', hqId: HqFieldId.DATA_ID_CHG_SPEED_1MIN, hasChildren: true },
{ id: 'fallSpeed', label: $r('app.string.market_ranking_tab_fall_speed'), api: 'fall_percent', hqId: HqFieldId.DATA_ID_CHG_SPEED_1MIN, hasChildren: true },
{ id: 'turnOver', label: $r('app.string.market_ranking_tab_turnover'), api: 'turnover', hqId: HqFieldId.DATA_ID_TURNOVER, hasChildren: false },
{ id: 'increPosit', label: $r('app.string.market_ranking_tab_increase_position'), api: 'increase_position', hqId: HqFieldId.DATA_ID_INCRE_POSIT, hasChildren: false },
];
// 二级 Tab 配置:仅涨速/跌速下出现,按周期筛选
export const PERIOD_RANGES: PeriodRange[] = [
{ id: '1min', label: $r('app.string.market_ranking_period_1min'), stat: 'min1', api: 'one_minute', hqId: '34874', index: 0 },
{ id: '5min', label: $r('app.string.market_ranking_period_5min'), stat: 'min5', api: 'five_minute', hqId: '34325', index: 1 },
{ id: '10min', label: $r('app.string.market_ranking_period_10min'), stat: 'min10', api: 'ten_minute', hqId: '34875', index: 2 },
{ id: '15min', label: $r('app.string.market_ranking_period_15min'), stat: 'min15', api: 'fifteen_minute', hqId: '34876', index: 3 },
{ id: '1min', label: $r('app.string.market_ranking_period_1min'), api: 'one_minute', hqId: HqFieldId.DATA_ID_CHG_SPEED_1MIN },
{ id: '5min', label: $r('app.string.market_ranking_period_5min'), api: 'five_minute', hqId: HqFieldId.DATA_ID_CHG_SPEED_5MIN },
{ id: '10min', label: $r('app.string.market_ranking_period_10min'), api: 'ten_minute', hqId: HqFieldId.DATA_ID_CHG_SPEED_10MIN },
{ id: '15min', label: $r('app.string.market_ranking_period_15min'), api: 'fifteen_minute', hqId: HqFieldId.DATA_ID_CHG_SPEED_15MIN },
];
@@ -7,9 +7,6 @@ import {
AllCardsResponse,
} from '../common/AllCardsModels';
import {
CardData,
MarketRankingQuoteData,
MarketRankingQuoteRequestParams,
RankItem,
RecommendFuturesItem,
} from './MarketRankingModels';
@@ -17,14 +14,11 @@ import { MOCK_RANK_LISTS, MOCK_SPEED_LISTS } from './MarketRankingMock';
import { PERIOD_RANGES, TAB_METRICS } from './MarketRankingConstant';
const MOCK_REQUEST_DELAY_MS = 300;
const MOCK_QUOTE_INTERVAL_MS = 2500;
const TEST_API_HOST = 'https://futures-test.10jqka.com.cn/';
const RELEASE_API_HOST = 'https://ftapi.10jqka.com.cn/';
const RECOMMEND_FUTURES_API_PATH = 'futgwapi/api/market/homepage/v1/recommend_futures';
const RECOMMEND_FUTURES_NUMBER = 50;
export type MarketRankingCardDataListener = (cardData: CardData) => void;
export class MarketRankingDataFetcher {
private static readonly instance: MarketRankingDataFetcher = new MarketRankingDataFetcher();
@@ -47,14 +41,8 @@ export class MarketRankingDataFetcher {
return undefined;
}
private findMarketRankingCard(floor: AllCardsFloor): AllCardsCard | undefined {
return floor.card_list.find((card: AllCardsCard): boolean =>
card.render_key === 'market-ranking'
);
}
/**
* 市场排名合约列表接口(当前暂用 Mock,后续只替换本方法内部实现):
* 市场排名合约列表接口(todo: 当前暂用 Mock,后续只替换本方法内部实现):
* GET {API_HOST}futgwapi/api/market/homepage/v1/recommend_futures
* dev/test API_HOST: https://futures-test.10jqka.com.cn/
* release API_HOST: https://ftapi.10jqka.com.cn/
@@ -97,37 +85,18 @@ export class MarketRankingDataFetcher {
});
}
private findMarketRankingCard(floor: AllCardsFloor): AllCardsCard | undefined {
return floor.card_list.find((card: AllCardsCard): boolean =>
card.render_key === 'market-ranking'
);
}
private buildRecommendFuturesUrl(quoteType: string): string {
const apiHost = BuildProfile.DEBUG ? TEST_API_HOST : RELEASE_API_HOST;
return `${apiHost}${RECOMMEND_FUTURES_API_PATH}` +
`?quote_type=${encodeURIComponent(quoteType)}&number=${RECOMMEND_FUTURES_NUMBER}`;
}
// 模拟行情订阅:先生成原始推送数据并完成合并,再返回完整 CardData。
subscribeQuotes(
contracts: RecommendFuturesItem[],
primaryIdx: number,
secondaryIdx: number,
listener: MarketRankingCardDataListener,
): () => void {
const requestParams = this.buildQuoteRequestParams(contracts, primaryIdx, secondaryIdx);
console.info(`[MarketRanking] subscribeQuotes params: ${JSON.stringify(requestParams)}`);
// todo: 接入真实的行情订阅
const quoteType = this.getQuoteType(primaryIdx, secondaryIdx);
let tick = 0;
const emit = (): void => {
tick += 1;
const quoteData = this.createMockQuoteData(contracts, quoteType, tick);
listener(this.mergeQuoteData(quoteData));
};
emit();
const timer = setInterval(emit, MOCK_QUOTE_INTERVAL_MS);
return (): void => {
clearInterval(timer);
};
}
private getQuoteType(primaryIdx: number, secondaryIdx: number): string {
const metric = TAB_METRICS[primaryIdx];
if (metric.hasChildren) {
@@ -136,134 +105,6 @@ export class MarketRankingDataFetcher {
return metric.api;
}
// 对齐 Vue generateHqParam,生成 4106 行情订阅所需的完整请求参数。
buildQuoteRequestParams(
contracts: RecommendFuturesItem[],
primaryIdx: number,
secondaryIdx: number,
): MarketRankingQuoteRequestParams {
const metric = TAB_METRICS[primaryIdx];
const sortId = metric.hasChildren ? PERIOD_RANGES[secondaryIdx].hqId : metric.hqId;
const sortOrder = metric.id === 'fall' || metric.id === 'fallSpeed' ? '1' : '0';
const dataItems: string[] = ['55', '10', '4', '36103', '34818', '6', '7', '66'];
if (!dataItems.includes(sortId)) {
dataItems.push(sortId);
}
const columnOrder = dataItems.join('|');
const requestDic = `sortid=${sortId}\r\n` +
`sortorder=${sortOrder}\r\n` +
`push=1\r\n` +
`dataitem=${dataItems.map((item: string): string => `${item},`).join('')}\r\n` +
`codelist=${this.formatCodeList(contracts)}\r\n` +
`scenario=qht_qihuo_sort\r\n` +
`precision=1\r\n` +
`pushtime=2.5\r\n`;
return {
protocolId: '4106',
// HarmonyOS 当前沿用 Android 行情页面标识。
pageId: '2201',
onlineId: 'marketRankingData',
columnOrder,
requestDic,
};
}
// 按市场拼成 4106 所需格式,例如 70(CU2501,RB2505,);。
private formatCodeList(contracts: RecommendFuturesItem[]): string {
const markets: string[] = [];
const codeGroups: string[][] = [];
contracts.forEach((contract: RecommendFuturesItem): void => {
const marketIndex = markets.indexOf(contract.market);
if (marketIndex >= 0) {
codeGroups[marketIndex].push(contract.contract_code);
} else {
markets.push(contract.market);
codeGroups.push([contract.contract_code]);
}
});
return markets.map((market: string, index: number): string =>
`${market}(${codeGroups[index].map((code: string): string => `${code},`).join('')});`
).join('');
}
// 模拟 4106 行情接口原始分列数据;非当前 Tab 请求的扩展字段返回空数组。
private createMockQuoteData(
contracts: RecommendFuturesItem[],
quoteType: string,
tick: number,
): MarketRankingQuoteData {
const data: MarketRankingQuoteData = {
code: [],
market: [],
name: [],
price: [],
price_chg: [],
pre_price: [],
open_price: [],
pre_settle_price: [],
turnover: [],
incre_posit: [],
chg_speed_1min: [],
chg_speed_5min: [],
chg_speed_10min: [],
chg_speed_15min: [],
};
contracts.forEach((contract: RecommendFuturesItem, index: number): void => {
const price = 100 + index * 10 + tick * 0.1;
const priceChg = ((tick + index) % 5 - 2) * 0.35;
data.code.push(contract.contract_code);
data.market.push(contract.market);
data.name.push(contract.contract_name);
data.price.push(price);
data.price_chg.push(priceChg);
data.pre_price.push(price - 0.5);
data.open_price.push(price - 0.2);
data.pre_settle_price.push(price - 0.4);
if (quoteType === 'turnover') {
data.turnover.push(100000 + index * 5000 + tick * 100);
}
if (quoteType === 'increase_position') {
data.incre_posit.push(1000 + index * 100 + tick * 10);
}
const speed = priceChg / 2;
if (quoteType === 'one_minute_rise_percent' || quoteType === 'one_minute_fall_percent') {
data.chg_speed_1min.push(speed);
} else if (quoteType === 'five_minute_rise_percent' || quoteType === 'five_minute_fall_percent') {
data.chg_speed_5min.push(speed);
} else if (quoteType === 'ten_minute_rise_percent' || quoteType === 'ten_minute_fall_percent') {
data.chg_speed_10min.push(speed);
} else if (quoteType === 'fifteen_minute_rise_percent' || quoteType === 'fifteen_minute_fall_percent') {
data.chg_speed_15min.push(speed);
}
});
return data;
}
// 对齐 Vue handleSpeedHqRes/handleNormalHqRes:按数组下标合并为 View 使用的行数据。
private mergeQuoteData(data: MarketRankingQuoteData): CardData {
let speedList: number[] = data.chg_speed_1min;
if (speedList.length === 0) {
speedList = data.chg_speed_5min;
}
if (speedList.length === 0) {
speedList = data.chg_speed_10min;
}
if (speedList.length === 0) {
speedList = data.chg_speed_15min;
}
const tableList = data.code.map((code: string, index: number): RankItem => ({
code,
market: data.market[index],
name: data.name[index],
price: data.price[index],
price_chg: data.price_chg[index],
chg_speed: speedList[index],
turnover: data.turnover[index],
incre_posit: data.incre_posit[index],
}));
return { tableList };
}
private mockRankList(quoteType: string): RankItem[] {
switch (quoteType) {
case 'rise_percent':
@@ -0,0 +1,210 @@
import {
CardData,
MarketRankingHqValue,
MarketRankingHqRequestParams,
MarketRankingTableData,
MarketRankingTableRow,
RankItem,
RecommendFuturesItem,
} from './MarketRankingModels';
import {
PERIOD_RANGES,
TAB_METRICS,
} from './MarketRankingConstant';
import { HqFieldId } from '../common/HqFieldId';
const MOCK_QUOTE_INTERVAL_MS = 2500;
export type MarketRankingCardDataListener = (cardData: CardData) => void;
// 市场排名行情请求客户端:负责构造 4106 参数、管理订阅及转换行情数据。
export class MarketRankingHqRequestClient {
private static readonly instance: MarketRankingHqRequestClient =
new MarketRankingHqRequestClient();
private constructor() {
}
static getInstance(): MarketRankingHqRequestClient {
return MarketRankingHqRequestClient.instance;
}
// 模拟行情订阅:先推送一次,之后每 2.5 秒更新完整卡片数据。
subscribeHq(
contracts: RecommendFuturesItem[],
primaryIdx: number,
secondaryIdx: number,
listener: MarketRankingCardDataListener,
): () => void {
const requestParams = this.buildHqRequestParams(contracts, primaryIdx, secondaryIdx);
console.info(`[MarketRanking] subscribeHq params: ${JSON.stringify(requestParams)}`);
// todo: 接入真实的行情订阅。
const quoteType = this.getQuoteType(primaryIdx, secondaryIdx);
let tick = 0;
const emit = (): void => {
tick += 1;
const tableData = this.createMockTableData(contracts, quoteType, tick);
listener(this.mergeTableData(tableData, this.getSortId(primaryIdx, secondaryIdx)));
};
emit();
const timer = setInterval(emit, MOCK_QUOTE_INTERVAL_MS);
return (): void => {
clearInterval(timer);
};
}
// 对齐 Vue generateHqParam,生成 4106 行情订阅所需的完整请求参数。
buildHqRequestParams(
contracts: RecommendFuturesItem[],
primaryIdx: number,
secondaryIdx: number,
): MarketRankingHqRequestParams {
const sortId = this.getSortId(primaryIdx, secondaryIdx);
const metric = TAB_METRICS[primaryIdx];
const sortOrder = metric.id === 'fall' || metric.id === 'fallSpeed' ? '1' : '0';
const dataItems: number[] = [
HqFieldId.DATA_ID_NAME,
HqFieldId.DATA_ID_PRICE,
HqFieldId.DATA_ID_CODE,
HqFieldId.DATA_ID_MARKET,
HqFieldId.DATA_ID_PRICE_CHG,
HqFieldId.DATA_ID_PRE_PRICE,
HqFieldId.DATA_ID_OPEN_PRICE,
HqFieldId.DATA_ID_PRE_SETTLE_PRICE,
];
if (!dataItems.includes(sortId)) {
dataItems.push(sortId);
}
const columnOrder = dataItems.join('|');
const requestDic = `sortid=${sortId}\r\n` +
`sortorder=${sortOrder}\r\n` +
`push=1\r\n` +
`dataitem=${dataItems.map((item: number): string => `${item},`).join('')}\r\n` +
`codelist=${this.formatCodeList(contracts)}\r\n` +
`scenario=qht_qihuo_sort\r\n` +
`precision=1\r\n` +
`pushtime=2.5\r\n`;
return {
protocolId: '4106',
// HarmonyOS 当前沿用 Android 行情页面标识。
pageId: '2201',
onlineId: 'marketRankingData',
columnOrder: columnOrder,
requestDic: requestDic,
};
}
private getQuoteType(primaryIdx: number, secondaryIdx: number): string {
const metric = TAB_METRICS[primaryIdx];
if (metric.hasChildren) {
return `${PERIOD_RANGES[secondaryIdx].api}_${metric.api}`;
}
return metric.api;
}
private getSortId(primaryIdx: number, secondaryIdx: number): number {
const metric = TAB_METRICS[primaryIdx];
return metric.hasChildren ? PERIOD_RANGES[secondaryIdx].hqId : metric.hqId;
}
// 按市场拼成 4106 所需格式,例如 70(CU2501,RB2505,);。
private formatCodeList(contracts: RecommendFuturesItem[]): string {
const markets: string[] = [];
const codeGroups: string[][] = [];
contracts.forEach((contract: RecommendFuturesItem): void => {
const marketIndex = markets.indexOf(contract.market);
if (marketIndex >= 0) {
codeGroups[marketIndex].push(contract.contract_code);
} else {
markets.push(contract.market);
codeGroups.push([contract.contract_code]);
}
});
return markets.map((market: string, index: number): string =>
`${market}(${codeGroups[index].map((code: string): string => `${code},`).join('')});`
).join('');
}
// 模拟真实鸿蒙行情 TableData,每个 Row 通过字段 ID 读取数据。
private createMockTableData(
contracts: RecommendFuturesItem[],
quoteType: string,
tick: number,
): MarketRankingTableData {
const tableData = new MarketRankingTableData();
contracts.forEach((contract: RecommendFuturesItem, index: number): void => {
const price = 100 + index * 10 + tick * 0.1;
const priceChg = ((tick + index) % 5 - 2) * 0.35;
const row = new MarketRankingTableRow();
row.set(HqFieldId.DATA_ID_CODE, contract.contract_code);
row.set(HqFieldId.DATA_ID_MARKET, contract.market);
row.set(HqFieldId.DATA_ID_NAME, contract.contract_name);
row.set(HqFieldId.DATA_ID_PRICE, price);
row.set(HqFieldId.DATA_ID_PRICE_CHG, priceChg);
row.set(HqFieldId.DATA_ID_PRE_PRICE, price - 0.5);
row.set(HqFieldId.DATA_ID_OPEN_PRICE, price - 0.2);
row.set(HqFieldId.DATA_ID_PRE_SETTLE_PRICE, price - 0.4);
if (quoteType === 'turnover') {
row.set(HqFieldId.DATA_ID_TURNOVER, 100000 + index * 5000 + tick * 100);
}
if (quoteType === 'increase_position') {
row.set(HqFieldId.DATA_ID_INCRE_POSIT, 1000 + index * 100 + tick * 10);
}
const speed = priceChg / 2;
if (quoteType === 'one_minute_rise_percent' || quoteType === 'one_minute_fall_percent') {
row.set(HqFieldId.DATA_ID_CHG_SPEED_1MIN, speed);
} else if (quoteType === 'five_minute_rise_percent' ||
quoteType === 'five_minute_fall_percent') {
row.set(HqFieldId.DATA_ID_CHG_SPEED_5MIN, speed);
} else if (quoteType === 'ten_minute_rise_percent' ||
quoteType === 'ten_minute_fall_percent') {
row.set(HqFieldId.DATA_ID_CHG_SPEED_10MIN, speed);
} else if (quoteType === 'fifteen_minute_rise_percent' ||
quoteType === 'fifteen_minute_fall_percent') {
row.set(HqFieldId.DATA_ID_CHG_SPEED_15MIN, speed);
}
tableData.addRow(row);
});
return tableData;
}
// 遍历 tableRows,通过字段 ID 转换为 View 使用的行数据。
private mergeTableData(data: MarketRankingTableData, sortFieldId: number): CardData {
const tableList = data.tableRows.map((row: MarketRankingTableRow): RankItem => {
const item: RankItem = {
code: this.getString(row, HqFieldId.DATA_ID_CODE),
market: this.getString(row, HqFieldId.DATA_ID_MARKET),
name: this.getString(row, HqFieldId.DATA_ID_NAME),
};
item.price = this.getNumber(row, HqFieldId.DATA_ID_PRICE);
item.price_chg = this.getNumber(row, HqFieldId.DATA_ID_PRICE_CHG);
if (sortFieldId === HqFieldId.DATA_ID_TURNOVER) {
item.turnover = this.getNumber(row, HqFieldId.DATA_ID_TURNOVER);
} else if (sortFieldId === HqFieldId.DATA_ID_INCRE_POSIT) {
item.incre_posit = this.getNumber(row, HqFieldId.DATA_ID_INCRE_POSIT);
} else if (sortFieldId !== HqFieldId.DATA_ID_PRICE_CHG) {
item.chg_speed = this.getNumber(row, sortFieldId);
}
return item;
});
return { tableList: tableList };
}
private getString(row: MarketRankingTableRow, fieldId: number): string {
const value: MarketRankingHqValue | undefined = row.get(fieldId);
return value === undefined ? '' : `${value}`;
}
private getNumber(row: MarketRankingTableRow, fieldId: number): number | undefined {
const value: MarketRankingHqValue | undefined = row.get(fieldId);
if (typeof value === 'number') {
return value;
}
if (typeof value === 'string' && value !== '') {
const result = Number(value);
return Number.isNaN(result) ? undefined : result;
}
return undefined;
}
}
@@ -21,29 +21,32 @@ export interface CardData {
tableList: RankItem[];
}
// 4106 行情推送的原始分列数据。Vue 中实际为“字段 ID -> 数组”的字典,
// ArkTS 使用显式属性描述同一结构,数组中相同下标代表同一份合约行情。
// 字段 IDcode=4、market=36103、name=55、price=10、price_chg=34818、
// pre_price=6、open_price=7、pre_settle_price=66、turnover=19、incre_posit=34355、
// chg_speed_1min=34874、chg_speed_5min=34325、chg_speed_10min=34875、chg_speed_15min=34876。
export interface MarketRankingQuoteData {
code: string[];
market: string[];
name: string[];
price: number[];
price_chg: number[];
pre_price: number[];
open_price: number[];
pre_settle_price: number[];
turnover: number[];
incre_posit: number[];
chg_speed_1min: number[];
chg_speed_5min: number[];
chg_speed_10min: number[];
chg_speed_15min: number[];
export type MarketRankingHqValue = string | number;
// 对齐真实鸿蒙行情返回行:通过行情字段 ID 获取当前合约的值。
export class MarketRankingTableRow {
private values: Map<number, MarketRankingHqValue> =
new Map<number, MarketRankingHqValue>();
set(fieldId: number, value: MarketRankingHqValue): void {
this.values.set(fieldId, value);
}
get(fieldId: number): MarketRankingHqValue | undefined {
return this.values.get(fieldId);
}
}
export interface MarketRankingQuoteRequestParams {
// 对齐真实鸿蒙行情 TableData:每个 tableRows 元素代表一份合约行情。
export class MarketRankingTableData {
tableRows: MarketRankingTableRow[] = [];
addRow(row: MarketRankingTableRow): void {
this.tableRows.push(row);
}
}
export interface MarketRankingHqRequestParams {
protocolId: string;
pageId: string;
onlineId: string;
@@ -54,21 +57,16 @@ export interface MarketRankingQuoteRequestParams {
export interface TabMetric {
id: string;
label: ResourceStr;
stat: string; // 埋点
api: string; // HTTP 接口 quote_type 参数
hqId: string; // 行情订阅 sortid 用
hqName: string; // 推送字段映射用
index: number;
api: string; // HTTP 接口 quote_type 参数
hqId: number; // 行情订阅 sortid
hasChildren: boolean; // Vue 用 children 数组本身判断,这里简化为布尔标记
}
export interface PeriodRange {
id: string;
label: ResourceStr;
stat: string;
api: string;
hqId: string;
index: number;
hqId: number;
}
// ============ 后端接口原始响应类型(recommend_futures ============
@@ -5,6 +5,7 @@ import {
RankItem, RecommendFuturesItem, TabMetric,
} from './MarketRankingModels';
import { MarketRankingDataFetcher } from './MarketRankingDataFetcher';
import { MarketRankingHqRequestClient } from './MarketRankingHqRequestClient';
import { formatGreatNumber, formatPercent, riseFallColor } from '../common/NumberFormat';
import { AllCardsCard } from '../common/AllCardsModels';
import {
@@ -32,7 +33,7 @@ export struct MarketRankingNodeComponent {
@State tabScrollOffset: number = 0;
@State showExplainSheet: boolean = false;
@StorageLink('enableDarkMode') isDarkMode: boolean = false;
private quoteUnsubscribe: (() => void) | undefined;
private hqUnsubscribe: (() => void) | undefined;
aboutToAppear(): void {
this.updateCardConfig();
@@ -43,7 +44,7 @@ export struct MarketRankingNodeComponent {
}
aboutToDisappear(): void {
this.stopSubscribeQuotes();
this.stopSubscribeHq();
}
// 只负责从公共 Mock 获取并更新卡片配置。
@@ -347,7 +348,7 @@ export struct MarketRankingNodeComponent {
// 先请求合约榜单填充列表,再根据当前 CardData 订阅行情。
private async updateCardData(primaryIdx: number, secondaryIdx: number): Promise<void> {
// 切换 Tab 后立即清理旧列表,避免新请求返回前闪现上一 Tab 的数据。
this.stopSubscribeQuotes();
this.stopSubscribeHq();
this.cardData = { tableList: [] };
try {
const contracts = await MarketRankingDataFetcher
@@ -360,31 +361,31 @@ export struct MarketRankingNodeComponent {
name: contract.contract_name,
})),
};
this.subscribeQuotes(primaryIdx, secondaryIdx);
this.subscribeHq(primaryIdx, secondaryIdx);
} catch {
this.cardData = { tableList: [] };
}
}
// 使用当前 CardData 中的合约列表订阅行情,推送后整体更新 CardData。
private subscribeQuotes(primaryIdx: number, secondaryIdx: number): void {
private subscribeHq(primaryIdx: number, secondaryIdx: number): void {
const contracts = this.cardData.tableList.map((item: RankItem): RecommendFuturesItem => ({
contract_code: item.code,
contract_name: item.name,
market: item.market,
}));
this.quoteUnsubscribe = MarketRankingDataFetcher
this.hqUnsubscribe = MarketRankingHqRequestClient
.getInstance()
.subscribeQuotes(contracts, primaryIdx, secondaryIdx, (cardData: CardData): void => {
.subscribeHq(contracts, primaryIdx, secondaryIdx, (cardData: CardData): void => {
this.cardData = cardData;
});
}
private stopSubscribeQuotes(): void {
if (this.quoteUnsubscribe !== undefined) {
this.quoteUnsubscribe();
this.quoteUnsubscribe = undefined;
private stopSubscribeHq(): void {
if (this.hqUnsubscribe !== undefined) {
this.hqUnsubscribe();
this.hqUnsubscribe = undefined;
}
}