diff --git a/entry/src/main/ets/ai-pick/AiPick.ets b/entry/src/main/ets/ai-pick/AiPick.ets index 589a046..1e64d1d 100644 --- a/entry/src/main/ets/ai-pick/AiPick.ets +++ b/entry/src/main/ets/ai-pick/AiPick.ets @@ -1,8 +1,18 @@ +import { common, ConfigurationConstant } from '@kit.AbilityKit'; import { CardData, PickContractItem, PickTabData } from './AiPickTypes'; import { AiPickModel } from './AiPickModel'; import { cellColor, cellValue, formatColName } from './AiPickUtils'; import { MAX_ITEMS } from './AiPickConstant'; +const RIGHT_ARROW_LIGHT = + 'https://u.thsi.cn/imgsrc/bbs/8c8290d904cd89250d4e828da5f1010c_300_230.png'; +const RIGHT_ARROW_DARK = + 'https://u.thsi.cn/imgsrc/bbs/f5ff0a4fcd103d7b3da180ad90d9db23_300_230.png'; +const EMPTY_IMAGE_LIGHT = + 'https://u.thsi.cn/imgsrc/bbs/8eee9cd4fa7e359032de11a3f7c2a70b_300_230.png'; +const EMPTY_IMAGE_DARK = + 'https://u.thsi.cn/imgsrc/bbs/1c4e679244ed87231befad2b04cddfe2_300_230.png'; + interface StrategyDescParams { tabIdx: number; } @@ -14,8 +24,41 @@ export struct AiPick { // 初值用 placeholder cardData 占位(仅类型占位,实际值由 aboutToAppear 用真实 cardData 创建) @State vm: AiPickModel = new AiPickModel(this.cardData); @State tabIdx: number = 0; + @State isDarkMode: boolean = false; + @State tabViewportWidth: number = 0; + @State tabContentWidth: number = 0; + @State tabScrollOffset: number = 0; + + private truncateText(text: string): string { + const maxLength = 20; + let currentLength = 0; + let truncatedText = ''; + for (const char of text) { + currentLength += /[\u4e00-\u9fa5]/.test(char) ? 2 : 1; + if (currentLength > maxLength) { + return `${truncatedText}...`; + } + truncatedText += char; + } + return truncatedText; + } + + private tabTitle(tab: PickTabData): string { + return `${this.truncateText(tab.strategy.name)}(${tab.strategy.count})`; + } + + private showTabGradient(): boolean { + const maxOffset = this.tabContentWidth - this.tabViewportWidth; + return maxOffset > 0 && this.tabScrollOffset < maxOffset; + } + + private syncColorMode(): void { + const context = getContext(this) as common.UIAbilityContext; + this.isDarkMode = context.config.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK; + } aboutToAppear(): void { + this.syncColorMode(); this.vm = new AiPickModel(this.cardData); this.vm.loadData(); } @@ -32,23 +75,26 @@ export struct AiPick { // 图片:imgs[0] 白天,imgs[1] 夜间 Row() { // 左侧:问句 + 标签 - Column({ space: 8 }) { + Column({ space: 4 }) { if (this.vm.tabs[$$.tabIdx].strategy.detail) { Text(this.vm.tabs[$$.tabIdx].strategy.detail) .fontSize(14) + .lineHeight(21) .fontColor($r('app.color.text_primary')) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) } if (this.vm.tabs[$$.tabIdx].strategy.tagList.length > 0) { - Row({ space: 8 }) { + Row({ space: 4 }) { ForEach(this.vm.tabs[$$.tabIdx].strategy.tagList, (tag: string) => { Text(tag) .fontSize(11) + .lineHeight(18) .fontColor($r('app.color.text_blue')) - .padding({ left: 6, right: 6, top: 2, bottom: 2 }) - .borderRadius(4) + .height(18) + .padding({ left: 3, right: 3 }) + .borderRadius(2) .border({ width: 0.5, color: $r('app.color.text_blue') }) .onClick((event: ClickEvent) => { @@ -65,6 +111,7 @@ export struct AiPick { } .layoutWeight(1) .alignItems(HorizontalAlign.Start) + .margin({ bottom: 10 }) // 右侧:策略图片(仅系统策略有图,右侧 image) // imgs 长度>0 且当前选中位(imgs[0],暂无主题切换)非空才渲染 @@ -77,7 +124,7 @@ export struct AiPick { } } .width('100%') - .margin({ top: 16 }) + .margin({ top: 10 }) .alignItems(VerticalAlign.Top) .onClick(() => { this.vm.jumpToDetail2( @@ -101,12 +148,19 @@ export struct AiPick { .width('100%') .padding({ bottom: 16 }) - Text(this.vm.dataError ? '数据加载失败' : '加载中...') - .fontSize(14) - .fontColor($r('app.color.text_tertiary')) + if (this.vm.dataError) { + this.EmptyState('暂无满足条件的期货合约') + } else { + Column() { + Text('数据加载中') + .fontSize(16) + .fontColor($r('app.color.text_grey')) + } .width('100%') - .textAlign(TextAlign.Center) - .padding({ top: 24, bottom: 24 }) + .height(186) + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + } } else { this.MainContent() } @@ -128,123 +182,171 @@ export struct AiPick { .padding({ bottom: 16 }) // ── Tab 栏 ────────────────────────────────────────────── - Scroll() { - Row({ space: 8 }) { - ForEach(this.vm.tabs, (tab: PickTabData, index: number) => { - Text(tab.title) - .fontSize(14) - .fontWeight(this.tabIdx === index ? FontWeight.Bold : FontWeight.Normal) - .fontColor(this.tabIdx === index - ? $r('app.color.text_blue') - : $r('app.color.text_secondary')) - .constraintSize({ minWidth: 80 }) - .textAlign(TextAlign.Center) - .padding({ top: 6, bottom: 6, left: 12, right: 12 }) - .borderRadius(4) - .backgroundColor(this.tabIdx === index - ? $r('app.color.pill_active_bg') - : $r('app.color.pill_inactive_bg')) - .onClick(() => { - this.tabIdx = index; - }) - }, (tab: PickTabData) => tab.strategy.id) + Stack({ alignContent: Alignment.End }) { + Scroll() { + Row({ space: 8 }) { + ForEach(this.vm.tabs, (tab: PickTabData, index: number) => { + Text(this.tabTitle(tab)) + .fontSize(14) + .fontWeight(this.tabIdx === index ? FontWeight.Bold : FontWeight.Normal) + .fontColor(this.tabIdx === index + ? $r('app.color.text_blue') + : $r('app.color.text_secondary')) + .constraintSize({ minWidth: 63 }) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .textAlign(TextAlign.Center) + .padding({ top: 6, bottom: 6, left: 12, right: 12 }) + .borderRadius(4) + .backgroundColor(this.tabIdx === index + ? $r('app.color.pill_active_bg') + : $r('app.color.pill_inactive_bg')) + .onClick(() => { + this.tabIdx = index; + }) + }, (tab: PickTabData) => tab.strategy.id) + } + .onAreaChange((oldValue: Area, newValue: Area) => { + this.tabContentWidth = Number(newValue.width); + }) + } + .width('100%') + .height(30) + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .align(Alignment.Start) + .onScroll((xOffset: number, _yOffset: number) => { + this.tabScrollOffset = xOffset; + }) + + if (this.showTabGradient()) { + Row() + .width(24) + .height(30) + .linearGradient({ + angle: 90, + colors: [['#00FFFFFF', 0], [$r('app.color.card_bg'), 1]], + }) + .hitTestBehavior(HitTestMode.None) } } .width('100%') - .scrollable(ScrollDirection.Horizontal) - .scrollBar(BarState.Off) - .align(Alignment.Start) + .height(30) + .onAreaChange((oldValue: Area, newValue: Area) => { + this.tabViewportWidth = Number(newValue.width); + }) // ── 策略问句(按引用传递,tabIdx 变化时自动刷新)──────── this.StrategyDesc({ tabIdx: this.tabIdx }) - // ── 表头 ──────────────────────────────────────────────── - Row() { - Text(formatColName(this.vm.tabs[this.tabIdx].keysList[0])) - .fontSize(12) - .fontColor($r('app.color.text_tertiary')) - .width('35%') - Text(formatColName(this.vm.tabs[this.tabIdx].keysList[1])) - .fontSize(12) - .fontColor($r('app.color.text_tertiary')) - .layoutWeight(1) - .textAlign(TextAlign.End) - Text(formatColName(this.vm.tabs[this.tabIdx].keysList[2])) - .fontSize(12) - .fontColor($r('app.color.text_tertiary')) - .layoutWeight(1) - .textAlign(TextAlign.End) - } - .width('100%') - .margin({ top: 12 }) - .padding({ bottom: 4 }) - .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) + if (this.vm.tabs[this.tabIdx].items.length === 0) { + this.EmptyState('没有符合的合约') + } else { + // ── 表头 ────────────────────────────────────────────── + Row() { + Text(formatColName(this.vm.tabs[this.tabIdx].keysList[0])) + .fontSize(12) + .fontColor($r('app.color.text_tertiary')) + .width('35%') + Text(formatColName(this.vm.tabs[this.tabIdx].keysList[1])) + .fontSize(12) + .fontColor($r('app.color.text_tertiary')) + .layoutWeight(1) + .margin({ left: 8 }) + .textAlign(TextAlign.End) + Text(formatColName(this.vm.tabs[this.tabIdx].keysList[2])) + .fontSize(12) + .fontColor($r('app.color.text_tertiary')) + .layoutWeight(1) + .margin({ left: 8 }) + .textAlign(TextAlign.End) + } + .width('100%') + .height(24) + .margin({ top: 8 }) + .padding({ bottom: 2 }) + .alignItems(VerticalAlign.Center) + .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) - // ── 合约列表 ──────────────────────────────────────────── - ForEach( - this.vm.tabs[this.tabIdx].items.slice(0, MAX_ITEMS), - (item: PickContractItem, index: number) => { - Row() { - Row({ space: 4 }) { - Text(item.name) + // ── 合约列表 ────────────────────────────────────────── + ForEach( + this.vm.tabs[this.tabIdx].items.slice(0, MAX_ITEMS), + (item: PickContractItem, index: number) => { + Row() { + Row({ space: 5 }) { + Text(item.name) + .fontSize(16) + .fontColor($r('app.color.text_primary')) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + if (item.isMain) { + Text('主') + .fontSize(11) + .fontColor($r('app.color.color_orange')) + .width(16) + .height(16) + .textAlign(TextAlign.Center) + .borderRadius(2) + .backgroundColor($r('app.color.bg_orange')) + } + } + .width('35%') + .alignItems(VerticalAlign.Center) + + Text(cellValue( + this.vm.tabs[this.tabIdx].keysList[1], + item, + this.vm.tabs[this.tabIdx].fieldList, + )) .fontSize(16) - .fontColor($r('app.color.text_primary')) + .fontFamily('monospace') + .fontColor(cellColor(this.vm.tabs[this.tabIdx].keysList[1], item)) + .layoutWeight(1) + .margin({ left: 8 }) + .textAlign(TextAlign.End) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + + Text(cellValue( + this.vm.tabs[this.tabIdx].keysList[2], + item, + this.vm.tabs[this.tabIdx].fieldList, + )) + .fontSize(16) + .fontFamily('monospace') + .fontColor(cellColor(this.vm.tabs[this.tabIdx].keysList[2], item)) + .layoutWeight(1) + .margin({ left: 8 }) + .textAlign(TextAlign.End) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) - if (item.isMain) { - Text('主') - .fontSize(11) - .fontColor($r('app.color.color_orange')) - .width(16) - .height(16) - .textAlign(TextAlign.Center) - .borderRadius(4) - .backgroundColor($r('app.color.bg_orange')) - } } - .width('35%') + .width('100%') + .height(44) .alignItems(VerticalAlign.Center) - - Text(cellValue( - this.vm.tabs[this.tabIdx].keysList[1], - item, - this.vm.tabs[this.tabIdx].fieldList, - )) - .fontSize(16) - .fontColor(cellColor(this.vm.tabs[this.tabIdx].keysList[1], item)) - .layoutWeight(1) - .textAlign(TextAlign.End) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - - Text(cellValue( - this.vm.tabs[this.tabIdx].keysList[2], - item, - this.vm.tabs[this.tabIdx].fieldList, - )) - .fontSize(16) - .fontColor(cellColor(this.vm.tabs[this.tabIdx].keysList[2], item)) - .layoutWeight(1) - .textAlign(TextAlign.End) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - } - .width('100%') - .padding({ top: 12, bottom: 12 }) - .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) - .onClick(() => { - this.vm.jumpToDetail(item.contract, item.market); - }) - }, - (item: PickContractItem) => item.contract + item.market, - ) + .border({ width: { bottom: 0.5 }, color: $r('app.color.divider_color') }) + .onClick(() => { + this.vm.jumpToDetail(item.contract, item.market); + }) + }, + (item: PickContractItem) => item.contract + item.market, + ) + } // ── 一句话定制策略 ────────────────────────────────────── - Text('一句话定制策略 ›') - .fontSize(14) - .fontColor($r('app.color.text_tertiary')) - .textAlign(TextAlign.Center) + Row({ space: 2 }) { + Text('一句话定制策略') + .fontSize(14) + .lineHeight(20) + .fontColor($r('app.color.text_tertiary')) + Image(this.isDarkMode ? RIGHT_ARROW_DARK : RIGHT_ARROW_LIGHT) + .width(14) + .height(14) + .margin({ top: 2 }) + } .width('100%') + .justifyContent(FlexAlign.Center) + .alignItems(VerticalAlign.Center) .margin({ top: 12 }) .onClick(() => { this.vm.jumpToNewHome( @@ -255,4 +357,21 @@ export struct AiPick { } .width('100%') } + + @Builder + EmptyState(message: string) { + Column() { + Image(this.isDarkMode ? EMPTY_IMAGE_DARK : EMPTY_IMAGE_LIGHT) + .width(120) + .height(120) + .margin({ bottom: 8 }) + Text(message) + .fontSize(16) + .fontColor($r('app.color.text_grey')) + } + .width('100%') + .height(186) + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + } } diff --git a/entry/src/main/ets/ai-pick/AiPickModel.ets b/entry/src/main/ets/ai-pick/AiPickModel.ets index c154219..648da2e 100644 --- a/entry/src/main/ets/ai-pick/AiPickModel.ets +++ b/entry/src/main/ets/ai-pick/AiPickModel.ets @@ -5,7 +5,7 @@ import { fetchAiPickTabs, parseRawTabs } from './AiPickApi'; // 顶层缓存 key:对应 Vue 端 __GLOBAL__.cardsDataCache(跨组件/跨页面共享) const APP_STORAGE_CACHE_KEY = 'cardsDataCache'; -// 空 CardData 占位(仅用于锺型调用场景,实际使用需设 cardData 后重新构造 Model) +// 空 CardData 占位(仅用于锺型调用场景,实际使用需设 cardData 后重新构造 Model), 后续接入后会删除 const EMPTY_CARD_DATA: CardData = { card_id: 0, card_key: '', @@ -83,6 +83,28 @@ export class AiPickModel { } } + /** + * Vue 版 AI 选期跳转协议备忘: + * + * domain:release 使用 fupage,dev/test 使用 futures-test。 + * Web 页面统一包装为: + * client.html?action=ymtz^ishiddenbar=1^mode=new^webid=2804^url={qUrl}^title=AI选期 + * + * 1. 点击合约: + * client://client.html?action=ymtz^webid=2205^stockcode={contract}^marketid={market} + * 2. 点击策略描述/图片: + * https://{domain}.10jqka.com.cn/ai-web/ai-diagnosis-detail.html + * ?id={strategyId}&type={periodType}&name={encodeURIComponent(name)} + * 3. 点击策略标签: + * https://{domain}.10jqka.com.cn/ai-web/ai-diagnosis-label.html + * ?id={strategyId}&type={periodType}&label={encodeURIComponent(label)} + * 4. 点击“一句话定制策略”: + * https://{domain}.10jqka.com.cn/ai-web/ai-diagnosis-home.html?sync=1 + * 5. 旧版“查看详情”: + * https://{domain}.10jqka.com.cn/ai-web/futures-ai-diagnosis.html + * ?id={strategyId}&name={encodeURIComponent(strategyDetail)} + * 6. 点击卡片标题:使用 cardData.card_url.ios/android;当前 AI 选期 Mock 均为空。 + */ jumpToDetail(contract: string, market: string): void { router.pushUrl({ url: 'pages/Detail',