Skip to content
霞露小伙 — HfWang
On this page

k 线页新增记账功能复盘

背景

原本在用户持仓页已经有比较完善的记账功能了,但是产品希望用户在 k 线页的时候就能够快速的记账,不用推出 k 线页在切到记账页面去

难点:

  1. UI 和原本记账页完全不同,其他逻辑一致
  2. 原本记账页是可以确定是那个账户记账的,k 线页不行,因为进入 k 线页的途径很多,不一定是从持仓页进入,同时一个用户可能有多个账户,包括自动账户、手工账户、模拟账户、两融账户等
  3. 时区判断,原本的记账功能由于可以选具体的时间,所以对于盘前盘后没有比较细的判断,但是 k 线页的记账只能记当天的流水,所以需要判断是否处于盘后,是否是交易日,同时还需要判断用户设备设置的时区,避免因时区设置不同导致记录了非交易日的流水
  4. 虚拟键盘和输入框联动,包括切换输入框键盘数据联动,手动修改光标键盘输入联动

实现效果图如下:

效果图

具体实现

买入卖出主题色切换

如上图,买入和卖出的主题色不一致,实现方式是采用 css 变量 + 动态类名

tsx
interface RecordTypeProps {
  recordType: 'buy' | 'sale'
}
// 点击买入、卖出按钮,传入的 recordType 不一样
export default function RecordModal(props: RecordTypeProps) {
  return <div className={
    `recod_modal_theme_${props.recordType === 'buy' ? 'buy' : 'sale'}`
    }>
    ...
  </div>
}
css
.record_modal_theme_buy {
  --modal-theme: red;
}
.record_modal_theme_sale {
  --modal-theme: blue;
}

后续需要设置主题色的时候只需要使用这个 css 变量就行

打开记账弹窗,默认选中账户

业务逻辑:

  • 用户从持仓页进入 (from_page 可以判断)
    • 手工账户 / 模拟账户:自动选中
    • 自动账户 / 两融账户: 不支持手工记账
  • 用户从非持仓页进入
    • 用户存在至少一个手工账户/模拟账户,默认选中服务端下发的第一个账户
    • 用户不存在手工账户/模拟账户,提示用户创建账户
  • 用户在记账弹窗上创建新账户
    • 用户退出 k 线页之前,或者用户手动切换账户之前,无论关闭打开多少次弹窗,都默认选中新创建的账户

判断是否是持仓页过来的这个比较简单,从持仓页进入 k 线页的时候 url 拼接上 账户 id + from_page 不就行了吗但问题是,我们这是金融软件,url 上是不允许带上用户敏感信息的,例如账户 id 这种就是比较敏感的,被证监会发现的话估计得被约谈了吧

后面是通过两外两个字段结合来判断当前账户得

然后就是弹窗打开关闭缓存上一次弹窗所选则账户的,两个方案:

  • 父级页面(k 线页)缓存
  • localStoarge 缓存

最终选了方案二,原因是这样代码比较内聚,后续如果其他页面也要支持快速记账话,就不需要修改页面逻辑,只需要增加记账弹窗开启关闭得逻辑就行

费用计算

停牌:手工账户支持,且停牌日费用为 0,模拟账户不支持,保存时报错没有价格退市:点击保存报错费用计算:(接口获取账户的佣金利率和是否免五)

  • 佣金(不区分买入卖出) = 成交金额 * 佣金利率
    • 佣金 < 5, 是否【免 5】,否则取 5
    • 佣金 > 5, 无特殊逻辑买入
  • 买入
    • 过户费:
      • 沪市:0.01 / 1000 * 成交金额
    • 印花税:
      • 北交所:0.0005 * 成交金额
  • 卖出:
    • 过户费:
      • 沪市:0.01 / 1000 * 成交金额
    • 印花税:
      • 沪市、深市、北交所
        • 0.0005 * 成交金额
  • 总费用 = 佣金 + 过户费 + 印花税

买入转换+气泡提示

效果图

如图所示,点价格、金融、数量输入框发生变化之后,右侧得输入框会有相应的提示内容,3 秒后自动消失

由于项目没有采用任何第三方组件库,所以这个气泡是自己实现的

而能说的点就是这么封装出现消失的逻辑,我是封装了一个在自定义 hooks 去实现的:

javascript
import { useBoolean } from 'ahooks'
import { useEffect } from 'react'

export default function useShowPopover({ text, delay = 3000, deps = [] }) {
  const [show, actions] = useBoolean()

  useEffect(() => {
    let timer = null
    const _show = deps.filter((item) => !!item)
    if (text && _show.length === deps.length) {
      actions.setTrue()
      timer = setTimeout(() => {
        actions.setFalse()
      }, delay)
    }
    return () => {
      if (timer) {
        actions.setFalse()
        clearTimeout(timer)
      }
    }
  }, [text, ...deps])
  return { show }
}

调用的时候根据买入卖出的气泡变换需要依赖那些变量去设置 deps 即可

虚拟键盘

TIP

Q:为什么使用虚拟键盘?

A:产品希望用户点击涨跌停或者左右加减按钮的时候,键盘不收起,所以使用系统键盘的话,会出现点击其他按钮键盘收起,就算点击的时候给输入框 foucs,也会出现键盘收起又快速弹出的效果,加上其他项目里有其他同事已经用过虚拟键盘了,所以决定使用 react-simple-keyboard 来实现

开发难点:

  • 多个输入框切换时虚拟键盘的正则匹配和数值同步
  • 手动点击输入框切换光标位置,虚拟键盘同步光标位置
  1. 首先就是输入框的正则匹配逻辑:
  • 价格(买入卖出时每一个的价格)输入框
    • 最大值: 9999.9999、最小值:0
    • 点击加减号,A 股每次变化 0.01,港股、etf 每次变化 0.001
    • 正则
      • 港股:/^[0-9]{1,4}(.[0-9]{0,4})?$/
      • A 股:/^[0-9]{1,4}(.[0-9]{0,3})?$/
  • 数量(买入卖出的股数)输入框
    • 最大值: 999999999.999、最小值: 0
    • 点击加减号,每次变化 100 股
    • 正则:/^[0-9]{1,9}(.[0-9]{0,4})?$/
  • 金额(买入的总金额)输入框
    • 最大值:9999999999999,9999、最小值:0
    • 点击加减号,每次变化 1000 元 / 1000 港元
    • 正则:
      • 港股:/^[0-9]{1,13}(.[0-9]{0,4})?$/
      • A 股:/^[0-9]{1,13}(.[0-9]{0,3})?$/
  1. 光标同步
  • 一开始聚焦输入框 A,虚拟键盘唤起,这时切换到输入框 B,且点击位置不在输入框最后面
  • 输入框 A 聚焦,手动点击输入框改变光标位置

针对上面两种情况,我是这么处理的:

  • 输入框聚焦,自动把光标设置到输入框字符串最后面
  • 手动改变当前输入框光标位置,再输入的第一个字符之后,把输入框和虚拟键盘的光标都设置到字符串后面

时区转换

问题描述:这个问题时用户反馈才知道的,这个用户人是去了美国,让后发现我们的实时行情显示异常,再和服务端的同学一块排查后发现,由于用户所处的时区不同,导致 Date 对象解析出来的 YYYY-MM-DD 比东八区的刚好少了一天,即实时行情的请求参数变成了请求昨天的实时行情数据。再排查这个页面的时候,发现记账功能也存在一样的问题,因为 k 线页面的记账功能是只能记录今天的流水,但是用户再更改手机上的时区的时候就可以绕过我们这个限制

修复方式:

使用 dayjs 对需要用到时间判断的接口、场景进行转换

javascript
// 这里维护一个时区 map,方便未来接入美股
export const TIME_ZONE_MAP = {
  BEI_JING: 'Asiz/Shanghai',
}
typescript
import dayjs from 'dayjs'
import utc from 'dayjs/plugins/utc'
import timezone from 'dayjs/plugins/timezone'
import { TIME_ZONE_MAP } from '../constant'

dayjs.extend(utc)
dayjs.extend(timezone)

type SetH5TimeZone = (
  formatStr: string, 
  timestamp: number, 
  timezone?: string
) => string

export default function setH5TimeZone: SetH5TimeZone(
  formatStr = 'YYYYMMDD',
  timeStamp = Date.now,
  timezone = TIME_ZONE_MAP.BEI_JING
) {
  return dayjs(timestamp).tz(timezone).format(formatStr)
}

对于 k 线页的时间判断主要是盘前的判断

  • 港股:9:15
  • A 股:9:25
  • 实时行情显示 -- 的时间段: 8:30-9:30
typescript
// 判断当前时间是否在某个时间段内
// 83001: 08:30:00  早上八点半;  92900:  09:30:00  早上九点半
export const isInAllowTimeSpan = (start = 83001, end = 93000) => {
  const now = +setH5TimeZone('HHmmss')
  if (now > start && now < end) {
    return false
  } else {
    return true
  }
}
// 判断今天是否已经开盘
export const isAfterOpen = (isHk = false) => {
  const now = +setH5TimeZone('HHmmss')
  if (isHk) {
    return now >= 91500
  } else {
    return now >= 92500
  }
}

本站中引用到的其他资料,如有侵权,请联系本人删除