diff --git a/doc/market-ranking-interfaces.md b/doc/market-ranking-interfaces.md new file mode 100644 index 0000000..390fe9d --- /dev/null +++ b/doc/market-ranking-interfaces.md @@ -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]` 前缀输出到控制台。 diff --git a/entry/src/main/ets/common/Card.ets b/entry/src/main/ets/common/Card.ets index 1324654..c09430c 100644 --- a/entry/src/main/ets/common/Card.ets +++ b/entry/src/main/ets/common/Card.ets @@ -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) } } diff --git a/entry/src/main/ets/market-ranking/MarketRanking.ets b/entry/src/main/ets/market-ranking/MarketRanking.ets index e903009..cbaba81 100644 --- a/entry/src/main/ets/market-ranking/MarketRanking.ets +++ b/entry/src/main/ets/market-ranking/MarketRanking.ets @@ -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 { + // 切换 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); + } } diff --git a/entry/src/main/ets/market-ranking/MarketRankingConstant.ets b/entry/src/main/ets/market-ranking/MarketRankingConstant.ets index e9ce299..67b6c52 100644 --- a/entry/src/main/ets/market-ranking/MarketRankingConstant.ets +++ b/entry/src/main/ets/market-ranking/MarketRankingConstant.ets @@ -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 }, ]; diff --git a/entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets b/entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets new file mode 100644 index 0000000..9bce7d0 --- /dev/null +++ b/entry/src/main/ets/market-ranking/MarketRankingDataFetcher.ets @@ -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 { + 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((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 []; + } + } +} diff --git a/entry/src/main/ets/market-ranking/MarketRankingModel.ets b/entry/src/main/ets/market-ranking/MarketRankingModel.ets deleted file mode 100644 index 92edf70..0000000 --- a/entry/src/main/ets/market-ranking/MarketRankingModel.ets +++ /dev/null @@ -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 { - } -} diff --git a/entry/src/main/ets/market-ranking/MarketRankingTypes.ets b/entry/src/main/ets/market-ranking/MarketRankingTypes.ets index bd4d299..41e74d2 100644 --- a/entry/src/main/ets/market-ranking/MarketRankingTypes.ets +++ b/entry/src/main/ets/market-ranking/MarketRankingTypes.ets @@ -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 { diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index ab11361..6aece50 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -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) } diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index f945955..64c665b 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -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分钟跌速" } ] -} \ No newline at end of file +}