fix(ai-pick): 对齐 Vue 卡片布局与跳转说明

This commit is contained in:
clz
2026-07-22 15:48:28 +08:00
parent 0be60b394c
commit 2e9bd0c275
2 changed files with 253 additions and 112 deletions
+230 -111
View File
@@ -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)
}
}
+23 -1
View File
@@ -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 选期跳转协议备忘:
*
* domainrelease 使用 fupagedev/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',