k 线页新增记账功能复盘
背景
原本在用户持仓页已经有比较完善的记账功能了,但是产品希望用户在 k 线页的时候就能够快速的记账,不用推出 k 线页在切到记账页面去
难点:
- UI 和原本记账页完全不同,其他逻辑一致
- 原本记账页是可以确定是那个账户记账的,k 线页不行,因为进入 k 线页的途径很多,不一定是从持仓页进入,同时一个用户可能有多个账户,包括自动账户、手工账户、模拟账户、两融账户等
- 时区判断,原本的记账功能由于可以选具体的时间,所以对于盘前盘后没有比较细的判断,但是 k 线页的记账只能记当天的流水,所以需要判断是否处于盘后,是否是交易日,同时还需要判断用户设备设置的时区,避免因时区设置不同导致记录了非交易日的流水
- 虚拟键盘和输入框联动,包括切换输入框键盘数据联动,手动修改光标键盘输入联动
实现效果图如下:
具体实现
买入卖出主题色切换
如上图,买入和卖出的主题色不一致,实现方式是采用 css 变量 + 动态类名
interface RecordTypeProps {
recordType: 'buy' | 'sale'
}
// 点击买入、卖出按钮,传入的 recordType 不一样
export default function RecordModal(props: RecordTypeProps) {
return <div className={
`recod_modal_theme_${props.recordType === 'buy' ? 'buy' : 'sale'}`
}>
...
</div>
}
.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 去实现的:
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 来实现
开发难点:
- 多个输入框切换时虚拟键盘的正则匹配和数值同步
- 手动点击输入框切换光标位置,虚拟键盘同步光标位置
- 首先就是输入框的正则匹配逻辑:
- 价格(买入卖出时每一个的价格)输入框
- 最大值: 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})?$/
- 光标同步
- 一开始聚焦输入框 A,虚拟键盘唤起,这时切换到输入框 B,且点击位置不在输入框最后面
- 输入框 A 聚焦,手动点击输入框改变光标位置
针对上面两种情况,我是这么处理的:
- 输入框聚焦,自动把光标设置到输入框字符串最后面
- 手动改变当前输入框光标位置,再输入的第一个字符之后,把输入框和虚拟键盘的光标都设置到字符串后面
时区转换
问题描述:这个问题时用户反馈才知道的,这个用户人是去了美国,让后发现我们的实时行情显示异常,再和服务端的同学一块排查后发现,由于用户所处的时区不同,导致 Date 对象解析出来的 YYYY-MM-DD 比东八区的刚好少了一天,即实时行情的请求参数变成了请求昨天的实时行情数据。再排查这个页面的时候,发现记账功能也存在一样的问题,因为 k 线页面的记账功能是只能记录今天的流水,但是用户再更改手机上的时区的时候就可以绕过我们这个限制
修复方式:
使用 dayjs 对需要用到时间判断的接口、场景进行转换
// 这里维护一个时区 map,方便未来接入美股
export const TIME_ZONE_MAP = {
BEI_JING: 'Asiz/Shanghai',
}
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
// 判断当前时间是否在某个时间段内
// 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
}
}