fix(ai-pick): 对齐 Vue 卡片布局与跳转说明
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user