HarmonyOS7股票组合看板实战:持仓汇总与盈亏计算

HarmonyOS7股票组合看板实战:持仓汇总与盈亏计算 文章目录前言StockItem_65tq 同时保存行情和持仓组合收益是一步一步推出来的搜索只影响列表不影响总资产涨跌颜色来自单只股票不来自组合selectedTab 目前只做高亮完整代码最后总结前言股票组合看板的重点不是把列表做漂亮而是把几个金额关系算对。持仓市值、持仓成本、盈亏金额、盈亏率这几个数字一旦口径不清页面再精致也没用。这个 Demo 的数据量很小但刚好能讲清楚组合看板里最基础的一套计算链路。这页没有买入卖出、没有实时行情订阅也没有真正的 tab 排序。它目前做的是展示持仓按名称或代码搜索并根据持仓数量、当前价和成本价计算组合收益。StockItem_65tq同时保存行情和持仓每只股票对象里既有行情字段也有持仓字段。price、change、changeRate、volume更像行情数据held和cost则是用户自己的持仓信息。interfaceStockItem_65tq{symbol:stringname:stringprice:numberchange:numberchangeRate:numbervolume:stringheld:numbercost:number}这个模型适合 Demo因为所有信息在一个数组里列表和汇总都能直接算。真实产品里通常会把行情和持仓拆开行情来自市场接口会高频变化持仓来自账户系统变化频率低得多。拆开之后前端再按股票代码合并展示。组合收益是一步一步推出来的总市值是当前价乘以持仓数量总成本是成本价乘以持仓数量总盈亏是两者相减盈亏率再除以总成本。代码把这几个值写成 getter而不是保存成状态。gettotalValue():number{returnthis.stocks.reduce((sum,s)sums.price*s.held,0)}gettotalCost():number{returnthis.stocks.reduce((sum,s)sums.cost*s.held,0)}gettotalProfit():number{returnthis.totalValue-this.totalCost}getprofitRate():number{returnthis.totalCost0?(this.totalProfit/this.totalCost)*100:0}这个顺序很重要。totalProfit依赖totalValue和totalCostprofitRate又依赖totalProfit。只要stocks变化所有汇总数字都会重新计算不需要手动同步。profitRate里判断this.totalCost 0也值得保留。虽然示例数据都有成本但真实数据里可能出现空持仓或成本为 0 的情况直接除会得到无意义结果。搜索只影响列表不影响总资产搜索框绑定searchText列表渲染读取filteredStocks。如果没有搜索词返回全部股票有搜索词时同时匹配股票代码和中文名称。getfilteredStocks():StockItem_65tq[]{if(!this.searchText)returnthis.stocksreturnthis.stocks.filter(ss.symbol.toLowerCase().includes(this.searchText.toLowerCase())||s.name.includes(this.searchText))}这里有一个产品口径要注意搜索只过滤列表不影响顶部总市值和总盈亏。也就是说用户搜索 TSLA 时顶部仍然显示整个组合的资产不是搜索结果的资产。这个选择没有绝对对错但要保持一致。如果想让搜索后的汇总跟着变就应该把汇总也改成基于filteredStocks计算。涨跌颜色来自单只股票不来自组合列表里单只股票的涨跌幅颜色由changeRate决定顶部组合盈亏颜色由totalProfit决定。这两个指标不能混在一起一只股票当天上涨不代表这只持仓一定盈利因为用户成本价可能更高。Text(${stock.changeRate0?:}${stock.changeRate.toFixed(2)}%).fontColor(#FFF).backgroundColor(stock.changeRate0?#2ED573:#FF4757)当前列表还展示了“成本 $xxx”但没有单独展示每只股票的持仓盈亏。如果要让看板更有投资分析价值我会补一个单股盈亏(stock.price - stock.cost) * stock.held再显示盈亏率(stock.price - stock.cost) / stock.cost。selectedTab目前只做高亮页面上有“持仓、涨幅、跌幅”三个 tab点击后会改变selectedTab但filteredStocks并没有读取这个状态。因此当前 tab 只改变视觉选中态不会真的排序或过滤列表。.onClick(()this.selectedTabidx)如果要补完整最直接的做法是在filteredStocks之后再根据selectedTab排序持仓页按市值排序涨幅页按changeRate从高到低跌幅页从低到高。这样 tab 才真的参与数据展示而不是只停留在 UI 状态。这也是这篇代码最适合继续练习的地方先把计算口径固定再把搜索、排序和汇总之间的关系讲清楚。资产类页面看似都是数字真正麻烦的是每个数字到底代表什么范围。完整代码下面保留案例完整代码方便你直接对照学习、复制和重构。import{router}fromkit.ArkUIinterfaceStockItem_65tq{symbol:stringname:stringprice:numberchange:numberchangeRate:numbervolume:stringheld:numbercost:number}EntryComponentstruct StockPortfolioDashboard{Statestocks:StockItem_65tq[][{symbol:AAPL,name:苹果,price:189.5,change:2.3,changeRate:1.23,volume:52.3M,held:10,cost:175.0},{symbol:TSLA,name:特斯拉,price:248.7,change:-5.1,changeRate:-2.01,volume:38.1M,held:5,cost:260.0},{symbol:MSFT,name:微软,price:415.2,change:3.8,changeRate:0.92,volume:21.7M,held:8,cost:390.0},{symbol:GOOGL,name:谷歌,price:172.4,change:-1.2,changeRate:-0.69,volume:18.5M,held:12,cost:168.0},{symbol:AMZN,name:亚马逊,price:198.3,change:4.5,changeRate:2.32,volume:29.8M,held:6,cost:185.0},]StateselectedTab:number0StatesearchText:stringgettotalValue():number{returnthis.stocks.reduce((sum,s)sums.price*s.held,0)}gettotalCost():number{returnthis.stocks.reduce((sum,s)sums.cost*s.held,0)}gettotalProfit():number{returnthis.totalValue-this.totalCost}getprofitRate():number{returnthis.totalCost0?(this.totalProfit/this.totalCost)*100:0}getfilteredStocks():StockItem_65tq[]{if(!this.searchText)returnthis.stocksreturnthis.stocks.filter(ss.symbol.toLowerCase().includes(this.searchText.toLowerCase())||s.name.includes(this.searchText))}build(){Column(){// HeaderRow(){Image($r(app.media.startIcon)).width(24).height(24).onClick(()router.back())Text(股票组合).fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)Text(⚙).fontSize(20).fontColor(#555)}.width(100%).padding({left:16,right:16,top:12,bottom:12})// Portfolio summaryColumn(){Text(总市值).fontSize(13).fontColor(#FFFFFFAA).margin({bottom:4})Text(\$\${this.totalValue.toFixed(2)}\).fontSize(28).fontWeight(FontWeight.Bold).fontColor(#FFF)Row(){Text(\\${this.totalProfit0?:}$\${this.totalProfit.toFixed(2)}\).fontSize(14).fontColor(this.totalProfit0?#2ED573:#FF4757)Text(\(\${this.profitRate0?:}\${this.profitRate.toFixed(2)}%)\).fontSize(14).fontColor(this.totalProfit0?#2ED573:#FF4757)}.margin({top:4})Text(累计持仓盈亏).fontSize(11).fontColor(#FFFFFFAA).margin({top:4})}.width(calc(100% - 32vp)).backgroundColor(#007AFF).borderRadius(16).padding(20).margin({bottom:12}).linearGradient({colors:[[#007AFF,0.0],[#0056CC,1.0]],angle:135})// SearchTextInput({placeholder:搜索股票名称或代码,text:this.searchText}).onChange(vthis.searchTextv).width(calc(100% - 32vp)).backgroundColor(#FFF).borderRadius(20).margin({bottom:12})// TabsRow(){ForEach([持仓,涨幅,跌幅],(tab:string,idx:number){Text(tab).fontSize(14).fontColor(this.selectedTabidx?#007AFF:#888).borderWidth({bottom:this.selectedTabidx?2:0}).borderColor(#007AFF).padding({bottom:8,left:16,right:16}).onClick(()this.selectedTabidx)})}.width(100%).padding({left:16}).margin({bottom:8})// Stock listList({space:8}){ForEach(this.filteredStocks,(stock:StockItem_65tq){ListItem(){Row(){Column(){Text(stock.symbol).fontSize(16).fontWeight(FontWeight.Bold).fontColor(#333)Text(stock.name).fontSize(12).fontColor(#888)Text(\持仓 \${stock.held}股\).fontSize(11).fontColor(#AAA).margin({top:2})}.alignItems(HorizontalAlign.Start).layoutWeight(1)Column(){Text(\$\${stock.price.toFixed(2)}\).fontSize(15).fontWeight(FontWeight.Bold).fontColor(#333)Text(\\${stock.change0?:}\${stock.change.toFixed(2)}\).fontSize(12).fontColor(stock.change0?#2ED573:#FF4757)}.alignItems(HorizontalAlign.End).margin({right:8})Column(){Text(\\${stock.changeRate0?:}\${stock.changeRate.toFixed(2)}%\).fontSize(13).fontColor(#FFF).backgroundColor(stock.changeRate0?#2ED573:#FF4757).borderRadius(6).padding({left:8,right:8,top:4,bottom:4})Text(\成本 $\${stock.cost.toFixed(0)}\).fontSize(11).fontColor(#AAA).margin({top:4})}.alignItems(HorizontalAlign.End)}.width(100%).backgroundColor(#FFF).borderRadius(12).padding(14).shadow({radius:4,color:#0000001A,offsetY:2})}})}.layoutWeight(1).padding({left:16,right:16})}.width(100%).height(100%).backgroundColor(#F5F5F5)}}最后总结股票组合看板 这个案例并不靠复杂技巧取胜它更像一个结构扎实、逻辑完整的业务页面样板。把这篇文章里的状态设计、函数组织和页面分层吃透之后你再去写同类型功能速度会明显快很多。