【山竹记账前端-react】18.自制日期选择器
大纲链接 §
[toc]
1. 搞定输入面板的样式 ⇧
数字面板的样式 ⇧
vscode多行文字内容包裹html标签技巧
|
|
- 选中需要包裹标签的所有行
- 按快捷键
ctrl+shift+p搜索框输入Emmet W - 选择
Emmet Wrap With Abbreviation - 输入
button的话只能添加一个标签包裹所有内容;输入button*可以每行都包裹标签,按回车确认 webstorm中类似快捷键为ctrl+alt+t,但必须多行选中
根据数字键盘UI,先创建数字按键列表文件
@src/modules/items/useNumpadButtons.tsx
|
|
extraClass可用于当前按钮额外unocss样式拼接- 注意使用
cn函数拼接类名,必须在.tsx文件中使用,在.ts的使用话无法识别 - 按钮操作功能未实现,
useNumpadAmountStore待实现按钮输入,金额显示逻辑 iconName为自定义图标组件名称属性<SvgIcon name={iconName} />待实现更具体的图标名称类型提示- 可使用图标/文字作为按钮内容
实现基本样式结构
@/modules/items/input/Numpad.tsx
|
|
- 注意
React.JSX,正确使用属性名称,否则会报错'grid-area'”不在类型“Properties<string | number, string & {}>- 使用
className,而不是class - 使用
gridTemplateAreas,而不是grid-template-areas - 使用
gridArea,而不是grid-area
- 使用
- 使用网格布局
grid grid-cols-[repeat(4,1fr)] grid-rows-[repeat(4,56px)]- 使用具体的高度数值撑开按钮高度
grid-rows-[repeat(4,56px)];宽度平均分配即可 - 在容器中使用
gridTemplateAreas指定具名按钮位置 - 在每一个网格,即按钮上,使用
gridArea指定按钮名称; - 遍历
numpadButtons指定gridArea为btn_${buttonItem.id} - 和
gridTemplateAreas的名称位置一一对应 - 比起一个个按钮元素上指定
row-start-* col-start-* row-end-* col-end-*更直观 - 缺点是无法使用
CSS Grid Generator Tool直接获取
- 使用具体的高度数值撑开按钮高度
- 键盘整体颜色设置为
#ddd,并且设置间隔gap-1px,各个按钮背景色为白色,从而透出类似边框的样式,这样不用考虑边框重合问题 - 使用按钮数据
const { numpadButtons } = useNumpadButtons() - 可使用图标/文字作为按钮内容
{buttonItem.hasIcon ? <SvgIcon name={buttonItem.iconName} /> : buttonItem.text}
日期输入框和金额显示框的样式 ⇧
@/modules/items/input/InputPad.tsx
|
|
@/modules/items/input/DateInput.tsx
|
|
@/modules/items/input/AmountNumberOutput.tsx
|
|
2. 创建 Popup 组件 ⇧
- 有两种形式可以控制组件的显示/隐藏
{ visible ? <PopUp /> : null }在组件外部控制,但不方便设置组件自身的动画<PopUp visible={visible} />将visible作为属性参数传入组件,方便控制组件自身动画
@/components/ui/PopUp.tsx
|
|
- 可复用
OverlayMask组件 useSpring动画配置从下往上显示transform: visible ? 'translateY(0%)' : 'translateY(100%)',- 调整
z-index层级z-[var(--z-popup)]
@/modules/items/input/DateInput.tsx
|
|
3. 封装 usePopup 钩子 ⇧
- 目前使用
PopUp时必须现在父组件声明变量visible,和点击隐藏的函数 - 可以封装一个钩子简化使用
初步封装 ⇧
@/hooks/usePopup.tsx
|
|
@/modules/items/input/DateInput.tsx
|
|
使用传送门 createPortal ⇧
@/hooks/usePopup.tsx
|
|
@/modules/items/input/DateInput.tsx
|
|
portalPopup元素被传送到 body 中,在root元素后portalPopup元素的绝对定位位置不会被任何元素的transform阻挡而导致定位以transform的元素 为父元素相对定位,而不是以根元素相对定位- 也可以将
portalPopup传送到root元素中- 需要在
main.ts中导出export const rootElement = document.getElementById('root') as HTMLElement - 在组件中引入
const portalPopup = createPortal(popup, rootElement)
- 需要在
const portalPopup = createPortal(popup, rootElement)自定义钩子逻辑中存在bug:避免每次调用钩子都会产生一个新的portalPopup
- 处理方式-在静态页面中,在
div#root的平级下方,手动添加一个专门的挂载容器div#portal-root - 在
Hook中直接通过原生DOM API获取
@/hooks/usePopup.tsx
|
|
参考
PopUp 接受子元素 ⇧
@/hooks/usePopup.tsx
|
|
usePopup接受传参children
@/components/ui/PopUp.tsx
|
|
@/modules/items/input/DateInput.tsx
|
|
4. 自制日期选择器(上):实现年份供用户选择 ⇧
@/components/form/DatePickerPopup.tsx
|
|
- 样式
- 外部容器高度写死
date-picker-wrapper,否则内部绝对定位元素会塌陷 - 高亮选择框
highlight-select-box绝对定位到容器一半的高度再向上自身一半高度,居中位置 - 选择框定位器
picker-positioner使其定位初始位置和高亮选择框一致 - 年份选择框
year-picker可移动样式style={{ transform:translateY(${translateY}px)}} - 选择框内部元素统一给高度、行高
h-[36px] leading-[36px]样式居中
- 外部容器高度写死
- 逻辑
- 开始滑动时
- 设置滑动状态
setIsTouching(true) - 记录滑动初始 Y 位置
setLastY(e.touches[0].clientY) - 滑动中
- 计算 Y 位置滑动距离
- 更新设置移动样式
setTranslateY(translateY + deltaY),年份选择框移动样式更新style={{ transform:translateY(${translateY}px)}} - 记录结束滑动 Y 位置
setLastY(currentY) - 结束滑动
setIsTouching(false)
目前选择年份的位置还不能精确确定,总是有偏移,下一章节处理
- 需要将偏移的 Y 改为 36px 的倍数即可
5. 自制日期选择器(中) ⇧
移动距离会在用户松开手指时变成36的整数倍 ⇧
- 将偏移的 Y 改为 36px 的倍数,找最接近的倍数值,取余数,和半值比较
- 小于半值的,减掉
- 大于半值的,加上不足的
- 还需要分情况:余数是否大于0
- 不可在
touchMove的时候设置整数值,需要在结束松开手指时设置,否则会滑不动
@/components/form/DatePickerPopup.tsx
|
|
重构余数逻辑逻辑,将移动距离吸附为36px整数倍的完整实现 ⇧
|
|
6. 插曲:封装一个时间库 ⇧
动态生成日期 ⇧
@/lib/helpers/DateTime.ts
|
|
createDateTime([2026, 12, 8].join('-'))- 支持获取年月日
(new DateTime()).year - 支持设置年月日
(new DateTime()).year = {year: 2026} - 格式化输出目前只支持
YYYY-MM-DDTHH:mm:ss.sssZ - 获取日期
t.add(-10, 'year').year
参考
7. 自制日期选择器(下) ⇧
动态渲染年份列 ⇧
@/components/form/DatePickerPopup.tsx
|
|
- 通过
dateItemHeight外部参数指定每项年份的高度- 取余值的运算都给为参数
const remainder = translateY % dateItemHeight
- 取余值的运算都给为参数
- 在组件根节点添加
style属性声明CSS变量--date-item-height和--half-date-item-height供组件内样式使用h-[var(--date-item-height)]- 定位半高值
top-[calc(50%-var(--half-date-item-height))] - 行高值
leading-[var(--date-item-height)]
- 根据
start/end动态生成年份列表,默认范围前后3年const startTime = start ? createDateTime(start) : createDateTime().add(-3, 'year')const endTime = end ? createDateTime(end) : createDateTime().add(3, 'year')const yearList = Array.from({ length: endTime.year - startTime.year + 1 }).map( (_, i) => startTime.year + i )
- 根据传入的 value 计算初始索引,使选择器定位到当前值
const valueTime = value ? createDateTime(value) : createDateTime()const currentYearIndex = yearList.indexOf(valueTime.year)- 设置位移初始值
const [translateY, setTranslateY] = useState(currentYearIndex * -dateItemHeight)
参考
-
1 2 3 4 5 6 7 8 9 10 11 12// 全局扩展 // global.d.ts import 'react'; declare module 'react' { interface CSSProperties { // 允许任意以 -- 开头的 CSS 变量 [key: `--${string}`]: string | number | undefined; } } // 配置完成后,在任何 .tsx 文件中都可以直接编写,无需类型断言 // <div style={{ '--bg-color': '#f3f4f6', '--columns': 3 }}>内容</div> 数字单位行为:与普通的
React内联样式(如{ width: 100 }会自动转为100px)不同,CSS变量传入纯数字(如{ '--gap': 10 })不会自动追加px如果该变量用于长度或尺寸,请务必显式传入带单位的字符串(如 ‘10px’)
动态渲染年份列 ⇧
``
|
|
8. 手机真机测试 ⇧
``
|
|
9. 一列变三列 ⇧
``
|
|
10. useRef 配合 forceUpdate ⇧
``
|
|
11. value变化时更新y ⇧
``
|
|
12. 确定和取消按钮 ⇧
``
|
|
13. 修复数据共享引起的bug ⇧
``
|
|
参考文章
- 初始代码:https://github.com/FrankFang/rB4yHD
- 提交历史(共14次提交):https://github.com/FrankFang/l3xHutO/commits/master
- 最终代码:https://github.com/FrankFang/l3xHutO
相关文章
- 无
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名