refactor(hq): 抽离卡片行情订阅客户端
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 使用显式属性描述同一结构,数组中相同下标代表同一份合约行情。
|
||||
// 字段 ID:code=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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user