Files
biz_firstpage/src/main/ets/node/view/AiDiagnosisNodeComponent.ets
T
clz af0025ce38 Initial commit: @b2c/first_page HarmonyOS stage-mode HAR
## Repository
- HarmonyOS stage-mode HAR module, consumed by host app
- No standalone build — hvigorw, oh_modules, and ../biz_quote absent here

## Architecture
### Feed Cards
- FirstPage.createNodeView dispatches cards by FirstPageConstant.KEY_*
- Card config from first_page_cards_config.json loaded by FirstPageCardsConfigLoader
- Public entrypoint: Index.ets

### Market Ranking (src/main/ets/market-ranking/)
- HTTP: MarketRankingDataFetcher fetches recommend_futures contracts
- HQ: MarketRankingHqRequestClient subscribes via 4106/frame 2201/scenario qht_qihuo_sort
- TCP reconnect rebuilds subscription from original HTTP contract list
- Async callbacks guarded by request/subscription versions

### Other Cards
- HotList, CommodityOptions, AiDiagnosis, AiRiseFall, SelfSelectedStock, CustomEntryList, FourEntryList
- Each has DataFetcher + HQ request client + NodeComponent

## Key Changes (from CardHarmonyOS reference)
- Added MetricId, PeriodId, SortOrder enums (MarketRankingModels.ets)
- Replaced string comparisons with enum switches in thirdColumnTitle/Value/Color
- Removed redundant undefined guards in thirdColumnColor/priceColor, delegated to riseFallColor
- Inlined priceColor(), use riseFallColor directly
- Tab bar Stack.height(30), Alignment.End (reverted from 46/TopEnd)
- Name column fontWeight removed (default regular)
2026-07-26 01:13:28 +08:00

782 lines
23 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { LengthMetrics, router } from '@kit.ArkUI';
import { emitter } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AiDiagnosisDataFetcher } from '../datacenter/AiDiagnosisDataFetcher';
import { AiDiagnosisData, AllCardsCard, FactorItem, RecommendItem } from '../model/AiDiagnosisNodeModel';
import { FirstPageNodeModel } from '../../datacenter/model/FirstPageNodeModel';
import { AiDiagnosisRequestClient } from '../clients/AiDiagnosisRequestClient';
import { HeaderItem, RowData, CellArray, TableData } from '@b2b/hq_table';
import { EmitterConstants, GlobalContext, HXLog } from 'biz_common';
import { RouterUtil } from '../../util/RouterUtil';
import { enableDarkMode } from '@kernel/theme_manager';
import { TableConstants } from '@b2c/lib_baseui';
import { QuoteCustomSettingManager, StandardPriceType } from 'biz_quote';
import { TabType } from 'biz_quote/src/main/ets/config/tabConfig';
import util from '@ohos.util';
// 评分背景图片配置文件
const AI_SCORE_BG_IMAGES_FILE = 'ai_score_background_images.json'
// 评分背景图片配置数据结构
interface ScoreBgImagesConfig {
light: Record<string, string>
dark: Record<string, string>
}
/**
* AI 诊断节点组件
* @author yansong@myhexin.com
*/
// AI诊断卡片行情请求 FrameId 和 PageId
const FRAME_ID_AI_DIAGNOSIS = 2201
const PAGE_ID_AI_DIAGNOSIS = 4106
// 因子列表最大显示数量
const FACTOR_LIST_MAX = 3
@Component
export struct AiDiagnosisNodeComponent {
nodeModel: FirstPageNodeModel | null = null
// 刷新触发器(父组件通过 @Link 传入,每次递增触发刷新)
@Link @Watch('onRefreshTriggerChange') refreshTrigger: number
// Tab 可见性(用于控制行情请求的 request/release
@Watch('onTabVisibilityChange') @Consume firstPageVisible: boolean = true
// 深色模式状态
@StorageProp(enableDarkMode) isDarkMode: boolean = false
@State private cardConfig: AllCardsCard | null = null
@State private cardData: AiDiagnosisData | null = null
// 行情数据
@State private priceChg: string = '--' // 涨跌幅
@State private priceChgColor: ResourceStr = $r('app.color.price_even') // 涨跌幅颜色
// 行情订阅客户端
private hqRequestClient: AiDiagnosisRequestClient | null = null
// 评分背景图片配置
private scoreBgImagesLight: Record<string, string> = {}
private scoreBgImagesDark: Record<string, string> = {}
// TCP 重连监听
private mIsEventSubscribed: boolean = false
private netRelinkCallBack = () => {
if (this.firstPageVisible) {
setTimeout(() => {
if (this.cardData && this.cardData.code && this.cardData.market) {
this.subscribeHqData(this.cardData.code, this.cardData.market)
}
})
}
}
// 上一次刷新时使用的基准价类型,用于在变可见时判断是否需要按新基准价重新请求
private lastStandardPriceType: StandardPriceType =
QuoteCustomSettingManager.getInstance().getStandardPriceType()
aboutToAppear(): void {
// 加载评分背景图片配置
this.loadScoreBgImages()
// 初始化数据获取器回调
AiDiagnosisDataFetcher.getInstance().onCardConfigChange = (config) => {
this.cardConfig = config
}
AiDiagnosisDataFetcher.getInstance().onCardDataChange = (data) => {
this.cardData = data
// 当卡片数据更新时,重新订阅行情
if (data && data.code && data.market) {
this.subscribeHqData(data.code, data.market)
}
}
// 初始请求数据
this.refreshCardData()
// 订阅 TCP 重连事件
if (this.firstPageVisible) {
this.subscribeTcpNetStatus()
}
}
/**
* 加载评分背景图片配置
*/
private loadScoreBgImages(): void {
const rawData = this.readRawFile(AI_SCORE_BG_IMAGES_FILE)
if (!rawData) {
HXLog.e('AiDiagnosisNodeComponent', 'loadScoreBgImages: read file failed')
return
}
try {
const config = JSON.parse(rawData) as ScoreBgImagesConfig
this.scoreBgImagesLight = config.light || {}
this.scoreBgImagesDark = config.dark || {}
HXLog.d('AiDiagnosisNodeComponent', 'loadScoreBgImages: success')
} catch (e) {
const error = e as BusinessError
HXLog.e('AiDiagnosisNodeComponent', 'loadScoreBgImages: parse error ' + error.message)
}
}
/**
* 从 rawfile 读取配置文件
*/
private readRawFile(fileName: string): string {
try {
const context = GlobalContext.get()
const mgr = context.resourceManager
const rawFile = mgr.getRawFileContentSync(fileName)
let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true })
return textDecoder.decodeToString(rawFile, { stream: false })
} catch (e) {
const error = e as BusinessError
HXLog.e('AiDiagnosisNodeComponent', `readRawFile ${fileName} error: ${error.code} ${error.message}`)
return ''
}
}
/**
* 刷新触发器变化时的回调
* 首页下拉刷新完成后会触发此方法
*/
onRefreshTriggerChange(): void {
HXLog.d('AiDiagnosisNodeComponent', 'onRefreshTriggerChange triggered, refreshTrigger: ' + this.refreshTrigger)
this.refreshCardData()
}
/**
* Tab 可见性变化时的回调
* 可见时请求行情数据,不可见时释放行情请求
*/
onTabVisibilityChange(): void {
HXLog.d('AiDiagnosisNodeComponent', 'onTabVisibilityChange: ' + this.firstPageVisible)
if (this.firstPageVisible) {
// 可见时重新请求数据
if (this.cardData && this.cardData.code && this.cardData.market) {
this.subscribeHqData(this.cardData.code, this.cardData.market)
}
// 订阅 TCP 重连事件
this.subscribeTcpNetStatus()
// 检测基准价是否切换,切换后重新订阅
const currentStandardPriceType = QuoteCustomSettingManager.getInstance().getStandardPriceType()
if (this.lastStandardPriceType !== currentStandardPriceType) {
HXLog.d('AiDiagnosisNodeComponent', 'Standard price type changed, resubscribe')
this.lastStandardPriceType = currentStandardPriceType
if (this.cardData && this.cardData.code && this.cardData.market) {
this.subscribeHqData(this.cardData.code, this.cardData.market)
}
}
} else {
// 不可见时释放行情订阅
this.releaseHqClient()
// 取消订阅 TCP 重连事件
this.unSubscribeTcpNetStatus()
}
}
/**
* 刷新卡片数据(诊断数据 + 重新订阅行情)
*/
refreshCardData(): void {
AiDiagnosisDataFetcher.getInstance().fetchCardConfig()
}
/**
* 释放行情订阅客户端
*/
private releaseHqClient(): void {
if (this.hqRequestClient) {
this.hqRequestClient.release(true)
this.hqRequestClient = null
}
}
aboutToDisappear(): void {
this.releaseHqClient()
this.unSubscribeTcpNetStatus()
}
/**
* 订阅 TCP 重连事件
*/
private subscribeTcpNetStatus(): void {
if (this.mIsEventSubscribed) {
return
}
this.mIsEventSubscribed = true
emitter.on({ eventId: EmitterConstants.NETWORK_TCP_RECONNECT }, this.netRelinkCallBack)
}
/**
* 取消订阅 TCP 重连事件
*/
private unSubscribeTcpNetStatus(): void {
if (!this.mIsEventSubscribed) {
return
}
this.mIsEventSubscribed = false
emitter.off(EmitterConstants.NETWORK_TCP_RECONNECT, this.netRelinkCallBack)
}
/**
* 订阅行情数据(涨跌幅)
* 参考自选股模块实现,使用 AiDiagnosisRequestClient
*/
private subscribeHqData(code: string, market: string): void {
// 释放之前的订阅
if (this.hqRequestClient) {
this.hqRequestClient.release(true)
this.hqRequestClient = null
}
// 创建行情请求头配置
const headerConfig: HeaderItem[] = [
new HeaderItem(TableConstants.DATA_ID_NAME, ''),
new HeaderItem(TableConstants.DATA_ID_PRICE, ''),
new HeaderItem(TableConstants.DATA_ID_ZF, '')
]
// 创建请求客户端(使用 AiDiagnosisRequestClient,参考自选股实现)
this.hqRequestClient = new AiDiagnosisRequestClient(
FRAME_ID_AI_DIAGNOSIS,
PAGE_ID_AI_DIAGNOSIS,
code,
market,
headerConfig,
null,
(tableData: TableData | string) => {
this.onHqDataReceive(tableData)
}
)
// 发送请求
this.hqRequestClient.request(0, 1)
}
/**
* 处理行情数据回调
*/
private onHqDataReceive(tableData: TableData | string): void {
if (tableData instanceof TableData) {
const rows = tableData?.tableRows
if (rows && rows.length > 0) {
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
if (row instanceof RowData) {
const cols: CellArray = row.scrollableCols
if (cols && cols.length > 0) {
// 解析涨跌幅数据
for (let j = 0; j < cols.length; j++) {
const cellData = cols[j]
if (cellData.dataId === TableConstants.DATA_ID_ZF) {
this.priceChg = cellData.data || '--'
// 根据涨跌幅设置颜色
const numValue = parseFloat(this.priceChg)
if (!isNaN(numValue)) {
if (numValue > 0) {
this.priceChgColor = $r('app.color.price_up_100') // 红色
} else if (numValue < 0) {
this.priceChgColor = $r('app.color.price_down_100') // 绿色
} else {
this.priceChgColor = $r('app.color.price_even') // 灰色
}
} else {
this.priceChgColor = $r('app.color.price_even')
}
return
}
}
}
}
}
}
}
}
/**
* 格式化涨跌幅,与AI看涨跌保持一致
*/
formatPriceChg(rawValue: string): string {
if (!rawValue || rawValue === '--') {
return '--'
}
const numValue = parseFloat(rawValue)
if (isNaN(numValue)) {
return '--'
}
if (numValue > 0) {
return `+${numValue.toFixed(2)}%`
} else if (numValue < 0) {
return `${numValue.toFixed(2)}%`
} else {
return '0.00%'
}
}
/**
* 获取评分文案
*/
getScoreText(score: string | number): ResourceStr {
const numScore = parseInt(String(score))
if (isNaN(numScore)) {
return '--'
}
if (numScore === 0) {
return $r('app.string.score_neutral')
}
if (numScore > 0) {
if (numScore < 40) return $r('app.string.score_weak_bull')
if (numScore < 80) return $r('app.string.score_medium_bull')
return $r('app.string.score_strong_bull')
} else {
const absScore = Math.abs(numScore)
if (absScore < 40) return $r('app.string.score_weak_bear')
if (absScore < 80) return $r('app.string.score_medium_bear')
return $r('app.string.score_strong_bear')
}
}
/**
* 获取评分颜色
*/
getScoreColor(score: string | number): ResourceStr {
const numScore = parseInt(String(score))
if (isNaN(numScore) || numScore === 0) {
return $r('app.color.ai_score_neutral')
}
return numScore > 0 ? $r('app.color.price_up_100') : $r('app.color.price_down_100') // 红涨绿跌
}
/**
* 获取因子方向颜色
*/
getFactorColor(judge: string): ResourceStr {
if (judge.includes('利多') || judge.includes('多')) {
return $r('app.color.price_up_100')
}
if (judge.includes('利空') || judge.includes('空')) {
return $r('app.color.price_down_100')
}
return $r('app.color.ai_score_neutral')
}
handleTitleClick() {
// 如果没有合约信息,则跳转到 WebView 详情页
if (!this.cardConfig?.card_url) {
return
}
const url = this.cardConfig.card_url.ios || this.cardConfig.card_url.android || ''
if (url.length === 0) {
return
}
// 使用 RouterUtil 处理 client.html 协议格式的 URL
RouterUtil.jumpPage(url, true)
}
/**
* 处理卡片点击
*/
handleCardClick(): void {
if (this.cardData && this.cardData.code && this.cardData.market) {
const codeList = this.cardData.code
const marketList = this.cardData.market
const nameList = this.cardData.name
this.jumpToQuote(codeList, marketList, codeList, marketList, nameList)
return
}
}
/**
* 处理推荐合约点击
*/
handleRecommendClick(item: RecommendItem): void {
if (!item.code || !item.market) {
return
}
// 构建推荐合约列表(包括当前推荐合约)
const codeList = this.cardData?.recommend?.map(r => r.code).join(',') || item.code
const marketList = this.cardData?.recommend?.map(r => r.market).join(',') || item.market
const nameList = this.cardData?.recommend?.map(r => r.name).join(',') || item.name
// 跳转到行情详情页(分时页面)
this.jumpToQuote(item.code, item.market, codeList, marketList, nameList)
}
/**
* 跳转到行情详情页
*/
private jumpToQuote(code: string, market: string, codeList: string, marketList: string, nameList: string): void {
router.pushUrl({
url: 'pages/QuoteDetailPage',
params: {
marketId: market,
code: code,
name: nameList,
codeList: codeList,
marketIdList: marketList,
nameList: nameList,
periodIndex: 0, // 默认显示分时
defaultTabType: TabType.F10, // 切换到基本面(F10) Tab,
enableScrollToTargetTab: false // 禁止跳转到目标tab,保持在分时页顶部
}
}, router.RouterMode.Standard, (err) => {
if (err) {
HXLog.e('AiDiagnosisNodeComponent', `jumpToQuote failed: ${err.code}, ${err.message}`)
}
})
}
build() {
Column() {
// 标题栏
this.buildTitleBar()
// 卡片内容
if (this.cardData) {
this.buildCardContent()
}
}
.width('100%')
.backgroundColor($r('app.color.surface_layer1_foreground'))
.borderRadius(4)
.padding({ left: 10, right: 10, bottom: 10 })
}
@Builder
buildTitleBar() {
Row() {
Row() {
Text(this.cardConfig?.card_title?.value || $r('app.string.ai_diagnosis_title'))
.fontColor($r('app.color.elements_text_primary_02'))
.fontSize(18)
.fontWeight(500)
Image($r('app.media.icon_arrow_forward_20'))
.width(20)
.height(20)
.fillColor($r('app.color.elements_icon_primary_02'))
}
.alignItems(VerticalAlign.Center)
.height('100%')
.onClick(() => this.handleTitleClick())
Blank()
}
.width('100%')
.height(48)
}
@Builder
buildCardContent() {
Column() {
Column({ space: 12 }) {
// 合约信息
this.buildContractInfo()
// 评分区域
this.buildScoreArea()
}
.borderRadius(4)
.padding(8)
.linearGradient(this.getScoreAreaGradientConfig())
.onClick(() => this.handleCardClick())
// AI诊断说明
if (this.cardData && this.cardData.message && this.cardData.message.length > 0) {
this.buildDiagnosisText()
}
// 推荐合约
if (this.cardData && this.cardData.recommend && this.cardData.recommend.length > 0) {
this.buildRecommendContracts()
}
}
.width('100%')
.borderRadius(8)
}
/**
* 获取评分区域渐变配置
* 从上到下渐变:从不透明到透明
*/
getScoreAreaGradientConfig(): LinearGradientOptions {
const score = this.cardData ? parseInt(this.cardData.score) : 0
if (isNaN(score) || score === 0) {
// 橙色渐变
return {
direction: GradientDirection.Top,
colors: [[$r('app.color.ai_score_gradient_neutral_start'), 0.0], [$r('app.color.ai_score_gradient_neutral_end'), 1.0]]
}
}
if (score > 0) {
// 红色渐变
return {
direction: GradientDirection.Top,
colors: [[$r('app.color.ai_score_gradient_bull_start'), 0.0], [$r('app.color.ai_score_gradient_bull_end'), 1.0]]
}
}
// 绿色渐变
return {
direction: GradientDirection.Top,
colors: [[$r('app.color.ai_score_gradient_bear_start'), 0.0], [$r('app.color.ai_score_gradient_bear_end'), 1.0]]
}
}
@Builder
buildContractInfo() {
Row() {
Text(this.cardData ? this.cardData.name : '--')
.fontColor($r('app.color.elements_text_primary_02'))
.fontSize(16)
.textAlign(TextAlign.Center)
.height(20)
.fontWeight(500)
Blank().width(10)
// 涨跌幅
Text(this.formatPriceChg(this.priceChg))
.fontColor(this.priceChgColor)
.fontSize(14)
.fontWeight(500)
}
.width('100%')
}
@Builder
buildScoreArea() {
Row() {
// 评分圆盘
this.buildScoreDial()
// 因子表格
this.buildFactorTable()
}
.alignItems(VerticalAlign.Center)
.width('100%')
}
@Builder
buildScoreDial() {
Stack({ alignContent: Alignment.Bottom }) {
// 背景图片
Image(this.getScoreBackgroundImage(this.cardData ? this.cardData.score : '0'))
.borderRadius(12)
.objectFit(ImageFit.Contain)
// 文字叠加层
Column() {
Text(this.cardData ? this.cardData.score : '--')
.fontColor(this.getScoreTextColor())
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(this.getScoreText(this.cardData ? this.cardData.score : 0))
.fontColor($r('app.color.elements_text_tertiary'))
.fontSize(14)
}
.width('100%')
.margin({ bottom: 10 })
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
}
.layoutWeight(1)
.margin({ right: 12 })
}
/**
* 获取评分文字颜色
* 根据评分正负返回:红涨绿跌
*/
getScoreTextColor(): ResourceStr {
const score = this.cardData ? parseInt(this.cardData.score) : 0
if (isNaN(score) || score === 0) {
// 0或无效值:深色(黑色)
return $r('app.color.price_even')
}
// 正数红色,负数绿色
return score > 0 ? $r('app.color.price_up_100') : $r('app.color.price_down_100')
}
/**
* 根据评分和主题获取背景图片 URL
*/
getScoreBackgroundImage(score: string): string {
const images = this.isDarkMode ? this.scoreBgImagesDark : this.scoreBgImagesLight
const numScore = parseInt(score)
if (isNaN(numScore)) {
return images['neutral']
}
// 根据评分区间返回对应图片
if (numScore <= -60) {
return images['strong_bear']
} else if (numScore <= -40) {
return images['medium_bear']
} else if (numScore <= -20) {
return images['weak_bear']
} else if (numScore < 0) {
return images['slight_bear']
} else if (numScore === 0) {
return images['neutral']
} else if (numScore < 20) {
return images['slight_bull']
} else if (numScore < 40) {
return images['weak_bull']
} else if (numScore < 60) {
return images['medium_bull']
} else {
return images['strong_bull']
}
}
@Builder
buildFactorTable() {
Column() {
// 表头
Row() {
// 因子列 - 有右边框
Column() {
Text($r('app.string.table_header_factor'))
.fontColor($r('app.color.elements_text_secondary'))
.fontSize(12)
.width('100%')
}
.layoutWeight(1)
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.alignItems(HorizontalAlign.Start)
.border({
width: { right: 1 },
color: $r('app.color.elements_others_divider_primary')
})
// 方向列
Column() {
Text($r('app.string.table_header_direction'))
.fontColor($r('app.color.elements_text_secondary'))
.fontSize(12)
.width('100%')
.textAlign(TextAlign.End)
}
.width(50)
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.alignItems(HorizontalAlign.End)
}
.width('100%')
// 因子列表
ForEach((this.cardData ? this.cardData.factor_list.slice(0, FACTOR_LIST_MAX) : []), (item: FactorItem, index: number) => {
Row() {
// 因子列 - 有右边框
Column() {
Text(item.factor)
.fontColor($r('app.color.elements_text_primary_02'))
.fontSize(12)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
}
.layoutWeight(1)
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.alignItems(HorizontalAlign.Start)
.border({
width: { right: 1 },
color: $r('app.color.elements_others_divider_primary')
})
// 方向列
Column() {
Text(item.judge)
.fontColor(this.getFactorColor(item.judge))
.fontSize(12)
.width('100%')
.textAlign(TextAlign.End)
.textVerticalAlign(TextVerticalAlign.CENTER)
}
.width(50)
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.alignItems(HorizontalAlign.End)
}
.border({
width: { top: 1 },
color: $r('app.color.elements_others_divider_primary')
})
.width('100%')
}, (item: FactorItem) => item.factor)
}
.layoutWeight(1)
.borderRadius(8)
.border({
width: 1,
color: $r('app.color.elements_others_divider_primary'),
radius: 4
})
}
@Builder
buildDiagnosisText() {
Column() {
Text() {
ImageSpan($r('app.media.ai_diagnosis_txt_icon'))
.width(41)
.height(18)
.objectFit(ImageFit.Contain)
.baselineOffset(LengthMetrics.vp(0))
.margin({right: 8})
Span(this.cardData ? this.cardData.message : '')
.fontColor($r('app.color.elements_text_secondary'))
.fontSize(14)
.lineHeight(20)
}
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
}
.padding(8)
.borderRadius(4)
.width('100%')
.backgroundColor($r('app.color.elements_others_bg_layer1'))
.onClick(() => this.handleCardClick())
}
@Builder
buildRecommendContracts() {
Row({ space: 8 }) {
Text($r('app.string.recommend_contracts'))
.fontColor($r('app.color.elements_text_secondary'))
.fontSize(12)
ForEach(this.cardData ? this.cardData.recommend : [], (item: RecommendItem, index: number) => {
Row({ space: 8 }) {
Text(item.name)
.fontColor($r('app.color.elements_text_primary_02'))
.fontSize(11)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.getScoreText(item.score))
.fontColor(this.getScoreColor(item.score))
.fontSize(11)
.fontWeight(FontWeight.Medium)
}
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.backgroundColor($r('app.color.elements_others_bg_layer1'))
.borderRadius(4)
.margin({ right: index < (this.cardData ? this.cardData.recommend.length : 0) - 1 ? 8 : 0 })
.onClick(() => this.handleRecommendClick(item))
}, (item: RecommendItem) => item.code + item.name)
}
.alignItems(VerticalAlign.Center)
.width('100%')
.margin({ top: 12 })
}
}