├── .dumi └── favicon.png ├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github └── workflows │ └── prod.yml ├── .gitignore ├── .stylelintrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── guide.md └── index.md ├── package.json ├── public └── logo.png ├── src ├── animation-wrap │ ├── animationWrap.tsx │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── type.ts │ └── utils.ts ├── assets │ ├── github.svg │ └── tom.jpeg ├── check-box │ ├── check-box.tsx │ ├── index.md │ ├── index.scss │ ├── index.tsx │ └── type.ts ├── circle │ ├── circle.tsx │ ├── index.md │ ├── index.scss │ ├── index.tsx │ └── type.ts ├── demo │ ├── animation-wrap │ │ ├── demo.tsx │ │ └── index.scss │ ├── check-box │ │ └── demo1.tsx │ ├── circle │ │ ├── demo1.tsx │ │ ├── demo2.tsx │ │ └── index.scss │ ├── floating-ball │ │ ├── demo1.tsx │ │ ├── demo2.tsx │ │ ├── demo3.tsx │ │ └── index.scss │ ├── hooks │ │ ├── useKeepInterval │ │ │ ├── demo1.tsx │ │ │ ├── demo2.tsx │ │ │ └── index.scss │ │ ├── useKeepIntervalMap │ │ │ ├── demo1.tsx │ │ │ └── index.scss │ │ ├── useRTDraw │ │ │ └── demo.tsx │ │ ├── useScrollBottom │ │ │ ├── demo.scss │ │ │ ├── demo1.tsx │ │ │ ├── demo2.tsx │ │ │ └── demo3.tsx │ │ ├── useSearchParamsFilter │ │ │ └── demo1.tsx │ │ ├── useSearchSetState │ │ │ ├── demo1.tsx │ │ │ └── index.scss │ │ ├── useSearchState │ │ │ ├── demo1.tsx │ │ │ ├── demo2.tsx │ │ │ ├── demo3.tsx │ │ │ └── demo4.tsx │ │ ├── useTouch │ │ │ └── demo1.tsx │ │ └── useTouchEvent │ │ │ ├── demo1.tsx │ │ │ └── index.scss │ ├── huarong-road │ │ ├── demo1.tsx │ │ ├── demo2.tsx │ │ ├── demo3.tsx │ │ ├── demo4.tsx │ │ └── index.scss │ ├── mobile-folder │ │ ├── demo1.tsx │ │ └── demo2.tsx │ ├── scroll-circle │ │ ├── demo1.tsx │ │ ├── demo2.tsx │ │ ├── demo3.tsx │ │ ├── demo4.tsx │ │ ├── demo5.tsx │ │ ├── demo6.scss │ │ ├── demo6.tsx │ │ ├── demo7.tsx │ │ ├── index.scss │ │ ├── lotteryDemo.scss │ │ └── lotteryDemo.tsx │ ├── scroll-view │ │ └── demo1.tsx │ ├── skus │ │ ├── demo1.tsx │ │ ├── demo2.tsx │ │ ├── demo3.tsx │ │ ├── index.scss │ │ └── utils.ts │ ├── slider-puzzle │ │ ├── demo1.tsx │ │ ├── demo2.tsx │ │ └── demo3.tsx │ ├── tabs │ │ ├── demo1.tsx │ │ └── demo2.tsx │ └── tree │ │ ├── data.ts │ │ ├── demo1.tsx │ │ ├── demo2.tsx │ │ └── demo3.tsx ├── floating-ball │ ├── floating-ball.tsx │ ├── index.md │ ├── index.scss │ ├── index.tsx │ └── type.ts ├── hooks │ ├── index.ts │ ├── useKeepInterval │ │ ├── doc.tsx │ │ ├── index.md │ │ └── index.ts │ ├── useKeepIntervalMap │ │ ├── doc.tsx │ │ ├── index.md │ │ └── index.ts │ ├── useLatest │ │ └── index.ts │ ├── useMergeProps │ │ └── index.ts │ ├── usePropsState │ │ └── index.ts │ ├── useRTDraw │ │ ├── index.md │ │ ├── index.ts │ │ ├── type.ts │ │ ├── useRTDraw.ts │ │ └── utils.ts │ ├── useRender │ │ └── index.ts │ ├── useScrollBottom │ │ ├── index.md │ │ └── index.ts │ ├── useSearchParamsFilter │ │ ├── index.md │ │ └── index.ts │ ├── useSearchSetState │ │ ├── index.md │ │ ├── index.ts │ │ └── utils.ts │ ├── useSearchState │ │ ├── index.md │ │ └── index.ts │ ├── useTouch │ │ ├── doc.tsx │ │ ├── index.md │ │ └── index.ts │ └── useTouchEvent │ │ ├── doc.tsx │ │ ├── index.md │ │ ├── index.ts │ │ ├── type.ts │ │ └── useTouchEvent.ts ├── huarong-road │ ├── config.ts │ ├── context.ts │ ├── huarong-road-item.tsx │ ├── huarong-road.tsx │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── type.ts │ └── utils.ts ├── index.ts ├── mobile-folder │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── mobile-folder.tsx │ └── type.ts ├── scroll-circle │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── scrollCircle.tsx │ ├── scrollCircleItem.tsx │ ├── type.ts │ └── utils.ts ├── scroll-view │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── scrollView.tsx │ └── type.ts ├── skus │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── skus.tsx │ └── type.ts ├── slider-puzzle │ ├── config.ts │ ├── context.ts │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── slider-puzzle-canvas.tsx │ ├── slider-puzzle-item.tsx │ ├── slider-puzzle.tsx │ ├── type.ts │ └── utils.ts ├── style │ ├── index.scss │ └── var.ts ├── tabs │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── tabs.tsx │ ├── traverse-react-node.tsx │ └── type.ts ├── tree │ ├── index.md │ ├── index.scss │ ├── index.tsx │ ├── tree.tsx │ ├── type.ts │ └── utils.ts └── utils │ ├── attach-properties-to-component.ts │ ├── clipboard.ts │ ├── compute.ts │ ├── format.ts │ ├── handleDom.ts │ ├── index.ts │ ├── native-props.ts │ ├── omit.ts │ ├── random.ts │ ├── replace.ts │ ├── sleep.ts │ └── validate.ts ├── tsconfig.json └── type └── index.ts /.dumi/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApeWhoLovesCode/lhh-ui/faf4c48a0547d1b2cb00566c6c70c7d6090afedd/.dumi/favicon.png -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | apiParser: {}, 5 | resolve: { 6 | // 配置入口文件路径,API 解析将从这里开始 7 | entryFile: './src/index.ts', 8 | atomDirs: [ 9 | {type: 'components', dir: './src'}, 10 | {type: 'hooks', dir: './src/hooks'}, 11 | ] 12 | }, 13 | outputPath: 'docs-dist', 14 | themeConfig: { 15 | name: 'lhh-ui', 16 | logo: '/logo.png', // 读取public文件夹 17 | nav: [ 18 | { title: '文档', link: '/guide' }, 19 | { title: '组件', link: '/components/slider-puzzle' }, 20 | { title: 'Hooks', link: '/hooks/use-keep-interval' }, 21 | ], 22 | socialLinks: { 23 | github: 'https://github.com/ApeWhoLovesCode/lhh-ui' 24 | } 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@umijs/lint/dist/config/eslint'), 3 | }; 4 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md 5 | esm: { 6 | output: 'dist', 7 | // 忽略这些文件的打包 8 | ignores: ['src/demo/**'], 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /.github/workflows/prod.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | # 代码 push main 分支的时候触发 4 | push: 5 | branches: main 6 | jobs: 7 | # 定义一个job,名字为 release 8 | release: 9 | # 使用github提供给我们的机器去跑 10 | runs-on: ubuntu-latest 11 | steps: 12 | # 拉取最新的代码 13 | - name: Checkout repository 14 | uses: actions/checkout@master 15 | # 安装node环境 16 | - name: Use Node.js 17 | uses: actions/setup-node@v3.8.1 18 | with: 19 | node-version: "20.x" 20 | # 为node_modules设置缓存 21 | - name: Cache 22 | # 缓存命中结果会存储在steps.[id].outputs.cache-hit里,该变量在继后的step中可读 23 | id: cache-dependencies 24 | uses: actions/cache@v3 25 | with: 26 | # 缓存文件目录的路径 27 | path: | 28 | **/node_modules 29 | key: ${{runner.OS}} 30 | # 安装依赖 31 | - name: Installing Dependencies 32 | # 如果命中缓存,就不需要安装依赖,使用缓存即可 33 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 34 | run: npm install 35 | # 打包 36 | - name: Build 37 | run: | 38 | npm run docs:build 39 | # 产物上传服务器 40 | - name: Upload to Deploy Server 41 | uses: easingthemes/ssh-deploy@v2.0.7 42 | env: 43 | # 免密登录的秘钥 宝塔/安全/SSH登录私钥 44 | SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_PASS }} 45 | # 服务器的公网IP 46 | REMOTE_HOST: ${{ secrets.SERVER_HOST }} 47 | # 服务器登录用户名 48 | REMOTE_USER: root 49 | # 你打包后产物的文件夹 50 | SOURCE: "docs-dist/" 51 | # 先清空目标目录 52 | ARGS: "-avzr --delete" 53 | # 上传到服务器目标目录 54 | TARGET: "/www/wwwroot/lhh-ui" 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /yarn-error.log 3 | /yarn.lock 4 | /package-lock.json 5 | /dist 6 | /docs-dist 7 | .dumi/tmp 8 | .dumi/tmp-test 9 | .dumi/tmp-production 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@umijs/lint/dist/config/stylelint" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lhh-ui 2 | 3 | [![NPM version](https://img.shields.io/npm/v/lhh-ui.svg?style=flat)](https://npmjs.org/package/lhh-ui) 4 | [![NPM downloads](http://img.shields.io/npm/dm/lhh-ui.svg?style=flat)](https://npmjs.org/package/lhh-ui) 5 | 6 | 主要存放一些个人开发的有意思 `组件` 和 `hooks`。 7 | 8 | ### 资源 9 | 10 | - [在线文档](lhhui.codeape.site) 11 | - [源码](https://github.com/ApeWhoLovesCode/lhh-ui) 12 | 13 | ### 使用技术 14 | 15 | `react`, `ts` 16 | 17 | ## Install 18 | 19 | ```bash 20 | npm i lhh-ui 21 | ``` 22 | 23 | ## Development 24 | 25 | ```bash 26 | # install dependencies 27 | $ yarn install 28 | 29 | # develop library by docs demo 30 | $ yarn start 31 | 32 | # build library source code 33 | $ yarn run build 34 | 35 | # build library source code in watch mode 36 | $ yarn run build:watch 37 | 38 | # build docs 39 | $ yarn run docs:build 40 | 41 | # check your project for potential problems 42 | $ yarn run doctor 43 | ``` 44 | 45 | ## LICENSE 46 | 47 | MIT 48 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | title: 快速上手 4 | order: 1 5 | --- 6 | 7 | # 介绍 8 | 9 | 个人开发的 react 组件库,主要用于存放一些有意思的组件。 10 | 11 | 涉及技术:`react` , `hooks` , `ts` 12 | 13 | ## 快速上手 14 | 15 | ### 安装 16 | 17 | ```bash 18 | npm i lhh-ui 19 | ``` 20 | 21 | ### 项目中使用 22 | 23 | ```js 24 | import { SliderPuzzle } from 'lhh-ui'; 25 | import React from 'react'; 26 | 27 | // 拼图滑块 28 | export default () => { 29 | return ( 30 | 34 | ); 35 | }; 36 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: lhh-ui 4 | description: 个人开发的react组件库(存放一些有意思的组件) 5 | actions: 6 | - text: 开始 7 | link: /components/slider-puzzle 8 | - text: Blog 9 | link: https://codeape.site 10 | # features: 11 | # - title: Hello 12 | # emoji: 💎 13 | # description: Put hello description here 14 | # - title: World 15 | # emoji: 🌈 16 | # description: Put world description here 17 | # - title: '!' 18 | # emoji: 🚀 19 | # description: Put ! description here 20 | --- 21 | 22 | #### 使用 23 | 24 | ```bash 25 | npm i lhh-ui 26 | ``` 27 | 28 | #### 相关技术 29 | 30 | `react` , `hooks` , `ts` 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lhh-ui", 3 | "version": "1.2.10", 4 | "description": "LHH's react Component Library", 5 | "license": "MIT", 6 | "author": "林桓恒 <970519495@qq.com>", 7 | "main": "dist/index.js", 8 | "module": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "build": "father build", 15 | "build:watch": "father dev", 16 | "dev": "cross-env PORT=8811 dumi dev", 17 | "docs:build": "dumi build", 18 | "doctor": "father doctor", 19 | "prepublishOnly": "father doctor && npm run build", 20 | "start": "npm run dev" 21 | }, 22 | "dependencies": { 23 | "ahooks": "^3.7.7", 24 | "classnames": "^2.3.2", 25 | "react-is": "^18.2.0" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.0.0", 29 | "@types/react-dom": "^18.0.0", 30 | "@types/react-is": "^18.2.1", 31 | "@umijs/lint": "^4.0.0", 32 | "@umijs/plugin-sass": "^1.1.1", 33 | "cross-env": "^7.0.3", 34 | "dumi": "^2.4.11", 35 | "eslint": "^8.23.0", 36 | "father": "^4.1.0", 37 | "react": "^18.0.0", 38 | "react-dom": "^18.0.0", 39 | "stylelint": "^14.9.1" 40 | }, 41 | "peerDependencies": { 42 | "react": ">=16.9.0", 43 | "react-dom": ">=16.9.0" 44 | }, 45 | "publishConfig": { 46 | "access": "public" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApeWhoLovesCode/lhh-ui/faf4c48a0547d1b2cb00566c6c70c7d6090afedd/public/logo.png -------------------------------------------------------------------------------- /src/animation-wrap/animationWrap.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useEffect, 4 | forwardRef, 5 | useImperativeHandle, 6 | useRef, 7 | } from "react"; 8 | import { withNativeProps } from "../utils/native-props"; 9 | import { 10 | AnimationWrapInstance, 11 | AnimationWrapName, 12 | AnimationWrapPosition, 13 | AnimationWrapProps, 14 | } from "./type"; 15 | import { classBem, replaceLineToSpace } from "../utils"; 16 | import { getTransformCurtain, handleLinearGradient } from "./utils"; 17 | import { useMergeProps, usePropsState } from "../hooks"; 18 | 19 | const classPrefix = `lhhui-animation-wrap`; 20 | 21 | const defaultProps = { 22 | name: "curtain" as AnimationWrapName, 23 | position: "right" as AnimationWrapPosition, 24 | duration: 4000, 25 | background: "#fff", 26 | isInitTrigger: true, 27 | delayTime: 100, 28 | }; 29 | type RequireType = keyof typeof defaultProps; 30 | 31 | const AnimationWrap = forwardRef( 32 | (comProps, ref) => { 33 | const props = useMergeProps( 34 | comProps, 35 | defaultProps 36 | ); 37 | const { 38 | name, 39 | position, 40 | duration: p_duration, 41 | background, 42 | isInitTrigger, 43 | delayTime, 44 | children, 45 | ...ret 46 | } = props; 47 | const [distance, setDistance] = useState(0); 48 | const [duration, setDuration] = usePropsState(p_duration); 49 | const delayTimer = useRef(); 50 | 51 | const init = () => { 52 | setDistance(100); 53 | }; 54 | 55 | useEffect(() => { 56 | if (isInitTrigger) { 57 | delayTimer.current = setTimeout(() => { 58 | init(); 59 | }, delayTime); 60 | } 61 | return () => { 62 | if (isInitTrigger) { 63 | clearTimeout(delayTimer.current); 64 | delayTimer.current = undefined; 65 | } 66 | }; 67 | }, [isInitTrigger, delayTime]); 68 | 69 | const run = () => { 70 | setDistance(0); 71 | setDuration(0); 72 | setTimeout(() => { 73 | setDistance(100); 74 | setDuration(p_duration); 75 | }, 100); 76 | }; 77 | 78 | useImperativeHandle(ref, () => ({ 79 | run, 80 | })); 81 | 82 | const renderAnimation = () => { 83 | if (name === "curtain") { 84 | return ( 85 |
99 | ); 100 | } 101 | }; 102 | 103 | return withNativeProps( 104 | ret, 105 |
106 | {children} 107 | {renderAnimation()} 108 |
109 | ); 110 | } 111 | ); 112 | 113 | export default AnimationWrap; 114 | -------------------------------------------------------------------------------- /src/animation-wrap/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AnimationWrap 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 非常规组件 9 | order: 1 10 | --- 11 | 12 | ## 动画包裹组件 13 | 14 | 为包裹住的组件,带来一些动画效果。 15 | 16 | ## 演示 17 | 18 | ### 遮帘式效果 curtain 19 | 20 | 21 | 22 | ## API 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/animation-wrap/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: "lhhui-animation-wrap"; 2 | .#{$class-prefix} { 3 | position: relative; 4 | display: inline-block; 5 | overflow: hidden; 6 | width: fit-content; 7 | height: fit-content; 8 | &-curtain { 9 | position: absolute; 10 | width: 200%; 11 | height: 200%; 12 | transition: transform 4s ease-in; 13 | // background: linear-gradient(to right, #ffffff00, #ffffff 50%, #ffffff); 14 | &-right { 15 | top: 0; 16 | left: -100%; 17 | } 18 | &-bottom { 19 | top: -100%; 20 | left: 0; 21 | } 22 | &-left { 23 | top: 0; 24 | right: -100%; 25 | } 26 | &-top { 27 | bottom: -100%; 28 | left: 0; 29 | } 30 | &-right-bottom { 31 | top: -100%; 32 | left: -100%; 33 | } 34 | &-right-top { 35 | bottom: -100%; 36 | left: -100%; 37 | } 38 | &-left-bottom { 39 | top: -100%; 40 | right: -100%; 41 | } 42 | &-left-top { 43 | bottom: -100%; 44 | right: -100%; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/animation-wrap/index.tsx: -------------------------------------------------------------------------------- 1 | import AnimationWrap from "./animationWrap"; 2 | import './index.scss'; 3 | 4 | export { AnimationWrap }; 5 | export default AnimationWrap; 6 | export type { 7 | AnimationWrapProps, 8 | AnimationWrapInstance, 9 | AnimationWrapName, 10 | AnimationWrapPosition 11 | } from './type'; -------------------------------------------------------------------------------- /src/animation-wrap/type.ts: -------------------------------------------------------------------------------- 1 | import { NativeProps } from "lhh-ui"; 2 | 3 | export type AnimationWrapProps = { 4 | /** 5 | * 动画的名称 6 | * @default curtain 7 | * */ 8 | name?: AnimationWrapName 9 | /** 10 | * 动画的方向 11 | * @default right 12 | */ 13 | position?: AnimationWrapPosition 14 | /** 15 | * 动画时间 16 | * @default 4000 (单位: ms) 17 | */ 18 | duration?: number 19 | /** 20 | * 文本的背景色 21 | * @default #ffffff 22 | */ 23 | background?: string 24 | /** 25 | * 初始化时触发动画 26 | * @default true 27 | */ 28 | isInitTrigger?: boolean 29 | /** 30 | * 初始化时开始动画的延迟时间 31 | * @default 100 (单位: ms) 32 | */ 33 | delayTime?: number 34 | } & NativeProps 35 | 36 | export type AnimationWrapName = 'curtain' 37 | export type AnimationWrapPosition = 'top' | 'right' | 'bottom' | 'left' | 'right-bottom' | 'right-top' | 'left-bottom' | 'left-top' 38 | 39 | export type AnimationWrapInstance = { 40 | run: () => void 41 | } -------------------------------------------------------------------------------- /src/animation-wrap/utils.ts: -------------------------------------------------------------------------------- 1 | import { AnimationWrapPosition } from "./type"; 2 | 3 | /** 获取 curtain 的 transform */ 4 | export const getTransformCurtain = ({ 5 | distance, 6 | position, 7 | }: { 8 | distance: number; 9 | position: AnimationWrapPosition; 10 | }) => { 11 | switch (position) { 12 | case "right": 13 | return `translateX(${distance}%)`; 14 | case "bottom": 15 | return `translateY(${distance}%)`; 16 | case "left": 17 | return `translateX(-${distance}%)`; 18 | case "top": 19 | return `translateY(-${distance}%)`; 20 | case "right-bottom": 21 | return `translate(${distance}%, ${distance}%)`; 22 | case "right-top": 23 | return `translate(${distance}%, -${distance}%)`; 24 | case "left-bottom": 25 | return `translate(-${distance}%, ${distance}%)`; 26 | case "left-top": 27 | return `translate(-${distance}%, -${distance}%)`; 28 | default: 29 | return `translateX(${distance}%)`; 30 | } 31 | }; 32 | 33 | /** 将颜色转化为透明色 */ 34 | export const handleLinearGradient = (color: string) => { 35 | if (color.includes("#")) { 36 | if (color.length === 4) { 37 | color = color.substring(1); 38 | // 处理 '#abc' 成 '#aabbcc' 39 | color = "#" + color.replace(/(.)/g, "$1$1"); 40 | } 41 | return color + "00"; 42 | } 43 | if (color.includes("rgb")) { 44 | return color.replace("rgb", "rgba").replace(")", ",0)"); 45 | } 46 | return color; 47 | }; 48 | -------------------------------------------------------------------------------- /src/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/tom.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApeWhoLovesCode/lhh-ui/faf4c48a0547d1b2cb00566c6c70c7d6090afedd/src/assets/tom.jpeg -------------------------------------------------------------------------------- /src/check-box/check-box.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withNativeProps } from '../utils/native-props'; 3 | import { CheckBoxProps } from './type' 4 | import { classBem } from '../utils'; 5 | // import RcCheckbox from 'rc-checkbox'; 6 | 7 | const classPrefix = `lhhui-check-box`; 8 | 9 | const CheckBox = (props: CheckBoxProps) => { 10 | const { checked, defaultChecked, disabled, indeterminate, onChange, children, ...ret } = props 11 | 12 | return withNativeProps( 13 | ret, 14 |
15 | { 21 | onChange?.(!checked) 22 | }} 23 | > 24 | {children} 25 |
26 | ) 27 | } 28 | 29 | export default CheckBox -------------------------------------------------------------------------------- /src/check-box/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CheckBox 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 一般组件 9 | order: 1 10 | --- 11 | 12 | ## 选择框组件 13 | 14 | 选择 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ## API 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/check-box/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-check-box'; 2 | 3 | .#{$class-prefix} { 4 | display: flex; 5 | align-items: center; 6 | &-input { 7 | width: 16px; 8 | height: 16px; 9 | cursor: pointer; 10 | margin: 4px; 11 | &-indeterminate { 12 | position: relative; 13 | &::after { 14 | content: ''; 15 | display: inline-block; 16 | width: 8px; 17 | height: 8px; 18 | position: absolute; 19 | top: 50%; 20 | left: 50%; 21 | transform: translate(-50%, -50%); 22 | background-color: #3772ff; 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/check-box/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import CheckBox from './check-box'; 3 | 4 | export { CheckBox }; 5 | export default CheckBox; 6 | export type { CheckBoxProps } from './type'; 7 | -------------------------------------------------------------------------------- /src/check-box/type.ts: -------------------------------------------------------------------------------- 1 | import { NativeProps } from '../utils/native-props'; 2 | 3 | export type CheckBoxProps = { 4 | /** 5 | * 指定当前是否选中 6 | * @default false 7 | */ 8 | checked?: boolean 9 | /** 10 | * 初始是否选中 11 | * @default false 12 | */ 13 | defaultChecked?: boolean 14 | /** 15 | * 禁用状态 16 | * @default false 17 | */ 18 | disabled?: boolean 19 | /** 20 | * 半选状态,只负责样式的控制 21 | * @default false 22 | */ 23 | indeterminate?: boolean 24 | /** 25 | * 选择的回调 26 | * @default - 27 | */ 28 | onChange?: (checked: boolean) => void 29 | } & NativeProps 30 | -------------------------------------------------------------------------------- /src/circle/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Circle 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 一般组件 9 | order: 1 10 | --- 11 | 12 | ## 环形进度条组件 13 | 14 | 环形进度条组件 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ### 自定义样式 23 | 24 | 25 | 26 | ## API 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/circle/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-circle'; 2 | .#{$class-prefix} { 3 | position: relative; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | 8 | &-text { 9 | position: absolute; 10 | top: 50%; 11 | left: 50%; 12 | color: var(--text-color); 13 | font-size: var(--text-font-size); 14 | background-color: inherit; 15 | transform: translate(-50%, -50%); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/circle/index.tsx: -------------------------------------------------------------------------------- 1 | import Circle from './circle'; 2 | import './index.scss'; 3 | 4 | export type { CirclePropsType } from './type'; 5 | export { Circle }; 6 | export default Circle; 7 | -------------------------------------------------------------------------------- /src/circle/type.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { NativeProps } from '../utils/native-props'; 3 | 4 | export type CirclePropsType = { 5 | /** 其中的文本信息 */ 6 | text?: ReactNode; 7 | /** 线条的端点样式 */ 8 | lineCap?: CanvasLineCap; 9 | /** 10 | * @description 进度值 最小为0 最大为100 11 | * @default 0 12 | */ 13 | value?: number; 14 | /** 15 | * 速度 16 | * @default 100 17 | */ 18 | speed?: number; 19 | /** 20 | * 大小 21 | * @default 60 22 | */ 23 | size?: number; 24 | /** 为圆环中填充颜色 */ 25 | fill?: string; 26 | /** 进度条的底色 */ 27 | layerColor?: string; 28 | /** 颜色 */ 29 | color?: string | Record; 30 | /** 31 | * 线的宽度 32 | * @default 6 33 | */ 34 | strokeWidth?: number; 35 | /** 36 | * 是否是顺时针方向的 37 | * @default true 38 | */ 39 | clockwise?: boolean; 40 | children?: ReactNode; 41 | } & NativeProps<'--text-color' | '--text-font-size'>; 42 | -------------------------------------------------------------------------------- /src/demo/animation-wrap/demo.tsx: -------------------------------------------------------------------------------- 1 | import { AnimationWrap, AnimationWrapInstance, AnimationWrapPosition } from "lhh-ui"; 2 | import React, { useRef } from "react"; 3 | import './index.scss'; 4 | 5 | const positionList: AnimationWrapPosition[] = [ 6 | 'right', 'bottom', 'left', 'top' 7 | ] 8 | const positionList2: AnimationWrapPosition[] = [ 9 | 'right-bottom', 'right-top', 'left-bottom', 'left-top' 10 | ] 11 | 12 | export default () => { 13 | const animationWrapRef = useRef<(AnimationWrapInstance | null)[]>([]) 14 | const animationWrapRef2 = useRef<(AnimationWrapInstance | null)[]>([]) 15 | 16 | return ( 17 |
18 |
19 | {positionList.map((p, i) => ( 20 | animationWrapRef.current[i] = ref} 22 | position={p} 23 | key={p} 24 | background="#333" 25 | > 26 |
27 | 你好 世界
28 | hello world
29 | HEllO WORLD
30 |
31 |
32 | ))} 33 |
34 |
35 | 36 |
37 |
38 | {positionList2.map((p, i) => ( 39 | animationWrapRef2.current[i] = ref} 41 | position={p} 42 | key={p} 43 | > 44 |
45 | 你好 世界
46 | hello world
47 | HEllO WORLD
48 |
49 |
50 | ))} 51 |
52 |
53 | 54 |
55 |
56 | ) 57 | } -------------------------------------------------------------------------------- /src/demo/animation-wrap/index.scss: -------------------------------------------------------------------------------- 1 | .demo-animationwrap { 2 | .wrap { 3 | width: fit-content; 4 | display: grid; 5 | grid-template: 'a a' 'b b'; 6 | gap: 12px; 7 | padding: 10px; 8 | color: #fff; 9 | background-color: #333; 10 | } 11 | .wrap2 { 12 | color: #000; 13 | background-color: #fff; 14 | } 15 | .divider { 16 | margin: 20px 0; 17 | } 18 | .text { 19 | display: inline-block; 20 | } 21 | } -------------------------------------------------------------------------------- /src/demo/check-box/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { CheckBox } from "lhh-ui"; 3 | 4 | export default () => { 5 | const [check, setCheck] = useState(false); 6 | return ( 7 |
8 | 9 | hello 10 | 11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /src/demo/circle/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { Circle } from 'lhh-ui'; 2 | import React, { useState } from 'react'; 3 | 4 | export default () => { 5 | const [value, setValue] = useState(50); 6 | return ( 7 |
8 | 9 |
10 | 18 | 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/demo/circle/demo2.tsx: -------------------------------------------------------------------------------- 1 | import { Circle, classBem, isMobile } from 'lhh-ui'; 2 | import React, { useState } from 'react'; 3 | import './index.scss'; 4 | 5 | export default () => { 6 | const [value, setValue] = useState(50); 7 | return ( 8 |
9 |
10 | 11 | 17 | 18 | 19 |
20 |
21 | 29 | 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/demo/circle/index.scss: -------------------------------------------------------------------------------- 1 | .demo-circle-wrap { 2 | display: grid; 3 | gap: 24px; 4 | grid-template: 'a a a a'; 5 | 6 | &-mobile { 7 | grid-template: 'a a' 'b b'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/demo/floating-ball/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FloatingBall } from 'lhh-ui'; 3 | import './index.scss'; 4 | 5 | export default () => { 6 | return ( 7 | 14 |
自由
15 |
16 | ); 17 | }; -------------------------------------------------------------------------------- /src/demo/floating-ball/demo2.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FloatingBall } from 'lhh-ui'; 3 | import './index.scss'; 4 | 5 | export default () => { 6 | return ( 7 | 15 |
吸边x
16 |
17 | ); 18 | }; -------------------------------------------------------------------------------- /src/demo/floating-ball/demo3.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FloatingBall } from 'lhh-ui'; 3 | import './index.scss'; 4 | 5 | export default () => { 6 | return ( 7 | 15 |
仅y动
16 |
17 | ); 18 | }; -------------------------------------------------------------------------------- /src/demo/floating-ball/index.scss: -------------------------------------------------------------------------------- 1 | .ball { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 50px; 6 | height: 50px; 7 | background: #4285fb; 8 | user-select: none; 9 | border-radius: 12px; 10 | color: #fff; 11 | } 12 | -------------------------------------------------------------------------------- /src/demo/hooks/useKeepInterval/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useKeepInterval } from 'lhh-ui'; 3 | 4 | export default function CountDown() { 5 | const [num, setNum] = useState(0) 6 | const [interval, setInterval] = useState(1000) 7 | const [remain, setRemain] = useState(0) 8 | const [isPause, setIsPause] = useState(false) 9 | const { setKeepInterval, pauseKeepInterval } = useKeepInterval() 10 | 11 | const onClick = () => { 12 | setIsPause(v => !v) 13 | if(isPause) { 14 | setKeepInterval() 15 | } else { 16 | const v = pauseKeepInterval() 17 | setRemain(v ?? 0) 18 | } 19 | } 20 | 21 | useEffect(() => { 22 | setKeepInterval(() => { 23 | setNum(n => ++n) 24 | }, interval) 25 | }, [interval]) 26 | 27 | return ( 28 |
29 |

30 | 每 31 | { 37 | let v = +e.target.value 38 | if(v < 100) v = 100; 39 | if(v > 10000) v = 10000 40 | setInterval(v) 41 | }} 42 | style={{width: 60}} 43 | /> 44 | ms加一 : {num} 45 |

46 |

剩余时间: {remain}ms

47 |
48 | 49 |
50 |
51 | ) 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/demo/hooks/useKeepInterval/demo2.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; 2 | import './index.scss'; 3 | import { useKeepInterval } from 'lhh-ui'; 4 | 5 | const arr = [{id:1},{id:2},{id:3},{id:4}] 6 | export default function CountDown() { 7 | const [isPause, setIsPause] = useState(true) 8 | const refMap = useRef<(CountDownItemInstance | null)[]>([]) 9 | 10 | const allPause = () => { 11 | setIsPause(!isPause) 12 | arr.forEach(item => { 13 | refMap.current[item.id]?.handlePause(isPause) 14 | }) 15 | } 16 | 17 | const allDelete = () => { 18 | setIsPause(false) 19 | arr.forEach(item => { 20 | refMap.current[item.id]?.handleDelete() 21 | }) 22 | } 23 | 24 | return ( 25 |
26 |
倒计时
27 |
28 |
29 |
时间
30 |
剩余时间
31 |
操作
32 |
33 | {arr.map(item => ( 34 | refMap.current[item.id] = f} id={item.id} key={item.id} /> 35 | ))} 36 |
37 |
38 |
汇总操作
39 |
40 | 41 | 42 |
43 |
44 |
45 | ) 46 | } 47 | 48 | type CountDownItemInstance = { 49 | handlePause: (isPause: boolean) => void 50 | handleDelete: () => void 51 | } 52 | interface CountDownItemProps { 53 | id: string | number 54 | } 55 | const CountDownItem = forwardRef((props, ref) => { 56 | const [count, setCount] = useState(0) 57 | const [total, setTotal] = useState(0) 58 | const [isPause, setIsPause] = useState(true) 59 | const [remain, setRemain] = useState(0) 60 | const {setKeepInterval, pauseKeepInterval} = useKeepInterval() 61 | const interval = 1000 62 | // 开始/暂停 63 | const handlePause = (pause: boolean) => { 64 | setIsPause(!pause) 65 | if(pause) { // 开始 66 | setKeepInterval() 67 | } else { // 暂停 68 | const _count = pauseKeepInterval() 69 | setRemain(_count ?? 0) 70 | } 71 | } 72 | // 清除定时器 73 | const handleDelete = () => { 74 | pauseKeepInterval() 75 | setCount(0) 76 | setTotal(0) 77 | setRemain(0) 78 | setIsPause(true) 79 | } 80 | 81 | useEffect(() => { 82 | setKeepInterval(() => { 83 | setCount(c => ++c) 84 | }, interval, {isInit: true}) 85 | }, [setKeepInterval]) 86 | 87 | useEffect(() => { 88 | if(count === 10) { 89 | setTotal(t => ++t) 90 | setCount(0) 91 | } 92 | }, [count]) 93 | useImperativeHandle(ref, () => ({ 94 | handlePause, 95 | handleDelete, 96 | })) 97 | return ( 98 |
99 |
100 | {total}{count} s 101 |
102 |
{remain ? remain + 'ms' : ''}
103 |
104 | 105 | 106 |
107 |
108 | ) 109 | }) 110 | 111 | -------------------------------------------------------------------------------- /src/demo/hooks/useKeepInterval/index.scss: -------------------------------------------------------------------------------- 1 | .demoUseKeepInterval { 2 | .title { 3 | font-size: 20px; 4 | font-weight: bold; 5 | margin-bottom: 12px; 6 | } 7 | .wrap { 8 | display: flex; 9 | .row { 10 | height: 50px; 11 | line-height: 50px; 12 | } 13 | .info-wrap { 14 | font-weight: bold; 15 | width: 6rem; 16 | } 17 | } 18 | .bottom { 19 | display: flex; 20 | border-top: 1px solid #eee; 21 | .text { 22 | width: 6rem; 23 | height: 60px; 24 | font-weight: bold; 25 | line-height: 60px; 26 | } 27 | .right { 28 | flex: 1; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | } 33 | } 34 | } 35 | 36 | .demoUseKeepIntervalItem { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | width: 16rem; 42 | border-left: 1px solid #eee; 43 | .count { 44 | font-size: 20px; 45 | font-weight: bold; 46 | .small { 47 | font-size: 14px; 48 | color: #1890ff; 49 | } 50 | } 51 | .remain { 52 | font-size: 12px; 53 | color: #1890ff; 54 | } 55 | .btn { 56 | margin-right: 8px; 57 | &:last-child { 58 | margin-right: 0; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/demo/hooks/useKeepIntervalMap/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useId, useImperativeHandle, useRef, useState } from 'react'; 2 | import { useKeepIntervalMap, classBem, isMobile, KeepIntervalMap } from 'lhh-ui'; 3 | import './index.scss'; 4 | 5 | const arr = [1,2,3,4] 6 | export default function CountDown() { 7 | const [isPause, setIsPause] = useState(true) 8 | const keepInterval = useKeepIntervalMap() 9 | const refMap = useRef<(ItemInstance | null)[]>([]) 10 | 11 | const allPause = () => { 12 | setIsPause(v => !v) 13 | arr.forEach(item => { 14 | if(isPause) { 15 | refMap.current[item]?.set() 16 | } else { 17 | refMap.current[item]?.pause() 18 | } 19 | }) 20 | } 21 | 22 | return ( 23 |
24 |
25 | {arr.map((item, index) => ( 26 |
27 | refMap.current[item] = f} keepInterval={keepInterval} interval={index + 1} /> 28 |
29 | ))} 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | ) 38 | } 39 | 40 | type ItemInstance = { 41 | set: () => void 42 | pause: () => void 43 | } 44 | const Item = forwardRef< 45 | ItemInstance, {keepInterval: KeepIntervalMap, interval: number} 46 | >(({keepInterval, interval}, ref) => { 47 | const id = useId() 48 | const [num, setNum] = useState(0) 49 | const [remain, setRemain] = useState(0) 50 | const [isPause, setIsPause] = useState(true) 51 | 52 | const set = () => { 53 | setIsPause(false) 54 | keepInterval.set(id, () => { 55 | setNum(v => ++v) 56 | }, interval * 1000) 57 | } 58 | 59 | const pause = () => { 60 | setIsPause(true) 61 | const v = keepInterval.pause(id) 62 | setRemain(v ?? 0) 63 | } 64 | 65 | useImperativeHandle(ref, () => ({ 66 | set, 67 | pause, 68 | })) 69 | 70 | return ( 71 |

72 | 每{interval}s加一 73 |
总数:{num}
74 |
剩余:{remain}ms
75 | 82 |

83 | ) 84 | }) 85 | 86 | -------------------------------------------------------------------------------- /src/demo/hooks/useKeepIntervalMap/index.scss: -------------------------------------------------------------------------------- 1 | .demoUseKeepIntervalMap { 2 | .content { 3 | display: grid; 4 | grid-template: 'a a a a'; 5 | gap: 20px; 6 | &-mobile { 7 | grid-template: 'a a' 'b b'; 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/demo/hooks/useRTDraw/demo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react" 2 | import { useRTDraw } from "lhh-ui"; 3 | 4 | export default () => { 5 | const [addTime, setAddTime] = useState(0); 6 | 7 | function drawTime() { 8 | const ctx = drawState.ctx!; 9 | const ratio = drawState.ratio; 10 | ctx.font = `${12 * ratio}px 宋体` 11 | const now = Date.now() 12 | ctx.fillText(now + "", 30 * ratio, 30 * ratio); 13 | ctx.fillText(`+${addTime}`, 30 * ratio, 50 * ratio); 14 | ctx.fillText(`=${now + addTime}`, 30 * ratio, 70 * ratio); 15 | } 16 | 17 | const {drawState, canvasRef, canvasInfo, startAnimation} = useRTDraw({ 18 | canvasInfo: {w: 200, h: 200}, 19 | onDraw() { 20 | drawState.ctx.clearRect(0, 0, canvasInfo.w * drawState.ratio, canvasInfo.h * drawState.ratio); 21 | drawTime() 22 | }, 23 | }) 24 | 25 | useEffect(() => { 26 | startAnimation() 27 | }, []) 28 | 29 | return ( 30 |
31 | 41 |
42 | 43 | 44 | 45 |
46 |
47 | ) 48 | } -------------------------------------------------------------------------------- /src/demo/hooks/useScrollBottom/demo.scss: -------------------------------------------------------------------------------- 1 | .use-scroll-bottom-list { 2 | border: 1px solid #ccc; 3 | padding: 10px; 4 | max-height: 500px; 5 | overflow: scroll; 6 | } 7 | 8 | .use-scroll-bottom-list-item { 9 | height: 50px; 10 | font-size: 18px; 11 | font-weight: bold; 12 | padding: 6px 12px; 13 | background-color: skyblue; 14 | margin-bottom: 10px; 15 | } 16 | -------------------------------------------------------------------------------- /src/demo/hooks/useScrollBottom/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useScrollBottom } from "lhh-ui"; 2 | import React from "react"; 3 | import "./demo.scss"; 4 | 5 | export default () => { 6 | const [list, setList] = React.useState( 7 | Array.from({ length: 10 }, (_, i) => i + 1) 8 | ); 9 | const listRef = React.useRef(null); 10 | 11 | useScrollBottom({ 12 | ref: listRef, 13 | bottom: 30, 14 | onScrollToLower() { 15 | console.log("----- 触底啦 -----"); 16 | setList((arr) => 17 | arr.concat(Array.from({ length: 10 }, (_, i) => arr.length + i + 1)) 18 | ); 19 | }, 20 | }); 21 | 22 | return ( 23 |
24 | {list.map((item) => ( 25 |
26 | {item} 27 |
28 | ))} 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/demo/hooks/useScrollBottom/demo2.tsx: -------------------------------------------------------------------------------- 1 | import { useScrollBottom } from "lhh-ui"; 2 | import React from "react"; 3 | import "./demo.scss"; 4 | 5 | const listId = "useScrollBottomTest"; 6 | 7 | export default () => { 8 | const [list, setList] = React.useState( 9 | Array.from({ length: 10 }, (_, i) => i + 1) 10 | ); 11 | 12 | useScrollBottom({ 13 | querySelector: `#${listId}`, 14 | bottom: 30, 15 | onScrollToLower() { 16 | console.log("----- 触底啦 -----"); 17 | setList((arr) => 18 | arr.concat(Array.from({ length: 10 }, (_, i) => arr.length + i + 1)) 19 | ); 20 | }, 21 | }); 22 | 23 | return ( 24 |
25 | {list.map((item) => ( 26 |
27 | {item} 28 |
29 | ))} 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/demo/hooks/useScrollBottom/demo3.tsx: -------------------------------------------------------------------------------- 1 | import { useScrollBottom } from "lhh-ui"; 2 | import React from "react"; 3 | import "./demo.scss"; 4 | 5 | export default () => { 6 | useScrollBottom({ 7 | bottom: 50, 8 | onScrollToLower() { 9 | alert("----- 页面触底啦 -----"); 10 | }, 11 | }); 12 | 13 | return <>; 14 | }; 15 | -------------------------------------------------------------------------------- /src/demo/hooks/useSearchParamsFilter/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchParamsFilter } from "lhh-ui"; 2 | import React, { useState } from "react" 3 | 4 | const pageSizeArr = [5, 10, 20, 30, 50] 5 | export default () => { 6 | const { setParam, getParam } = useSearchParamsFilter<'pageSize'>(); 7 | const [pageSize, setPageSize] = useState(getParam('pageSize') ?? '0'); 8 | 9 | return ( 10 |
11 |
12 | 设置页码: 13 | {pageSizeArr.map(v => ( 14 | 22 | ))} 23 |
24 |

当前页码: {pageSize}

25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /src/demo/hooks/useSearchSetState/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.scss'; 3 | import { useSearchSetState } from 'lhh-ui'; 4 | 5 | type StateType = { 6 | name: string 7 | persons: { 8 | length: number 9 | } 10 | china: { 11 | guangdong: { 12 | shenzhen: string 13 | } 14 | } 15 | } 16 | 17 | export default () => { 18 | const [state, setState] = useSearchSetState( 19 | ['name', 'persons.length', 'china.guangdong.shenzhen'], 20 | {name: 'lhh', persons: {length: 10}, china: {guangdong: {shenzhen: 'baoan'}}} 21 | ) 22 | 23 | return ( 24 |
25 |
26 | 输入昵称: (name) 27 | {setState(v => ({...v, name: e.target.value}))}} /> 28 |
29 |
30 |
31 | 输入人数: (persons.len) 32 | {setState(v => ({...v, persons: {length: +e.target.value}}))}} /> 33 |
34 |
35 |
36 | 深圳地区: (china.guangdong.shenzhen) 37 | { 38 | setState(obj => { 39 | obj.china.guangdong.shenzhen = e.target.value 40 | return {...obj} 41 | }) 42 | }} /> 43 |
44 |
45 | ) 46 | } -------------------------------------------------------------------------------- /src/demo/hooks/useSearchSetState/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApeWhoLovesCode/lhh-ui/faf4c48a0547d1b2cb00566c6c70c7d6090afedd/src/demo/hooks/useSearchSetState/index.scss -------------------------------------------------------------------------------- /src/demo/hooks/useSearchState/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchState } from "lhh-ui"; 2 | import React from "react" 3 | 4 | const pageSizeArr = [5, 10, 20, 30, 50] 5 | export default () => { 6 | const [pageSize, setPageSize] = useSearchState('pageSize', '10'); 7 | const [searchVal, setSearchVal] = useSearchState('searchVal'); 8 | 9 | return ( 10 |
11 |

当前页码: {pageSize}

12 |
13 | 设置页码: 14 | {pageSizeArr.map(v => ( 15 | 16 | ))} 17 |
18 |
19 | 搜索: setSearchVal(e.target.value)} /> 20 |
21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /src/demo/hooks/useSearchState/demo2.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchState } from "lhh-ui"; 2 | import React from "react" 3 | 4 | const pageSizeArr = [5, 10, 20, 30, 50] 5 | export default () => { 6 | const [pageSize, setPageSize] = useSearchState('pageSize', '10'); 7 | const [pageSize2, setPageSize2] = useSearchState('pageSize2', '10'); 8 | 9 | return ( 10 |
11 |

当前页码1: {pageSize}

12 |

当前页码2: {pageSize2}

13 |
14 | 设置页码: (❌) 15 | {pageSizeArr.map(v => ( 16 | 25 | ))} 26 |
27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/demo/hooks/useSearchState/demo3.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchParamsFilter } from "lhh-ui"; 2 | import React, { useState } from "react" 3 | 4 | const pageSizeArr = [5, 10, 20, 30, 50] 5 | export default () => { 6 | const { setParam, getParam } = useSearchParamsFilter<'pageSize' | 'pageSize2'>(); 7 | const [pageSize, setPageSize] = useState(getParam('pageSize') ?? '10'); 8 | const [pageSize2, setPageSize2] = useState(getParam('pageSize2') ?? '10'); 9 | 10 | return ( 11 |
12 |

当前页码1: {pageSize}

13 |

当前页码2: {pageSize2}

14 |
15 | 设置页码: (✅) 16 | {pageSizeArr.map(v => ( 17 | 27 | ))} 28 |
29 |
30 | ) 31 | } -------------------------------------------------------------------------------- /src/demo/hooks/useSearchState/demo4.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchState } from "lhh-ui"; 2 | import React from "react" 3 | 4 | export default () => { 5 | const [num, setNum] = useSearchState('number', 10, {isNotString: true}); 6 | const [arr, setArr] = useSearchState('arr', [1,2,3], {isNotString: true}); 7 | const [person, setPerson] = useSearchState('person', {name: 'lhh'}, {isNotString: true}); 8 | 9 | return ( 10 |
11 |

----- Number -----

12 |

typeof: {typeof num}, num: {num}

13 |
14 | 15 |
16 |

----- Array -----

17 |

arr: {JSON.stringify(arr)}

18 |
19 | 25 | 31 |
32 |

----- Object -----

33 |

person: {JSON.stringify(person)}

34 |
35 | 48 | 51 |
52 |
53 | ) 54 | } -------------------------------------------------------------------------------- /src/demo/hooks/useTouch/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useRender, useTouch } from "lhh-ui"; 2 | import React from "react"; 3 | 4 | export default () => { 5 | const touch = useTouch(); 6 | const { renderFn } = useRender(); 7 | function onTouchStart(e: React.MouseEvent | React.TouchEvent) { 8 | touch.start(e) 9 | } 10 | function onTouchMove(e: React.MouseEvent | React.TouchEvent) { 11 | touch.move(e); 12 | renderFn(); 13 | } 14 | return ( 15 |
23 |

deltaX: {touch.info.startX}

24 |

deltaY: {touch.info.startY}

25 |

deltaX: {touch.info.deltaX}

26 |

deltaY: {touch.info.deltaY}

27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/demo/hooks/useTouchEvent/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { useTouchEvent } from "lhh-ui"; 2 | import React, { useRef, useState } from "react"; 3 | import './index.scss'; 4 | 5 | export default () => { 6 | const domInfo = useRef({ 7 | startX: 0, 8 | startY: 0, 9 | }) 10 | const [dom, setDom] = useState({ 11 | x: 0, 12 | y: 0, 13 | }); 14 | 15 | const { info, onTouchFn } = useTouchEvent({ 16 | onTouchStart() { 17 | domInfo.current.startX = dom.x 18 | domInfo.current.startY = dom.y 19 | }, 20 | onTouchMove() { 21 | setDom({ 22 | x: domInfo.current.startX + info.deltaX, 23 | y: domInfo.current.startY + info.deltaY 24 | }); 25 | }, 26 | onTouchEnd(e) { 27 | console.log('onTouchEnd: ', e); 28 | }, 29 | }); 30 | 31 | return ( 32 |
33 |
42 | 移动 43 |
44 |
45 | ); 46 | } -------------------------------------------------------------------------------- /src/demo/hooks/useTouchEvent/index.scss: -------------------------------------------------------------------------------- 1 | .demo-useTouchEvent { 2 | position: relative; 3 | height: 200px; 4 | background-color: #f1f1f1; 5 | .ball { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | width: 80px; 10 | height: 80px; 11 | color: #fff; 12 | font-size: 16px; 13 | line-height: 80px; 14 | text-align: center; 15 | background-color: skyblue; 16 | border-radius: 50%; 17 | user-select: none; 18 | } 19 | } -------------------------------------------------------------------------------- /src/demo/huarong-road/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { HuarongRoad, HuarongRoadInstance, isMobile } from "lhh-ui" 2 | import React, { useRef } from "react" 3 | import './index.scss'; 4 | 5 | const list = ['曹操','张飞','赵云','马超','关羽','黄忠','卒','卒','卒','卒'] 6 | 7 | export default () => { 8 | const huarongRoadRef = useRef(null) 9 | 10 | return ( 11 | <> 12 |
13 | { 17 | setTimeout(() => { 18 | alert('曹操跑了') 19 | }, 400); 20 | }} 21 | > 22 | {list.map((name, index) => ( 23 | 24 |
{name}
25 |
26 | ))} 27 |
28 |
29 |
30 | 31 |
32 | 33 | ) 34 | } -------------------------------------------------------------------------------- /src/demo/huarong-road/demo2.tsx: -------------------------------------------------------------------------------- 1 | import { HuarongRoad, isMobile } from "lhh-ui" 2 | import React from "react" 3 | import './index.scss'; 4 | 5 | const list = ['曹操','关羽','马超','黄忠','张飞','赵云','卒','卒','卒','卒'] 6 | 7 | export default () => { 8 | return ( 9 |
10 | { 20 | setTimeout(() => { 21 | alert('曹操跑了') 22 | }, 400); 23 | }} 24 | > 25 | {list.map((name, index) => ( 26 | 27 |
{name}
28 |
29 | ))} 30 |
31 |
32 | ) 33 | } -------------------------------------------------------------------------------- /src/demo/huarong-road/demo3.tsx: -------------------------------------------------------------------------------- 1 | import { HuarongRoad, isMobile } from "lhh-ui" 2 | import React from "react" 3 | import './index.scss'; 4 | 5 | const list = ['曹操','关羽','马超','黄忠','张飞','赵云','卒','卒','卒','卒'] 6 | 7 | export default () => { 8 | return ( 9 |
10 | { 14 | setTimeout(() => { 15 | alert('曹操跑了') 16 | }, 400); 17 | }} 18 | > 19 | {list.map((name, index) => ( 20 | 21 |
{name}
22 |
23 | ))} 24 |
25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /src/demo/huarong-road/demo4.tsx: -------------------------------------------------------------------------------- 1 | import { HuarongRoad, getRandomHexColor, isMobile } from "lhh-ui" 2 | import React from "react" 3 | import './index.scss'; 4 | 5 | const list: string[] = [] 6 | for(let i = 0; i < 10; i++) { 7 | list.push(getRandomHexColor()) 8 | } 9 | 10 | export default () => { 11 | return ( 12 |
13 | { 16 | setTimeout(() => { 17 | alert(`${list[0]}跑了`) 18 | }, 400); 19 | }} 20 | > 21 | {list.map((color, index) => ( 22 | 23 |
24 |
25 | ))} 26 |
27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/demo/huarong-road/index.scss: -------------------------------------------------------------------------------- 1 | .demo1-huarongRoadWrap { 2 | padding: 12px; 3 | width: fit-content; 4 | border: 2px solid #bbb; 5 | border-radius: 4px; 6 | } 7 | .demo1-huarongRoad-item { 8 | width: 100%; 9 | height: 100%; 10 | box-sizing: border-box; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | font-size: 18px; 15 | font-weight: bold; 16 | border: 1px solid #ccc; 17 | user-select: none; 18 | color: #000; 19 | } 20 | .demo1-huarongRoad-item-demo3 { 21 | border-radius: 10px; 22 | } -------------------------------------------------------------------------------- /src/demo/mobile-folder/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { MobileFolder, MobileFolderItem } from "lhh-ui" 2 | import React, { useEffect } from "react" 3 | import GitHubIcon from "../../assets/github.svg" 4 | const LTDIcon = 'https://lhh.codeape.site/img/legend-td-icon.png'; 5 | const BlogIcon = 'https://lhh.codeape.site/img/blog.svg'; 6 | 7 | export default () => { 8 | 9 | const toGitHub = () => { 10 | window.open('https://github.com/ApeWhoLovesCode/lhh-ui') 11 | } 12 | 13 | const list: MobileFolderItem[] = [ 14 | {icon: GitHubIcon, title: 'Lhh-ui', onClick: toGitHub}, 15 | {icon: LTDIcon, title: 'LegendTD', onClick: () => {window.open('http://game.codeape.site/')}}, 16 | {icon: BlogIcon, title: '我的博客', onClick: () => {window.open('https://codeape.site/')}}, 17 | {icon: GitHubIcon, title: 'Github', onClick: (item, i) => {console.log(item, i)}}, 18 | {icon: GitHubIcon}, 19 | {icon: GitHubIcon}, 20 | {icon: GitHubIcon}, 21 | {icon: GitHubIcon}, 22 | {icon: GitHubIcon}, 23 | {icon: GitHubIcon}, 24 | {icon: GitHubIcon}, 25 | ] 26 | 27 | // 不显示滚动条的样式 28 | useEffect(() => { 29 | const styleEle = document.createElement('style'); 30 | styleEle.innerText = 'body::-webkit-scrollbar { display: none; }' 31 | document.body.appendChild(styleEle) 32 | }, []) 33 | 34 | return ( 35 |
36 | 37 |
38 | ) 39 | } -------------------------------------------------------------------------------- /src/demo/mobile-folder/demo2.tsx: -------------------------------------------------------------------------------- 1 | import { MobileFolder, MobileFolderItem } from "lhh-ui" 2 | import React from "react" 3 | import GitHubIcon from "../../assets/github.svg" 4 | 5 | const ChildBox = ({index}: {index: number}) => { 6 | return ( 7 |
8 | child {index} 9 |
10 | ) 11 | } 12 | 13 | export default () => { 14 | 15 | const list: MobileFolderItem[] = [ 16 | {icon: GitHubIcon, title: 'Github', onClick: () => {window.open('https://github.com/ApeWhoLovesCode/lhh-ui')}}, 17 | ] 18 | 19 | for(let i = 0; i < 10; i++) { 20 | list.push({ 21 | children: , 22 | title: `child ${i}` 23 | }) 24 | } 25 | 26 | return ( 27 |
28 | 29 |
30 | ) 31 | } -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollCircle, ScrollCircleItem, isMobile } from 'lhh-ui'; 3 | 4 | const list = Array.from({length: 12}).map((_, i) => ({ id: 'id' + i, title: 'Hello' + i })); 5 | export default () => { 6 | 7 | return ( 8 |
9 | 14 | {list?.map((item, i) => ( 15 | { 19 | console.log('点击了卡片的回调'); 20 | }} 21 | > 22 |
23 |

{item.title}

24 |
25 |
26 | ))} 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo2.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { ScrollCircle, ScrollCircleInstance, isMobile } from 'lhh-ui'; 3 | import { ScrollCirclePageType } from 'lhh-ui/scroll-circle/type'; 4 | 5 | export default () => { 6 | const [list, setList] = useState([]); 7 | const [items, setItems] = useState([]); 8 | const [pageNum, setPageNum] = useState(1); 9 | const [pageSize, setPageSize] = useState(20); 10 | const scrollCircleRef = useRef(null) 11 | 12 | useEffect(() => { 13 | setTimeout(() => { 14 | const newList = new Array(30).fill('Hello').map((v, i) => ({ _id: 'id' + i, title: v + i })); 15 | const preIndex = (pageNum - 1) * pageSize; 16 | const newItems = newList.slice(preIndex, preIndex + pageSize); 17 | setItems(newItems); 18 | setList(newList); 19 | }, 50); 20 | }, []); 21 | const onPageChange = ({ pageNum, pageSize }: ScrollCirclePageType) => { 22 | const preIndex = (pageNum - 1) * pageSize; 23 | const newItems = list.slice(preIndex, preIndex + pageSize); 24 | // 填充空数据 25 | const length = newItems.length; 26 | for (let i = 0; i < pageSize - length; i++) { 27 | newItems.push({ _id: 'id-i-' + i, title: 'World-' + i }); 28 | } 29 | setItems(newItems); 30 | setPageNum(pageNum); 31 | setPageSize(pageSize); 32 | }; 33 | const disabledPre = pageNum <= 1 34 | const disabledNext = pageNum * pageSize >= list.length 35 | const onReducePage = () => { 36 | if(disabledPre) return 37 | scrollCircleRef.current?.onPageChange({pageNum: pageNum - 1}) 38 | onPageChange({pageNum: pageNum - 1, pageSize}) 39 | } 40 | const onAddPage = () => { 41 | if(disabledNext) return 42 | scrollCircleRef.current?.onPageChange({pageNum: pageNum + 1}) 43 | onPageChange({pageNum: pageNum + 1, pageSize}) 44 | } 45 | 46 | return ( 47 | <> 48 |
49 | 54 | {items?.map((item, i) => ( 55 | 59 |
60 |

{item.title}

61 |
62 |
63 | ))} 64 |
65 |
66 | 67 | 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollCircle, isMobile } from 'lhh-ui'; 3 | 4 | const list = Array.from({length: 10}, (_, i) => ({ id: 'id' + i, title: 'Hello' + i })) 5 | export default () => { 6 | 7 | return ( 8 |
9 | 15 | {list?.map((item, i) => ( 16 | 20 |
21 |

{item.title}

22 |
23 |
24 | ))} 25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo4.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollCircle, classBem, isMobile } from 'lhh-ui'; 3 | import './index.scss'; 4 | import { CenterPointType } from 'lhh-ui/scroll-circle/type'; 5 | 6 | const arrCenter: CenterPointType[] = ['center', 'center'] 7 | const arr: CenterPointType[] = ['left','left', 'right','right', 'top','top', 'bottom','bottom'] 8 | const arrAuto: CenterPointType[] = ['auto', 'auto', 'auto', 'auto'] 9 | const listFew = Array.from({length: 10}, (_, i) => ({ id: 'id' + i, title: i })) 10 | const list = Array.from({length: 16}, (_, i) => ({ id: 'id' + i, title: i })) 11 | export default () => { 12 | 13 | const item = (v: CenterPointType, i: number, isCenter?: boolean) => ( 14 | 21 | {(isCenter ? listFew : list)?.map((item, i) => ( 22 | 26 |
27 | {item.title} 28 |
29 |
30 | ))} 31 |
32 | ) 33 | 34 | return ( 35 |
36 |
37 | {arrCenter.map((v, i) => ( 38 |
39 |
{v}{i % 2 === 1 ? ' + 翻转方向' : null}
40 |
41 | {item(v, i, true)} 42 |
43 |
44 | ))} 45 |
46 |
47 | {arr.map((v, i) => ( 48 |
49 |
{v}{i % 2 === 1 ? ' + 翻转方向' : null}
50 |
51 | {item(v, i)} 52 |
53 |
54 | ))} 55 |
56 |

------- auto -------

57 |
58 | {arrAuto.map((v, i) => ( 59 |
60 |
{i < 2 ? '横向' : '纵向'}{i % 2 === 1 ? ' + 翻转方向' : null}
61 |
= 2})}> 62 | {item(v, i)} 63 |
64 |
65 | ))} 66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo5.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { ScrollCircle, ScrollCircleInstance, isMobile } from 'lhh-ui'; 3 | 4 | const list = Array.from({length: 16}, (_, i) => ({ _id: 'id' + i, title: i })) 5 | export default () => { 6 | const scrollCircleRef = useRef(null) 7 | 8 | return ( 9 | <> 10 |
11 | 17 | {list?.map((item, i) => ( 18 | 22 |
23 |

{item.title}

24 |
25 |
26 | ))} 27 |
28 |
29 |
30 | 31 | 32 | 33 |
34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo6.scss: -------------------------------------------------------------------------------- 1 | .circleItem { 2 | width: 200px; 3 | height: 200px; 4 | border-radius: 50%; 5 | border: 1px solid #ccc; 6 | &-active { 7 | box-shadow: 2px 2px 6px 2px rgba(51, 108, 252, 0.6); 8 | } 9 | } 10 | .com-circle-item { 11 | width: 100%; 12 | height: 100%; 13 | .item { 14 | user-select: none; 15 | width: 30px; 16 | height: 30px; 17 | text-align: center; 18 | line-height: 30px; 19 | border-radius: 50%; 20 | &-active { 21 | background-color: #3d74ff; 22 | color: #fff; 23 | } 24 | } 25 | .centerItem { 26 | position: absolute; 27 | left: 50%; 28 | top: 50%; 29 | transform: translate(-50%, -50%); 30 | font-size: 50px; 31 | font-weight: bold; 32 | user-select: none; 33 | } 34 | } -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo6.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react'; 2 | import { ScrollCircle, ScrollCircleInstance, isMobile } from 'lhh-ui'; 3 | import './demo6.scss'; 4 | 5 | const list = Array.from({length: isMobile() ? 10 : 16}, (_, i) => ({ id: 'id' + i, title: i + '' })) 6 | export default () => { 7 | const scrollCircleRef = useRef(null) 8 | const scrollCircleListRef = useRef<(CircleItemInstance | null)[]>([]) 9 | const [curIndex, setCurIndex] = useState(2); 10 | const [curIndex2, setCurIndex2] = useState(0); 11 | const isGo = useRef(false) 12 | 13 | const onScrollCircle = () => { 14 | isGo.current = true; 15 | scrollCircleRef.current?.scrollTo({ 16 | index: Math.floor(Math.random() * list.length), 17 | duration: 1500, 18 | onEnd(index) { 19 | setIndexs(index) 20 | } 21 | }) 22 | scrollCircleListRef.current.forEach((ref, index) => { 23 | ref?.scrollTo?.({index: Math.floor(Math.random() * list.length), duration: 3000}) 24 | }) 25 | } 26 | 27 | const setIndexs = (index: number) => { 28 | setCurIndex(index) 29 | if(isGo.current) { 30 | setTimeout(() => { 31 | setCurIndex2(scrollCircleListRef.current[index]!.getIndex()) 32 | isGo.current = false; 33 | }, 1600); 34 | } else { 35 | setCurIndex2(scrollCircleListRef.current[index]!.getIndex()) 36 | } 37 | } 38 | 39 | const renderScrollCircle = useMemo(() => { 40 | return list?.map((item, i) => ( 41 | 45 |
46 | scrollCircleListRef.current[i] = ref} 48 | title={item.title} 49 | isSelect={curIndex === i} 50 | setCurIndex2={setCurIndex2} 51 | /> 52 |
53 |
54 | )) 55 | }, [list, curIndex]) 56 | 57 | return ( 58 | <> 59 |
60 | { 66 | setIndexs(index) 67 | }} 68 | > 69 | {renderScrollCircle} 70 | 71 |
72 |

当前选中的索引是:{curIndex}{curIndex2}

73 | 74 | 75 | ); 76 | }; 77 | 78 | type CircleItemInstance = Partial & {getIndex: () => number} 79 | type CircleItemProps = { 80 | title: string 81 | isSelect: boolean 82 | setCurIndex2: (i: number) => void 83 | } 84 | const CircleItem = forwardRef(( 85 | {title, isSelect, setCurIndex2}, ref 86 | ) => { 87 | const list = Array.from({length: 12}, (_, i) => ({ id: 'id' + i, title: i })) 88 | const scrollCircleRef = useRef(null) 89 | const [curIndex, setCurIndex] = useState(0); 90 | 91 | useImperativeHandle(ref, () => ({ 92 | scrollTo: (e) => { 93 | scrollCircleRef.current?.scrollTo(e) 94 | }, 95 | getIndex: () => curIndex 96 | })) 97 | 98 | const renderScrollCircle = useMemo(() => { 99 | return list?.map((item, i) => ( 100 | 104 |
105 |
{item.title}
106 |
107 |
108 | )) 109 | }, [list, curIndex]) 110 | 111 | return ( 112 |
113 | { 120 | setCurIndex(index) 121 | if(isSelect) { 122 | setCurIndex2(index) 123 | } 124 | }} 125 | > 126 | {renderScrollCircle} 127 | 128 |
{title}
129 |
130 | ) 131 | }) -------------------------------------------------------------------------------- /src/demo/scroll-circle/demo7.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollCircle } from 'lhh-ui'; 3 | 4 | const list = Array.from({length: 10}, (_, i) => ({ id: 'id' + i, title: i + '' })) 5 | export default () => { 6 | 7 | return ( 8 | <> 9 |
10 | 15 | {list?.map((item, i) => ( 16 | 20 |
21 |

{item.title}

22 |
23 |
24 | ))} 25 |
26 |
27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/demo/scroll-circle/index.scss: -------------------------------------------------------------------------------- 1 | .demo-scrollcircle { 2 | .wrap { 3 | width: 420px; 4 | display: grid; 5 | gap: 16px; 6 | grid-template: 'a a'; 7 | margin-bottom: 10px; 8 | } 9 | .title { 10 | font-weight: bold; 11 | font-size: 14px; 12 | margin-bottom: 8px; 13 | text-align: center; 14 | } 15 | .item { 16 | border: 1px solid #ccc; 17 | width: 200px; 18 | height: 200px; 19 | &-col { 20 | height: 250px; 21 | } 22 | } 23 | .card { 24 | border: 2px solid #aaa; 25 | user-select: none; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | font-size: 14px; 30 | font-weight: bold; 31 | width: 30px; 32 | height: 30px; 33 | } 34 | &-isMobile { 35 | .wrap { 36 | width: 100%; 37 | gap: 12px; 38 | } 39 | .item { 40 | width: 150px; 41 | height: 150px; 42 | &-col { 43 | height: 200px; 44 | } 45 | } 46 | .card { 47 | width: 20px; 48 | height: 20px; 49 | } 50 | } 51 | } 52 | 53 | .demo5-btnWrap { 54 | margin-top: 10px; 55 | display: grid; 56 | gap: 20px; 57 | grid: 'a a a'; 58 | .btn { 59 | width: 100px; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/demo/scroll-circle/lotteryDemo.scss: -------------------------------------------------------------------------------- 1 | .lotteryDemo { 2 | position: relative; 3 | .item { 4 | position: relative; 5 | display: flex; 6 | justify-content: center; 7 | align-items: flex-end; 8 | width: 50px; 9 | height: 80px; 10 | .text { 11 | position: relative; 12 | z-index: 1; 13 | font-size: 16px; 14 | font-weight: bold; 15 | width: 16px; 16 | user-select: none; 17 | color: #a57ae2; 18 | } 19 | .triangle { 20 | width: 0; 21 | height: 0; 22 | position: absolute; 23 | top: 0; 24 | border-style: solid; 25 | border-width: 50px 16.5px; 26 | z-index: -1; 27 | &-left { 28 | left: 50%; 29 | border-color: #ccc #ccc transparent transparent; 30 | transform: translate(-100%); 31 | } 32 | &-right { 33 | right: 50%; 34 | border-color: #ccc transparent transparent #ccc; 35 | transform: translate(100%); 36 | } 37 | &-center { 38 | position: absolute; 39 | left: 50%; 40 | top: 50%; 41 | height: 130px; 42 | transform: translate(-50%); 43 | width: 2px; 44 | } 45 | } 46 | } 47 | .pointer { 48 | position: absolute; 49 | top: 50%; 50 | left: 50%; 51 | transform: translate(-50%, -50%); 52 | font-size: 32px; 53 | font-weight: bold; 54 | width: 50px; 55 | height: 50px; 56 | text-align: center; 57 | line-height: 50px; 58 | background-color: #fff; 59 | border-radius: 50%; 60 | } 61 | } -------------------------------------------------------------------------------- /src/demo/scroll-circle/lotteryDemo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { ScrollCircle, ScrollCircleInstance, shuffleArray } from 'lhh-ui'; 3 | import './lotteryDemo.scss'; 4 | 5 | type Item = { 6 | level: number 7 | text: string 8 | color: string 9 | } 10 | 11 | export default () => { 12 | const scrollCircleRef = useRef(null) 13 | const [list, setList] = useState([]); 14 | const [selectIndex, setSelectIndex] = useState(0); 15 | 16 | const init = () => { 17 | const arr: Item[] = [ 18 | {text: '特等奖', color: 'orange', level: 0}, 19 | {text: '一等奖', color: 'yellow', level: 1}, 20 | {text: '一等奖', color: 'yellow', level: 1} 21 | ] 22 | for(let i = 3; i < 10; i++) { 23 | const res = (Math.random() * 10) > 6 ? { 24 | level: 2, 25 | text: '二等奖', 26 | color: 'greenyellow', 27 | } : { 28 | level: 3, 29 | text: '三等奖', 30 | color: 'green', 31 | } 32 | arr.push(res) 33 | } 34 | shuffleArray(arr) 35 | setList(arr) 36 | } 37 | 38 | const onScrollCicle = () => { 39 | const itemDeg = 360 / list.length 40 | const index = Math.floor(Math.random() * 10) 41 | const deg = index * itemDeg + 360 * 4; 42 | scrollCircleRef.current?.scrollTo({ 43 | deg: deg, 44 | duration: 4000, 45 | onEnd() { 46 | setSelectIndex(index) 47 | scrollCircleRef.current?.scrollTo({deg: deg % 360, duration: 0}) 48 | }, 49 | }) 50 | } 51 | 52 | useEffect(() => { 53 | init() 54 | }, []) 55 | 56 | const triangleStyle = { 57 | borderWidth: `${90}px ${30}px` 58 | } 59 | 60 | return ( 61 | <> 62 |
63 | 75 | {list?.map((item, i) => ( 76 | 80 | {/*
*/} 81 |
82 |
86 |
87 |
91 |
{item.text}
92 |
93 | 94 | ))} 95 | 96 |
97 |
98 |
99 |
当前选中的奖项是:{list[selectIndex]?.text}
100 | 101 |
102 | 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /src/demo/scroll-view/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollView } from "lhh-ui" 2 | import React, { useRef, useState } from "react" 3 | 4 | export default () => { 5 | const [list, setList] = React.useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 6 | const [loading, setLoading] = useState(false); 7 | const timer = useRef(null) 8 | 9 | function onScrollToLower() { 10 | clearTimeout(timer.current) 11 | setLoading(true); 12 | timer.current = setTimeout(() => { 13 | setList(arr => arr.concat(Array.from({length: 10}, (_, i) => arr.length + i + 1))) 14 | setLoading(false) 15 | }, 300); 16 | } 17 | 18 | return ( 19 | 23 | {list.map((item) => ( 24 |

{item}

25 | ))} 26 | {loading &&
加载中...
} 27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/demo/skus/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import './index.scss'; 3 | import { Skus, SkusItem } from 'lhh-ui'; 4 | import { getSkusData, skuNames } from './utils'; 5 | 6 | export default () => { 7 | const [checkValArr, setCheckValArr] = useState([4, 5, 2, 3, 0, 0]); 8 | // const [checkValArr, setCheckValArr] = useState([6,6,6,6,6,6]); 9 | const [skusList, setSkusList] = useState([]); 10 | // 库存为零对应的sku数组 11 | const [noStockSkus, setNoStockSkus] = useState([]) 12 | const [stock, setStock] = useState(); 13 | 14 | useEffect(() => { 15 | const checkValTrueArr = checkValArr.filter(Boolean) 16 | const _noStockSkus: string[][] = [[]] 17 | const list = getSkusData(checkValTrueArr, _noStockSkus) 18 | setSkusList(list) 19 | setNoStockSkus([..._noStockSkus]) 20 | setStock(void 0) 21 | }, [checkValArr]) 22 | 23 | const onChangeRadio = (i: number, value: number) => { 24 | setCheckValArr(arr => { 25 | arr[i] = value 26 | return [...arr] 27 | }) 28 | } 29 | 30 | return ( 31 |
32 |
33 | {checkValArr.map((checkVal, i) => ( 34 |
35 | {skuNames[i]}: 36 | {[0,1,2,3,4,5,6].map((value) => ( 37 | onChangeRadio(i, value)} 41 | >{value} 42 | ))} 43 |
44 | ))} 45 |
46 | { 49 | console.log('onChange: ', checkSkus, curSku); 50 | setStock(curSku?.stock) 51 | }} 52 | /> 53 |
还剩库存:{stock ?? '-'}
54 |
------------- 库存为零的sku: -------------
55 |
56 | {noStockSkus.map((skus, i) => ( 57 |
58 | {skus.map(sku => ( 59 |
{sku}
60 | ))} 61 |
62 | ))} 63 |
64 |
65 | ) 66 | } -------------------------------------------------------------------------------- /src/demo/skus/demo2.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './index.scss'; 3 | import { Skus, SkusItem } from 'lhh-ui'; 4 | import { getSkusData } from './utils'; 5 | 6 | export default () => { 7 | const [skusList] = useState(getSkusData([5, 4, 3, 2])); 8 | 9 | return ( 10 |
11 | { 14 | console.log('onChange: ', checkSkus, curSku); 15 | }} 16 | customRender={(list, selectSkus) => list.map(p => ( 17 |
18 |
{p.name}
19 |
20 | {p.values.map((sku) => ( 21 |
selectSkus(p.name, sku)} 24 | > 25 | 26 | {sku.value} 27 | 28 |
29 | ))} 30 |
31 |
32 | ))} 33 | /> 34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /src/demo/skus/demo3.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './index.scss'; 3 | import { Skus, SkusItem } from 'lhh-ui'; 4 | import { getSkusData } from './utils'; 5 | 6 | type NewSkusItem = { 7 | newStock: number 8 | params: SkusItem['params'] 9 | } 10 | 11 | export default () => { 12 | const [skusList] = useState( 13 | getSkusData([5, 4, 3, 2], void 0, true) as unknown as NewSkusItem[] 14 | ); 15 | 16 | return ( 17 |
18 | { 24 | console.log('onChange: ', curSku?.dataItem); 25 | }} 26 | /> 27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/demo/skus/index.scss: -------------------------------------------------------------------------------- 1 | .demo-skus { 2 | .skus-info { 3 | .radio-wrap { 4 | display: flex; 5 | gap: 10px; 6 | font-size: 14px; 7 | align-items: center; 8 | margin-bottom: 10px; 9 | .radio { 10 | width: 20px; 11 | height: 20px; 12 | text-align: center; 13 | line-height: 20px; 14 | font-size: 12px; 15 | border: 1px solid #ccc; 16 | border-radius: 50%; 17 | cursor: pointer; 18 | &-active { 19 | color: #fff; 20 | background-color: #3d74ff; 21 | } 22 | } 23 | } 24 | } 25 | .bottom { 26 | display: flex; 27 | flex-wrap: wrap; 28 | gap: 10px; 29 | .list { 30 | padding: 5px 10px; 31 | border: 1px solid #ccc; 32 | max-height: 200px; 33 | overflow-y: scroll; 34 | &::-webkit-scrollbar { 35 | display: none !important; 36 | } 37 | .item { 38 | font-size: 12px; 39 | min-width: 150px; 40 | padding: 5px 0; 41 | white-space: nowrap; 42 | } 43 | } 44 | } 45 | } 46 | 47 | .demo2-skus { 48 | h5 { 49 | margin: 10px 0; 50 | } 51 | .sku-wrap { 52 | display: flex; 53 | align-items: center; 54 | flex-wrap: wrap; 55 | .sku { 56 | box-sizing: border-box; 57 | display: inline-block; 58 | padding: 4px 8px; 59 | margin-right: 5px; 60 | border: 1px solid #c193f1; 61 | font-size: 12px; 62 | cursor: pointer; 63 | user-select: none; 64 | &-active { 65 | color: #b8f3cc; 66 | background-color: #c193f1; 67 | } 68 | &-disabled { 69 | color: #c6c6c6; 70 | border-color: #c6c6c6; 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/demo/skus/utils.ts: -------------------------------------------------------------------------------- 1 | import { SkusItem } from 'lhh-ui'; 2 | 3 | export const skuData: Record = { 4 | '颜色': ['红','绿','蓝','黑','白','黄'], 5 | '大小': ['S','M','L','XL','XXL','MAX'], 6 | '款式': ['圆领','V领','条纹','渐变','轻薄','休闲'], 7 | '面料': ['纯棉','涤纶','丝绸','蚕丝','麻','鹅绒'], 8 | '群体': ['男','女','中性','童装','老年','青少年'], 9 | '价位': ['<30','<50','<100','<300','<800','<1500'], 10 | } 11 | 12 | export const skuNames = Object.keys(skuData) 13 | 14 | export function getSkusData( 15 | skuCategories: number[], 16 | noStockSkus?: string[][], 17 | /** 是否采用新的key */ 18 | isNewKey?: boolean 19 | ) { 20 | const skusList: SkusItem[] = [] 21 | // 对应 skuState 中各 sku ,主要用于下面遍历时,对 product 中 skus 的索引操作 22 | const indexArr = Array.from({length: skuCategories.length}, () => 0); 23 | // 需要遍历的总次数 24 | const total = skuCategories.reduce((pre, cur) => pre * (cur || 1), 1) 25 | for(let i = 1; i <= total; i++) { 26 | const sku: SkusItem = { 27 | // 库存:60%的几率为0-50,40%几率为0 28 | [!isNewKey ? 'stock' : 'newStock']: Math.floor(Math.random() * 10) >= 4 ? Math.floor(Math.random() * 50) : 0, 29 | params: [], 30 | } 31 | // 生成每个 sku 对应的 params 32 | let skuI = 0; 33 | skuNames.forEach((name, j) => { 34 | if(skuCategories[j]) { 35 | const value = skuData[name][indexArr[skuI]] 36 | sku.params.push({ 37 | name, 38 | value, 39 | }) 40 | skuI++; 41 | } 42 | }) 43 | skusList.push(sku) 44 | 45 | indexArr[indexArr.length - 1]++; 46 | for(let j = indexArr.length - 1; j >= 0; j--) { 47 | if(indexArr[j] >= skuCategories[j] && j !== 0) { 48 | indexArr[j - 1]++ 49 | indexArr[j] = 0 50 | } 51 | } 52 | 53 | if(noStockSkus) { 54 | if(!sku[!isNewKey ? 'stock' : ('newStock' as keyof SkusItem)]) { 55 | noStockSkus.at(-1)?.push(sku.params.map(p => p.value).join(' / ')) 56 | } 57 | if(indexArr[0] === noStockSkus.length && noStockSkus.length < skuCategories[0]) { 58 | noStockSkus.push([]) 59 | } 60 | } 61 | } 62 | return skusList 63 | } -------------------------------------------------------------------------------- /src/demo/slider-puzzle/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { isMobile, SliderPuzzle, SliderPuzzleInstance } from 'lhh-ui'; 2 | import React, { useRef, useState } from 'react'; 3 | 4 | const list = Array.from({ length: 8 }, (_, i) => ({ id: `id-${i}` })); 5 | export default () => { 6 | const puzzleRef = useRef(null); 7 | const [isGameMode, setIsGameMode] = useState(false); 8 | return ( 9 |
10 | { 18 | setTimeout(() => { 19 | alert('恭喜你完成了拼图'); 20 | }, 400); 21 | }} 22 | > 23 | {list.map((item, index) => ( 24 | 25 |

{item.id}

26 | 27 |
28 | ))} 29 | {/* 这里是完整的拼图 */} 30 | {isGameMode ? ( 31 | 39 | ) : null} 40 |
41 |
42 | 50 | 58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/demo/slider-puzzle/demo2.tsx: -------------------------------------------------------------------------------- 1 | import { SliderPuzzle, isMobile } from 'lhh-ui'; 2 | import React from 'react'; 3 | 4 | const list = Array.from({ length: 15 }, (_, i) => ({ id: `${i}` })); 5 | export default () => { 6 | return ( 7 | { 16 | setTimeout(() => { 17 | alert('恭喜你完成了拼图'); 18 | }, 400); 19 | }} 20 | > 21 | {list.map((item, index) => ( 22 | 27 |
{item.id}
28 | 29 |
30 | ))} 31 | {/* 这里是完整的拼图 */} 32 | 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/demo/slider-puzzle/demo3.tsx: -------------------------------------------------------------------------------- 1 | import { isMobile, SliderPuzzle } from 'lhh-ui'; 2 | import React from 'react'; 3 | 4 | export default () => { 5 | return ( 6 | { 16 | setTimeout(() => { 17 | alert('恭喜你完成了拼图'); 18 | }, 400); 19 | }} 20 | > 21 | {/* 这里是完整的拼图 */} 22 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/demo/tabs/demo1.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from "lhh-ui"; 2 | import React from "react"; 3 | 4 | const tabsList = ['鸡肉', '牛肉', '烤羊排', '火腿', '菲力', '小龙虾', '火鸡', '鱿鱼', '螃蟹', '小笼包', '汉堡包', '寿司']; 5 | export default () => { 6 | return ( 7 |
8 | 9 | {tabsList.map((item, i) => ( 10 | 11 | ))} 12 | 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/demo/tabs/demo2.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from "lhh-ui"; 2 | import React from "react"; 3 | 4 | const tabsList = ['鸡肉', '牛肉', '烤羊排', '火腿', '菲力', '小龙虾', '火鸡', '鱿鱼', '螃蟹', '小笼包', '汉堡包', '寿司']; 5 | export default () => { 6 | return ( 7 |
8 | 15 | } 16 | > 17 | {tabsList.map((item, i) => ( 18 | 19 | ))} 20 | 21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /src/demo/tree/data.ts: -------------------------------------------------------------------------------- 1 | import { TreeNode } from "lhh-ui"; 2 | 3 | export const treeData: TreeNode[] = [ 4 | { 5 | title: '0-0', 6 | key: '0-0', 7 | children: [ 8 | { 9 | title: '0-0-0', 10 | key: '0-0-0', 11 | children: [ 12 | { title: '0-0-0-0', key: '0-0-0-0' }, 13 | { title: '0-0-0-1', key: '0-0-0-1' }, 14 | { title: '0-0-0-2', key: '0-0-0-2' }, 15 | ], 16 | }, 17 | { 18 | title: '0-0-1', 19 | key: '0-0-1', 20 | children: [ 21 | { 22 | title: '0-0-1-0', 23 | key: '0-0-1-0', 24 | children: [ 25 | { title: '0-0-1-0-0', key: '0-0-1-0-0' }, 26 | { title: '0-0-1-0-1', key: '0-0-1-0-1' }, 27 | { title: '0-0-1-0-2', key: '0-0-1-0-2' }, 28 | ], 29 | }, 30 | { title: '0-0-1-1', key: '0-0-1-1' }, 31 | { title: '0-0-1-2', key: '0-0-1-2' }, 32 | ], 33 | }, 34 | { 35 | title: '0-0-2', 36 | key: '0-0-2', 37 | }, 38 | ], 39 | }, 40 | { 41 | title: '0-1', 42 | key: '0-1', 43 | children: [ 44 | { title: '0-1-0', key: '0-1-0' }, 45 | { title: '0-1-1', key: '0-1-1' }, 46 | { title: '0-1-2', key: '0-1-2' }, 47 | ], 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /src/demo/tree/demo1.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { Tree } from "lhh-ui"; 3 | import { TreeNode } from "lhh-ui/tree/type"; 4 | 5 | const treeData: TreeNode[] = [ 6 | { 7 | title: '0-0', 8 | key: '0-0', 9 | children: [ 10 | { 11 | title: '0-0-0', 12 | key: '0-0-0', 13 | children: [ 14 | { title: '0-0-0-0', key: '0-0-0-0' }, 15 | { title: '0-0-0-1', key: '0-0-0-1' }, 16 | { title: '0-0-0-2', key: '0-0-0-2' }, 17 | ], 18 | }, 19 | { 20 | title: '0-0-1', 21 | key: '0-0-1', 22 | children: [ 23 | { 24 | title: '0-0-1-0', 25 | key: '0-0-1-0', 26 | children: [ 27 | { title: '0-0-1-0-0', key: '0-0-1-0-0', disabled: true }, 28 | { title: '0-0-1-0-1', key: '0-0-1-0-1', disableCheckbox: true }, 29 | { title: '0-0-1-0-2', key: '0-0-1-0-2' }, 30 | ], 31 | }, 32 | { title: '0-0-1-1', key: '0-0-1-1' }, 33 | { title: '0-0-1-2', key: '0-0-1-2' }, 34 | ], 35 | }, 36 | { 37 | title: '0-0-2', 38 | key: '0-0-2', 39 | }, 40 | ], 41 | }, 42 | { 43 | title: '0-1', 44 | key: '0-1', 45 | children: [ 46 | { title: '0-1-0', key: '0-1-0' }, 47 | { title: '0-1-1', key: '0-1-1' }, 48 | { title: '0-1-2', key: '0-1-2' }, 49 | ], 50 | }, 51 | { 52 | title:
~~hello~~
, 53 | key: '0-2', 54 | }, 55 | ]; 56 | 57 | export default () => { 58 | const [checkedKeys, setCheckedKeys] = useState(['0-1', '0-0-2']); 59 | return ( 60 |
61 | { 67 | setCheckedKeys(keys) 68 | }} 69 | onSelect={(keys, p) => { 70 | console.log('keys, p: ', keys, p.treeDataItem); 71 | }} 72 | /> 73 |
74 | ) 75 | } -------------------------------------------------------------------------------- /src/demo/tree/demo2.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { Tree } from "lhh-ui"; 3 | import { treeData } from "./data"; 4 | 5 | export default () => { 6 | const [checkedKeys, setCheckedKeys] = useState([]); 7 | return ( 8 |
9 | { 16 | console.log(keys, p); 17 | setCheckedKeys(keys) 18 | }} 19 | /> 20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /src/demo/tree/demo3.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react" 2 | import { Tree, TreeInstance } from "lhh-ui"; 3 | import { treeData } from "./data"; 4 | 5 | export default () => { 6 | const treeRef = useRef(null) 7 | const [searchVal, setSearchVal] = useState('0-0-0-1'); 8 | const [keyList, setKeyList] = useState([]); 9 | 10 | return ( 11 |
12 | setSearchVal(e.target.value)} /> 13 |
14 | 17 | 20 | 23 | 26 | 27 | 28 |
29 |

30 | 获取到的节点信息:{JSON.stringify(keyList)} 31 |

32 | 39 |
40 | ) 41 | } -------------------------------------------------------------------------------- /src/floating-ball/floating-ball.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, FC, useRef, useLayoutEffect } from 'react'; 2 | import useTouchEvent from '../hooks/useTouchEvent'; 3 | import { FloatingBallProps } from './type'; 4 | 5 | const classPrefix = `lhhui-floating-ball`; 6 | 7 | const FloatingBall: FC = ({ axis = 'xy', magnetic, ...props }) => { 8 | /** 悬浮球的宽,高,上下左右距离 */ 9 | const ball = useRef({ w: 0, h: 0, r: 0, l: 0, t: 0, b: 0 }); 10 | const touchRef = useRef({ 11 | startX: 0, 12 | startY: 0, 13 | }); 14 | const [info, setInfo] = useState({ 15 | x: 0, 16 | y: 0, 17 | }); 18 | const buttonRef = useRef(null); 19 | const duration = useRef(0.1); 20 | 21 | const { info: _info, onTouchFn } = useTouchEvent({ 22 | onTouchStart: () => { 23 | touchRef.current.startX = info.x; 24 | touchRef.current.startY = info.y; 25 | duration.current = 0.1; 26 | }, 27 | onTouchMove: () => { 28 | const x = axis === 'y' ? 0 : _info.deltaX + touchRef.current.startX; 29 | const y = axis === 'x' ? 0 : _info.deltaY + touchRef.current.startY; 30 | setInfo({ x, y }); 31 | props.onOffsetChange?.({ x, y }); 32 | }, 33 | onTouchEnd: () => { 34 | const screenW = window.innerWidth, screenH = window.innerHeight; 35 | let x = axis === 'y' ? 0 : _info.deltaX + touchRef.current.startX; 36 | let y = axis === 'x' ? 0 : _info.deltaY + touchRef.current.startY; 37 | const { w, h, l, r, t, b } = ball.current; 38 | if (magnetic === 'x') { 39 | const l_r = l < r ? l : r; 40 | const _v = l < r ? -1 : 1; 41 | const middleX = screenW / 2 - l_r - w / 2; // 中间分隔线的值 42 | const distance = -1 * _v * (screenW - w - l_r * 2); // 另一边的位置 43 | x = Math.abs(x) > middleX ? (x * _v < 0 ? distance : 0) : 0; 44 | props.onMagnetic?.(x === 0 ? l < r : l > r); 45 | } else if (magnetic === 'y') { 46 | const l_r = t < b ? t : b; 47 | const _v = t < b ? -1 : 1; 48 | const middleX = screenH / 2 - l_r - h / 2; // 中间分隔线的值 49 | const distance = -1 * _v * (screenH - h - l_r * 2); // 另一边的位置 50 | y = Math.abs(y) > middleX ? (y * _v < 0 ? distance : 0) : 0; 51 | props.onMagnetic?.(y === 0 ? t < b : t > b); 52 | } 53 | duration.current = 0.3; 54 | setInfo({ x, y }); 55 | }, 56 | }); 57 | 58 | useLayoutEffect(() => { 59 | const init = () => { 60 | const ballInfo = buttonRef.current!.getBoundingClientRect() 61 | ball.current.w = ballInfo.width 62 | ball.current.h = ballInfo.height 63 | ball.current.l = ballInfo.left 64 | ball.current.r = window.innerWidth - ballInfo.right 65 | ball.current.t = ballInfo.top 66 | ball.current.b = window.innerHeight - ballInfo.bottom 67 | } 68 | init() 69 | }, []) 70 | 71 | return ( 72 |
73 |
82 | {props.children} 83 |
84 |
85 | ) 86 | } 87 | 88 | export default FloatingBall; -------------------------------------------------------------------------------- /src/floating-ball/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FloatingBall 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 一般组件 9 | order: 1 10 | --- 11 | 12 | ## 悬浮球组件 13 | 14 | 可在页面悬浮的一个悬浮球组件 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ### 吸附 X 边 23 | 24 | 25 | 26 | ### 仅 Y 轴方向可移动 27 | 28 | 29 | 30 | ## API 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/floating-ball/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-floating-ball'; 2 | 3 | .#{$class-prefix} { 4 | display: inline-block; 5 | &-button { 6 | display: inline-block; 7 | position: fixed; 8 | top: var(--initial-position-top); 9 | right: var(--initial-position-right); 10 | bottom: var(--initial-position-bottom); 11 | left: var(--initial-position-left); 12 | z-index: var(--z-index); 13 | transition: transform ease-out 0.1s; 14 | user-select: none; 15 | touch-action: none; 16 | overscroll-behavior: none; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/floating-ball/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import FloatingBall from './floating-ball'; 3 | 4 | export { FloatingBall }; 5 | export default FloatingBall; 6 | export type { FloatingBallProps } from './type'; 7 | -------------------------------------------------------------------------------- /src/floating-ball/type.ts: -------------------------------------------------------------------------------- 1 | import { NativeProps } from '../utils/native-props'; 2 | 3 | export type FloatingBallProps = { 4 | /** 5 | * 可以进行拖动的方向,xy 表示自由移动 6 | * @default xy 7 | */ 8 | axis?: 'x' | 'y' | 'xy'; 9 | /** 自动磁吸到边界 */ 10 | magnetic?: 'x' | 'y'; 11 | /** 贴边时触发 isLeft: true 代表是左或上方向上贴边 */ 12 | onMagnetic?: (isLeft: boolean) => void; 13 | /** 位置偏移时触发 */ 14 | onOffsetChange?: (offset: { x: number; y: number }) => void; 15 | } & NativeProps< 16 | | '--initial-position-left' 17 | | '--initial-position-right' 18 | | '--initial-position-top' 19 | | '--initial-position-bottom' 20 | | '--z-index' 21 | >; 22 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useKeepInterval"; 2 | export * from "./useKeepIntervalMap"; 3 | export * from "./useLatest"; 4 | export * from "./useMergeProps"; 5 | export * from "./usePropsState"; 6 | export * from "./useRender"; 7 | export * from "./useRTDraw"; 8 | export * from "./useScrollBottom"; 9 | export * from "./useSearchParamsFilter"; 10 | export * from "./useSearchSetState"; 11 | export * from "./useSearchState"; 12 | export * from "./useTouch"; 13 | export * from "./useTouchEvent"; 14 | -------------------------------------------------------------------------------- /src/hooks/useKeepInterval/doc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { KeepIntervalSetOthParams } from '../useKeepIntervalMap'; 3 | import { KeepInterval } from '.'; 4 | 5 | /** 函数类型 */ 6 | export const KeepIntervalMd: React.FC = () => { 7 | return <> 8 | } 9 | 10 | type KeepIntervalSetFun = { 11 | /** 执行函数 */ 12 | fn?: () => void, 13 | /** 间隔时间 */ 14 | intervalTime?: number, 15 | /** 其他参数 */ 16 | othParams?: KeepIntervalSetOthParams 17 | } 18 | 19 | /** set 函数的参数 md 文档 */ 20 | export const KeepIntervalSetFunMd: React.FC = () => { 21 | return <> 22 | } 23 | 24 | /** othParams 其他参数 md 文档 */ 25 | export const KeepIntervalSetOthParamsMd: React.FC = () => { 26 | return <> 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useKeepInterval/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useKeepInterval 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: 计时器 9 | order: 3 10 | --- 11 | 12 | ## useKeepInterval 13 | 14 | 可以(暂停 / 继续)并保留剩余倒计时的计时器钩子 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ### 多个倒计时使用 23 | 24 | 25 | 26 | ## API 27 | 28 | ### KeepInterval 29 | 30 | 31 | 32 | ### set 函数的参数的类型 33 | 34 | 35 | 36 | ### othParams 其他参数 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/hooks/useKeepInterval/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import { KeepIntervalSetOthParams, UseKeepIntervalItem } from '../useKeepIntervalMap'; 3 | import { KeepIntervalMd, KeepIntervalSetFunMd, KeepIntervalSetOthParamsMd } from './doc'; 4 | 5 | function useKeepInterval() { 6 | const timerRef = useRef({ 7 | timeout: null, 8 | interval: null, 9 | cur: 0, 10 | end: 0, 11 | fn: () => {}, 12 | intervalTime: 0, 13 | remainTime: 0, 14 | isTimeOut: false, 15 | }) 16 | 17 | /** 18 | * 设置/开启计时器 19 | * @param fn 执行函数 20 | * @param intervalTime 间隔时间 21 | * @param isInit 是否是初始化设置计时器 22 | * @param p 23 | */ 24 | const set = ( 25 | fn?: () => void, 26 | intervalTime?: number, 27 | {isInit, isTimeOut = false, isCover} : KeepIntervalSetOthParams = {} 28 | ) => { 29 | const timeItem = timerRef.current 30 | if(isTimeOut !== timeItem.isTimeOut) { 31 | timeItem.isTimeOut = isTimeOut 32 | } 33 | if(((!timeItem.interval && !timeItem.timeout) || isCover) && fn) { 34 | timeItem.fn = fn 35 | } 36 | // 覆盖倒计时的持续时间 37 | if(intervalTime && timeItem.intervalTime !== intervalTime) { 38 | timeItem.intervalTime = intervalTime 39 | timeItem.remainTime = intervalTime 40 | } 41 | if(isInit) return 42 | stopTime() 43 | timeItem.remainTime -= timeItem.end - timeItem.cur 44 | timeItem.cur = Date.now() 45 | timeItem.end = timeItem.cur 46 | timeItem.timeout = setTimeout(() => { 47 | timeItem.cur = Date.now() 48 | timeItem.end = timeItem.cur 49 | timeItem.remainTime = timeItem.intervalTime 50 | if(!timeItem.isTimeOut) { 51 | timeItem.interval = setInterval(() => { 52 | timeItem.cur = Date.now() 53 | timeItem.end = timeItem.cur 54 | timeItem.fn() 55 | }, timeItem.intervalTime) 56 | } 57 | timeItem.fn() 58 | if(timeItem.isTimeOut) { 59 | stopTime() 60 | } 61 | }, timeItem.remainTime) 62 | } 63 | /** 关闭计时器 */ 64 | const pause = () => { 65 | if(timerRef.current.timeout || timerRef.current.interval) { 66 | timerRef.current.end = Date.now() 67 | stopTime() 68 | return timerRef.current.remainTime - (timerRef.current.end - timerRef.current.cur) 69 | } 70 | } 71 | /** 停止定时器 */ 72 | const stopTime = () => { 73 | clearTimeout(timerRef.current.timeout!) 74 | clearInterval(timerRef.current.interval!) 75 | timerRef.current.timeout = null 76 | timerRef.current.interval = null 77 | } 78 | useEffect(() => { 79 | return () => stopTime() 80 | },[]) 81 | return { 82 | setKeepInterval: useCallback(set, []), 83 | pauseKeepInterval: useCallback(pause, []), 84 | } 85 | } 86 | 87 | export type KeepInterval = { 88 | /** 设置/开启计时器,othParams 参数请看下面的介绍 */ 89 | setKeepInterval: (fn?: () => void, intervalTime?: number, othParams?: any) => void; 90 | /** 暂停某一个计时器 */ 91 | pauseKeepInterval: (key: string) => number; 92 | } 93 | 94 | export default useKeepInterval 95 | export { useKeepInterval, KeepIntervalMd, KeepIntervalSetFunMd, KeepIntervalSetOthParamsMd } 96 | -------------------------------------------------------------------------------- /src/hooks/useKeepIntervalMap/doc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { KeepIntervalMap, KeepIntervalSetOthParams } from '.'; 3 | 4 | /** 各函数类型 */ 5 | export const KeepIntervalMapMd: React.FC = () => { 6 | return <> 7 | } 8 | 9 | type KeepIntervalSetFun = { 10 | /** 计时器的id索引 */ 11 | key: string, 12 | /** 执行函数 */ 13 | fn?: () => void, 14 | /** 间隔时间 */ 15 | intervalTime?: number, 16 | /** 其他参数 */ 17 | othParams?: KeepIntervalSetOthParams 18 | } 19 | 20 | /** set 函数的参数 md 文档 */ 21 | export const KeepIntervalMapSetFunMd: React.FC = () => { 22 | return <> 23 | } 24 | 25 | /** othParams 其他参数 md 文档 */ 26 | export const KeepIntervalMapSetOthParamsMd: React.FC = () => { 27 | return <> 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useKeepIntervalMap/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useKeepIntervalMap 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: 计时器 9 | order: 3 10 | --- 11 | 12 | ## useKeepIntervalMap 13 | 14 | 可以(暂停 / 继续)并保留剩余倒计时的计时器钩子集合; 15 | 16 | 该钩子用于需要统一管理多个可中途暂停并继续的计时器的场景;比如在游戏中的暂停,将多个函数暂停下来,开始时将根据上一次倒计时剩余的时间继续执行。 17 | 18 | ## 演示 19 | 20 | ### 常规使用 21 | 22 | 23 | 24 | ## API 25 | 26 | ### KeepIntervalMap 27 | 28 | 29 | 30 | ### set 函数的参数的类型 31 | 32 | 33 | 34 | ### othParams 其他参数 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/hooks/useLatest/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | function useLatest(value: T) { 4 | const ref = useRef(value); 5 | ref.current = value; 6 | 7 | return ref; 8 | } 9 | 10 | export default useLatest; 11 | export { useLatest }; 12 | -------------------------------------------------------------------------------- /src/hooks/useMergeProps/index.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import omit from '../../utils/omit'; 3 | 4 | export type MergePropsOptions = { 5 | _ignorePropsFromGlobal?: boolean; 6 | }; 7 | 8 | /** 将某些属性变为必选 */ 9 | type RequireKey = Omit & { [P in K]-?: T[P] }; 10 | 11 | function useMergeProps( 12 | componentProps: PropsType & MergePropsOptions, 13 | defaultProps: Partial, 14 | globalComponentConfig: Partial = {}, 15 | ): RequireKey { 16 | const { _ignorePropsFromGlobal } = componentProps; 17 | const _defaultProps = useMemo(() => { 18 | return { 19 | ...defaultProps, 20 | ...(_ignorePropsFromGlobal ? {} : globalComponentConfig), 21 | }; 22 | }, [defaultProps, globalComponentConfig, _ignorePropsFromGlobal]); 23 | 24 | const props = useMemo(() => { 25 | // Must remove property of MergePropsOptions before passing it to component 26 | const mProps = omit(componentProps, [ 27 | '_ignorePropsFromGlobal', 28 | ]) as PropsType; 29 | 30 | // https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react/src/ReactElement.js#L312 31 | for (const propName in _defaultProps) { 32 | if (mProps[propName] === undefined) { 33 | mProps[propName] = _defaultProps[propName]!; 34 | } 35 | } 36 | 37 | return mProps; 38 | }, [componentProps, _defaultProps]); 39 | 40 | return props as RequireKey; 41 | } 42 | 43 | export default useMergeProps; 44 | export { useMergeProps }; 45 | -------------------------------------------------------------------------------- /src/hooks/usePropsState/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | function usePropsState(value: S) { 4 | const [state, setState] = useState(value); 5 | const isMounted = useRef(false) 6 | 7 | useEffect(() => { 8 | if(isMounted.current) { 9 | setState(value) 10 | } else { 11 | isMounted.current = true 12 | } 13 | }, [value]) 14 | 15 | return [state, setState] as const 16 | } 17 | 18 | export default usePropsState 19 | export { usePropsState } -------------------------------------------------------------------------------- /src/hooks/useRTDraw/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useRTDraw 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: Dom 9 | order: 3 10 | --- 11 | 12 | ## useRTDraw 13 | 14 | 一个用于 `Canvas` 实时绘画的钩子,大概为每秒 60 次的刷新。 15 | 16 | 主要采用 `requestAnimationFrame` 来进行实时绘画,并对高刷屏和高分辨率屏做了一定兼容。 17 | 18 | 对于不支持 `requestAnimationFrame` 的采用 `setInterval` 替代。 19 | 20 | ## 演示 21 | 22 | ### 常规使用 23 | 24 | 25 | 26 | ## 返回参数 27 | 28 | | 属性名 | 描述 | 类型 | 29 | | -- | -- | -- | 30 | | canvasRef | 用于和 canvas 的 ref 相绑定 | HTMLCanvasElement | 31 | | drawState | 用于绘画的 state 包含 上下文,设备像素比和是否为高刷屏的判断 | {ctx: CanvasRenderingContext2D \| null; ratio: number; isHighRefreshScreen: boolean | undefined;} | 32 | | canvasInfo | 用于设置 canvas 的宽高 | {w: number, h: number} | 33 | | setCanvasInfo | 设置 canvas 的宽高信息 | (p: {w: number, h: number}) => void | 34 | | getCtx | 用于获取 canvas 的上下文 | () => void | 35 | | startAnimation | 开启动画的回调 | () => void | 36 | | cancelAnimation | 结束动画的回调 | () => void | 37 | -------------------------------------------------------------------------------- /src/hooks/useRTDraw/index.ts: -------------------------------------------------------------------------------- 1 | import useRTDraw from "./useRTDraw" 2 | 3 | export { useRTDraw } 4 | export default useRTDraw 5 | export type { UseRTDrawParams } from "./type" -------------------------------------------------------------------------------- /src/hooks/useRTDraw/type.ts: -------------------------------------------------------------------------------- 1 | export type CanvasInfo = {w: number, h: number} 2 | 3 | export type DrawStateType = { 4 | ctx: CanvasRenderingContext2D 5 | ratio: number 6 | isHighRefreshScreen: boolean 7 | } 8 | 9 | export type UseRTDrawParams = { 10 | canvasInfo?: CanvasInfo 11 | onDraw: (p: {ctx: CanvasRenderingContext2D, ratio: number}) => void 12 | } -------------------------------------------------------------------------------- /src/hooks/useRTDraw/useRTDraw.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import useLatest from "../useLatest"; 3 | import { CanvasInfo, DrawStateType, UseRTDrawParams } from "./type"; 4 | import sleep from "../../utils/sleep"; 5 | import { getScreenFps } from "./utils"; 6 | 7 | const useRTDraw = ({canvasInfo: pCanvasInfo, onDraw}: UseRTDrawParams) => { 8 | const timer = useRef(0) 9 | const canvasRef = useRef(null) 10 | const [ratio, setRatio] = useState(1); 11 | const drawState = useRef({ 12 | ctx: void 0 as CanvasRenderingContext2D | undefined, 13 | isHighRefreshScreen: void 0 as boolean | undefined, 14 | }) 15 | const [canvasInfo, setCanvasInfo] = useState(pCanvasInfo ?? {w: 200, h: 200}) 16 | const latest = useLatest({onDraw}) 17 | 18 | function getCtx() { 19 | if(drawState.current.ctx) return 20 | const ctx = canvasRef.current?.getContext('2d') 21 | if(ctx) { 22 | drawState.current.ctx = ctx 23 | } 24 | } 25 | 26 | useEffect(() => { 27 | getCtx() 28 | setRatio(window.devicePixelRatio) 29 | getScreenFps().then(fps => { 30 | drawState.current.isHighRefreshScreen = fps > 65 31 | }) 32 | return () => { 33 | cancelAnimation() 34 | } 35 | }, []) 36 | 37 | function onDrawFn() { 38 | latest.current.onDraw({ctx: drawState.current.ctx!, ratio}); 39 | } 40 | 41 | /** 高刷屏锁帧,锁帧会使绘画出现掉帧 */ 42 | function startAnimationLockFrame() { 43 | const fps = 60; 44 | let fpsInterval = 1000 / fps; 45 | let then = Date.now(); 46 | (function go() { 47 | timer.current = requestAnimationFrame(go); 48 | const now = Date.now(); 49 | const elapsed = now - then; 50 | if (elapsed > fpsInterval) { 51 | onDrawFn() 52 | then = now - (elapsed % fpsInterval); 53 | } 54 | })(); 55 | } 56 | 57 | /** 开启动画绘画 */ 58 | function startAnimation() { 59 | getCtx() 60 | if(!canvasRef.current) { 61 | console.warn('useRTDraw: Please bind the ref of canvas') 62 | return 63 | } 64 | if(!drawState.current.ctx) { 65 | console.warn('useRTDraw: Canvas context retrieval failed, can call getCtx to retrieve again') 66 | return 67 | } 68 | // 兼容性处理 69 | if(typeof requestAnimationFrame === 'undefined') { 70 | clearInterval(timer.current) 71 | timer.current = setInterval(() => { 72 | onDrawFn() 73 | }, 16.6) 74 | return 75 | } 76 | function runDraw() { 77 | if(timer.current) { 78 | cancelAnimationFrame(timer.current as number); 79 | } 80 | if(drawState.current.isHighRefreshScreen) { 81 | startAnimationLockFrame() 82 | } else { 83 | (function go() { 84 | timer.current = requestAnimationFrame(go); 85 | onDrawFn() 86 | })(); 87 | } 88 | } 89 | // 等待是否为高刷屏的判断 90 | if(drawState.current.isHighRefreshScreen === void 0) { 91 | sleep(1200).then(() => { 92 | runDraw() 93 | }) 94 | return 95 | } 96 | runDraw() 97 | } 98 | 99 | function cancelAnimation() { 100 | if(typeof cancelAnimationFrame === 'undefined') { 101 | clearInterval(timer.current) 102 | } else { 103 | cancelAnimationFrame(timer.current as number) 104 | } 105 | } 106 | 107 | return { 108 | canvasRef, 109 | canvasInfo, 110 | drawState: { 111 | ...drawState.current, 112 | ratio, 113 | } as DrawStateType, 114 | getCtx, 115 | setCanvasInfo, 116 | startAnimation, 117 | cancelAnimation, 118 | } 119 | } 120 | 121 | export default useRTDraw -------------------------------------------------------------------------------- /src/hooks/useRTDraw/utils.ts: -------------------------------------------------------------------------------- 1 | /** 经过多少次计算后,获取fps */ 2 | export const getScreenFps = (total: number = 60): Promise => { 3 | return new Promise(resolve => { 4 | if(typeof requestAnimationFrame === 'undefined') { 5 | return resolve(60) 6 | } 7 | const begin = Date.now(); 8 | let count = 0; 9 | (function run() { 10 | requestAnimationFrame(() => { 11 | if (++count >= total) { 12 | const fps = Math.ceil((count / (Date.now() - begin)) * 1000) 13 | return resolve(fps) 14 | } 15 | run() 16 | }) 17 | })() 18 | }) 19 | } -------------------------------------------------------------------------------- /src/hooks/useRender/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | const useRender = () => { 4 | const [isRender, setIsRender] = useState(false); 5 | return { 6 | isRender, 7 | /** 手动render页面 */ 8 | renderFn: useCallback(() => { 9 | setIsRender((v) => !v); 10 | }, []), 11 | }; 12 | }; 13 | 14 | export { useRender }; 15 | export default useRender; 16 | -------------------------------------------------------------------------------- /src/hooks/useScrollBottom/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useScrollBottom 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: Dom 9 | order: 3 10 | --- 11 | 12 | ## useScrollBottom 13 | 14 | 监听滚动到底部触发的钩子,可指定到达底部的距离。 15 | 16 | ## 演示 17 | 18 | ### 通过 ref 来监听 19 | 20 | 21 | 22 | ### 通过 querySelector 来监听 23 | 24 | 25 | 26 | ### 监听浏览器窗口是否到底 27 | 28 | 不穿 `ref` 和 `querySelector` 默认监听浏览器窗口的滚动 29 | 30 | 31 | 32 | ## API 33 | 34 | ### UseScrollBottomParams 35 | 36 | | 属性 | 描述 | 类型 | 默认值 | 37 | | ----------------- | -------------------- | ------------------------------ | ------ | 38 | | `ref` | 绑定的滚动元素 | `React.RefObject` | `-` | 39 | | `querySelector` | 监听其他元素 | `string` | `-` | 40 | | `bottom` | 距离底部触发的距离 | `number` | `30` | 41 | | `onScrollToLower` | 滚动到底部触发的事件 | `() => void` | `-` | 42 | -------------------------------------------------------------------------------- /src/hooks/useScrollBottom/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | export type UseScrollBottomParams = { 4 | /** 绑定的滚动元素 */ 5 | ref?: React.RefObject; 6 | /** 监听其他元素 */ 7 | querySelector?: string; 8 | /** 9 | * 距离底部触发的距离 10 | * @default 30 11 | */ 12 | bottom?: number; 13 | /** 滚动到底部触发的事件 */ 14 | onScrollToLower?: () => void; 15 | }; 16 | 17 | function useScrollBottom(p?: UseScrollBottomParams) { 18 | const { bottom = 30, ...oth } = p || {}; 19 | const timerRef = useRef(null); 20 | const [isScrollToBottom, setIsScrollToBottom] = useState(false); 21 | const option = useRef({ ...oth }); 22 | option.current = { ...oth }; 23 | 24 | useEffect(() => { 25 | let dom: HTMLElement | null = document.documentElement; 26 | const handleScroll = () => { 27 | if (!dom) return; 28 | if (timerRef.current) { 29 | clearTimeout(timerRef.current); 30 | } 31 | timerRef.current = setTimeout(() => { 32 | const { scrollTop, clientHeight, scrollHeight } = dom!; 33 | const isB = scrollTop + clientHeight + bottom >= scrollHeight; 34 | setIsScrollToBottom(isB); 35 | if (isB) { 36 | option.current.onScrollToLower?.(); 37 | } 38 | }, 30); 39 | }; 40 | 41 | if (option.current.ref) { 42 | option.current.ref.current?.addEventListener("scroll", handleScroll); 43 | dom = option.current.ref.current; 44 | } else if (option.current.querySelector) { 45 | const el = document.querySelector( 46 | option.current.querySelector 47 | ) as HTMLElement; 48 | el?.addEventListener("scroll", handleScroll); 49 | dom = el; 50 | } else { 51 | window.addEventListener("scroll", handleScroll); 52 | } 53 | return () => { 54 | if (option.current.ref || option.current.querySelector) { 55 | dom?.removeEventListener("scroll", handleScroll); 56 | } else { 57 | window.removeEventListener("scroll", handleScroll); 58 | } 59 | }; 60 | }, [bottom]); 61 | 62 | return { isScrollToBottom }; 63 | } 64 | 65 | export { useScrollBottom }; 66 | export default useScrollBottom; 67 | -------------------------------------------------------------------------------- /src/hooks/useSearchParamsFilter/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSearchParamsFilter 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: url地址栏 9 | order: 3 10 | --- 11 | 12 | ## useSearchParamsFilter 13 | 14 | 用来给地址栏设置参数。 15 | 16 | ## 演示 17 | 18 | 注意查看地址栏的改变 19 | 20 | ### 常规使用 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/hooks/useSearchParamsFilter/index.ts: -------------------------------------------------------------------------------- 1 | function useSearchParamsFilter() { 2 | const searchParams = new URLSearchParams(window.location.search); 3 | 4 | const setParam = (name: T, value?: string | number) => { 5 | if (value) { 6 | searchParams.set(name, encodeURIComponent(String(value))); 7 | } else { 8 | if (!searchParams.get(name)) { 9 | return; 10 | } 11 | searchParams.delete(name); 12 | } 13 | // 更新地址栏 14 | window.history.replaceState({}, "", "?" + searchParams.toString()); 15 | }; 16 | 17 | const getParam = (name: T) => { 18 | const value = searchParams.get(name); 19 | return value ? decodeURIComponent(value ?? "") : void 0; 20 | }; 21 | 22 | return { 23 | searchParams, 24 | setParam, 25 | getParam, 26 | } as const; 27 | } 28 | 29 | export default useSearchParamsFilter; 30 | export { useSearchParamsFilter }; 31 | -------------------------------------------------------------------------------- /src/hooks/useSearchSetState/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSearchSetState 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: url地址栏 9 | order: 3 10 | --- 11 | 12 | ## useSearchSetState 13 | 14 | 跟 `useState` 中的 `Object` 用法基本一致,需要传入地址栏的参数的 `key` 值,`state` 的值会与地址栏参数保持一致。 15 | 16 | - keys: 以 字符串.字符串 的形式代表嵌套对象的key;例: ['name','a.b'] => {name: '', a: {b: ''}} 17 | 18 | ## 演示 19 | 20 | 注意查看地址栏的改变 21 | 22 | ### 常规使用 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/hooks/useSearchSetState/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useSearchParamsFilter from '../useSearchParamsFilter'; 3 | import { forEachObject, getUrlParamsToObject, getInitialState } from './utils'; 4 | 5 | /** 6 | * 设置一个与地址栏参数相绑定对象 7 | * @param keys: 以 字符串.字符串 的形式代表嵌套对象的key;例: ['name','a.b'] => {name: '', a: {b: ''}} 8 | */ 9 | function useSearchSetState>( 10 | keys?: string[], 11 | initialState?: S | (() => S), 12 | ) { 13 | const { setParam, getParam } = useSearchParamsFilter(); 14 | const [state, setState] = useState(() => { 15 | const initState = getInitialState(initialState); 16 | let numberKeys: string[] = []; 17 | if (initState) { 18 | forEachObject(initState, (itemObj, key, allKey) => { 19 | // @ts-ignore 20 | if (typeof itemObj[key] === 'number') { 21 | numberKeys.push(allKey); 22 | } 23 | }); 24 | } 25 | const urlValue = getUrlParamsToObject(keys, getParam, numberKeys); 26 | return { ...initState, ...urlValue }; 27 | }); 28 | 29 | const setSearchSetState = (patch: S | ((v: S) => S)) => { 30 | const newState = typeof patch === 'function' ? patch(state) : patch; 31 | setState(newState); 32 | if (keys) { 33 | forEachObject({ ...state, ...newState }, (itemObj, key, allKey, i) => { 34 | if (keys.includes(allKey)) { 35 | const val = !i ? newState[key] : ( 36 | // @ts-ignore 37 | newState[allKey.split('.')[0]] ? itemObj[key] : '' 38 | ) 39 | setParam(allKey, val ? String(val) : ''); 40 | } 41 | }); 42 | } 43 | }; 44 | 45 | return [state, setSearchSetState] as const; 46 | } 47 | 48 | export default useSearchSetState 49 | export { useSearchSetState, getUrlParamsToObject }; 50 | -------------------------------------------------------------------------------- /src/hooks/useSearchSetState/utils.ts: -------------------------------------------------------------------------------- 1 | import { isObj } from "../../utils"; 2 | 3 | /** 4 | * 获取url参数并将其转化为一个对象 5 | * @param keys: 以 字符串.字符串 的形式代表嵌套对象的key;例: ['name','a.b'] => {name: '', a: {b: ''}} 6 | * @param getParam: 获取地址参数的方法: const {getParam} = useSearchParamsFilter(); 7 | * @param numberKeys?: 需要将字符串转化为number的key 8 | */ 9 | export function getUrlParamsToObject( 10 | keys?: string[], 11 | getParam?: (v: string) => string | undefined, 12 | numberKeys?: string[], 13 | ) { 14 | const urlVal: any = {}; 15 | keys?.forEach((key) => { 16 | const paramsVal = getParam?.(key) ?? ''; 17 | if (paramsVal) { 18 | const keysArr = key.split('.'); 19 | let tempObj = urlVal; 20 | keysArr.forEach((k, index) => { 21 | if (index === keysArr.length - 1) { 22 | tempObj[k] = numberKeys?.includes(key) ? +paramsVal : paramsVal; 23 | } else { 24 | if (!tempObj[k]) { 25 | tempObj[k] = {}; 26 | } 27 | } 28 | tempObj = tempObj[k]; 29 | }); 30 | } 31 | }); 32 | return urlVal; 33 | } 34 | 35 | /** 遍历一个多层嵌套的对象 */ 36 | export function forEachObject( 37 | obj: object, 38 | fn: (itemObj: object, key: string, allKey: string, i: number) => void, 39 | ) { 40 | if (!obj) return; 41 | function forEachObjectFn(itemObj: object, prefixKey?: string, i = 0) { 42 | Object.keys(itemObj).forEach((key) => { 43 | const allKey = !prefixKey ? key : `${prefixKey}.${key}`; 44 | // @ts-ignore 45 | if (isObj(itemObj[key])) { 46 | // @ts-ignore 47 | forEachObjectFn(itemObj[key], allKey, i + 1); 48 | } else { 49 | fn?.(itemObj, key, allKey, i); 50 | } 51 | }); 52 | } 53 | forEachObjectFn(obj); 54 | } 55 | 56 | export const getInitialState = (initialState: T | (() => T)): T => ( 57 | initialState instanceof Function ? initialState() : initialState 58 | ) -------------------------------------------------------------------------------- /src/hooks/useSearchState/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useSearchState 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: url地址栏 9 | order: 3 10 | --- 11 | 12 | ## useSearchState 13 | 14 | 跟 `useState` 用法基本一致,需要传入地址栏的参数的 `key` 值,`state` 的值会与地址栏参数保持一致。 15 | 16 | ## 演示 17 | 18 | 注意查看地址栏的改变 19 | 20 | ### 常规使用 21 | 22 | 23 | 24 | ### 数字,数组,对象等非String类型 25 | 26 | - 建议:处理 `Object` 类型时最好用 `useSearchSetState`,这样地址栏会清晰很多。 27 | 28 | 29 | 30 | ## 注意 31 | 32 | 使用该钩子时,不要在一个函数内 `setParam` 多次,否则会有闭包问题,导致url参数前值被后值覆盖; 33 | 34 | 如果真的需要这样做,请用 `useSearchParamsFilter` 替代,下面是一个使用例子。 35 | 36 | #### 错误用法 error 37 | 38 | 39 | 40 | #### 正确用法 true 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/hooks/useSearchState/index.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useSearchParamsFilter from '../useSearchParamsFilter'; 3 | 4 | /** 5 | * 注意:使用该钩子时,不要在一个函数内 setParam 多次,否则会有闭包问题,导致url参数前值被后值覆盖 6 | */ 7 | function useSearchState( 8 | key: string, 9 | initialState?: S | (() => S), 10 | p?: UseSearchStateParams 11 | ) { 12 | const { setParam, getParam } = useSearchParamsFilter(); 13 | const [state, setState] = useState(() => { 14 | let param = getParam(key) 15 | if(param && p?.isNotString) { 16 | param = JSON.parse(param) 17 | } 18 | return (param ?? initialState ?? '') as S 19 | }); 20 | 21 | const setSearchState = (patch: S | ((v: S) => S)) => { 22 | const v = patch instanceof Function ? patch(state) : patch; 23 | setState(v); 24 | let params = '' 25 | if(p?.isNotString) { 26 | params = JSON.stringify(v) 27 | } else { 28 | params = v ? String(v) : '' 29 | } 30 | setParam(key, params); 31 | p?.clearKeys?.forEach((key) => { 32 | setParam(key, ''); 33 | }); 34 | }; 35 | 36 | return [state, setSearchState] as const; 37 | } 38 | 39 | export default useSearchState 40 | export { useSearchState } 41 | 42 | export type UseSearchStateParams = { 43 | /** 设置key的同时清除其他的key */ 44 | clearKeys?: string[] 45 | /** 是否不是string类型 */ 46 | isNotString?: boolean 47 | } -------------------------------------------------------------------------------- /src/hooks/useTouch/doc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchState } from '.'; 3 | 4 | /** 函数类型 */ 5 | export const UseTouchStateMd: React.FC = () => { 6 | return <> 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/useTouch/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useTouch 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: Dom 9 | order: 3 10 | --- 11 | 12 | ## useTouch 13 | 14 | 用于计算触摸事件的参数,获取触摸开始的坐标和触摸距离、位移等信息。 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ## API 23 | 24 | ### TouchState 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/hooks/useTouch/index.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { UseTouchStateMd } from './doc'; 3 | import { TouchEventType } from '../useTouchEvent/type'; 4 | const MIN_DISTANCE = 10; 5 | 6 | export type TouchDirection = '' | 'vertical' | 'horizontal'; 7 | export type TouchState = { 8 | /** x的起始的位置 */ 9 | startX: number; 10 | /** y的起始的位置 */ 11 | startY: number; 12 | /** x的偏移量 */ 13 | deltaX: number; 14 | /** y的偏移量 */ 15 | deltaY: number; 16 | /** x的位移 正数 */ 17 | offsetX: number; 18 | /** y的位移 正数 */ 19 | offsetY: number; 20 | /** 相对于浏览器的x坐标 */ 21 | clientX: number; 22 | /** 相对于浏览器的y坐标 */ 23 | clientY: number; 24 | /** 当前移动的方向 */ 25 | direction: TouchDirection; 26 | /** 触摸开始到结束的时间 */ 27 | time: number; 28 | }; 29 | 30 | /** 返回的类型 */ 31 | export type MouseTouchE = { 32 | pageX: number; 33 | pageY: number; 34 | clientX: number; 35 | clientY: number; 36 | screenX: number; 37 | screenY: number; 38 | }; 39 | 40 | function getDirection(x: number, y: number) { 41 | if (x > y && x > MIN_DISTANCE) { 42 | return 'horizontal'; 43 | } 44 | if (y > x && y > MIN_DISTANCE) { 45 | return 'vertical'; 46 | } 47 | return ''; 48 | } 49 | const changeEvent = (event: TouchEventType | MouseTouchE) => { 50 | // changedTouches 是 touchEnd 的值 51 | return ( 52 | (event as TouchEventType)?.touches?.[0] ?? 53 | (event as TouchEventType)?.changedTouches?.[0] ?? 54 | (event as MouseTouchE) 55 | ); 56 | }; 57 | 58 | const useTouch = () => { 59 | const state = useRef({ 60 | startX: 0, 61 | startY: 0, 62 | deltaX: 0, 63 | deltaY: 0, 64 | offsetX: 0, 65 | offsetY: 0, 66 | clientX: 0, 67 | clientY: 0, 68 | direction: '', 69 | time: 0, 70 | }); 71 | /** 触摸开始时间 */ 72 | const startTime = useRef(0); 73 | 74 | const setState = (options: Partial) => { 75 | Object.keys(options).forEach((_key) => { 76 | const key = _key as keyof TouchState 77 | state.current[key] = options[key] as never; 78 | }); 79 | }; 80 | 81 | const reset = () => { 82 | setState({ 83 | deltaX: 0, 84 | deltaY: 0, 85 | offsetX: 0, 86 | offsetY: 0, 87 | direction: '', 88 | }); 89 | }; 90 | 91 | const start = (event: TouchEventType | MouseTouchE) => { 92 | reset(); 93 | const touch = changeEvent(event); 94 | setState({ 95 | startX: touch.clientX, 96 | startY: touch.clientY, 97 | clientX: touch.clientX, 98 | clientY: touch.clientY, 99 | }); 100 | startTime.current = Date.now(); 101 | }; 102 | 103 | const move = (event: TouchEventType | MouseTouchE) => { 104 | const touch = changeEvent(event); 105 | // Fix: Safari back will set clientX to negative number 106 | const { startX, startY, direction } = state.current; 107 | const deltaX = touch.clientX < 0 ? 0 : touch.clientX - startX; 108 | const deltaY = touch.clientY - startY; 109 | const offsetX = Math.abs(deltaX); 110 | const offsetY = Math.abs(deltaY); 111 | const time = Date.now() - startTime.current; 112 | 113 | setState({ 114 | deltaX, 115 | deltaY, 116 | offsetX, 117 | offsetY, 118 | clientX: touch.clientX, 119 | clientY: touch.clientY, 120 | time, 121 | direction: !direction ? getDirection(offsetX, offsetY) : '', 122 | }); 123 | }; 124 | 125 | return { 126 | info: state.current, 127 | move, 128 | start, 129 | reset, 130 | }; 131 | }; 132 | 133 | export default useTouch; 134 | export { useTouch, UseTouchStateMd } -------------------------------------------------------------------------------- /src/hooks/useTouchEvent/doc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { UseTouchEventParams } from '.'; 3 | 4 | /** 函数类型 */ 5 | export const UseTouchEventParamsMd: React.FC = () => { 6 | return <> 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/useTouchEvent/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: useTouchEvent 3 | nav: 4 | title: 钩子 5 | path: /components 6 | order: 2 7 | group: 8 | title: Dom 9 | order: 3 10 | --- 11 | 12 | ## useTouchEvent 13 | 14 | 一个兼容pc端和移动端的触摸事件的钩子。 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ## API 23 | 24 | ### UseTouchEventParams 25 | 26 | 27 | 28 | ### TouchState 29 | 30 | -------------------------------------------------------------------------------- /src/hooks/useTouchEvent/index.ts: -------------------------------------------------------------------------------- 1 | import { UseTouchEventParamsMd } from './doc'; 2 | import useTouchEvent from './useTouchEvent'; 3 | 4 | export { useTouchEvent, UseTouchEventParamsMd }; 5 | export default useTouchEvent; 6 | export type { UseTouchEventParams, UseTouchesOptions, UseTouchesParams } from './type'; 7 | -------------------------------------------------------------------------------- /src/hooks/useTouchEvent/type.ts: -------------------------------------------------------------------------------- 1 | import { TouchState } from '../useTouch'; 2 | 3 | /** 鼠标事件 */ 4 | // export type MouseEventType = React.MouseEvent; 5 | // export type TouchEventType = React.TouchEvent 6 | export type MouseEventType = React.MouseEvent | MouseEvent; 7 | export type TouchEventType = React.TouchEvent; 8 | /** 鼠标或手指事件 */ 9 | export type MouseTouchEvent = MouseEventType | TouchEventType; 10 | 11 | export type UseTouchesOptions = { 12 | /** 点击触摸开始 */ 13 | onTouchStart?: (e: MouseTouchEvent) => void; 14 | /** 触摸移动 */ 15 | onTouchMove?: (e: MouseTouchEvent, touchState?: TouchState) => void; 16 | /** 触摸结束 */ 17 | onTouchEnd?: (e: MouseTouchEvent) => void; 18 | }; 19 | 20 | export type UseTouchesParams = { 21 | /** 是否鼠标所有事件(左键右键中键)都触发 */ 22 | isAllMouseClick?: boolean 23 | /** 都阻止 */ 24 | isStopEvent?: boolean; 25 | /** 是否阻止事件冒泡 */ 26 | isStopPropagation?: boolean; 27 | /** 是否阻止事件默认行为 */ 28 | isPreventDefault?: boolean; 29 | /** 是否禁用事件 */ 30 | isDisable?: { 31 | /** 禁用所有事件 */ 32 | all?: boolean 33 | onTouchStart?: boolean 34 | onTouchMove?: boolean 35 | onTouchEnd?: boolean 36 | } 37 | } & IsTouchEvent; 38 | export type IsTouchEvent = { 39 | /** 是否需要监听 onMouseUp 注意:会导致 onTouchEnd 触发两次 */ 40 | isOnMouseUp?: boolean; 41 | /** 是否需要监听 OnTouchCancel 注意:会导致 onTouchEnd 触发两次 */ 42 | isOnTouchCancel?: boolean; 43 | }; 44 | export type UseTouchEventParams = UseTouchesOptions & UseTouchesParams; -------------------------------------------------------------------------------- /src/hooks/useTouchEvent/useTouchEvent.ts: -------------------------------------------------------------------------------- 1 | import { isMobile } from '../../utils/handleDom'; 2 | import useTouch from '../useTouch'; 3 | import useLatest from '../useLatest'; 4 | import { IsTouchEvent, MouseTouchEvent, UseTouchesOptions, UseTouchEventParams } from './type'; 5 | 6 | /** 绑定手指触摸或鼠标事件 */ 7 | export default function useTouchEvent(options: UseTouchEventParams = {}) { 8 | const touch = useTouch(); 9 | const optionsRef = useLatest(options); 10 | 11 | const onStopEvent = (e: MouseTouchEvent) => { 12 | if (options.isStopEvent || options.isStopPropagation) { 13 | e.stopPropagation(); 14 | } 15 | if (options.isStopEvent || options.isPreventDefault) { 16 | e.preventDefault(); 17 | } 18 | }; 19 | 20 | const onTouchStart = (e: MouseTouchEvent) => { 21 | /** 鼠标左击才触发 */ 22 | if(!isMobile() && !options.isAllMouseClick && (e as MouseEvent).button !== 0) { 23 | return 24 | } 25 | if(options.isDisable?.all || options.isDisable?.onTouchStart) return 26 | onStopEvent(e); 27 | touch.start(e); 28 | if (!isMobile()) { 29 | document.addEventListener('mousemove', onTouchMove, true); 30 | document.addEventListener('mouseup', onTouchEnd, true); 31 | } 32 | optionsRef.current.onTouchStart?.(e); 33 | }; 34 | const onTouchMove = (e: MouseTouchEvent) => { 35 | if(options.isDisable?.all || options.isDisable?.onTouchMove) return 36 | onStopEvent(e); 37 | touch.move(e); 38 | optionsRef.current.onTouchMove?.(e, touch.info); 39 | }; 40 | const onTouchEnd = (e: MouseTouchEvent) => { 41 | if(options.isDisable?.all || options.isDisable?.onTouchEnd) return 42 | onStopEvent(e); 43 | touch.move(e); 44 | if (!isMobile()) { 45 | document.removeEventListener('mousemove', onTouchMove, true); 46 | document.removeEventListener('mouseup', onTouchEnd, true); 47 | } 48 | optionsRef.current.onTouchEnd?.(e); 49 | }; 50 | 51 | return { 52 | ...touch, 53 | onTouchFn: onTouchMouse({ 54 | onTouchStart, 55 | onTouchMove, 56 | onTouchEnd, 57 | isOnMouseUp: options.isOnMouseUp, 58 | isOnTouchCancel: options.isOnTouchCancel, 59 | }), 60 | }; 61 | } 62 | 63 | /** 处理鼠标或手指触摸事件 */ 64 | export const onTouchMouse = ({ 65 | onTouchStart, 66 | onTouchMove, 67 | onTouchEnd, 68 | isOnMouseUp, 69 | isOnTouchCancel, 70 | }: UseTouchesOptions & IsTouchEvent) => { 71 | if (!isMobile()) { 72 | return { 73 | onMouseDown: onTouchStart, 74 | ...(isOnMouseUp ? { onMouseUp: onTouchEnd } : null), 75 | }; 76 | } else { 77 | return { 78 | onTouchStart: onTouchStart, 79 | onTouchMove: onTouchMove, 80 | onTouchEnd: onTouchEnd, 81 | ...(isOnTouchCancel ? { onTouchCancel: onTouchEnd } : null), 82 | }; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/huarong-road/config.ts: -------------------------------------------------------------------------------- 1 | export const ITEM_NUM = 10; 2 | 3 | export const heroesList = ['曹操','张飞','赵云','马超','关羽','黄忠','卒','卒','卒','卒'] -------------------------------------------------------------------------------- /src/huarong-road/context.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HeroesIndex } from "./type"; 3 | import { GridPosition } from "../slider-puzzle/type"; 4 | import { Direction } from "../utils"; 5 | 6 | export const HuarongRoadCtx = React.createContext({ 7 | gap: 2, 8 | locationArr: [], 9 | gridArr: [], 10 | gridSize: 50, 11 | isReset: false, 12 | onChangeGrid: (p: onChangeGridParams) => {} 13 | }) 14 | 15 | export type HuarongRoadCtxType = { 16 | gap: number 17 | /** 初始的格子信息 */ 18 | locationArr: HeroesIndex[][] 19 | /** 变化的格子信息 */ 20 | gridArr: HeroesIndex[][] 21 | /** 每个格子的大小 */ 22 | gridSize: number 23 | isReset: boolean 24 | onChangeGrid: (p: onChangeGridParams) => void 25 | } 26 | 27 | export type onChangeGridParams = { 28 | p: GridPosition 29 | target: GridPosition 30 | /** 1:上 2:右 3:下 4:左 */ 31 | direction: Direction 32 | index: number 33 | } -------------------------------------------------------------------------------- /src/huarong-road/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: HuarongRoad 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 非常规组件 9 | order: 1 10 | --- 11 | 12 | ## 华容道组件 13 | 14 | 由曹操,五虎将和4个小卒组成的华容道小游戏。 15 | 16 | 整体是 4 * 5 的格子,曹操占4格,五虎将横或竖向占2格,卒占1格,还有两个空格,当曹操移动到最底下时,游戏获胜。 17 | 18 | ## 演示 19 | 20 | ### 常规使用 21 | 22 | 23 | 24 | ### 自定义曹操,五虎将和小兵的位置 25 | 26 | 27 | 28 | ### 调整滑块间的间隙 29 | 30 | 31 | 32 | ### 随机色块华容道 33 | 34 | 35 | 36 | ## API 37 | 38 | ### HuarongRoad 39 | 40 | 41 | 42 | ### HuarongRoad.Item 43 | 44 | 45 | 46 | ### HeroesIndex 47 | 48 | | 值 | 描述 | 占位 | 49 | | ---- | ---- | ---- | 50 | | 1 | 曹操(boss) | 占4格 | 51 | | 21 - 25 | 五虎将 | 横或竖向占2格 | 52 | | 31 - 34 | 卒 | 占1格 | -------------------------------------------------------------------------------- /src/huarong-road/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-huarongRoad'; 2 | $class-prefix-item: 'lhhui-huarongRoadItem'; 3 | 4 | .#{$class-prefix} { 5 | box-sizing: border-box; 6 | width: 100%; 7 | height: 100%; 8 | &-area { 9 | width: 100%; 10 | height: 100%; 11 | position: relative; 12 | } 13 | &-fillItem { 14 | box-sizing: border-box; 15 | border: 1px solid #ccc; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | font-size: 20px; 20 | font-weight: bold; 21 | user-select: none; 22 | } 23 | } 24 | 25 | .#{$class-prefix-item} { 26 | position: absolute; 27 | transition: transform ease .4s; 28 | -webkit-tap-highlight-color: transparent; 29 | // 使触摸事件不产生默认行为 30 | touch-action: none; 31 | cursor: grab; 32 | &-hover:hover { 33 | filter: drop-shadow(1px 1px 2px rgba(82, 82, 82, 0.5)); 34 | } 35 | &-disableTouch { 36 | cursor: not-allowed; 37 | &:hover { 38 | filter: drop-shadow(1px 1px 1px rgba(168, 168, 168, 0.5)); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/huarong-road/index.tsx: -------------------------------------------------------------------------------- 1 | import { attachPropertiesToComponent } from '../utils/attach-properties-to-component'; 2 | import './index.scss'; 3 | import HuarongRoad_ from './huarong-road'; 4 | import HuarongRoadItem from './huarong-road-item'; 5 | 6 | const HuarongRoad = attachPropertiesToComponent(HuarongRoad_, { 7 | Item: HuarongRoadItem, 8 | }); 9 | 10 | export { HuarongRoad, HuarongRoadItem }; 11 | export type { 12 | HuarongRoadProps, 13 | HuarongRoadItemProps, 14 | HuarongRoadInstance, 15 | HeroesIndex, 16 | } from './type'; 17 | export default HuarongRoad; 18 | -------------------------------------------------------------------------------- /src/huarong-road/type.ts: -------------------------------------------------------------------------------- 1 | import { NativeProps } from "../utils/native-props"; 2 | 3 | export type HuarongRoadProps = { 4 | /** 5 | * 整体的宽度(高度等于宽度的1.25倍) 6 | * @default 100% 7 | */ 8 | width?: string | number 9 | /** 10 | * 英雄的位置 11 | * @default 12 | * [ 13 | * [21, 1, 1, 22], 14 | * [21, 1, 1, 22], 15 | * [23, 24, 24, 25], 16 | * [23, 31, 32, 25], 17 | * [33, 0, 0, 34], 18 | * ] 19 | */ 20 | locationArr?: HeroesIndex[][] 21 | /** 22 | * 遍历item的数组的长度 (当children的长度和item不一致时请传入该值) 23 | * @default children 的长度 24 | */ 25 | listLength?: number 26 | /** 27 | * 滑块之间的间隙 单位px 28 | * @default 2 29 | */ 30 | gap?: number 31 | /** 数量不够作为补充的卡片的类名*/ 32 | fillItemClassName?: string 33 | /** 拼图完成了的回调 */ 34 | onComplete?: () => void 35 | /** 拼图整体大小发生了变化的回调 */ 36 | onResize?: (gridSize: number) => void 37 | } & NativeProps 38 | 39 | export type HuarongRoadItemProps = { 40 | /** 当前item的索引(0:曹操,1-5:五虎将,6-9:卒) */ 41 | index: number 42 | /** 43 | * 触摸时间,触摸时长大于该值就无法触发 onClick 44 | * @default 150 45 | */ 46 | touchTime?: number 47 | /** 48 | * 触摸距离,触摸距离大于该值就无法触发 onClick 49 | * @default 8 50 | */ 51 | touchDistance?: number 52 | /** 是否需要鼠标hover的样式,想自定义hover,可以直接传入className */ 53 | isHover?: boolean 54 | /** 点击了卡片(触摸时间小于150ms) */ 55 | onClick?: (i: number) => void; 56 | } & NativeProps 57 | 58 | export type HuarongRoadInstance = { 59 | reset: () => void 60 | } 61 | 62 | /** 英雄的索引,1:boss,2?:五虎将(21代表两格是一个英雄),3?:卒,0:空格 */ 63 | export type HeroesIndex = 1 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 0 64 | -------------------------------------------------------------------------------- /src/huarong-road/utils.ts: -------------------------------------------------------------------------------- 1 | import { Direction, DirectionType } from "../utils"; 2 | import { HeroesIndex } from "./type"; 3 | 4 | export const getPositionItem = ( 5 | {gridSize, index, locationArr, gap}: { 6 | gridSize: number 7 | index: number 8 | locationArr: HeroesIndex[][] 9 | gap: number 10 | } 11 | ) => { 12 | const obj = { 13 | row: 0, 14 | col: 0, 15 | width: gridSize, 16 | height: gridSize, 17 | } 18 | locationArr.some((item, rowIndex) => { 19 | const colIndex = item.indexOf(handleIndex(index)) 20 | if(colIndex !== -1) { 21 | obj.row = rowIndex 22 | obj.col = colIndex 23 | if(index === 0) { 24 | obj.width = 2 * gridSize + gap 25 | obj.height = 2 * gridSize + gap 26 | } else if(index <= 5) { 27 | if(item[colIndex + 1] === item[colIndex]) { // 该五虎将是横着的 28 | obj.width = 2 * gridSize + gap 29 | } else { // 该五虎将是竖着的 30 | obj.height = 2 * gridSize + gap 31 | } 32 | } 33 | return true 34 | } 35 | return false 36 | }) 37 | return obj 38 | } 39 | 40 | export const handleIndex = (index: number): HeroesIndex => { 41 | if(index === 0) { 42 | return 1 43 | } else if(index <= 5) { 44 | return 20 + index as HeroesIndex 45 | } else { 46 | return 25 + index as HeroesIndex 47 | } 48 | } 49 | 50 | /** 获取行列的位置 */ 51 | export function getRowColItem(gridArr: HeroesIndex[][], index: number) { 52 | const obj = { 53 | row: 0, 54 | col: 0, 55 | } 56 | gridArr.some((item, rowIndex) => { 57 | const colIndex = item.indexOf(handleIndex(index)) 58 | if(colIndex !== -1) { 59 | obj.row = rowIndex 60 | obj.col = colIndex 61 | return true 62 | } 63 | return false 64 | }) 65 | return obj 66 | } 67 | 68 | type CheckDirectionRes = {[key in Direction]: number} | 0 69 | 70 | /** 检查华容道item可以移动的方向 */ 71 | export function checkRoadDirection(arr: HeroesIndex[][], row: number, col: number): CheckDirectionRes { 72 | if(!arr?.length) return 0 73 | const value = arr[row][col] 74 | if(value > 30) { // 小兵 75 | return handleHeroDirectionVal({arr, row, col, status: 4}) 76 | } else { 77 | let status: HeroesStatus = 1 78 | if(value > 20) { // 五虎将 79 | status = arr[row][col + 1] === value ? 2 : 3 80 | } 81 | return handleHeroDirectionVal({arr, row, col, status}) 82 | } 83 | } 84 | 85 | type HeroesStatus = 1 | 2 | 3 | 4 86 | /** 87 | * @param status 1: boss 2: 横着的英雄 3: 竖着的英雄 4: 卒 88 | */ 89 | function handleHeroDirectionVal({arr, row, col, status}: { 90 | arr: HeroesIndex[][], row: number, col: number, status: HeroesStatus 91 | }): CheckDirectionRes { 92 | const colNext = status === 2 || status === 1 93 | const rowNext = status === 3 || status === 1 94 | // 上右下左四个位置组成的数组。 95 | const checkArr: checkItem[] = [ 96 | {addRow: -1, addCol: 0, colNext}, 97 | {addRow: 0, addCol: 1, rowNext}, 98 | {addRow: 1, addCol: 0, colNext}, 99 | {addRow: 0, addCol: -1, rowNext}, 100 | ] 101 | const res: CheckDirectionRes = {1: 0, 2: 0, 3: 0, 4: 0} 102 | // 检查下一个格子是否为空 103 | const checkNextGrid = ({addRow, addCol, rowNext, colNext}: checkItem, i: number) => { 104 | const isColNext = colNext ? arr[row + addRow]?.[col + addCol + 1] === 0 : true 105 | const isRowNext = rowNext ? arr[row + addRow + 1]?.[col + addCol] === 0 : true 106 | if(arr[row + addRow]?.[col + addCol] === 0 && isColNext && isRowNext) { 107 | res[(i + 1) as Direction]++ 108 | checkNextGrid({ 109 | addRow: addRow += checkArr[i].addRow, 110 | addCol: addCol += checkArr[i].addCol, 111 | rowNext, 112 | colNext 113 | }, i) 114 | } 115 | } 116 | for(let i = 0; i < checkArr.length; i++) { 117 | let {addRow, addCol, ...p} = checkArr[i] 118 | if(i === 1 && colNext) addCol++; 119 | if(i === 2 && rowNext) addRow++; 120 | checkNextGrid({addRow, addCol, ...p}, i) 121 | } 122 | return Object.values(res).some(v => v) ? res : 0 123 | } 124 | type checkItem = { 125 | addRow: number 126 | addCol: number 127 | rowNext?: boolean 128 | colNext?: boolean 129 | } 130 | 131 | /** 检查是否获胜 */ 132 | export function checkToWin(arr: HeroesIndex[][]) { 133 | return arr[3][1] === 1 && arr[3][2] === 1 && arr[4][1] === 1 && arr[4][2] === 1 134 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './style/index.scss'; 2 | 3 | export * from './animation-wrap'; 4 | export * from './check-box'; 5 | export * from './circle'; 6 | export * from './floating-ball'; 7 | export * from './hooks'; 8 | export * from './huarong-road'; 9 | export * from './mobile-folder'; 10 | export * from './scroll-circle'; 11 | export * from './scroll-view'; 12 | export * from './skus'; 13 | export * from './slider-puzzle'; 14 | export * from './tabs'; 15 | export * from './tree'; 16 | export * from './utils'; 17 | -------------------------------------------------------------------------------- /src/mobile-folder/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: MobileFolder 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 非常规组件 9 | order: 1 10 | --- 11 | 12 | ## 手机文件夹组件 13 | 14 | 将一个列表类似手机文件夹那样包裹起来 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ### 自定义 children 23 | 24 | 25 | 26 | ## API 27 | 28 | ### MobileFolderProps 29 | 30 | 31 | 32 | ### MobileFolderItem 33 | 34 | | 属性名 | 描述 | 类型 | 默认值 | 35 | | ---- | ---- | ---- | ---- | 36 | | icon | 渲染的内容为图片 | `string` | `--` | 37 | | title | 标题 | `string` | `--` | 38 | | children | 自定义渲染的内容 | `ReactNode` | `--` | 39 | | onClick | 点击的回调 | `(item: MobileFolderItem, i: number) => void` | `--` | 40 | -------------------------------------------------------------------------------- /src/mobile-folder/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-mobile-folder'; 2 | $popZIndex: 1000; 3 | $popZIndexGt: 1001; 4 | .#{$class-prefix} { 5 | $size: var(--size); 6 | transition: border-radius .3s; 7 | background-color: rgba(255, 255, 255, .3); 8 | border-radius: calc($size / 6); 9 | &-area { 10 | box-sizing: border-box; 11 | display: grid; 12 | grid-template: 'a a' 'b b'; 13 | gap: calc($size / 4); 14 | padding: calc($size / 4); 15 | cursor: pointer; 16 | -webkit-tap-highlight-color: transparent; 17 | } 18 | &:hover { 19 | border-radius: calc($size / 4); 20 | } 21 | &-item { 22 | position: relative; 23 | width: $size; 24 | height: $size; 25 | user-select: none; 26 | -webkit-user-drag: none; 27 | transition: transform .3s, opacity .2s; 28 | &-last { 29 | box-sizing: border-box; 30 | display: grid; 31 | grid-template: 'a a' 'b b'; 32 | gap: calc($size / 9); 33 | } 34 | &-sub { 35 | width: calc($size / 9 * 4); 36 | height: calc($size / 9 * 4); 37 | // opacity 控制大于 4 的溢出项的提前隐藏; 38 | transition: transform 0.4s, opacity .2s; 39 | } 40 | &-overflowHide { 41 | overflow: hidden; 42 | } 43 | &-more { 44 | z-index: $popZIndexGt; 45 | overflow: visible; 46 | .#{$class-prefix}-item-sub { 47 | width: $size; 48 | height: $size; 49 | } 50 | } 51 | &-title { 52 | margin-top: 5px; 53 | font-size: 14px; 54 | color: #fff; 55 | text-align: center; 56 | } 57 | } 58 | &-icon { 59 | display: block; 60 | width: 100%; 61 | height: 100%; 62 | transition: scale .4s; 63 | user-select: none; 64 | -webkit-user-drag: none; 65 | &:hover { 66 | scale: 1.05; 67 | } 68 | } 69 | &-pop { 70 | position: fixed; 71 | left: 0; 72 | right: 0; 73 | top: 0; 74 | bottom: 0; 75 | display: none; 76 | justify-content: center; 77 | align-items: center; 78 | z-index: $popZIndex; 79 | background-color: rgba(0, 0, 0, .2); 80 | &-content { 81 | display: grid; 82 | gap: calc($size / 2); 83 | grid-template: 'a a a'; 84 | } 85 | &-item { 86 | width: $size; 87 | height: $size; 88 | user-select: none; 89 | -webkit-user-drag: none; 90 | } 91 | &-show { 92 | display: flex; 93 | opacity: 1; 94 | .#{$class-prefix} { 95 | &-pop-item { 96 | animation: popShow 0.3s ease; 97 | } 98 | } 99 | } 100 | @keyframes popShow { 101 | 0% { 102 | opacity: 0; 103 | } 104 | 99% { 105 | opacity: 0; 106 | } 107 | 100% { 108 | opacity: 1; 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/mobile-folder/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import MobileFolder from './mobile-folder'; 3 | 4 | export type { MobileFolderProps, MobileFolderItem } from './type' 5 | export { MobileFolder } 6 | export default MobileFolder -------------------------------------------------------------------------------- /src/mobile-folder/type.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { NativeProps } from "../utils/native-props"; 3 | 4 | export type MobileFolderProps = { 5 | /** 需要渲染的列表 */ 6 | list: MobileFolderItem[] 7 | } & NativeProps 8 | 9 | export type MobileFolderItem = { 10 | /** 自定义渲染的内容 */ 11 | children?: ReactNode 12 | /** 渲染的内容为图片 */ 13 | icon?: string 14 | /** 标题 */ 15 | title?: string 16 | /** 点击的回调 */ 17 | onClick?: (item: MobileFolderItem, i: number) => void 18 | } -------------------------------------------------------------------------------- /src/scroll-circle/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ScrollCircle 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 非常规组件 9 | order: 1 10 | --- 11 | 12 | ## 圆形滚动组件 13 | 14 | 将每一个卡片绕圆形旋转放置,并支持圆形滚动的方式 15 | 16 | ## 演示 17 | 18 | ### 组件多层嵌套 19 | 20 | 21 | 22 | 23 | 24 | ### 模拟一个抽奖转盘 25 | 26 | 27 | 28 | ### 常规使用 29 | 30 | 31 | 32 | ### 分页使用 33 | 34 | 35 | 36 | ### 设置卡片间距和不均分排列 37 | 38 | 39 | 40 | ### 设置各方向上的圆心 (centerPoint) 41 | 42 | 43 | 44 | ### 操作旋转 45 | 46 | 47 | 48 | ## API 49 | 50 | ### ScrollCircle 51 | 52 | 53 | 54 | ### ScrollCircle.Item 55 | 56 | 57 | 58 | ### centerPoint CenterPointType 59 | 60 | | 属性名 | 描述 | 61 | | ---- | ---- | 62 | | auto | 自动适应,当圆形区域宽度大于高度时,圆心会自动在底部,否则在右边 | 63 | | center | 让整个圆形在盒子内 | 64 | | left | 让圆心在左边 | 65 | | top | 让圆心在顶部 | 66 | | right | 让圆心在右边 | 67 | | bottom | 让圆心在底部 | 68 | -------------------------------------------------------------------------------- /src/scroll-circle/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-scroll-circle'; 2 | .#{$class-prefix} { 3 | position: relative; 4 | overflow: hidden; 5 | touch-action: none; 6 | cursor: grab; 7 | &-area { 8 | position: absolute; 9 | top: 50%; 10 | left: 50%; 11 | transform-origin: center center; 12 | transition: transform 600ms ease-out; 13 | } 14 | &-cardWrap { 15 | position: absolute; 16 | transform-origin: center center; 17 | } 18 | &-arrow { 19 | position: absolute; 20 | top: 50%; 21 | transform: translateY(-50%); 22 | cursor: pointer; 23 | &-right { 24 | right: 0; 25 | } 26 | &-left { 27 | left: 0; 28 | } 29 | &-disable { 30 | cursor: not-allowed; 31 | opacity: 0.6; 32 | } 33 | &-area { 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | height: 100vh; 38 | padding: 0 20px; 39 | color: #fff; 40 | font-size: 24px; 41 | user-select: none; 42 | &-left { 43 | background: linear-gradient(to left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4)); 44 | &:hover { 45 | background: linear-gradient(to left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.45)); 46 | } 47 | } 48 | &-right { 49 | background: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4)); 50 | &:hover { 51 | background: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.45)); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/scroll-circle/index.tsx: -------------------------------------------------------------------------------- 1 | import { attachPropertiesToComponent } from '../utils/attach-properties-to-component'; 2 | import './index.scss'; 3 | import _ScrollCircle from './scrollCircle'; 4 | import ScrollCircleItem from './scrollCircleItem'; 5 | 6 | const ScrollCircle = attachPropertiesToComponent(_ScrollCircle, { 7 | Item: ScrollCircleItem, 8 | }); 9 | 10 | export type { ScrollCircleItemType, ScrollCircleProps, ScrollCircleInstance } from './type'; 11 | export { ScrollCircle, ScrollCircleItem }; 12 | export default ScrollCircle; 13 | -------------------------------------------------------------------------------- /src/scroll-circle/scrollCircleItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { ScrollCircleItemCtxProps, ScrollCircleItemType } from "./type"; 3 | import { getCardDegXY } from "./utils"; 4 | import { withNativeProps } from "../utils"; 5 | 6 | const classPrefix = 'lhhui-scroll-circle'; 7 | 8 | const ScrollCircleItem = ({ index, onClick, children, ...props }: ScrollCircleItemType) => { 9 | const {circleR, cardDeg, isVertical, centerPoint, isFlipDirection, isClick, ...ret} = props as ScrollCircleItemCtxProps 10 | 11 | const cardStyle = useMemo(() => { 12 | const {initDeg, nx, ny, isAddDeg} = getCardDegXY({centerPoint, isFlipDirection, isVertical}) 13 | const deg = initDeg + cardDeg * index; 14 | const top = circleR * (1 - ny * Math.cos((deg * Math.PI) / 180)); 15 | const left = circleR * (1 - nx * Math.sin((deg * Math.PI) / 180)); 16 | const rotate = initDeg - nx * ny * deg + (isAddDeg ? 180 : 0); 17 | return { 18 | top: `${top}px`, 19 | left: `${left}px`, 20 | transform: `translate(-50%, -50%) rotate(${rotate}deg)`, 21 | }; 22 | }, [circleR, cardDeg, isVertical, centerPoint, isFlipDirection]); 23 | 24 | return withNativeProps(ret, 25 |
{ 29 | isClick && onClick?.(index); 30 | }} 31 | > 32 | {children} 33 |
34 | ); 35 | }; 36 | 37 | export default React.memo(ScrollCircleItem) -------------------------------------------------------------------------------- /src/scroll-circle/type.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { NativeProps } from '../utils/native-props'; 3 | 4 | export type ScrollCircleProps = { 5 | /** 传入卡片的数组长度 */ 6 | listLength: number; 7 | /** 8 | * 滚动列表的宽度 9 | * @default 100% 10 | */ 11 | width?: string; 12 | /** 13 | * 滚动列表的高度 14 | * @default 100% 15 | */ 16 | height?: string; 17 | /** 18 | * 圆心的位置 19 | * @default auto (宽度大于高度时在底部,否则在右侧) 20 | */ 21 | centerPoint?: CenterPointType; 22 | /** 23 | * 圆的半径 24 | * @default 取组件宽和高中的最大值 25 | */ 26 | radius?: number 27 | /** 28 | * 当 centerPoint = 'center' 时,设置圆形的内边距 29 | * @default 5 30 | */ 31 | circlePadding?: number 32 | /** 33 | * 卡片间增加的角度 34 | * @default 1 35 | */ 36 | cardAddDeg?: number; 37 | /** 38 | * 索引为多少的卡片位于中间区域 从0开始算 39 | * @default 0 40 | */ 41 | initCartNum?: number; 42 | /** 43 | * 卡片是否平均分配圆形轨迹 44 | * @default true 45 | */ 46 | isAverage?: boolean; 47 | /** 48 | * 是否将当前方向翻转 49 | */ 50 | isFlipDirection?: boolean; 51 | /** 52 | * 是否分页 53 | * @default true 54 | */ 55 | isPagination?: boolean 56 | /** 左边分页箭头的内容区域 */ 57 | leftArrow?: ReactNode; 58 | /** 右边分页箭头的内容区域 */ 59 | rightArrow?: ReactNode; 60 | /** 禁止触摸滚动 */ 61 | disableTouch?: boolean 62 | /** 发生触摸的回调 */ 63 | onTouchStart?: () => void; 64 | /** 发生滚动的回调 */ 65 | onTouchMove?: () => void; 66 | /** 触摸结束的回调 */ 67 | onTouchEnd?: () => void; 68 | /** 69 | * 触摸结束的回调 70 | * @curIndex 当前处于正中的索引 71 | * @deg 当前旋转的角度 72 | */ 73 | onScrollEnd?: (curIndex: number, deg: number) => void 74 | /** 分页触发回调改变页码 */ 75 | onPageChange?: (page: ScrollCirclePageType) => void; 76 | } & NativeProps 77 | 78 | export type ScrollCircleItemType = { 79 | /** 当前item的索引 */ 80 | index: number; 81 | /** 点击了卡片(触摸时间小于150ms) */ 82 | onClick?: (i: number) => void; 83 | } & NativeProps 84 | 85 | /** 传递给 item 的 props 属性 */ 86 | export type ScrollCircleItemCtxProps = { 87 | circleR: number; 88 | cardDeg: number; 89 | isVertical: boolean; 90 | isFlipDirection: boolean; 91 | isClick: boolean; 92 | centerPoint: CenterPointType; 93 | } 94 | 95 | export type ScrollCircleInstance = { 96 | /** 旋转到指定角度或者指定索引 */ 97 | scrollTo: (params: ScrollCircleScrollToParams) => void 98 | /** 触发分页改变页码 */ 99 | onPageChange: (page: Partial) => void; 100 | } 101 | 102 | export type ScrollCircleScrollToParams = { 103 | deg?: number 104 | index?: number 105 | duration?: number 106 | onEnd?: (curIndex: number, deg: number) => void 107 | } 108 | 109 | export type ScrollCirclePageType = { pageNum: number; pageSize: number } 110 | 111 | export type CenterPointType = 'auto' | 'center' | 'left' | 'top' | 'right' | 'bottom' 112 | -------------------------------------------------------------------------------- /src/scroll-circle/utils.ts: -------------------------------------------------------------------------------- 1 | import { CenterPointType } from "./type"; 2 | 3 | /** 获取圆形区域应该移动的xy */ 4 | export const getCircleTransformXy = ( 5 | centerPoint: CenterPointType, 6 | isVertical: boolean, 7 | circleR: number 8 | ) => { 9 | let x = 0, 10 | y = 0; 11 | if (centerPoint === "auto") { 12 | if (isVertical) { 13 | x = circleR; 14 | } else { 15 | y = circleR; 16 | } 17 | } else if (centerPoint === "left") { 18 | x = -circleR; 19 | } else if (centerPoint === "top") { 20 | y = -circleR; 21 | } else if (centerPoint === "right") { 22 | x = circleR; 23 | } else if (centerPoint === "bottom") { 24 | y = circleR; 25 | } 26 | return { x, y }; 27 | }; 28 | 29 | /** 获取旋转的角度是否需要取反 */ 30 | export const getRotateDegAbs = ( 31 | centerPoint: CenterPointType, 32 | isVertical: boolean, 33 | isFlipDirection?: boolean 34 | ) => { 35 | let num = 1; 36 | if (centerPoint === "auto") { 37 | num = isVertical ? 1 : -1; 38 | } else if ( 39 | centerPoint === "left" || 40 | centerPoint === "bottom" || 41 | centerPoint === "center" 42 | ) { 43 | num = -1; 44 | } 45 | return (num * (isFlipDirection ? -1 : 1)) as -1 | 1; 46 | }; 47 | 48 | /** 计算两点之间的角度 */ 49 | export function calcAngle( 50 | event: { x: number; y: number }, 51 | center: { x: number; y: number } 52 | ) { 53 | const angle = Math.atan2(event.y - center.y, event.x - center.x); 54 | const deg = angle * (180 / Math.PI); 55 | return deg < 0 ? 360 + deg : deg; 56 | } 57 | 58 | /** 角度取整,处理转动的角度为:卡片的角度的倍数 */ 59 | export function roundingAngle({ 60 | changeDeg, 61 | cardDeg, 62 | deg, 63 | }: { 64 | changeDeg: number; 65 | cardDeg: number; 66 | deg: number; 67 | }) { 68 | let mathMethods: "ceil" | "floor" = "ceil"; 69 | if (Math.abs(changeDeg) < cardDeg / 3) { 70 | mathMethods = changeDeg < 0 ? "ceil" : "floor"; 71 | } else { 72 | mathMethods = changeDeg < 0 ? "floor" : "ceil"; 73 | } 74 | return cardDeg * Math[mathMethods](deg / cardDeg); 75 | } 76 | 77 | /** 获取卡片的度数和xy等信息 */ 78 | export function getCardDegXY({ 79 | centerPoint, 80 | isFlipDirection, 81 | isVertical, 82 | }: { 83 | centerPoint: CenterPointType; 84 | isFlipDirection: boolean; 85 | isVertical: boolean; 86 | }) { 87 | let initDeg = 0, 88 | nx = 1, 89 | ny = 1, 90 | isAddDeg = false; 91 | if (centerPoint === "left") { 92 | initDeg = -90; 93 | if (!isFlipDirection) { 94 | ny = -1; 95 | isAddDeg = true; 96 | } 97 | } else if (centerPoint === "top") { 98 | initDeg = 180; 99 | nx = !isFlipDirection ? 1 : -1; 100 | } else if ( 101 | centerPoint === "right" || 102 | (centerPoint === "auto" && isVertical) 103 | ) { 104 | initDeg = 90; 105 | if (isFlipDirection) { 106 | ny = -1; 107 | isAddDeg = true; 108 | } 109 | } else if ( 110 | centerPoint === "bottom" || 111 | (centerPoint === "auto" && !isVertical) || 112 | centerPoint === "center" 113 | ) { 114 | initDeg = 0; 115 | nx = !isFlipDirection ? -1 : 1; 116 | } 117 | return { initDeg, nx, ny, isAddDeg }; 118 | } 119 | -------------------------------------------------------------------------------- /src/scroll-view/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ScrollView 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 一般组件 9 | order: 1 10 | --- 11 | 12 | ## 滚动盒子 13 | 14 | 滚动盒子,可用于监听触底滚动等行为 15 | 16 | ## 演示 17 | 18 | ### 触底监听 19 | 20 | 21 | 22 | ## API 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/scroll-view/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-scroll-view'; 2 | .#{$class-prefix} { 3 | overflow-y: scroll; 4 | &::-webkit-scrollbar { 5 | width: 0; 6 | height: 0; 7 | display: none !important; 8 | } 9 | } -------------------------------------------------------------------------------- /src/scroll-view/index.tsx: -------------------------------------------------------------------------------- 1 | import ScrollView from './scrollView'; 2 | import './index.scss'; 3 | 4 | export { ScrollView }; 5 | export default ScrollView; 6 | export type { ScrollViewProps } from './type'; 7 | -------------------------------------------------------------------------------- /src/scroll-view/scrollView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { ScrollViewProps } from './type'; 3 | 4 | const classPrefix = `lhhui-scroll-view`; 5 | 6 | const ScrollView = ({ 7 | height, children, style, lowerThreshold = 30, onScrollToLower, className 8 | }: ScrollViewProps) => { 9 | const scrollViewRef = useRef(null) 10 | const [divInfo, setDivInfo] = useState({ 11 | top: 0, 12 | }); 13 | const timerRef = useRef(null) 14 | 15 | const init = () => { 16 | const rect = scrollViewRef.current?.getBoundingClientRect() 17 | if(rect) { 18 | setDivInfo({top: rect.top}) 19 | } 20 | } 21 | 22 | useEffect(() => { 23 | if(!height) { 24 | init() 25 | } 26 | }, [height]) 27 | 28 | const onScroll = (e: React.UIEvent) => { 29 | if(timerRef.current) { 30 | clearTimeout(timerRef.current) 31 | timerRef.current = null 32 | } 33 | const {scrollTop, clientHeight, scrollHeight} = e.currentTarget 34 | timerRef.current = setTimeout(({scrollTop, clientHeight, scrollHeight}) => { 35 | if(clientHeight + scrollTop + lowerThreshold >= scrollHeight) { 36 | onScrollToLower?.() 37 | } 38 | }, 100, {scrollTop, clientHeight, scrollHeight}); 39 | } 40 | 41 | return ( 42 |
51 | {children} 52 |
53 | ) 54 | } 55 | 56 | export default ScrollView -------------------------------------------------------------------------------- /src/scroll-view/type.ts: -------------------------------------------------------------------------------- 1 | import { NativeProps } from '../utils/native-props'; 2 | 3 | export type ScrollViewProps = { 4 | /** 5 | * 滚动区域的高度 6 | * @default 整个页面中可占据的高度 7 | */ 8 | height?: number | string 9 | /** 10 | * 距离底部多远时触发 onScrollToLower 11 | * @default 30 12 | */ 13 | lowerThreshold?: number 14 | /** 滚动到底部触发的事件 */ 15 | onScrollToLower?: () => void 16 | } & NativeProps; -------------------------------------------------------------------------------- /src/skus/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Skus 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 非常规组件 9 | order: 1 10 | --- 11 | 12 | ## Sku组件 13 | 14 | 主要用于处理商品的 sku 在受到库存的影响下,sku项的选中和禁用问题; 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ### 自定义渲染内容 23 | 24 | 25 | 26 | ### 自定义不同 data 结构 27 | 28 | 29 | 30 | ## API 31 | 32 | ### SkusProps 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/skus/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-skus'; 2 | .#{$class-prefix} { 3 | &-params-title { 4 | font-size: 16px; 5 | font-weight: bold; 6 | margin: 10px 0; 7 | } 8 | &-params-area { 9 | display: flex; 10 | align-items: center; 11 | flex-wrap: wrap; 12 | } 13 | &-sku { 14 | box-sizing: border-box; 15 | display: inline-block; 16 | padding: 6px 12px; 17 | margin-right: 10px; 18 | border: 1px solid #000; 19 | border-radius: 4px; 20 | font-size: 14px; 21 | cursor: pointer; 22 | user-select: none; 23 | &-active { 24 | color: #87ceeb; 25 | background-color: #000; 26 | } 27 | &-disabled { 28 | color: #aaabab; 29 | border-color: #aaabab; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/skus/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import Skus from './skus'; 3 | 4 | export type { 5 | SkusProps, 6 | SkusItem, 7 | RenderSkuItem, 8 | RenderSkuItemValue 9 | } from './type' 10 | 11 | export { Skus } 12 | export default Skus -------------------------------------------------------------------------------- /src/skus/type.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { NativeProps } from "../utils/native-props"; 3 | 4 | export type SkusProps = { 5 | /** 传入的skus数据列表 */ 6 | data: SkusItem[] 7 | /** 8 | * 用于替换 data 中 item 的 key 值 9 | * (即 SkusItem 类型中对应的 key ) 10 | */ 11 | skuItemKey?: SkuItemKey 12 | /** 13 | * sku 库存的限制值,当小于等于该值时会禁用该sku的选择 14 | * @default 0 15 | */ 16 | stockLimitValue?: number 17 | /** 18 | * stockLimitValue 的限制值是否是大于等于 19 | * @default false (小于等于) 20 | */ 21 | isStockGreaterThan?: boolean 22 | /** 点击sku的改变回调 */ 23 | onChange?: (checkSkus: Record, cur?: SkusChangeParams) => void 24 | /** 自定义渲染的sku */ 25 | customRender?: ( 26 | /** 用于渲染的列表 */ 27 | list: RenderSkuItem[], 28 | /** 点击选中sku */ 29 | selectSkus: (skuName: string, sku: RenderSkuItemValue) => void 30 | ) => ReactNode 31 | } & NativeProps 32 | 33 | export type SkusItem = { 34 | /** 库存 */ 35 | stock?: number; 36 | /** sku参数 */ 37 | params: SkusItemParam[]; 38 | // ... 其他省略 39 | }; 40 | 41 | export type SkusItemParam = { 42 | name: string; 43 | value: string; 44 | } 45 | 46 | export type RenderSkuItem = { 47 | name: string; 48 | values: RenderSkuItemValue[]; 49 | } 50 | 51 | export type RenderSkuItemValue = { 52 | /** sku的值 */ 53 | value: string; 54 | /** 选中状态 */ 55 | isChecked: boolean 56 | /** 禁用状态 */ 57 | disabled: boolean; 58 | } 59 | 60 | export type SkuItemKey = { 61 | stock?: string 62 | params?: string 63 | paramName?: string 64 | paramValue?: string 65 | } 66 | 67 | export type SkusChangeParams = { 68 | /** 当前点击的sku分类名称 */ 69 | skuName: string 70 | /** 当前点击的 sku */ 71 | value: string 72 | /** 点击后是否是选中的 */ 73 | isChecked: boolean 74 | /** 是否是禁用的 */ 75 | disabled: boolean 76 | /** 当前选中sku在data中对应的item */ 77 | dataItem?: any 78 | /** 当前选中的库存 */ 79 | stock?: number 80 | } -------------------------------------------------------------------------------- /src/slider-puzzle/config.ts: -------------------------------------------------------------------------------- 1 | export const classPrefix = `lhhui-sliderPuzzle`; 2 | export const classPrefixItem = `lhhui-sliderPuzzleItem`; 3 | export const classPrefixPuzzleCanvas = `lhhui-sliderPuzzle-canvas`; 4 | 5 | /** 最多行数 */ 6 | export const MAX_ROW = 99; -------------------------------------------------------------------------------- /src/slider-puzzle/context.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GridPosition, SliderPuzzleCanvasProps } from "./type"; 3 | 4 | export const SliderPuzzleCtx = React.createContext({ 5 | size: 3, 6 | initSpaceIndex: 0, 7 | grid: {w: 0, h: 0}, 8 | gridArr: undefined, 9 | puzzleGridArr: undefined, 10 | letter: '', 11 | isReset: false, 12 | gap: 2, 13 | isGameMode: false, 14 | onChangeGrid: (p: GridPosition, preParams: GridPosition) => {} 15 | }); 16 | 17 | export type SliderPuzzleCtxType = { 18 | /** n*n */ 19 | size: number 20 | /** 初始空格索引 */ 21 | initSpaceIndex: number 22 | /** 格子的宽高 */ 23 | grid: {w: number, h: number} 24 | /** 格子中的二维数组 */ 25 | gridArr?: number[][] 26 | /** 拼图块的位置随机数组 */ 27 | puzzleGridArr?: number[] 28 | /** 绘画的字母 */ 29 | letter: string, 30 | /** 是否重新设置拼图 */ 31 | isReset: boolean 32 | gap: number 33 | isGameMode?: boolean 34 | onChangeGrid: (p: GridPosition, preParams: GridPosition) => void 35 | } & Omit -------------------------------------------------------------------------------- /src/slider-puzzle/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SliderPuzzle 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 非常规组件 9 | order: 1 10 | --- 11 | 12 | ## 滑块拼图组件 13 | 14 | 一种拼图滑块,类似华容道那样。 15 | 16 | ## 演示 17 | 18 | ### 自定义拼图图片 19 | 20 | 21 | 22 | ### 常规使用 23 | 24 | 25 | 26 | ### 4\*4 拼图块 以及间距等调整 27 | 28 | 29 | 30 | ## API 31 | 32 | ### SliderPuzzle 33 | 34 | 35 | 36 | ### SliderPuzzle.Item 37 | 38 | 39 | 40 | ### SliderPuzzle.Canvas 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/slider-puzzle/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-sliderPuzzle'; 2 | $class-prefix-item: 'lhhui-sliderPuzzleItem'; 3 | $class-prefix-canvas: 'lhhui-sliderPuzzle-canvas'; 4 | 5 | .#{$class-prefix} { 6 | box-sizing: border-box; 7 | // background-color: #262626; 8 | width: 100%; 9 | height: 100%; 10 | &-area { 11 | width: 100%; 12 | height: 100%; 13 | position: relative; 14 | } 15 | } 16 | 17 | .#{$class-prefix-item} { 18 | position: absolute; 19 | transition: transform ease .4s; 20 | -webkit-tap-highlight-color: transparent; 21 | // 使触摸事件不产生默认行为 22 | touch-action: none; 23 | cursor: grab; 24 | &-hover:hover { 25 | filter: drop-shadow(1px 1px 3px rgba(37, 37, 37, 0.5)); 26 | } 27 | &-disableTouch { 28 | cursor: not-allowed; 29 | &:hover { 30 | filter: drop-shadow(1px 1px 1px rgba(168, 168, 168, 0.5)); 31 | } 32 | } 33 | } 34 | 35 | .#{$class-prefix-canvas} { 36 | position: absolute; 37 | left: 0; 38 | top: 0; 39 | width: 100%; 40 | height: 100%; 41 | user-select: none; 42 | -webkit-user-drag: none; 43 | &-full { 44 | width: auto; 45 | height: auto; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/slider-puzzle/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import { attachPropertiesToComponent } from '../utils/attach-properties-to-component'; 3 | import SliderPuzzle_ from './slider-puzzle'; 4 | import SliderPuzzleCanvas from './slider-puzzle-canvas'; 5 | import SliderPuzzleItem from './slider-puzzle-item'; 6 | 7 | const SliderPuzzle = attachPropertiesToComponent(SliderPuzzle_, { 8 | Item: SliderPuzzleItem, 9 | Canvas: SliderPuzzleCanvas, 10 | }); 11 | 12 | export { SliderPuzzle, SliderPuzzleItem, SliderPuzzleCanvas }; 13 | export type { 14 | SliderPuzzleProps, 15 | SliderPuzzleItemProps, 16 | SliderPuzzleCanvasProps, 17 | SliderPuzzleInstance, 18 | } from './type'; 19 | export default SliderPuzzle; 20 | -------------------------------------------------------------------------------- /src/slider-puzzle/type.ts: -------------------------------------------------------------------------------- 1 | import { NativeProps } from "../utils/native-props"; 2 | 3 | export type SliderPuzzleProps = { 4 | /** 5 | * 遍历item的数组的长度,建议是 size * size - 1 6 | * @default children 的长度 7 | */ 8 | listLength?: number 9 | /** 10 | * n*n的拼图 11 | * @default 3 12 | */ 13 | size?: number 14 | /** 15 | * 拼图块之间的间隙 单位px 16 | * @default 2 17 | */ 18 | gap?: number 19 | /** 20 | * 背景颜色 21 | * @default #1f1f1f 22 | */ 23 | background?: string 24 | /** 25 | * 是否开启游戏模式 26 | * @default false 27 | */ 28 | isGameMode?: boolean 29 | /** 30 | * 数量不够作为补充的卡片的背景颜色 31 | * @default #3e3e3e 32 | */ 33 | fillPuzzleItemBackground?: string 34 | /** 拼图完成了的回调 */ 35 | onComplete?: () => void 36 | /** 拼图整体大小发生了变化的回调 */ 37 | onResize?: (grid: {w: number, h: number}) => void 38 | } & NativeProps & Omit 39 | 40 | export type SliderPuzzleItemProps = { 41 | /** 当前item的索引 */ 42 | index: number 43 | /** 44 | * 触摸时间,触摸时长大于该值就无法触发 onClick 45 | * @default 150 46 | */ 47 | touchTime?: number 48 | /** 49 | * 触摸距离,触摸距离大于该值就无法触发 onClick 50 | * @default 8 51 | */ 52 | touchDistance?: number 53 | /** 是否需要鼠标hover的样式,想自定义hover,可以直接传入className */ 54 | isHover?: boolean 55 | /** 点击了卡片(触摸时间小于150ms) */ 56 | onClick?: (i: number) => void; 57 | } & NativeProps 58 | 59 | export type SliderPuzzleCanvasProps = { 60 | /** 当前item的索引,不传则代表的是全图 */ 61 | index?: number 62 | /** 63 | * 设置拼图的不透明度 0-1 64 | * @default 0.5 65 | */ 66 | globalAlpha?: number 67 | /** 68 | * 拼图颜色 69 | * @default #ddeafb 70 | */ 71 | puzzleColor?: string 72 | /** 73 | * 游戏模式下的背景颜色 74 | * @default #3e3e3e 75 | */ 76 | gameModeBackground?: string 77 | /** 传入拼图的图片 */ 78 | puzzleImg?: string 79 | } & NativeProps 80 | 81 | export type SliderPuzzleInstance = { 82 | reset: () => void 83 | } 84 | 85 | export type GridPosition = {row: number, col: number} -------------------------------------------------------------------------------- /src/slider-puzzle/utils.ts: -------------------------------------------------------------------------------- 1 | import { shuffleArray } from "../utils/random"; 2 | 3 | /** 根据反转次数和空瓦片的位置来检查打乱的数组是否可解 */ 4 | function isSolvable(arr: any[], size: number) { 5 | let inversions = 0; 6 | for (let i = 0; i < arr.length; i++) { 7 | for (let j = i + 1; j < arr.length; j++) { 8 | if (arr[i] !== size * size && arr[j] !== size * size && arr[i] > arr[j]) { 9 | inversions++; 10 | } 11 | } 12 | } 13 | if (size % 2 === 1) { 14 | return inversions % 2 === 0; 15 | } else { 16 | const emptyTileRow = Math.ceil(arr.indexOf(size * size) / size); 17 | return (inversions + emptyTileRow) % 2 === 1; 18 | } 19 | } 20 | 21 | /** 创建一个打乱的不重复数字的数组 */ 22 | export function randomNumberArray(size: number) { 23 | const arr = Array.from({length: size * size}, (_, i) => i + 1); 24 | do { 25 | shuffleArray(arr); 26 | } while (!isSolvable(arr, size)); 27 | return arr 28 | } 29 | 30 | /** 判断是否完成拼图 */ 31 | export function isPuzzleSolved(arr: number[][]) { 32 | const length = arr.length 33 | for (let i1 = 0; i1 < length; i1++) { 34 | const len = length - (i1 === length - 1 ? 1 : 0 ) 35 | for (let i2 = 0; i2 < len; i2++) { 36 | if (arr[i1][i2] !== i1 * length + i2 + 1) { 37 | return false; 38 | } 39 | } 40 | } 41 | return arr.at(-1)?.at(-1) === 0 42 | } 43 | 44 | /** 获取行列的位置 */ 45 | export function getRowColItem(index: number, spaceIndex: number, size: number) { 46 | index += index >= spaceIndex ? 1 : 0 47 | return { 48 | rowNum: Math.floor(index / size), 49 | colNum: index % size, 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --lhhui-gray: #f6f6f6; 3 | --lhhui-blue: #4285fb; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/style/var.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | gray1: '#F6F6F6', 3 | blue: '#4285fb', 4 | }; 5 | -------------------------------------------------------------------------------- /src/tabs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tabs 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 一般组件 9 | order: 1 10 | --- 11 | 12 | ## 标签组件 13 | 14 | 选中,切换和滚动标签 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ### 自定义下标 23 | 24 | 25 | 26 | 27 | ## API 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/tabs/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-tabs'; 2 | .#{$class-prefix} { 3 | position: relative; 4 | display: flex; 5 | width: 100%; 6 | height: 37px; 7 | background-color: #fff; 8 | &-scrollView { 9 | width: 100%; 10 | height: 100%; 11 | overflow-x: auto; 12 | scroll-behavior: smooth; 13 | &::-webkit-scrollbar { 14 | display: none; 15 | width: 0; 16 | height: 0; 17 | } 18 | } 19 | &-content { 20 | position: relative; 21 | display: flex; 22 | justify-content: space-between; 23 | height: 100%; 24 | } 25 | &-tabWrap { 26 | height: 100%; 27 | padding: 0 13px; 28 | cursor: pointer; 29 | -webkit-tap-highlight-color: transparent; 30 | } 31 | &-tab { 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | height: 100%; 36 | font-size: 12px; 37 | white-space: nowrap; 38 | } 39 | &-tab-active { 40 | font-weight: bold; 41 | } 42 | &-line { 43 | position: absolute; 44 | bottom: 0; 45 | left: 0; 46 | // 在h5页面会有默认行高,导致高度异常不能由icon撑开 47 | line-height: 16px; 48 | &-animate { 49 | transition: transform 0.3s ease; 50 | } 51 | &-hide { 52 | opacity: 0; 53 | } 54 | &-show { 55 | opacity: 1; 56 | } 57 | &-line { 58 | width: 30px; 59 | height: 6px; 60 | background-color: var(--lhhui-blue); 61 | border-radius: 3px; 62 | } 63 | } 64 | &-left-placeholder { 65 | flex-shrink: 0; 66 | width: 17px; 67 | height: 100%; 68 | } 69 | &-right-placeholder { 70 | flex-shrink: 0; 71 | width: 17px; 72 | height: 100%; 73 | } 74 | &-header { 75 | position: absolute; 76 | top: 0; 77 | bottom: 0; 78 | z-index: 1; 79 | display: flex; 80 | height: 100%; 81 | } 82 | &-header-left { 83 | left: 0; 84 | } 85 | &-header-right { 86 | right: 0; 87 | } 88 | &-mask { 89 | width: 30px; 90 | height: 100%; 91 | pointer-events: none; 92 | } 93 | &-mask-left { 94 | background: linear-gradient(to right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); 95 | } 96 | &-mask-right { 97 | background: linear-gradient(to left, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs as _Tabs, Tab } from './tabs'; 2 | import './index.scss'; 3 | import { attachPropertiesToComponent } from '../utils/attach-properties-to-component'; 4 | 5 | const Tabs = attachPropertiesToComponent(_Tabs, { 6 | Tab, 7 | }); 8 | 9 | export { Tabs, Tab }; 10 | export default Tabs; 11 | export type { TabsProps, TabsInstance, TabProps } from './type'; 12 | -------------------------------------------------------------------------------- /src/tabs/traverse-react-node.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { isFragment } from 'react-is'; 3 | 4 | export function traverseReactNode( 5 | children: ReactNode, 6 | fn: (child: ReactNode, index: number) => void, 7 | ) { 8 | let i = 0; 9 | function handle(target: ReactNode) { 10 | React.Children.forEach(target, (child) => { 11 | if (!isFragment(child)) { 12 | fn(child, i); 13 | i += 1; 14 | } else { 15 | handle(child.props.children); 16 | } 17 | }); 18 | } 19 | handle(children); 20 | } 21 | -------------------------------------------------------------------------------- /src/tabs/type.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { NativeProps } from '../utils/native-props'; 3 | 4 | export type TabProps = { 5 | /** 标题 */ 6 | title?: ReactNode; 7 | /** key值 */ 8 | key: string; 9 | } & NativeProps; 10 | 11 | export type TabsProps = { 12 | /** 当前激活 tab 的索引 默认为: 0 */ 13 | activeIndex?: number; 14 | /** 传入数组,主要用于获取宽度等信息 */ 15 | list?: any[]; 16 | /** 激活的文字类名 */ 17 | activeTextClass?: string; 18 | /** 激活的文字下划线,默认为一个图标 */ 19 | activeLine?: ReactNode; 20 | /** 是否有默认动画 默认为true */ 21 | isAnimate?: boolean; 22 | /** 激活的tab是否是基于屏幕居中 默认为false(基于tabs盒子居中) */ 23 | isMiddleScreen?: boolean; 24 | /** 每个tab的类名,主要用于设置padding */ 25 | tabClassName?: string; 26 | /** 左右预占位的盒子的类名,主要用于设置宽度 */ 27 | placeholderBoxClass?: string; 28 | /** 当tab发生改变触发 */ 29 | onChange?: (i: number) => void; 30 | } & NativeProps; 31 | 32 | export type TabsInstance = { 33 | /** 滚动到指定的位置 默认为 9999 滚动到最右边 */ 34 | scrollTo: (v?: number) => void; 35 | }; 36 | -------------------------------------------------------------------------------- /src/tree/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tree 3 | nav: 4 | title: 组件 5 | path: /components 6 | order: 2 7 | group: 8 | title: 一般组件 9 | order: 1 10 | --- 11 | 12 | ## 树形控件 13 | 14 | 支持多层级的树形结构列表。 15 | 16 | ## 演示 17 | 18 | ### 常规使用 19 | 20 | 21 | 22 | ### 单选节点 23 | 24 | 25 | 26 | ### 通过 ref 获取节点信息 27 | 28 | 29 | 30 | ## API 31 | 32 | 33 | 34 | ### ref 方法 35 | 36 | | 属性名 | 描述 | 类型 | 37 | | -- | -- | -- | 38 | | getCheckTree | 获取当前选中的树形结构 | `() => CheckTree \| undefined` | 39 | | getParentKeys | 根据 key 值获取其父节点,从 key 节点的最亲关系开始排列 | `(key: string) => string[] \| undefined` | 40 | | getSiblingKeys | 根据 key 值获取其兄弟节点,会包括自身节点 | `(key: string) => string[] \| undefined` | 41 | | getChildKeys | 根据 key 值获取其子节点 | `(key: string) => string[] \| undefined` | 42 | | getCheckKeys | 获取当前 check 中的所有 key | `() => string[]` | 43 | | getTreeDataItem | 获取当前 treeData 中的节点数据 | `(key: string) => TreeNode \| undefined` | 44 | | onAllCheck | 全选/全不选 | `(checked?: boolean) => void` | 45 | -------------------------------------------------------------------------------- /src/tree/index.scss: -------------------------------------------------------------------------------- 1 | $class-prefix: 'lhhui-tree'; 2 | 3 | .#{$class-prefix} { 4 | &-node { 5 | &-content { 6 | display: flex; 7 | align-items: center; 8 | margin-bottom: 6px; 9 | } 10 | &-expand { 11 | width: 24px; 12 | height: 24px; 13 | cursor: pointer; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | border-radius: 4px; 18 | &:hover { 19 | color: #09090b; 20 | background-color: #f0f0f0; 21 | } 22 | &-icon { 23 | font-size: 12px; 24 | line-height: 12px; 25 | width: 12px; 26 | height: 12px; 27 | transition: transform 0.3s; 28 | transform: rotate(-90deg); 29 | &-show { 30 | transform: rotate(0deg); 31 | } 32 | } 33 | &-placeholder { 34 | width: 24px; 35 | height: 24px; 36 | } 37 | } 38 | &-title { 39 | margin-left: 2px; 40 | font-size: 14px; 41 | line-height: 16px; 42 | padding: 3px 6px; 43 | border-radius: 4px; 44 | cursor: pointer; 45 | &:hover { 46 | color: #09090b; 47 | background-color: #f0f0f0; 48 | } 49 | &-selected { 50 | background-color: rgba(158, 205, 244, 0.3); 51 | &:hover { 52 | background-color: rgba(158, 205, 244, 0.4); 53 | } 54 | } 55 | &-disabled { 56 | color: rgba(0, 0, 0, 0.28); 57 | cursor: not-allowed; 58 | &:hover { 59 | background-color: transparent; 60 | } 61 | } 62 | } 63 | &-children { 64 | padding-left: 24px; 65 | overflow-y: hidden; 66 | transition: max-height 0.3s ease; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/tree/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import Tree from './tree'; 3 | 4 | export { Tree }; 5 | export default Tree; 6 | export type { TreeProps, TreeInstance, TreeNode } from './type'; 7 | -------------------------------------------------------------------------------- /src/tree/type.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { NativeProps } from '../utils/native-props'; 3 | 4 | export type TreeProps = { 5 | /** 6 | * 是否有选择框 7 | * @default false 8 | */ 9 | checkable?: boolean 10 | /** 11 | * (受控)选中复选框的树节点的key,当不在数组中的父节点需要被选中时,对应节点也将选中,触发 onCheck 回调,使该值保持正确 12 | * @default undefined 13 | */ 14 | checkedKeys?: string[] 15 | /** 16 | * 默认展开所有树节点 17 | * @default false 18 | */ 19 | defaultExpandAll?: boolean 20 | /** 21 | * 支持点选多个节点(节点本身) 22 | * @default false 23 | */ 24 | multiple?: boolean 25 | /** 26 | * 是否只能单选一个节点 27 | * @default false 28 | */ 29 | singleSelected?: boolean 30 | /** 31 | * 是否可选中 32 | * @default true 33 | */ 34 | selectable?: boolean 35 | /** 36 | * (受控)设置选中的树节点,多选需设置 multiple 为 true 37 | * @default - 38 | */ 39 | selectedKeys?: string[] 40 | /** 树形结构的数据 */ 41 | treeData?: TreeNode[] 42 | /** 点击复选框触发 */ 43 | onCheck?: (checkedKeys: string[], params?: OnCheckParams) => void 44 | /** 点击树节点触发 */ 45 | onSelect?: (selectKeys: string[], params: OnSelectParams) => void 46 | /** 点击右键触发 */ 47 | onRightClick?: (params: onRightClickParams) => void 48 | } & NativeProps 49 | 50 | export type TreeNode = { 51 | /** 52 | * 当树为 checkable 时,设置独立节点是否展示 Checkbox 53 | * @default true 54 | */ 55 | checkable?: boolean 56 | /** 标题 */ 57 | title: ReactNode 58 | /** 整个树型结构中 key 值请保持唯一 */ 59 | key: string 60 | /** 禁掉 checkbox */ 61 | disableCheckbox?: boolean 62 | /** 禁用该节点的点击和选择,不包括子节点 */ 63 | disabled?: boolean 64 | /** 内容 */ 65 | children?: TreeNode[] 66 | } 67 | 68 | export type TreeInstance = { 69 | /** 获取当前选中的树形结构 */ 70 | getCheckTree: () => CheckTree | undefined 71 | /** 根据 key 值获取其父节点,从 key 节点的最亲关系开始排列 */ 72 | getParentKeys: (key: string) => string[] | undefined 73 | /** 根据 key 值获取其兄弟节点,会包括自身节点 */ 74 | getSiblingKeys: (key: string) => string[] | undefined 75 | /** 根据 key 值获取其子节点 */ 76 | getChildKeys: (key: string) => string[] | undefined 77 | /** 获取当前 check 中的所有 key */ 78 | getCheckKeys: () => string[] 79 | /** 获取当前 treeData 中的节点数据 */ 80 | getTreeDataItem: (key: string) => TreeNode | undefined 81 | /** 全选/全不选 */ 82 | onAllCheck: (checked?: boolean) => void 83 | } 84 | 85 | export type CheckTreeItem = { 86 | /** 父节点的 key 值 */ 87 | parentKey?: string 88 | /** 子节点的 key 数组 */ 89 | childKeys?: string[] 90 | /** 是否展开 */ 91 | show: boolean 92 | /** 是否选中 */ 93 | checked: boolean 94 | checkable?: boolean 95 | disableCheckbox?: boolean 96 | disabled?: boolean 97 | } 98 | 99 | export type CheckTree = Record 100 | 101 | export type OnCheckParams = { 102 | /** 当前的状态 */ 103 | checked: boolean 104 | } & OnCheckCommonParams 105 | 106 | export type OnSelectParams = { 107 | /** 当前的状态 */ 108 | selected: boolean 109 | } & OnCheckCommonParams 110 | 111 | export type onRightClickParams = { 112 | event: React.MouseEvent 113 | } & OnCheckCommonParams 114 | 115 | export type OnCheckCommonParams = { 116 | /** 当前点击的 key */ 117 | key: string 118 | /** 父节点的key数组,从子节点的最亲关系开始排列 */ 119 | parentKeys?: string[] 120 | /** treeData 中对应该节点的数据 */ 121 | treeDataItem?: TreeNode 122 | } 123 | -------------------------------------------------------------------------------- /src/tree/utils.ts: -------------------------------------------------------------------------------- 1 | import { CheckTree, CheckTreeItem, TreeNode } from "./type"; 2 | 3 | /** 获取父节点的所有 key */ 4 | export const getParentKeys = (key: string, checkTree?: CheckTree, list: string[] = []): string[] | undefined => { 5 | if(!checkTree?.[key]) return void 0 6 | if(!checkTree[key].parentKey) return list 7 | list.push(checkTree[key].parentKey!) 8 | return getParentKeys(checkTree[key].parentKey!, checkTree, list) 9 | } 10 | 11 | /** 获取当前 check 的所有 key */ 12 | export const getCheckKeys = (tree?: CheckTree) => { 13 | if(!tree) return []; 14 | return Object.keys(tree).reduce((pre, key) => { 15 | if(tree[key].checked) { 16 | pre.push(key) 17 | } 18 | return pre 19 | }, [] as string[]) 20 | } 21 | 22 | /** 从 treeData 中找到对应的数据 */ 23 | export const getTreeDataItem = (key: string, treeData?: TreeNode[]): TreeNode | undefined => { 24 | if(!treeData) return void 0 25 | const item = treeData.find(t => t.key === key) 26 | if(item) return item 27 | for(let t of treeData) { 28 | const newItem = getTreeDataItem(key, t.children) 29 | if(newItem) return newItem 30 | } 31 | return void 0 32 | } 33 | 34 | 35 | /** 获取是否有子节点 check 了 */ 36 | export const getIsSomeChildCheck = (checkItem: CheckTreeItem, checkTree: CheckTree): boolean => { 37 | const isSomeCheck = checkItem.childKeys?.some(cKey => ( 38 | checkTree![cKey].checked || getIsSomeChildCheck(checkTree![cKey], checkTree) 39 | )) 40 | return Boolean(isSomeCheck) 41 | } 42 | 43 | /** 暂时没用 */ 44 | export const getIsEveryChildCheck = (checkItem: CheckTreeItem, checkTree: CheckTree): boolean => { 45 | if(!checkItem.childKeys?.length) return true 46 | const isAllCheck = checkItem.childKeys.every(cKey => ( 47 | checkTree![cKey].checked && getIsEveryChildCheck(checkTree![cKey], checkTree) 48 | )) 49 | return Boolean(isAllCheck) 50 | } 51 | 52 | /** 获取孩子节点的数量 */ 53 | export const getTreeChildLength = (list: TreeNode[], checkTree: CheckTree) => { 54 | return list?.reduce((pre, cur) => { 55 | if(checkTree![cur.key].show && cur.children?.length) { 56 | pre += getTreeChildLength(cur.children, checkTree) 57 | } 58 | return pre 59 | }, list.length) ?? 0 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/attach-properties-to-component.ts: -------------------------------------------------------------------------------- 1 | export function attachPropertiesToComponent>( 2 | component: C, 3 | properties: P, 4 | ): C & P { 5 | const ret = component as any; 6 | for (const key in properties) { 7 | if (properties.hasOwnProperty(key)) { 8 | ret[key] = properties[key]; 9 | } 10 | } 11 | return ret; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/feross/clipboard-copy/blob/master/index.js 2 | 3 | export default async function clipboard(text: string) { 4 | if (navigator.clipboard && navigator.clipboard.writeText) { 5 | try { 6 | await navigator.clipboard.writeText(text); 7 | // 仅在执行成功时返回 8 | return; 9 | } catch (err) { 10 | console.error(err ?? new DOMException('The request is not allowed', 'NotAllowedError')); 11 | } 12 | } 13 | 14 | const span = document.createElement('span'); 15 | span.textContent = text; 16 | 17 | span.style.whiteSpace = 'pre'; 18 | 19 | document.body.appendChild(span); 20 | 21 | const selection = window.getSelection(); 22 | const range = window.document.createRange(); 23 | selection?.removeAllRanges(); 24 | range.selectNode(span); 25 | selection?.addRange(range); 26 | 27 | let success = false; 28 | try { 29 | success = window.document.execCommand('copy'); 30 | } catch (err) { 31 | // eslint-disable-next-line 32 | console.log('error', err); 33 | } 34 | 35 | selection?.removeAllRanges(); 36 | window.document.body.removeChild(span); 37 | 38 | return success 39 | ? Promise.resolve() 40 | : Promise.reject(new DOMException('The request is not allowed', 'NotAllowedError')); 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/compute.ts: -------------------------------------------------------------------------------- 1 | /** 用于比较num 最大和最小不能超过边界值 */ 2 | export function range(num: number, min: number, max: number): number { 3 | return Math.min(Math.max(num, min), max); 4 | } 5 | 6 | /** 初始化创建二维数组 */ 7 | export function createTwoArray(rowNum: number, colNum: number, cb: (rowNum: number, colNum: number) => T) { 8 | return Array.from({length: rowNum}, (_, i1) => ( 9 | Array.from({length: colNum}, (_, i2) => cb(i1, i2)) 10 | )) 11 | } 12 | 13 | /** 方向 1:上 2:右 3:下 4:左 */ 14 | export type Direction = 1 | 2 | 3 | 4 15 | /** 方向 1:上 2:右 3:下 4:左 0:不能移动 */ 16 | export type DirectionType = Direction | 0 17 | /** 18 | * 检查可以移动的方向 19 | * @isArr 代表以数组形式返回可以移动的方向值 20 | * @returns 0: 代表没方向可移动 21 | */ 22 | export function checkDirectionVal({arr, row, col, isArr}: { 23 | arr?: number[][], row: number, col: number, isArr?: boolean 24 | }): DirectionType | DirectionType[] { 25 | if(!arr?.length) return 0 26 | const checkArr = [ 27 | {row: row - 1, col: col}, 28 | {row: row, col: col + 1}, 29 | {row: row + 1, col: col}, 30 | {row: row, col: col - 1}, 31 | ] 32 | const res: DirectionType[] = [] 33 | for(let i = 0; i < checkArr.length; i++) { 34 | if(arr[checkArr[i].row]?.[checkArr[i].col] === 0) { 35 | if(isArr) { 36 | res.push(i + 1 as DirectionType) 37 | } else { 38 | return i + 1 as DirectionType 39 | } 40 | } 41 | } 42 | return res.length ? res : 0 43 | } 44 | /** 检查xy的移动方向 */ 45 | export function checkDirectionXY(deltaX: number, deltaY: number) { 46 | const directionX = !deltaX ? 0 : (deltaX > 0 ? 2 : 4) as DirectionType 47 | const directionY = !deltaY ? 0 : (deltaY > 0 ? 3 : 1) as DirectionType 48 | return {directionX, directionY} 49 | } -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | type OptKey = 'y+' | 'm+' | 'd+' | 'H+' | 'M+' | 'S+'; 2 | /** 格式化日期 yy-mm-dd HH:MM:SS */ 3 | export function dateFormat(_date?: Date | number, _fmt = 'yy-mm-dd HH:MM:SS') { 4 | let date = _date; 5 | if (!(date instanceof Date)) { 6 | date = new Date(date ?? Date.now()); 7 | } 8 | let ret; 9 | const opt = { 10 | 'y+': date.getFullYear().toString(), // 年 11 | 'm+': (date.getMonth() + 1).toString(), // 月 12 | 'd+': date.getDate().toString(), // 日 13 | 'H+': date.getHours().toString(), // 时 14 | 'M+': date.getMinutes().toString(), // 分 15 | 'S+': date.getSeconds().toString(), // 秒 16 | // 有其他格式化字符需求可以继续添加,必须转化成字符串 17 | }; 18 | let fmt = _fmt; 19 | Object.keys(opt).forEach((k) => { 20 | const _k = k as OptKey; 21 | ret = new RegExp('(' + k + ')').exec(fmt); 22 | if (ret) { 23 | fmt = fmt.replace( 24 | ret[1], 25 | ret[1].length === 1 ? opt[_k] : opt[_k].padStart(ret[1].length, '0'), 26 | ); 27 | } 28 | }); 29 | return fmt; 30 | } 31 | 32 | /** 加0 */ 33 | export const addZero = (v: number | string) => (+v >= 10 ? '' : '0') + v; 34 | 35 | /** 格式化剩余时间 */ 36 | export function formatRemainTime(time?: number, format = 'D天HH时mm分ss秒') { 37 | // 当初始化时间为 undefined 时返回 38 | if (time === void 0) return '0'; 39 | // 处理 中括号[] 中的替换文本 40 | let _format = format; 41 | let keyObj = format.match(/\[(.+?)\]/g)?.reduce((pre: any, cur, i) => { 42 | const key = `$${i + 1}$`; 43 | pre[key] = cur.match(/\[(.*)\]/)?.[1] ?? cur; 44 | _format = _format.replace(cur, key); 45 | return pre; 46 | }, {}); 47 | const opt = { 48 | D: 86400, 49 | H: 3600, 50 | m: 60, 51 | s: 1, 52 | }; 53 | let _time = Math.ceil(time / 1000); 54 | const arr = Object.keys(opt) as ('D' | 'H' | 'm' | 's')[]; 55 | arr.forEach((k, i) => { 56 | let time = ''; 57 | if (_time >= opt[k]) { 58 | time = String(~~(_time / opt[k])); 59 | _time %= opt[k]; 60 | } 61 | if (!time && i < arr.length - 1) { 62 | // 删除为0的时间 63 | _format = _format.slice(_format.indexOf(arr[i + 1])); 64 | } else { 65 | const ret = new RegExp(`${k}+`).exec(_format); 66 | if (ret) { 67 | _format = _format.replace( 68 | ret[0], 69 | ret[0].length === 1 ? time : time.padStart(ret[0].length, '0'), 70 | ); 71 | } 72 | } 73 | }); 74 | Object.keys(keyObj).forEach((k) => { 75 | _format = _format.replace(k, keyObj[k]); 76 | }); 77 | return _format; 78 | } 79 | 80 | /** 字母大写转-加小写 (helloWorld => hello-world) */ 81 | export function letterUpperTolower(v: string) { 82 | return v.replace(/([A-Z])/g, (_: string, $1: string) => { 83 | return '-' + $1?.toLowerCase(); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/handleDom.ts: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | 3 | /** 处理类名与需要判断的类名 */ 4 | export const classBem = ( 5 | className: string, 6 | obj?: { [key in string]?: boolean }, 7 | ) => { 8 | let str = className; 9 | if (obj) { 10 | Object.keys(obj).forEach((key) => { 11 | str += ' ' + (obj[key] ? className + '-' + key : ''); 12 | }); 13 | } 14 | return str; 15 | }; 16 | 17 | /** 处理style的需要判断的类名 */ 18 | export const classBemStyle = ( 19 | className: string, 20 | styles: any, 21 | obj?: { [key in string]?: boolean }, 22 | ) => { 23 | const arr: string[] = [styles[className]]; 24 | if (obj) { 25 | Object.keys(obj).forEach((key) => { 26 | if (obj[key]) { 27 | arr.push(styles[className + '-' + key]); 28 | } 29 | }); 30 | } 31 | return classNames(arr); 32 | }; 33 | 34 | /** 处理并合并类名 */ 35 | export const classMergeBem = (classnames: string, arr?: string[]) => { 36 | let str = classnames; 37 | arr?.forEach((key) => { 38 | str += ' ' + classnames + '-' + key; 39 | }); 40 | return str; 41 | }; 42 | 43 | /** 判断是移动端还是pc端 */ 44 | export const isMobile = () => { 45 | // 为了兼容服务端对客户端组件的预渲染 46 | if(typeof window === 'undefined' || typeof navigator === 'undefined') return false 47 | return !!navigator.userAgent.match( 48 | /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './attach-properties-to-component'; 2 | export * from './clipboard'; 3 | export * from './compute'; 4 | export * from './format'; 5 | export * from './handleDom'; 6 | export * from './native-props'; 7 | export * from './omit'; 8 | export * from './random'; 9 | export * from './replace'; 10 | export * from './sleep'; 11 | export * from './validate'; 12 | -------------------------------------------------------------------------------- /src/utils/native-props.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { CSSProperties, ReactElement } from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | export type NativeProps = { 6 | /** 类名 */ 7 | className?: string; 8 | /** style样式 */ 9 | style?: CSSProperties & Partial>; 10 | /** children节点 */ 11 | children?: React.ReactNode 12 | } 13 | 14 | export function withNativeProps

(props: P, element: ReactElement) { 15 | const p = { 16 | ...element.props, 17 | }; 18 | if (props.className) { 19 | p.className = classNames(element.props.className, props.className); 20 | } 21 | if (props.style) { 22 | p.style = { 23 | ...p.style, 24 | ...props.style, 25 | }; 26 | } 27 | return React.cloneElement(element, p); 28 | } -------------------------------------------------------------------------------- /src/utils/omit.ts: -------------------------------------------------------------------------------- 1 | /** 删除一个对象中的key */ 2 | export default function omit( 3 | obj: T, 4 | keys: Array // string 为了某些没有声明的属性被omit 5 | ): Omit { 6 | const clone = { 7 | ...obj, 8 | }; 9 | keys.forEach((key) => { 10 | if ((key as K) in clone) { 11 | delete clone[key as K]; 12 | } 13 | }); 14 | return clone; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/random.ts: -------------------------------------------------------------------------------- 1 | /** 生成随机字符串 */ 2 | export const randomStr = (v?: string | number) => 3 | `${v !== void 0 ? v + "-" : ""}${Math.ceil(Math.random() * 10e5).toString( 4 | 36 5 | )}-${Date.now().toString(36)}`; 6 | 7 | /** 打乱数组 Fisher Yates shuffle 算法 */ 8 | export function shuffleArray(array: any[]) { 9 | for (let i = array.length - 1; i > 0; i--) { 10 | const j = Math.floor(Math.random() * (i + 1)); 11 | [array[i], array[j]] = [array[j], array[i]]; 12 | } 13 | } 14 | 15 | /** 生成随机字母 isUppercase 是否大写 */ 16 | export function randomLetter(isUppercase = true) { 17 | return String.fromCharCode( 18 | Math.floor(Math.random() * 26) + (isUppercase ? 65 : 97) 19 | ); 20 | } 21 | 22 | /** 随机生成十六进制颜色 */ 23 | export function getRandomHexColor() { 24 | const letters = "0123456789ABCDEF"; 25 | let color = "#"; 26 | for (let i = 0; i < 6; i++) { 27 | color += letters[Math.floor(Math.random() * 16)]; 28 | } 29 | return color; 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/replace.ts: -------------------------------------------------------------------------------- 1 | /** 将横线替换成空格 */ 2 | export function replaceLineToSpace(str: string) { 3 | return str.replace(/\-/, ' ') 4 | } 5 | 6 | /** 替换掉类名中的特殊字符 */ 7 | export function replaceClassName(title: string) { 8 | return title.replace(/[.#]/g, '') 9 | } 10 | 11 | /** 转化颜色 例:将 #fff 转化为 rgb(255, 255, 255) */ 12 | export const replaceHexToRgb = (hex: string) => { 13 | if(!hex.includes('#')) return hex 14 | const rgb: number[] = []; 15 | //去除前缀 # 号 16 | hex = hex.substring(1); 17 | if (hex.length === 3) { 18 | // 处理 '#abc' 成 '#aabbcc' 19 | hex = hex.replace(/(.)/g, '$1$1'); 20 | } 21 | hex.replace(/../g, (color: string) => { 22 | // 按16进制将字符串转换为数字 23 | rgb.push(parseInt(color, 0x10)); 24 | return color; 25 | }); 26 | return 'rgb(' + rgb.join(',') + ')'; 27 | }; -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export default function sleep(time = 1000) { 2 | return new Promise(resolve => setTimeout(resolve, time)); 3 | } -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 校验手机 */ 2 | export const validatePhone = (rule: any, value: any, callback: any) => { 3 | if (value === '') { 4 | callback(new Error('请输入手机号')); 5 | } else { 6 | const reg = /^1(3|4|5|6|7|8|9)\d{9}$/; 7 | if (!reg.test(value)) { 8 | callback(new Error('请输入正确的手机号')); 9 | } 10 | callback(); 11 | } 12 | }; 13 | 14 | export function isFunction(val: any) { 15 | return typeof val === 'function'; 16 | } 17 | export function isPlainObject(val: any) { 18 | return val !== null && typeof val === 'object' && !Array.isArray(val); 19 | } 20 | export function isPromise(val: any) { 21 | return isPlainObject(val) && isFunction(val.then) && isFunction(val.catch); 22 | } 23 | export function isDef(value: any) { 24 | return value !== undefined && value !== null; 25 | } 26 | export function isObj(x: any) { 27 | const type = typeof x; 28 | return x !== null && (type === 'object' || type === 'function'); 29 | } 30 | export function isNumber(value: any) { 31 | return /^\d+(\.\d+)?$/.test(value); 32 | } 33 | export function isBoolean(value: any) { 34 | return typeof value === 'boolean'; 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "jsx": "react", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@@/*": [ 11 | ".dumi/tmp/*" 12 | ], 13 | "lhh-ui": [ 14 | "src" 15 | ], 16 | "lhh-ui/*": [ 17 | "src/*", 18 | "*" 19 | ] 20 | } 21 | }, 22 | "include": [ 23 | ".dumirc.ts", 24 | "src/**/*", 25 | "type/index.ts" 26 | ] 27 | } -------------------------------------------------------------------------------- /type/index.ts: -------------------------------------------------------------------------------- 1 | // 导入图片的后缀 2 | declare module '*.svg' 3 | declare module '*.png' 4 | declare module '*.jpg' 5 | declare module '*.jpeg' 6 | declare module '*.gif' 7 | declare module '*.bmp' 8 | declare module '*.tiff' --------------------------------------------------------------------------------