feat(market-ranking): 完善数据请求与行情订阅
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# 市场排名接口文档
|
||||
|
||||
本文记录 `MarketRanking` 卡片使用的合约榜单 HTTP 接口及 4106 行情订阅协议。当前实现位于
|
||||
`entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets`。
|
||||
|
||||
## 合约榜单 HTTP 接口
|
||||
|
||||
### 请求
|
||||
|
||||
```text
|
||||
GET {API_HOST}futgwapi/api/market/homepage/v1/recommend_futures
|
||||
```
|
||||
|
||||
| 构建模式 | API_HOST |
|
||||
| --- | --- |
|
||||
| Debug | `https://futures-test.10jqka.com.cn/` |
|
||||
| Release | `https://ftapi.10jqka.com.cn/` |
|
||||
|
||||
查询参数:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `quote_type` | string | 当前排行榜维度 |
|
||||
| `number` | number | 返回数量,固定为 `50` |
|
||||
|
||||
`quote_type` 示例:
|
||||
|
||||
- 涨幅:`rise_percent`
|
||||
- 跌幅:`fall_percent`
|
||||
- 成交额:`turnover`
|
||||
- 日增仓:`increase_position`
|
||||
- 5 分钟涨速:`five_minute_rise_percent`
|
||||
- 10 分钟跌速:`ten_minute_fall_percent`
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"contract_code": "CU2501",
|
||||
"contract_name": "沪铜2501",
|
||||
"market": "70"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
该接口只确定榜单合约及顺序;价格、涨跌幅等数据由行情订阅补充。
|
||||
|
||||
## 4106 行情订阅
|
||||
|
||||
行情通过客户端统一行情协议订阅,不是普通 HTTP 请求。
|
||||
|
||||
```json
|
||||
{
|
||||
"protocolId": "4106",
|
||||
"pageId": "2201",
|
||||
"onlineId": "marketRankingData",
|
||||
"columnOrder": "55|10|4|36103|34818|6|7|66",
|
||||
"requestDic": "sortid=34818\r\nsortorder=0\r\npush=1\r\ndataitem=55,10,4,36103,34818,6,7,66,\r\ncodelist=70(CU2501,RB2505,);\r\nscenario=qht_qihuo_sort\r\nprecision=1\r\npushtime=2.5\r\n"
|
||||
}
|
||||
```
|
||||
|
||||
关键参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `sortid` | 当前 Tab 对应的行情字段 ID |
|
||||
| `sortorder` | 跌幅、跌速为 `1`,其余为 `0` |
|
||||
| `push` | `1` 表示持续推送 |
|
||||
| `dataitem` | 本次需要返回的字段 ID |
|
||||
| `codelist` | 按市场分组的合约列表 |
|
||||
| `pushtime` | 推送间隔,`2.5` 秒 |
|
||||
|
||||
### 行情字段
|
||||
|
||||
| 字段 | ID | 使用场景 |
|
||||
| --- | ---: | --- |
|
||||
| `name` | 55 | 始终 |
|
||||
| `price` | 10 | 始终 |
|
||||
| `code` | 4 | 始终 |
|
||||
| `market` | 36103 | 始终 |
|
||||
| `price_chg` | 34818 | 始终 |
|
||||
| `pre_price` | 6 | 始终 |
|
||||
| `open_price` | 7 | 始终 |
|
||||
| `pre_settle_price` | 66 | 始终 |
|
||||
| `turnover` | 19 | 成交额 |
|
||||
| `incre_posit` | 34355 | 日增仓 |
|
||||
| `chg_speed_1min` | 34874 | 1 分钟涨跌速 |
|
||||
| `chg_speed_5min` | 34325 | 5 分钟涨跌速 |
|
||||
| `chg_speed_10min` | 34875 | 10 分钟涨跌速 |
|
||||
| `chg_speed_15min` | 34876 | 15 分钟涨跌速 |
|
||||
|
||||
原始推送为“字段 ID → 数组”的分列结构,相同数组下标表示同一合约。Fetcher 将其合并为
|
||||
`RankItem[]`,再通过完整替换 `CardData` 刷新 UI。
|
||||
|
||||
## 当前 Mock 行为
|
||||
|
||||
- HTTP:使用 `@ohos.net.http` 构造 GET URL 和请求选项,但不调用 `request()`。
|
||||
- 行情:构造并输出完整 4106 参数,不连接真实行情桥。
|
||||
- Mock 行情每 `2500ms` 推送一次,与 `pushtime=2.5` 保持一致。
|
||||
- 每次请求及订阅参数均使用 `[MarketRanking]` 前缀输出到控制台。
|
||||
@@ -11,9 +11,9 @@ export struct Card {
|
||||
this.content()
|
||||
}
|
||||
.width('100%')
|
||||
.padding(20)
|
||||
.borderRadius(12)
|
||||
.padding(10)
|
||||
.borderRadius(6)
|
||||
.backgroundColor($r('app.color.card_bg'))
|
||||
.shadow({ radius: 12, color: '#1A000000', offsetX: 0, offsetY: 4 })
|
||||
.clip(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { PeriodRange, RankItem, TabMetric } from './MarketRankingTypes';
|
||||
import { MarketRankingModel } from './MarketRankingModel';
|
||||
import router from '@ohos.router';
|
||||
import {
|
||||
CardData,
|
||||
PeriodRange,
|
||||
RankItem, RecommendFuturesItem, TabMetric,
|
||||
} from './MarketRankingTypes';
|
||||
import { MarketRankingDataFetcher } from './MarketRankingDataFetcher';
|
||||
import { formatGreatNumber, formatPercent, riseFallColor } from '../common/NumberFormat';
|
||||
import { PERIOD_RANGES, TAB_METRICS } from './MarketRankingConstant';
|
||||
|
||||
@@ -8,7 +13,7 @@ const MAX_SIZE = 5;
|
||||
|
||||
@Component
|
||||
export struct MarketRanking {
|
||||
@State vm: MarketRankingModel = new MarketRankingModel();
|
||||
@State cardData: CardData = { tableList: [] };
|
||||
// Tab 选中/展开状态是纯 UI 交互状态,留在 View 里
|
||||
@State primaryIdx: number = 0;
|
||||
@State secondaryIdx: number = 0;
|
||||
@@ -16,70 +21,30 @@ export struct MarketRanking {
|
||||
@State tabViewportWidth: number = 0;
|
||||
@State tabContentWidth: number = 0;
|
||||
@State tabScrollOffset: number = 0;
|
||||
private quoteUnsubscribe: (() => void) | undefined;
|
||||
|
||||
private showTabGradient(): boolean {
|
||||
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
|
||||
return maxOffset > 0 && this.tabScrollOffset < maxOffset;
|
||||
aboutToAppear(): void {
|
||||
// Mock 网络延迟:模拟真实接口返回前的等待过程,接入真实请求后移除该 setTimeout。
|
||||
setTimeout(() => {
|
||||
this.updateCardData(this.primaryIdx, this.secondaryIdx);
|
||||
}, 600)
|
||||
}
|
||||
|
||||
private emptyHeight(): number {
|
||||
const rowHeight = 45;
|
||||
const headerHeight = 24;
|
||||
return (this.unfolded ? MAX_SIZE : NORMAL_SIZE) * rowHeight + headerHeight;
|
||||
}
|
||||
|
||||
// 第三列标题随 Tab 变化
|
||||
private thirdColumnTitle(): string {
|
||||
const metric = TAB_METRICS[this.primaryIdx];
|
||||
if (metric.hasChildren) {
|
||||
return `${PERIOD_RANGES[this.secondaryIdx].label}${metric.label}`;
|
||||
}
|
||||
if (metric.id === 'rise' || metric.id === 'fall') {
|
||||
return '涨跌幅';
|
||||
}
|
||||
return metric.label;
|
||||
}
|
||||
|
||||
// 成交额/日增仓列用中性色,其余按涨跌上色
|
||||
private isNeutralColumn(): boolean {
|
||||
const id = TAB_METRICS[this.primaryIdx].id;
|
||||
return id === 'turnOver' || id === 'increPosit';
|
||||
}
|
||||
|
||||
// 第三列展示值:按当前一级 Tab 从对应原始字段取值并格式化
|
||||
private thirdColumnValue(item: RankItem): string {
|
||||
const id = TAB_METRICS[this.primaryIdx].id;
|
||||
if (id === 'turnOver') {
|
||||
return formatGreatNumber(item.turnover);
|
||||
}
|
||||
if (id === 'increPosit') {
|
||||
return formatGreatNumber(item.incre_posit);
|
||||
}
|
||||
if (id === 'riseSpeed' || id === 'fallSpeed') {
|
||||
return formatPercent(item.chg_speed);
|
||||
}
|
||||
return formatPercent(item.price_chg);
|
||||
}
|
||||
|
||||
// 第三列取色:成交额/日增仓走中性色,其余按对应原始字段的正负取色
|
||||
private thirdColumnColor(item: RankItem): Resource {
|
||||
if (this.isNeutralColumn()) {
|
||||
return $r('app.color.text_primary');
|
||||
}
|
||||
const id = TAB_METRICS[this.primaryIdx].id;
|
||||
const raw = id === 'riseSpeed' || id === 'fallSpeed' ? item.chg_speed : item.price_chg;
|
||||
return riseFallColor(raw);
|
||||
aboutToDisappear(): void {
|
||||
this.stopSubscribeQuotes();
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 标题栏:标题 + 展开/收起胶囊紧挨着,都在左侧
|
||||
Row() {
|
||||
Text('市场排名')
|
||||
Text($r('app.string.market_ranking_title'))
|
||||
.fontSize(18)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
Text(this.unfolded ? '收起' : '展开')
|
||||
Text(this.unfolded
|
||||
? $r('app.string.market_ranking_collapse')
|
||||
: $r('app.string.market_ranking_expand'))
|
||||
.fontSize(12)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.height(20)
|
||||
@@ -117,6 +82,7 @@ export struct MarketRanking {
|
||||
.onClick(() => {
|
||||
this.primaryIdx = index;
|
||||
this.secondaryIdx = 0;
|
||||
this.updateCardData(index, 0);
|
||||
})
|
||||
}, (metric: TabMetric) => metric.id)
|
||||
}
|
||||
@@ -133,7 +99,6 @@ export struct MarketRanking {
|
||||
.onScroll((xOffset: number, _yOffset: number) => {
|
||||
this.tabScrollOffset = xOffset;
|
||||
})
|
||||
|
||||
if (this.showTabGradient()) {
|
||||
Row()
|
||||
.width(24)
|
||||
@@ -171,6 +136,7 @@ export struct MarketRanking {
|
||||
.padding({ top: 4, bottom: 4 })
|
||||
.onClick(() => {
|
||||
this.secondaryIdx = index;
|
||||
this.updateCardData(this.primaryIdx, index);
|
||||
})
|
||||
}, (period: PeriodRange) => period.id)
|
||||
}
|
||||
@@ -180,9 +146,9 @@ export struct MarketRanking {
|
||||
.margin({ top: 8, left: -10, right: -10 })
|
||||
}
|
||||
|
||||
if (this.vm.currentList(this.primaryIdx, this.secondaryIdx).length === 0) {
|
||||
if (this.cardData.tableList.length === 0) {
|
||||
Column() {
|
||||
Text('暂无数据')
|
||||
Text($r('app.string.market_ranking_empty'))
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_grey'))
|
||||
}
|
||||
@@ -194,12 +160,12 @@ export struct MarketRanking {
|
||||
} else {
|
||||
// 表头
|
||||
Row() {
|
||||
Text('合约名称')
|
||||
Text($r('app.string.market_ranking_contract_name'))
|
||||
.fontSize(12)
|
||||
.lineHeight(21)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
.width('35%')
|
||||
Text('最新价')
|
||||
Text($r('app.string.market_ranking_latest_price'))
|
||||
.fontSize(12)
|
||||
.lineHeight(21)
|
||||
.fontColor($r('app.color.text_tertiary'))
|
||||
@@ -223,21 +189,23 @@ export struct MarketRanking {
|
||||
|
||||
// 表格列表
|
||||
ForEach(
|
||||
this.vm.currentList(this.primaryIdx, this.secondaryIdx).slice(0, this.unfolded ? MAX_SIZE : NORMAL_SIZE),
|
||||
this.cardData.tableList.slice(0, this.displaySize()),
|
||||
(item: RankItem, index: number) => {
|
||||
Row() {
|
||||
Text(item.name)
|
||||
.fontSize(16)
|
||||
.minFontSize(10)
|
||||
.lineHeight(21)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.width('35%')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(`${item.price}`)
|
||||
Text(item.price === undefined ? '--' : `${item.price}`)
|
||||
.fontSize(16)
|
||||
.minFontSize(10)
|
||||
.lineHeight(21)
|
||||
.fontFamily('monospace')
|
||||
.fontColor(riseFallColor(item.price_chg))
|
||||
.fontColor(this.priceColor(item))
|
||||
.layoutWeight(1)
|
||||
.margin({ left: 8 })
|
||||
.textAlign(TextAlign.End)
|
||||
@@ -245,6 +213,7 @@ export struct MarketRanking {
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(this.thirdColumnValue(item))
|
||||
.fontSize(16)
|
||||
.minFontSize(10)
|
||||
.lineHeight(21)
|
||||
.fontFamily('monospace')
|
||||
.fontColor(this.thirdColumnColor(item))
|
||||
@@ -258,19 +227,175 @@ export struct MarketRanking {
|
||||
.height(45)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.border(index === Math.min(
|
||||
this.unfolded ? MAX_SIZE : NORMAL_SIZE,
|
||||
this.vm.currentList(this.primaryIdx, this.secondaryIdx).length,
|
||||
this.displaySize(),
|
||||
this.cardData.tableList.length,
|
||||
) - 1
|
||||
? undefined
|
||||
: { width: { bottom: 0.5 }, color: $r('app.color.divider_color') })
|
||||
.onClick(() => {
|
||||
this.vm.jumpToDetail(item);
|
||||
if (item.market !== undefined) {
|
||||
this.jumpToDetail(item);
|
||||
}
|
||||
})
|
||||
},
|
||||
(item: RankItem) => item.code,
|
||||
// 行情更新时字段参与 key,确保整体替换 CardData 后列表行同步刷新。
|
||||
(item: RankItem) =>
|
||||
`${item.code}_${item.market}_${item.price ?? ''}_${item.price_chg ?? ''}_` +
|
||||
`${item.chg_speed ?? ''}_${item.turnover ?? ''}_${item.incre_posit ?? ''}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
// 先请求合约榜单填充列表,再根据当前 CardData 订阅行情。
|
||||
private async updateCardData(primaryIdx: number, secondaryIdx: number): Promise<void> {
|
||||
// 切换 Tab 后立即清理旧列表,避免新请求返回前闪现上一 Tab 的数据。
|
||||
this.stopSubscribeQuotes();
|
||||
this.cardData = { tableList: [] };
|
||||
try {
|
||||
const contracts = await MarketRankingDataFetcher
|
||||
.getInstance()
|
||||
.fetchRecommendFutures(primaryIdx, secondaryIdx);
|
||||
this.cardData = {
|
||||
tableList: contracts.map((contract: RecommendFuturesItem): RankItem => ({
|
||||
code: contract.contract_code,
|
||||
market: contract.market,
|
||||
name: contract.contract_name,
|
||||
})),
|
||||
};
|
||||
this.subscribeQuotes(primaryIdx, secondaryIdx);
|
||||
} catch {
|
||||
this.cardData = { tableList: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// 使用当前 CardData 中的合约列表订阅行情,推送后整体更新 CardData。
|
||||
private subscribeQuotes(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
|
||||
.getInstance()
|
||||
.subscribeQuotes(contracts, primaryIdx, secondaryIdx, (cardData: CardData): void => {
|
||||
this.cardData = cardData;
|
||||
});
|
||||
}
|
||||
|
||||
private stopSubscribeQuotes(): void {
|
||||
if (this.quoteUnsubscribe !== undefined) {
|
||||
this.quoteUnsubscribe();
|
||||
this.quoteUnsubscribe = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到分时详情页
|
||||
// todo: 后续跳转为真实的页面
|
||||
private jumpToDetail(item: RankItem): void {
|
||||
const clientUrl =
|
||||
`client://client.html?action=ymtz^webid=2205^stockcode=${item.code}^marketid=${item.market}`;
|
||||
console.info(`[MarketRanking] detail url: ${clientUrl}`);
|
||||
// 当前仍跳转本地 Mock 页面;接入客户端能力后改为使用 clientUrl。
|
||||
router.pushUrl({
|
||||
url: 'pages/Detail',
|
||||
params: {
|
||||
code: item.code,
|
||||
market: item.market,
|
||||
clientUrl: clientUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 以下是UI相关
|
||||
private showTabGradient(): boolean {
|
||||
const maxOffset = this.tabContentWidth - this.tabViewportWidth;
|
||||
return maxOffset > 0 && this.tabScrollOffset < maxOffset;
|
||||
}
|
||||
|
||||
private emptyHeight(): number {
|
||||
const rowHeight = 45;
|
||||
const headerHeight = 24;
|
||||
return this.displaySize() * rowHeight + headerHeight;
|
||||
}
|
||||
|
||||
private displaySize(): number {
|
||||
return this.unfolded ? MAX_SIZE : NORMAL_SIZE;
|
||||
}
|
||||
|
||||
// 第三列标题随 Tab 变化
|
||||
private thirdColumnTitle(): ResourceStr {
|
||||
const metric = TAB_METRICS[this.primaryIdx];
|
||||
if (metric.id === 'rise' || metric.id === 'fall') {
|
||||
return $r('app.string.market_ranking_change_rate');
|
||||
}
|
||||
if (metric.id === 'riseSpeed') {
|
||||
switch (this.secondaryIdx) {
|
||||
case 0:
|
||||
return $r('app.string.market_ranking_1min_rise_speed');
|
||||
case 1:
|
||||
return $r('app.string.market_ranking_5min_rise_speed');
|
||||
case 2:
|
||||
return $r('app.string.market_ranking_10min_rise_speed');
|
||||
default:
|
||||
return $r('app.string.market_ranking_15min_rise_speed');
|
||||
}
|
||||
}
|
||||
if (metric.id === 'fallSpeed') {
|
||||
switch (this.secondaryIdx) {
|
||||
case 0:
|
||||
return $r('app.string.market_ranking_1min_fall_speed');
|
||||
case 1:
|
||||
return $r('app.string.market_ranking_5min_fall_speed');
|
||||
case 2:
|
||||
return $r('app.string.market_ranking_10min_fall_speed');
|
||||
default:
|
||||
return $r('app.string.market_ranking_15min_fall_speed');
|
||||
}
|
||||
}
|
||||
return metric.label;
|
||||
}
|
||||
|
||||
// 成交额/日增仓列用中性色,其余按涨跌上色
|
||||
private isNeutralColumn(): boolean {
|
||||
const id = TAB_METRICS[this.primaryIdx].id;
|
||||
return id === 'turnOver' || id === 'increPosit';
|
||||
}
|
||||
|
||||
// 第三列展示值:按当前一级 Tab 从对应原始字段取值并格式化
|
||||
private thirdColumnValue(item: RankItem): string {
|
||||
const id = TAB_METRICS[this.primaryIdx].id;
|
||||
if (id === 'turnOver') {
|
||||
return formatGreatNumber(item.turnover);
|
||||
}
|
||||
if (id === 'increPosit') {
|
||||
return formatGreatNumber(item.incre_posit);
|
||||
}
|
||||
if (id === 'riseSpeed' || id === 'fallSpeed') {
|
||||
return formatPercent(item.chg_speed);
|
||||
}
|
||||
return formatPercent(item.price_chg);
|
||||
}
|
||||
|
||||
// 第三列取色:成交额/日增仓走中性色,其余按对应原始字段的正负取色
|
||||
private thirdColumnColor(item: RankItem): Resource {
|
||||
if (this.isNeutralColumn()) {
|
||||
return $r('app.color.text_primary');
|
||||
}
|
||||
const id = TAB_METRICS[this.primaryIdx].id;
|
||||
const raw = id === 'riseSpeed' || id === 'fallSpeed' ? item.chg_speed : item.price_chg;
|
||||
if (raw === undefined) {
|
||||
return $r('app.color.text_primary');
|
||||
}
|
||||
return riseFallColor(raw);
|
||||
}
|
||||
|
||||
private priceColor(item: RankItem): Resource {
|
||||
if (item.price === undefined || item.price_chg === undefined) {
|
||||
return $r('app.color.text_primary');
|
||||
}
|
||||
return riseFallColor(item.price_chg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import { PeriodRange, TabMetric } from './MarketRankingTypes';
|
||||
|
||||
// 一级 Tab 配置:涨幅/跌幅/涨速/跌速/成交额/日增仓,hqId/hqName 用于后续接入行情推送
|
||||
export const TAB_METRICS: TabMetric[] = [
|
||||
{ id: 'rise', label: '涨幅', stat: 'rise', api: 'rise_percent', hqId: '34818', hqName: 'price_chg', index: 0, hasChildren: false },
|
||||
{ id: 'fall', label: '跌幅', stat: 'fall', api: 'fall_percent', hqId: '34818', hqName: 'price_chg', index: 1, hasChildren: false },
|
||||
{ id: 'riseSpeed', label: '涨速', stat: 'riseSpeed', api: 'rise_percent', hqId: '34821', hqName: 'chg_speed', index: 2, hasChildren: true },
|
||||
{ id: 'fallSpeed', label: '跌速', stat: 'fallSpeed', api: 'fall_percent', hqId: '34821', hqName: 'chg_speed', index: 3, hasChildren: true },
|
||||
{ id: 'turnOver', label: '成交额', stat: 'turnOver', api: 'turnover', hqId: '19', hqName: 'turnover', index: 4, hasChildren: false },
|
||||
{ id: 'increPosit', label: '日增仓', stat: 'increPosit', api: 'increase_position', hqId: '34355', hqName: 'incre_posit', index: 5, hasChildren: false },
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
// 二级 Tab 配置:仅涨速/跌速下出现,按周期筛选
|
||||
export const PERIOD_RANGES: PeriodRange[] = [
|
||||
{ id: '1min', label: '1分钟', stat: 'min1', api: 'one_minute', hqId: '34874', index: 0 },
|
||||
{ id: '5min', label: '5分钟', stat: 'min5', api: 'five_minute', hqId: '34325', index: 1 },
|
||||
{ id: '10min', label: '10分钟', stat: 'min10', api: 'ten_minute', hqId: '34875', index: 2 },
|
||||
{ id: '15min', label: '15分钟', stat: 'min15', api: 'fifteen_minute', hqId: '34876', index: 3 },
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import http from '@ohos.net.http';
|
||||
import BuildProfile from 'BuildProfile';
|
||||
import {
|
||||
CardData,
|
||||
MarketRankingQuoteData,
|
||||
MarketRankingQuoteRequestParams,
|
||||
RankItem,
|
||||
RecommendFuturesItem,
|
||||
} from './MarketRankingTypes';
|
||||
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();
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
static getInstance(): MarketRankingDataFetcher {
|
||||
return MarketRankingDataFetcher.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 市场排名合约列表接口(当前暂用 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/
|
||||
*
|
||||
* Query:
|
||||
* - quote_type: 当前一级/二级 Tab 对应的排序维度
|
||||
* - number: 固定 50
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* code: 0,
|
||||
* data: [{ contract_code: string, contract_name: string, market: string }]
|
||||
* }
|
||||
*
|
||||
* 该接口只返回榜单合约及顺序;最新价、涨跌幅等展示数据由行情订阅提供。
|
||||
*/
|
||||
async fetchRecommendFutures(primaryIdx: number, secondaryIdx: number): Promise<RecommendFuturesItem[]> {
|
||||
const quoteType = this.getQuoteType(primaryIdx, secondaryIdx);
|
||||
const requestUrl = this.buildRecommendFuturesUrl(quoteType);
|
||||
const requestOptions: http.HttpRequestOptions = {
|
||||
method: http.RequestMethod.GET,
|
||||
connectTimeout: 60000,
|
||||
readTimeout: 60000,
|
||||
};
|
||||
const httpRequest = http.createHttp();
|
||||
console.info(
|
||||
`[MarketRanking] recommendFutures request: url=${requestUrl}, options=${JSON.stringify(requestOptions)}`,
|
||||
);
|
||||
// 当前只模拟真实请求构造,不调用 httpRequest.request()。
|
||||
httpRequest.destroy();
|
||||
return new Promise<RecommendFuturesItem[]>((resolve: (value: RecommendFuturesItem[]) => void) => {
|
||||
setTimeout(() => {
|
||||
const data = this.mockRankList(quoteType).map((item: RankItem): RecommendFuturesItem => ({
|
||||
contract_code: item.code,
|
||||
contract_name: item.name,
|
||||
market: item.market,
|
||||
}));
|
||||
resolve(data);
|
||||
}, MOCK_REQUEST_DELAY_MS);
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
return `${PERIOD_RANGES[secondaryIdx].api}_${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[] {
|
||||
switch (quoteType) {
|
||||
case 'rise_percent':
|
||||
return MOCK_RANK_LISTS[0];
|
||||
case 'fall_percent':
|
||||
return MOCK_RANK_LISTS[1];
|
||||
case 'turnover':
|
||||
return MOCK_RANK_LISTS[4];
|
||||
case 'increase_position':
|
||||
return MOCK_RANK_LISTS[5];
|
||||
case 'one_minute_rise_percent':
|
||||
return MOCK_SPEED_LISTS[0][0];
|
||||
case 'five_minute_rise_percent':
|
||||
return MOCK_SPEED_LISTS[0][1];
|
||||
case 'ten_minute_rise_percent':
|
||||
return MOCK_SPEED_LISTS[0][2];
|
||||
case 'fifteen_minute_rise_percent':
|
||||
return MOCK_SPEED_LISTS[0][3];
|
||||
case 'one_minute_fall_percent':
|
||||
return MOCK_SPEED_LISTS[1][0];
|
||||
case 'five_minute_fall_percent':
|
||||
return MOCK_SPEED_LISTS[1][1];
|
||||
case 'ten_minute_fall_percent':
|
||||
return MOCK_SPEED_LISTS[1][2];
|
||||
case 'fifteen_minute_fall_percent':
|
||||
return MOCK_SPEED_LISTS[1][3];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import router from '@ohos.router';
|
||||
import { PeriodRange, RankItem, TabMetric } from './MarketRankingTypes';
|
||||
import { TAB_METRICS, PERIOD_RANGES } from './MarketRankingConstant';
|
||||
import { MOCK_RANK_LISTS, MOCK_SPEED_LISTS } from './MarketRankingMock';
|
||||
|
||||
// 状态持有 + 编排类;
|
||||
@Observed
|
||||
export class MarketRankingModel {
|
||||
private tabMetrics: TabMetric[] = TAB_METRICS;
|
||||
private periodRanges: PeriodRange[] = PERIOD_RANGES;
|
||||
rankLists: RankItem[][] = MOCK_RANK_LISTS;
|
||||
speedLists: RankItem[][][] = MOCK_SPEED_LISTS;
|
||||
|
||||
// 按当前一级/二级 Tab 取展示列表:View 只需直接调用,不用自己判断 rankLists/speedLists 分支
|
||||
currentList(primaryIdx: number, secondaryIdx: number): RankItem[] {
|
||||
if (this.tabMetrics[primaryIdx].hasChildren) {
|
||||
const speedGroupIdx = primaryIdx === 2 ? 0 : 1;
|
||||
return this.speedLists[speedGroupIdx][secondaryIdx];
|
||||
}
|
||||
return this.rankLists[primaryIdx];
|
||||
}
|
||||
|
||||
// 跳转到分时详情页
|
||||
jumpToDetail(item: RankItem): void {
|
||||
router.pushUrl({
|
||||
url: 'pages/Detail',
|
||||
params: { code: item.code, market: item.market },
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: 接入后端接口后,在此方法内按 (primaryIdx, secondaryIdx) 发起请求并更新 rankLists/speedLists
|
||||
async updateRankList(primaryIdx: number, secondaryIdx: number): Promise<void> {
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
// ============ 视图数据类型(合并接口 + 行情后,供 View 消费) ============
|
||||
// 保留原始字段,不在数据层预先格式化/预先判断涨跌色,格式化和取色都留给 View 层做。
|
||||
// 保留原始字段,不在数据层预先格式化/预先判断涨跌色;HTTP 占位行允许行情字段暂缺。
|
||||
export interface RankItem {
|
||||
code: string;
|
||||
market: string;
|
||||
name: string;
|
||||
price: number;
|
||||
price?: number;
|
||||
// 涨跌幅(%数值,如 3.21 表示 +3.21%)。行情推送里固定字段,任何 Tab 下都存在,
|
||||
// 用于「最新价」列的取色,也是 涨幅/跌幅 Tab 第三列的数据来源。
|
||||
price_chg?: number;
|
||||
@@ -16,9 +16,44 @@ export interface RankItem {
|
||||
incre_posit?: number;
|
||||
}
|
||||
|
||||
// 当前市场排名卡片的完整展示数据;网络/行情更新后整体替换以触发 UI 刷新。
|
||||
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 interface MarketRankingQuoteRequestParams {
|
||||
protocolId: string;
|
||||
pageId: string;
|
||||
onlineId: string;
|
||||
columnOrder: string;
|
||||
requestDic: string;
|
||||
}
|
||||
|
||||
export interface TabMetric {
|
||||
id: string;
|
||||
label: string;
|
||||
label: ResourceStr;
|
||||
stat: string; // 埋点用
|
||||
api: string; // HTTP 接口 quote_type 参数用
|
||||
hqId: string; // 行情订阅 sortid 用
|
||||
@@ -29,7 +64,7 @@ export interface TabMetric {
|
||||
|
||||
export interface PeriodRange {
|
||||
id: string;
|
||||
label: string;
|
||||
label: ResourceStr;
|
||||
stat: string;
|
||||
api: string;
|
||||
hqId: string;
|
||||
@@ -42,6 +77,7 @@ export interface PeriodRange {
|
||||
export interface RecommendFuturesItem {
|
||||
contract_code: string;
|
||||
contract_name: string;
|
||||
market: string;
|
||||
}
|
||||
|
||||
export interface RecommendFuturesResponse {
|
||||
|
||||
@@ -24,7 +24,7 @@ const aiPickCardData: CardData = {
|
||||
struct Index {
|
||||
build() {
|
||||
Scroll() {
|
||||
Column({ space: 16 }) {
|
||||
Column({ space: 8 }) {
|
||||
Card() {
|
||||
AIView()
|
||||
}
|
||||
@@ -36,7 +36,7 @@ struct Index {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(16)
|
||||
.padding({ left: 6, right: 6 })
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
.justifyContent(FlexAlign.Start)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,106 @@
|
||||
{
|
||||
"name": "EntryAbility_label",
|
||||
"value": "label"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_title",
|
||||
"value": "市场排名"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_expand",
|
||||
"value": "展开"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_collapse",
|
||||
"value": "收起"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_empty",
|
||||
"value": "暂无数据"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_contract_name",
|
||||
"value": "合约名称"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_latest_price",
|
||||
"value": "最新价"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_change_rate",
|
||||
"value": "涨跌幅"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_tab_rise",
|
||||
"value": "涨幅"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_tab_fall",
|
||||
"value": "跌幅"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_tab_rise_speed",
|
||||
"value": "涨速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_tab_fall_speed",
|
||||
"value": "跌速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_tab_turnover",
|
||||
"value": "成交额"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_tab_increase_position",
|
||||
"value": "日增仓"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_period_1min",
|
||||
"value": "1分钟"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_period_5min",
|
||||
"value": "5分钟"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_period_10min",
|
||||
"value": "10分钟"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_period_15min",
|
||||
"value": "15分钟"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_1min_rise_speed",
|
||||
"value": "1分钟涨速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_5min_rise_speed",
|
||||
"value": "5分钟涨速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_10min_rise_speed",
|
||||
"value": "10分钟涨速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_15min_rise_speed",
|
||||
"value": "15分钟涨速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_1min_fall_speed",
|
||||
"value": "1分钟跌速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_5min_fall_speed",
|
||||
"value": "5分钟跌速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_10min_fall_speed",
|
||||
"value": "10分钟跌速"
|
||||
},
|
||||
{
|
||||
"name": "market_ranking_15min_fall_speed",
|
||||
"value": "15分钟跌速"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user