refactor(hq): 抽离卡片行情订阅客户端
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# 市场排名接口文档
|
# 市场排名接口文档
|
||||||
|
|
||||||
本文记录 `MarketRankingNodeComponent` 卡片使用的合约榜单 HTTP 接口及 4106 行情订阅协议。当前实现位于
|
本文记录 `MarketRankingNodeComponent` 卡片使用的合约榜单 HTTP 接口及 4106 行情订阅协议。HTTP
|
||||||
`entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets`。
|
实现位于 `MarketRankingDataFetcher.ets`,行情实现位于 `MarketRankingHqRequestClient.ets`。
|
||||||
|
|
||||||
## 合约榜单 HTTP 接口
|
## 合约榜单 HTTP 接口
|
||||||
|
|
||||||
@@ -93,12 +93,13 @@ GET {API_HOST}futgwapi/api/market/homepage/v1/recommend_futures
|
|||||||
| `chg_speed_10min` | 34875 | 10 分钟涨跌速 |
|
| `chg_speed_10min` | 34875 | 10 分钟涨跌速 |
|
||||||
| `chg_speed_15min` | 34876 | 15 分钟涨跌速 |
|
| `chg_speed_15min` | 34876 | 15 分钟涨跌速 |
|
||||||
|
|
||||||
原始推送为“字段 ID → 数组”的分列结构,相同数组下标表示同一合约。Fetcher 将其合并为
|
鸿蒙原始推送为 `TableData` 行式结构。`tableRows` 中每个 Row 代表一份合约行情,通过
|
||||||
`RankItem[]`,再通过完整替换 `CardData` 刷新 UI。
|
`row.get(字段 ID)` 读取对应值。`MarketRankingHqRequestClient` 将其转换为 `RankItem[]`,再通过
|
||||||
|
完整替换 `CardData` 刷新 UI。
|
||||||
|
|
||||||
## 当前 Mock 行为
|
## 当前 Mock 行为
|
||||||
|
|
||||||
- HTTP:使用 `@ohos.net.http` 构造 GET URL 和请求选项,但不调用 `request()`。
|
- HTTP:使用 `@ohos.net.http` 构造 GET URL 和请求选项,但不调用 `request()`。
|
||||||
- 行情:构造并输出完整 4106 参数,不连接真实行情桥。
|
- 行情:构造并输出完整 4106 参数,不连接真实行情桥;Mock 使用 `tableRows + row.get(id)` 模拟回调。
|
||||||
- Mock 行情每 `2500ms` 推送一次,与 `pushtime=2.5` 保持一致。
|
- Mock 行情每 `2500ms` 推送一次,与 `pushtime=2.5` 保持一致。
|
||||||
- 每次请求及订阅参数均使用 `[MarketRanking]` 前缀输出到控制台。
|
- 每次请求及订阅参数均使用 `[MarketRanking]` 前缀输出到控制台。
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
// AI选期数据层:负责网络请求、行情订阅及原始数据到视图数据的转换。
|
// AI 选期数据层:负责网络请求及原始数据到视图数据的转换。
|
||||||
import http from '@ohos.net.http';
|
import http from '@ohos.net.http';
|
||||||
import allCardsMock from '../common/mock/AllCardsMock.json';
|
import allCardsMock from '../common/mock/AllCardsMock.json';
|
||||||
import {
|
import {
|
||||||
AiPickQuoteData,
|
|
||||||
AiPickQuoteRequestParams,
|
|
||||||
CardData,
|
|
||||||
PickContractItem,
|
PickContractItem,
|
||||||
PickStrategy,
|
PickStrategy,
|
||||||
PickTabData,
|
PickTabData,
|
||||||
@@ -24,7 +21,6 @@ import { MAX_ITEMS, MAX_TABS, MAX_TAGS } from './AiPickConstant';
|
|||||||
|
|
||||||
// 模拟网络延迟(毫秒),验证 loading 状态和异步流程用;接入真实接口后删除
|
// 模拟网络延迟(毫秒),验证 loading 状态和异步流程用;接入真实接口后删除
|
||||||
const MOCK_DELAY_MS = 300;
|
const MOCK_DELAY_MS = 300;
|
||||||
const MOCK_QUOTE_INTERVAL_MS = 2500;
|
|
||||||
const EMPTY_RAW_STRATEGY_ITEM: RawStrategyItem = {};
|
const EMPTY_RAW_STRATEGY_ITEM: RawStrategyItem = {};
|
||||||
const EMPTY_RAW_STRATEGY_DETAIL: RawStrategyDetail = {
|
const EMPTY_RAW_STRATEGY_DETAIL: RawStrategyDetail = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -32,8 +28,6 @@ const EMPTY_RAW_STRATEGY_DETAIL: RawStrategyDetail = {
|
|||||||
strategy_type: '',
|
strategy_type: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AiPickCardDataListener = (cardData: CardData) => void;
|
|
||||||
|
|
||||||
// 对应 Vue 端 index.vue 中 formatTabData 的单项处理(新版 isCustomVersion=true)
|
// 对应 Vue 端 index.vue 中 formatTabData 的单项处理(新版 isCustomVersion=true)
|
||||||
function parseRawTab(item: RawPickTabItem): PickTabData {
|
function parseRawTab(item: RawPickTabItem): PickTabData {
|
||||||
const isSystemPeriod = item.type === 'system_period';
|
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 {
|
private buildRequestUrl(configuredUrl: string, timestamp: number): string {
|
||||||
const separator = configuredUrl.includes('?') ? '&' : '?';
|
const separator = configuredUrl.includes('?') ? '&' : '?';
|
||||||
return `${configuredUrl}${separator}_=${timestamp}`;
|
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[];
|
tabs: PickTabData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiPickQuoteData {
|
export interface AiPickHqData {
|
||||||
code: string[];
|
code: string[];
|
||||||
market: string[];
|
market: string[];
|
||||||
price: number[];
|
price: number[];
|
||||||
priceChg: number[];
|
priceChg: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiPickQuoteRequestParams {
|
export interface AiPickHqRequestParams {
|
||||||
protocolId: string;
|
protocolId: string;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
onlineId: string;
|
onlineId: string;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from './AiPickModels';
|
} from './AiPickModels';
|
||||||
import { AllCardsCard } from '../common/AllCardsModels';
|
import { AllCardsCard } from '../common/AllCardsModels';
|
||||||
import { AiPickDataFetcher } from './AiPickDataFetcher';
|
import { AiPickDataFetcher } from './AiPickDataFetcher';
|
||||||
|
import { AiPickHqRequestClient } from './AiPickHqRequestClient';
|
||||||
import { cellColor, cellValue, formatColName } from './AiPickUtils';
|
import { cellColor, cellValue, formatColName } from './AiPickUtils';
|
||||||
import {
|
import {
|
||||||
EMPTY_IMAGE_DARK,
|
EMPTY_IMAGE_DARK,
|
||||||
@@ -34,7 +35,7 @@ export struct AiPickNodeComponent {
|
|||||||
@State tabContentWidth: number = 0;
|
@State tabContentWidth: number = 0;
|
||||||
@State tabScrollOffset: number = 0;
|
@State tabScrollOffset: number = 0;
|
||||||
@State showExplainSheet: boolean = false;
|
@State showExplainSheet: boolean = false;
|
||||||
private quoteUnsubscribe: (() => void) | undefined;
|
private hqUnsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
private showTabGradient(): boolean {
|
private showTabGradient(): boolean {
|
||||||
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
|
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
|
||||||
@@ -49,7 +50,7 @@ export struct AiPickNodeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
aboutToDisappear(): void {
|
aboutToDisappear(): void {
|
||||||
this.stopSubscribeQuotes();
|
this.stopSubscribeHq();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只负责从公共 Mock 获取并更新卡片配置。
|
// 只负责从公共 Mock 获取并更新卡片配置。
|
||||||
@@ -59,25 +60,25 @@ export struct AiPickNodeComponent {
|
|||||||
|
|
||||||
// 只负责请求并更新卡片展示数据。
|
// 只负责请求并更新卡片展示数据。
|
||||||
private async updateCardData(): Promise<void> {
|
private async updateCardData(): Promise<void> {
|
||||||
this.stopSubscribeQuotes();
|
this.stopSubscribeHq();
|
||||||
this.cardData = { tabs: [] };
|
this.cardData = { tabs: [] };
|
||||||
const fetcher = AiPickDataFetcher.getInstance();
|
const fetcher = AiPickDataFetcher.getInstance();
|
||||||
try {
|
try {
|
||||||
const tabs = await fetcher.fetchCardData();
|
const tabs = await fetcher.fetchCardData();
|
||||||
this.cardData = { tabs };
|
this.cardData = { tabs };
|
||||||
if (tabs.length > 0) {
|
if (tabs.length > 0) {
|
||||||
this.subscribeQuotes();
|
this.subscribeHq();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
this.stopSubscribeQuotes();
|
this.stopSubscribeHq();
|
||||||
this.cardData = { tabs: [] };
|
this.cardData = { tabs: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用当前 CardData 的全部 Tab 合约订阅行情,推送后整体更新 CardData。
|
// 使用当前 CardData 的全部 Tab 合约订阅行情,推送后整体更新 CardData。
|
||||||
private subscribeQuotes(): void {
|
private subscribeHq(): void {
|
||||||
this.stopSubscribeQuotes();
|
this.stopSubscribeHq();
|
||||||
this.quoteUnsubscribe = AiPickDataFetcher.getInstance().subscribeQuotes(
|
this.hqUnsubscribe = AiPickHqRequestClient.getInstance().subscribeHq(
|
||||||
this.cardData,
|
this.cardData,
|
||||||
(cardData: CardData): void => {
|
(cardData: CardData): void => {
|
||||||
this.cardData = cardData;
|
this.cardData = cardData;
|
||||||
@@ -85,10 +86,10 @@ export struct AiPickNodeComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopSubscribeQuotes(): void {
|
private stopSubscribeHq(): void {
|
||||||
if (this.quoteUnsubscribe !== undefined) {
|
if (this.hqUnsubscribe !== undefined) {
|
||||||
this.quoteUnsubscribe();
|
this.hqUnsubscribe();
|
||||||
this.quoteUnsubscribe = undefined;
|
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 { PeriodRange, TabMetric } from './MarketRankingModels';
|
||||||
|
import { HqFieldId } from '../common/HqFieldId';
|
||||||
|
|
||||||
export const TITLE_ARROW_LIGHT =
|
export const TITLE_ARROW_LIGHT =
|
||||||
'https://s.thsi.cn/staticS3/mobileweb-upload-static-server.img/offline/pkg/e6b471c4-b9a6-4c73-a2e4-7c3176308daf.png';
|
'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 =
|
export const INFO_IMAGE_DARK =
|
||||||
'https://u.thsi.cn/imgsrc/bbs/19f7f20bcf800ec2b8e9fc0fa0d73369.png';
|
'https://u.thsi.cn/imgsrc/bbs/19f7f20bcf800ec2b8e9fc0fa0d73369.png';
|
||||||
|
|
||||||
// 一级 Tab 配置:涨幅/跌幅/涨速/跌速/成交额/日增仓,hqId/hqName 用于后续接入行情推送
|
// 一级 Tab 配置:涨幅/跌幅/涨速/跌速/成交额/日增仓。
|
||||||
export const TAB_METRICS: TabMetric[] = [
|
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: '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'), stat: 'fall', api: 'fall_percent', hqId: '34818', hqName: 'price_chg', index: 1, 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'), stat: 'riseSpeed', api: 'rise_percent', hqId: '34821', hqName: 'chg_speed', index: 2, hasChildren: true },
|
{ 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'), stat: 'fallSpeed', api: 'fall_percent', hqId: '34821', hqName: 'chg_speed', index: 3, 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'), stat: 'turnOver', api: 'turnover', hqId: '19', hqName: 'turnover', index: 4, hasChildren: false },
|
{ 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'), stat: 'increPosit', api: 'increase_position', hqId: '34355', hqName: 'incre_posit', index: 5, 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 配置:仅涨速/跌速下出现,按周期筛选
|
// 二级 Tab 配置:仅涨速/跌速下出现,按周期筛选
|
||||||
export const PERIOD_RANGES: PeriodRange[] = [
|
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: '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'), stat: 'min5', api: 'five_minute', hqId: '34325', index: 1 },
|
{ 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'), stat: 'min10', api: 'ten_minute', hqId: '34875', index: 2 },
|
{ 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'), stat: 'min15', api: 'fifteen_minute', hqId: '34876', index: 3 },
|
{ 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,
|
AllCardsResponse,
|
||||||
} from '../common/AllCardsModels';
|
} from '../common/AllCardsModels';
|
||||||
import {
|
import {
|
||||||
CardData,
|
|
||||||
MarketRankingQuoteData,
|
|
||||||
MarketRankingQuoteRequestParams,
|
|
||||||
RankItem,
|
RankItem,
|
||||||
RecommendFuturesItem,
|
RecommendFuturesItem,
|
||||||
} from './MarketRankingModels';
|
} from './MarketRankingModels';
|
||||||
@@ -17,14 +14,11 @@ import { MOCK_RANK_LISTS, MOCK_SPEED_LISTS } from './MarketRankingMock';
|
|||||||
import { PERIOD_RANGES, TAB_METRICS } from './MarketRankingConstant';
|
import { PERIOD_RANGES, TAB_METRICS } from './MarketRankingConstant';
|
||||||
|
|
||||||
const MOCK_REQUEST_DELAY_MS = 300;
|
const MOCK_REQUEST_DELAY_MS = 300;
|
||||||
const MOCK_QUOTE_INTERVAL_MS = 2500;
|
|
||||||
const TEST_API_HOST = 'https://futures-test.10jqka.com.cn/';
|
const TEST_API_HOST = 'https://futures-test.10jqka.com.cn/';
|
||||||
const RELEASE_API_HOST = 'https://ftapi.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_API_PATH = 'futgwapi/api/market/homepage/v1/recommend_futures';
|
||||||
const RECOMMEND_FUTURES_NUMBER = 50;
|
const RECOMMEND_FUTURES_NUMBER = 50;
|
||||||
|
|
||||||
export type MarketRankingCardDataListener = (cardData: CardData) => void;
|
|
||||||
|
|
||||||
export class MarketRankingDataFetcher {
|
export class MarketRankingDataFetcher {
|
||||||
private static readonly instance: MarketRankingDataFetcher = new MarketRankingDataFetcher();
|
private static readonly instance: MarketRankingDataFetcher = new MarketRankingDataFetcher();
|
||||||
|
|
||||||
@@ -47,14 +41,8 @@ export class MarketRankingDataFetcher {
|
|||||||
return undefined;
|
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
|
* GET {API_HOST}futgwapi/api/market/homepage/v1/recommend_futures
|
||||||
* dev/test API_HOST: https://futures-test.10jqka.com.cn/
|
* dev/test API_HOST: https://futures-test.10jqka.com.cn/
|
||||||
* release API_HOST: https://ftapi.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 {
|
private buildRecommendFuturesUrl(quoteType: string): string {
|
||||||
const apiHost = BuildProfile.DEBUG ? TEST_API_HOST : RELEASE_API_HOST;
|
const apiHost = BuildProfile.DEBUG ? TEST_API_HOST : RELEASE_API_HOST;
|
||||||
return `${apiHost}${RECOMMEND_FUTURES_API_PATH}` +
|
return `${apiHost}${RECOMMEND_FUTURES_API_PATH}` +
|
||||||
`?quote_type=${encodeURIComponent(quoteType)}&number=${RECOMMEND_FUTURES_NUMBER}`;
|
`?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 {
|
private getQuoteType(primaryIdx: number, secondaryIdx: number): string {
|
||||||
const metric = TAB_METRICS[primaryIdx];
|
const metric = TAB_METRICS[primaryIdx];
|
||||||
if (metric.hasChildren) {
|
if (metric.hasChildren) {
|
||||||
@@ -136,134 +105,6 @@ export class MarketRankingDataFetcher {
|
|||||||
return metric.api;
|
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[] {
|
private mockRankList(quoteType: string): RankItem[] {
|
||||||
switch (quoteType) {
|
switch (quoteType) {
|
||||||
case 'rise_percent':
|
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[];
|
tableList: RankItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4106 行情推送的原始分列数据。Vue 中实际为“字段 ID -> 数组”的字典,
|
export type MarketRankingHqValue = string | number;
|
||||||
// ArkTS 使用显式属性描述同一结构,数组中相同下标代表同一份合约行情。
|
|
||||||
// 字段 ID:code=4、market=36103、name=55、price=10、price_chg=34818、
|
// 对齐真实鸿蒙行情返回行:通过行情字段 ID 获取当前合约的值。
|
||||||
// pre_price=6、open_price=7、pre_settle_price=66、turnover=19、incre_posit=34355、
|
export class MarketRankingTableRow {
|
||||||
// chg_speed_1min=34874、chg_speed_5min=34325、chg_speed_10min=34875、chg_speed_15min=34876。
|
private values: Map<number, MarketRankingHqValue> =
|
||||||
export interface MarketRankingQuoteData {
|
new Map<number, MarketRankingHqValue>();
|
||||||
code: string[];
|
|
||||||
market: string[];
|
set(fieldId: number, value: MarketRankingHqValue): void {
|
||||||
name: string[];
|
this.values.set(fieldId, value);
|
||||||
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 interface MarketRankingQuoteRequestParams {
|
get(fieldId: number): MarketRankingHqValue | undefined {
|
||||||
|
return this.values.get(fieldId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对齐真实鸿蒙行情 TableData:每个 tableRows 元素代表一份合约行情。
|
||||||
|
export class MarketRankingTableData {
|
||||||
|
tableRows: MarketRankingTableRow[] = [];
|
||||||
|
|
||||||
|
addRow(row: MarketRankingTableRow): void {
|
||||||
|
this.tableRows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketRankingHqRequestParams {
|
||||||
protocolId: string;
|
protocolId: string;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
onlineId: string;
|
onlineId: string;
|
||||||
@@ -54,21 +57,16 @@ export interface MarketRankingQuoteRequestParams {
|
|||||||
export interface TabMetric {
|
export interface TabMetric {
|
||||||
id: string;
|
id: string;
|
||||||
label: ResourceStr;
|
label: ResourceStr;
|
||||||
stat: string; // 埋点用
|
|
||||||
api: string; // HTTP 接口 quote_type 参数用
|
api: string; // HTTP 接口 quote_type 参数用
|
||||||
hqId: string; // 行情订阅 sortid 用
|
hqId: number; // 行情订阅 sortid 用
|
||||||
hqName: string; // 推送字段映射用
|
|
||||||
index: number;
|
|
||||||
hasChildren: boolean; // Vue 用 children 数组本身判断,这里简化为布尔标记
|
hasChildren: boolean; // Vue 用 children 数组本身判断,这里简化为布尔标记
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeriodRange {
|
export interface PeriodRange {
|
||||||
id: string;
|
id: string;
|
||||||
label: ResourceStr;
|
label: ResourceStr;
|
||||||
stat: string;
|
|
||||||
api: string;
|
api: string;
|
||||||
hqId: string;
|
hqId: number;
|
||||||
index: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 后端接口原始响应类型(recommend_futures) ============
|
// ============ 后端接口原始响应类型(recommend_futures) ============
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
RankItem, RecommendFuturesItem, TabMetric,
|
RankItem, RecommendFuturesItem, TabMetric,
|
||||||
} from './MarketRankingModels';
|
} from './MarketRankingModels';
|
||||||
import { MarketRankingDataFetcher } from './MarketRankingDataFetcher';
|
import { MarketRankingDataFetcher } from './MarketRankingDataFetcher';
|
||||||
|
import { MarketRankingHqRequestClient } from './MarketRankingHqRequestClient';
|
||||||
import { formatGreatNumber, formatPercent, riseFallColor } from '../common/NumberFormat';
|
import { formatGreatNumber, formatPercent, riseFallColor } from '../common/NumberFormat';
|
||||||
import { AllCardsCard } from '../common/AllCardsModels';
|
import { AllCardsCard } from '../common/AllCardsModels';
|
||||||
import {
|
import {
|
||||||
@@ -32,7 +33,7 @@ export struct MarketRankingNodeComponent {
|
|||||||
@State tabScrollOffset: number = 0;
|
@State tabScrollOffset: number = 0;
|
||||||
@State showExplainSheet: boolean = false;
|
@State showExplainSheet: boolean = false;
|
||||||
@StorageLink('enableDarkMode') isDarkMode: boolean = false;
|
@StorageLink('enableDarkMode') isDarkMode: boolean = false;
|
||||||
private quoteUnsubscribe: (() => void) | undefined;
|
private hqUnsubscribe: (() => void) | undefined;
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
this.updateCardConfig();
|
this.updateCardConfig();
|
||||||
@@ -43,7 +44,7 @@ export struct MarketRankingNodeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
aboutToDisappear(): void {
|
aboutToDisappear(): void {
|
||||||
this.stopSubscribeQuotes();
|
this.stopSubscribeHq();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只负责从公共 Mock 获取并更新卡片配置。
|
// 只负责从公共 Mock 获取并更新卡片配置。
|
||||||
@@ -347,7 +348,7 @@ export struct MarketRankingNodeComponent {
|
|||||||
// 先请求合约榜单填充列表,再根据当前 CardData 订阅行情。
|
// 先请求合约榜单填充列表,再根据当前 CardData 订阅行情。
|
||||||
private async updateCardData(primaryIdx: number, secondaryIdx: number): Promise<void> {
|
private async updateCardData(primaryIdx: number, secondaryIdx: number): Promise<void> {
|
||||||
// 切换 Tab 后立即清理旧列表,避免新请求返回前闪现上一 Tab 的数据。
|
// 切换 Tab 后立即清理旧列表,避免新请求返回前闪现上一 Tab 的数据。
|
||||||
this.stopSubscribeQuotes();
|
this.stopSubscribeHq();
|
||||||
this.cardData = { tableList: [] };
|
this.cardData = { tableList: [] };
|
||||||
try {
|
try {
|
||||||
const contracts = await MarketRankingDataFetcher
|
const contracts = await MarketRankingDataFetcher
|
||||||
@@ -360,31 +361,31 @@ export struct MarketRankingNodeComponent {
|
|||||||
name: contract.contract_name,
|
name: contract.contract_name,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
this.subscribeQuotes(primaryIdx, secondaryIdx);
|
this.subscribeHq(primaryIdx, secondaryIdx);
|
||||||
} catch {
|
} catch {
|
||||||
this.cardData = { tableList: [] };
|
this.cardData = { tableList: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用当前 CardData 中的合约列表订阅行情,推送后整体更新 CardData。
|
// 使用当前 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 => ({
|
const contracts = this.cardData.tableList.map((item: RankItem): RecommendFuturesItem => ({
|
||||||
contract_code: item.code,
|
contract_code: item.code,
|
||||||
contract_name: item.name,
|
contract_name: item.name,
|
||||||
market: item.market,
|
market: item.market,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.quoteUnsubscribe = MarketRankingDataFetcher
|
this.hqUnsubscribe = MarketRankingHqRequestClient
|
||||||
.getInstance()
|
.getInstance()
|
||||||
.subscribeQuotes(contracts, primaryIdx, secondaryIdx, (cardData: CardData): void => {
|
.subscribeHq(contracts, primaryIdx, secondaryIdx, (cardData: CardData): void => {
|
||||||
this.cardData = cardData;
|
this.cardData = cardData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopSubscribeQuotes(): void {
|
private stopSubscribeHq(): void {
|
||||||
if (this.quoteUnsubscribe !== undefined) {
|
if (this.hqUnsubscribe !== undefined) {
|
||||||
this.quoteUnsubscribe();
|
this.hqUnsubscribe();
|
||||||
this.quoteUnsubscribe = undefined;
|
this.hqUnsubscribe = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user