diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..a68ecb6
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,17 @@
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.vscode
+.idea
+*/dist
+/public
+/docs
+.husky
+.local
+/bin
+Dockerfile
+.npmrc
+config
+*.config.js
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..0865207
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,11 @@
+{
+ "plugins": ["prettier"],
+ "extends": ["taro", "plugin:react/jsx-runtime", "plugin:prettier/recommended"],
+ "rules": {
+ "react-hooks/exhaustive-deps": 0,
+ "prettier/prettier": 2,
+ "@typescript-eslint/no-unused-vars": 2,
+ "import/no-commonjs": 0,
+ "@typescript-eslint/no-shadow": 0
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0820734
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+.DS_Store
+.history
+.idea
+node_modules
+package-lock.json
+*/dist
+lib
+*.zip
+
+#packages
+/packages/hooks
+/packages/utils
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directornd files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..ce242c1
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,13 @@
+/dist/*
+.local
+.output.js
+/node_modules/**
+
+**/*.svg
+**/*.sh
+
+/public/*
+
+.md
+.npmrc
+*/dist
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..f088add
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,16 @@
+{
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "semi": false,
+ "singleQuote": true,
+ "quoteProps": "as-needed",
+ "bracketSpacing": true,
+ "trailingComma": "all",
+ "jsxSingleQuote": true,
+ "arrowParens": "always",
+ "insertPragma": false,
+ "requirePragma": false,
+ "proseWrap": "never",
+ "endOfLine": "auto"
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9ba9705
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+## **ygp-taro-react-design**
+
+基于taro react的跨端ui库
+
+目录结构
+
+```json
+taro-demo 组件库小程序demo
+packages 包文件夹
+ components 组件
+ hooks 钩子函数
+ utils 工具类
+ styles 样式
+site 文档静态站点
+
+```
+
+###### **组件列表**
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 0000000..c192b34
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1,27 @@
+module.exports = {
+ ignores: [(commit) => commit.includes('init')],
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'body-leading-blank': [2, 'always'],
+ 'footer-leading-blank': [1, 'always'],
+ 'header-max-length': [2, 'always', 108],
+ 'subject-empty': [2, 'never'],
+ 'type-empty': [2, 'never'],
+ 'type-enum': [
+ 2,
+ 'always',
+ [
+ 'feat', //新增功能
+ 'fix', //缺陷修复
+ 'perf', //优化相关
+ 'style', //格式
+ 'docs', //文档
+ 'test', //测试
+ 'refactor', //功能重构
+ 'build', //版本构建
+ 'chore', //构建过程或辅助工具的变动。
+ 'revert', //回滚到上一个版本
+ ],
+ ],
+ },
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e5539b2
--- /dev/null
+++ b/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "ygp-taro-react-design",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "dev:weapp": "cd taro-demo && npm run dev:weapp",
+ "dev:h5": "cd taro-demo && npm run dev:h5",
+ "build": "cd packages && npm run build",
+ "publish": "cd packages && npm run publish",
+ "prepare": "husky install"
+ },
+ "repository": {
+ "type": "git",
+ "url": "http://gitlab.yigongpin.net/ygp-bciscm-frontend/ygp-taro-react-design.git"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@babel/runtime": "^7.7.7",
+ "@tarojs/components": "3.4.3",
+ "@tarojs/plugin-framework-react": "3.4.3",
+ "@tarojs/react": "3.4.3",
+ "@tarojs/runtime": "3.4.3",
+ "@tarojs/taro": "3.4.3",
+ "async-validator": "^4.2.5",
+ "classnames": "^2.3.1",
+ "lodash": "^4.17.21",
+ "rc-field-form": "^1.27.1",
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.8.0",
+ "@babel/eslint-parser": "^7.18.9",
+ "@commitlint/cli": "^17.1.2",
+ "@commitlint/config-conventional": "^17.1.0",
+ "@rollup/plugin-typescript": "^8.3.3",
+ "@tarojs/mini-runner": "3.4.3",
+ "@tarojs/webpack-runner": "3.4.3",
+ "@types/react": "^17.0.2",
+ "@types/uuid": "^8.3.4",
+ "@types/webpack-env": "^1.13.6",
+ "@typescript-eslint/eslint-plugin": "^5.6.0",
+ "@typescript-eslint/parser": "^5.6.0",
+ "babel-preset-taro": "3.4.3",
+ "cz-conventional-changelog": "^3.3.0",
+ "eslint": "^8.23.0",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-config-taro": "^3.4.3",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-react": "^7.31.1",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-taro": "^3.3.20",
+ "fs-extra": "^8.1.0",
+ "husky": "^8.0.1",
+ "lint-staged": "^13.0.3",
+ "prettier": "^2.7.1",
+ "rimraf": "^2.7.1",
+ "rollup": "^2.79.1",
+ "rollup-plugin-copy": "^3.4.0",
+ "rollup-plugin-typescript2": "^0.34.1",
+ "stylelint": "^14.4.0",
+ "tslib": "^2.4.0",
+ "typescript": "^4.8.4"
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx}": [
+ "eslint --fix",
+ "prettier --write",
+ "eslint"
+ ],
+ "*.less": [
+ "prettier --write"
+ ]
+ },
+ "config": {
+ "commitizen": {
+ "path": "./node_modules/cz-conventional-changelog"
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/README.md b/packages/README.md
new file mode 100644
index 0000000..3d422d2
--- /dev/null
+++ b/packages/README.md
@@ -0,0 +1,3 @@
+## **ygp-taro-react-design**
+
+基于taro react的跨端ui库
diff --git a/packages/build/after-build.js b/packages/build/after-build.js
new file mode 100644
index 0000000..a5537c2
--- /dev/null
+++ b/packages/build/after-build.js
@@ -0,0 +1,11 @@
+import fs from 'fs-extra'
+import { resolveFile, modules } from './scripts.js'
+
+const refs = modules.map((dir) => `/// `)
+const dtsPath = resolveFile('dist/index.d.ts')
+const dts = await fs.readFileSync(dtsPath, 'utf-8').split(/\r\n|\n|\r/gm)
+const newDts = refs.concat(dts).join('\r\n')
+
+fs.writeFileSync(dtsPath, newDts)
+
+console.info('success!')
diff --git a/packages/build/before-build.js b/packages/build/before-build.js
new file mode 100644
index 0000000..d4ce330
--- /dev/null
+++ b/packages/build/before-build.js
@@ -0,0 +1,11 @@
+import fs from 'fs-extra'
+import { resolveFile } from './scripts.js'
+
+const Package = await fs.readJSON('package.json')
+const files = Package.files
+
+// 删除文件夹
+files.forEach((f) => {
+ const folderPath = resolveFile(f)
+ fs.removeSync(folderPath)
+})
diff --git a/packages/build/scripts.js b/packages/build/scripts.js
new file mode 100644
index 0000000..3dbf1c5
--- /dev/null
+++ b/packages/build/scripts.js
@@ -0,0 +1,7 @@
+import path from 'path'
+
+const cwd = process.cwd()
+
+export const modules = ['hooks']
+
+export const resolveFile = (filePath) => path.join(cwd, filePath)
diff --git a/packages/package.json b/packages/package.json
new file mode 100644
index 0000000..bf6b48f
--- /dev/null
+++ b/packages/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "taro-react-form",
+ "version": "1.0.0",
+ "description": "",
+ "main": "dist/index.js",
+ "type": "module",
+ "types": "dist/index.d.ts",
+ "typings": "dist/index.d.ts",
+ "scripts": {
+ "build": "node build/before-build.js && rollup -c rollup.config.js && node build/after-build.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/RickTam/taro-react-form.git"
+ },
+ "publishConfig": {
+ "registry": "http://localhost:4873/"
+ },
+ "files": [
+ "dist",
+ "hooks",
+ "utils"
+ ],
+ "author": "tanyufeng",
+ "license": "ISC",
+ "dependencies": {
+ "async-validator": "^4.2.5",
+ "classnames": "^2.3.1",
+ "rc-field-form": "^1.27.1"
+ }
+}
diff --git a/packages/rollup.config.js b/packages/rollup.config.js
new file mode 100644
index 0000000..2bef208
--- /dev/null
+++ b/packages/rollup.config.js
@@ -0,0 +1,66 @@
+import RollupTypescript from 'rollup-plugin-typescript2'
+import RollupCopy from 'rollup-plugin-copy'
+import { resolveFile, modules } from './build/scripts.js'
+
+const externalPackages = [
+ 'classnames',
+ 'react',
+ 'react-dom',
+ '@tarojs/components',
+ '@tarojs/runtime',
+ '@tarojs/taro',
+ '@tarojs/react',
+ 'rc-field-form',
+]
+
+const genConfig = (module) => {
+ return {
+ input: resolveFile(`src/${module}/index.ts`),
+ output: [
+ {
+ file: resolveFile(`${module}/index.js`),
+ format: 'esm',
+ },
+ ],
+ plugins: [
+ RollupTypescript({
+ tsconfig: resolveFile(`src/${module}/tsconfig.json`),
+ }),
+ ],
+ }
+}
+
+export default [
+ ...modules.map((m) => genConfig(m)),
+ {
+ input: resolveFile('src/index.ts'),
+ output: [
+ {
+ file: resolveFile('dist/index.js'),
+ format: 'esm',
+ sourcemap: false,
+ globals: {
+ react: 'React',
+ 'react-dom': 'ReactDOM',
+ },
+ },
+ ],
+ external: externalPackages,
+ extensions: ['json', 'js', 'ts'],
+ plugins: [
+ RollupTypescript({
+ clean: true,
+ tsconfig: resolveFile('tsconfig.json'),
+ }),
+ RollupCopy({
+ verbose: true,
+ targets: [
+ {
+ src: resolveFile('src/styles'),
+ dest: resolveFile('dist'),
+ },
+ ],
+ }),
+ ],
+ },
+]
diff --git a/packages/src/components/button/index.tsx b/packages/src/components/button/index.tsx
new file mode 100644
index 0000000..d8f4994
--- /dev/null
+++ b/packages/src/components/button/index.tsx
@@ -0,0 +1,214 @@
+import { CSSProperties, FC } from 'react'
+import Taro from '@tarojs/taro'
+import { ButtonProps as TaroButtonProps } from '@tarojs/components/types/Button'
+import { View, Button as TaroButton, Form } from '@tarojs/components'
+import { BaseEventOrig, CommonEvent, ITouchEvent } from '@tarojs/components/types/common'
+import classNames from 'classnames'
+import { isFunction } from '../../utils/is'
+
+export interface ButtonProps extends Omit {
+ /**
+ *按钮类型
+ * @defalt default
+ */
+ type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
+ /**
+ *自定义classname
+ */
+ className?: string
+ /**
+ * 是否填充背景
+ * @default false
+ */
+ fill?: boolean
+ /**
+ * 自动充满父容器
+ * @default false
+ */
+ full?: boolean
+ /**
+ * 按钮尺寸大小
+ */
+ size?: 'large' | 'normal' | 'small' | 'mini'
+ /**
+ * 是否圆角
+ * @default normal
+ */
+ round?: string
+ /**
+ * 按钮颜色
+ */
+ color?: string
+ /**
+ * 按钮填充颜色
+ */
+ fillColor?: string
+ /**
+ * 是否使用边框
+ */
+ border?: boolean
+ /**
+ * 边框颜色
+ */
+ borderColor?: string
+ /**
+ * 按钮自定义圆角
+ */
+ radius?: number
+ /**
+ * 自定义样式对象
+ */
+ style?: CSSProperties
+ /**
+ * 设置按钮为禁用态(不可点击)
+ */
+ disabled?: boolean
+}
+
+const Button: FC = (props) => {
+ const isWEB = Taro.getEnv() === Taro.ENV_TYPE.WEB
+ const isWEAPP = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
+
+ const {
+ type = 'default',
+ size = 'normal',
+ className,
+ radius = 0,
+ fill,
+ full,
+ lang,
+ round,
+ disabled,
+ formType,
+ openType,
+ color,
+ border,
+ borderColor,
+ fillColor,
+ style,
+ sessionFrom,
+ sendMessageTitle,
+ sendMessagePath,
+ sendMessageImg,
+ showMessageCard,
+ appParameter,
+ } = props
+
+ const rootClassName = [`ygp-button`, `ygp-button--${type}`]
+ const classObject = {
+ [`ygp-button--${size}`]: size,
+ [`ygp-button--disabled`]: disabled,
+ [`ygp-button--full`]: full,
+ [`ygp-button--${round}`]: round,
+ [`ygp-button--fill`]: fill,
+ [`ygp-button--${type}--border`]: border && type,
+ // 'slc-button__no-border': (fill || fillColor) && !borderColor,
+ }
+
+ const onClick = (event: ITouchEvent) => {
+ if (!props.disabled) {
+ isFunction(props.onClick) && props.onClick(event)
+ }
+ }
+
+ const onGetUserInfo = (event: CommonEvent) => {
+ isFunction(props.onGetUserInfo) && props.onGetUserInfo(event)
+ }
+
+ const onContact = (event: BaseEventOrig) => {
+ isFunction(props.onContact) && props.onContact(event)
+ }
+
+ const onGetPhoneNumber = (event: CommonEvent) => {
+ isFunction(props.onGetPhoneNumber) && props.onGetPhoneNumber(event)
+ }
+
+ const onError = (event: CommonEvent) => {
+ isFunction(props.onError) && props.onError(event)
+ }
+
+ const onOpenSetting = (event: CommonEvent) => {
+ isFunction(props.onOpenSetting) && props.onOpenSetting(event)
+ }
+
+ const onSumit = (event: CommonEvent) => {
+ if (isWEAPP || isWEB) {
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ $scope.triggerEvent('submit', event.detail, {
+ bubbles: true,
+ composed: true,
+ })
+ }
+ }
+
+ const onReset = (event: CommonEvent) => {
+ if (isWEAPP || isWEB) {
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ $scope.triggerEvent('reset', event.detail, {
+ bubbles: true,
+ composed: true,
+ })
+ }
+ }
+
+ const selfColor: any = { color, borderColor, fillColor }
+ if (fillColor && border) {
+ selfColor.color = fillColor
+ selfColor.borderColor = fillColor
+ selfColor.fillColor = 'none'
+ }
+ if (fillColor && !border) {
+ selfColor.color = '#fff'
+ selfColor.border = 'none'
+ }
+ const borderColorObj = selfColor.borderColor ? { 'border-color': selfColor.borderColor } : {}
+ const background = selfColor.fillColor ? { background: selfColor.fillColor } : {}
+ const mergedStyle = {
+ ...style,
+ 'border-radius': radius,
+ color: selfColor.color,
+ ...borderColorObj,
+ ...background,
+ }
+ const webButton =
+
+ const button = (
+
+ )
+
+ return (
+
+ {isWEB && !disabled && webButton}
+ {isWEAPP && !disabled && (
+
+ )}
+ {props.children}
+
+ )
+}
+
+export default Button
diff --git a/packages/src/components/cascader/index.tsx b/packages/src/components/cascader/index.tsx
new file mode 100644
index 0000000..3ff42f7
--- /dev/null
+++ b/packages/src/components/cascader/index.tsx
@@ -0,0 +1,180 @@
+import { FC, useEffect, useState, useCallback, useMemo } from 'react'
+import { View, ScrollView } from '@tarojs/components'
+import classnames from 'classnames'
+import { isArray } from '../../utils/is'
+import Popup from '../popup'
+import type { Props as PopupProps } from '../popup'
+
+type CascaderOptionType = {
+ label?
+ value?
+ children?
+ [key: string]: any
+}
+
+export interface CascaderProps
+ extends Omit {
+ value?: (string | number)[]
+ className?: string
+ options: CascaderOptionType[]
+ fieldMaps?: CascaderOptionType
+ autoClose?: boolean
+ poppable?: boolean
+ onChange?: (
+ value: (string | number)[] | undefined,
+ selectedOptions: CascaderOptionType[],
+ currentOption: Object,
+ ) => void
+}
+
+const Cascader: FC = (props) => {
+ const {
+ value,
+ className,
+ options,
+ fieldMaps,
+ poppable = true,
+ autoClose = false,
+ visible,
+ onClose,
+ onChange,
+ } = props
+ const [selected, setSeleted] = useState(undefined)
+ const [renderOptions, setRenderOptions] = useState([])
+ const [hasChild, setHasChild] = useState(true)
+
+ const mergedFieldMaps = useMemo(() => {
+ return {
+ label: 'label',
+ value: 'value',
+ children: 'children',
+ ...fieldMaps,
+ }
+ }, [])
+
+ useEffect(() => {
+ if (visible) {
+ if (value?.length && value.every((v) => v)) {
+ const selectedOpts = getSelected(value, options)
+ setRenderOptions(selectedOpts[selectedOpts.length - 2][mergedFieldMaps.children])
+ setSeleted(selectedOpts)
+ setHasChild(false)
+ } else {
+ setRenderOptions(options)
+ setHasChild(true)
+ setSeleted(undefined)
+ }
+ }
+ }, [visible, value])
+
+ const getSelected = useCallback((val, data) => {
+ let arr: any = []
+ val?.forEach((el) => {
+ let option = data.find((item) => item[mergedFieldMaps.value] === el)
+ if (option) {
+ arr.push(option)
+ if (option[mergedFieldMaps.children]) data = option[mergedFieldMaps.children]
+ }
+ })
+ return arr
+ }, [])
+
+ useEffect(() => {
+ setRenderOptions(options)
+ }, [])
+
+ const onClick = useCallback(
+ (item) => {
+ let selectedOpts = selected || []
+ const children = item[mergedFieldMaps.children]
+ if (children && isArray(children) && children.length) {
+ setRenderOptions(children)
+ setHasChild(true)
+ selectedOpts = selectedOpts.concat([item])
+ } else {
+ let lastIndex = selectedOpts.length - 1
+ let lastEl = selectedOpts[lastIndex]
+ let lastChildren = lastEl[mergedFieldMaps.children]
+ if (lastChildren && isArray(lastChildren) && lastChildren.length) {
+ selectedOpts = selectedOpts?.concat([item])
+ } else {
+ selectedOpts.splice(lastIndex, 1, item)
+ }
+ setHasChild(false)
+ const val = selectedOpts.map((e) => e[mergedFieldMaps.value])
+ onChange?.(val, selectedOpts, item)
+ autoClose && onClose?.()
+ }
+ setSeleted(selectedOpts)
+ },
+ [onChange, selected, autoClose],
+ )
+
+ const onTabClick = useCallback(
+ (index) => {
+ if (index === 0) {
+ setRenderOptions(options)
+ setSeleted(undefined)
+ setHasChild(true)
+ } else {
+ // @ts-ignore
+ const children = selected[index - 1][mergedFieldMaps.children]
+ setRenderOptions(children as CascaderOptionType[])
+ setSeleted(selected?.splice(0, index))
+ setHasChild(true)
+ }
+ },
+ [options, selected],
+ )
+
+ const cascaderClassName = useMemo(() => classnames('ygp-cascader', className), [className])
+
+ const renderChild = useMemo(() => {
+ return (
+
+
+ {selected?.map((item, index) => {
+ return (
+ onTabClick(index)}
+ >
+ {item[mergedFieldMaps.label]}
+
+ )
+ })}
+ {hasChild && 请选择}
+
+
+
+ {renderOptions.map((item) => {
+ return (
+ onClick(item)}
+ >
+ {item[mergedFieldMaps.label]}
+
+ )
+ })}
+
+
+
+ )
+ }, [selected, mergedFieldMaps, hasChild, renderOptions])
+
+ return poppable ? (
+
+ {renderChild}
+
+ ) : (
+ renderChild
+ )
+}
+Cascader.displayName = 'Cascader'
+
+export default Cascader
diff --git a/packages/src/components/cell/index.tsx b/packages/src/components/cell/index.tsx
new file mode 100644
index 0000000..5fe4c10
--- /dev/null
+++ b/packages/src/components/cell/index.tsx
@@ -0,0 +1,60 @@
+import { View, Image } from '@tarojs/components'
+import { FC, useCallback, useMemo } from 'react'
+import { ITouchEvent } from '@tarojs/components/types/common'
+import classNames from 'classnames'
+
+export type CellProps = {
+ className?: string
+ title?: string
+ desc?: string
+ arrow?: boolean
+ border?: boolean
+ onClick?: (event: ITouchEvent) => void
+}
+
+const Cell: FC = (props) => {
+ const { children, className = '', title, desc, border = false, arrow = true, onClick } = props
+ const cellClassName = useMemo(
+ () =>
+ classNames(
+ 'ygp-cell',
+
+ className,
+ ),
+ [className],
+ )
+
+ const handleClick = useCallback(
+ (event: ITouchEvent) => {
+ onClick?.(event)
+ },
+ [onClick],
+ )
+
+ return (
+
+
+
+
+ {title}
+
+
+
+ {children ? children : desc}
+ {arrow ? (
+
+ ) : null}
+
+
+
+ )
+}
+
+export default Cell
diff --git a/packages/src/components/checkbox/index.tsx b/packages/src/components/checkbox/index.tsx
new file mode 100644
index 0000000..21b0256
--- /dev/null
+++ b/packages/src/components/checkbox/index.tsx
@@ -0,0 +1,88 @@
+import { View } from '@tarojs/components'
+import { CSSProperties, FC, useCallback, useMemo } from 'react'
+import classNames from 'classnames'
+
+export type CheckOptionType = { label?; value?; [key: string]: any }
+
+export type CheckboxProps = {
+ /** value 选中激活的key */
+ value: (string | number)[]
+ /** 指定一行几个元素 */
+ columnNum?: 2 | 3 | 4
+ /** 是否多选,默认:false */
+ isMultiple?: boolean
+ /** 选项列表 */
+ options: CheckOptionType[]
+ /** 自定义激活属性 */
+ activeStyle?: CSSProperties
+ /** 字段映射关系, 可部分覆盖 */
+ fieldMaps?: Partial
+ /** 选择改变触发方法 */
+ onChange: (active?: (number | string)[], option?: CheckOptionType) => void
+}
+/**
+ * 按钮形式的checkbox
+ */
+const CheckBox: FC = (props) => {
+ const {
+ isMultiple = false,
+ options,
+ onChange,
+ value = [],
+ columnNum = 4,
+ activeStyle,
+ fieldMaps,
+ } = props
+
+ const mergedFieldMaps = useMemo(() => {
+ return {
+ label: 'label',
+ value: 'value',
+ ...fieldMaps,
+ }
+ }, [fieldMaps])
+
+ const itemClick = useCallback(
+ (item) => {
+ if (!value.some((ac) => ac == item[mergedFieldMaps.value])) {
+ if (!isMultiple) {
+ onChange([item[mergedFieldMaps.value]], item)
+ } else {
+ onChange([...value, item[mergedFieldMaps.value]], item)
+ }
+ } else {
+ let arr: (string | number)[] = []
+ for (let i = 0; i < value.length; i++) {
+ if (value[i] == item[mergedFieldMaps.value]) {
+ arr = [...value]
+ arr.splice(i, 1)
+ break
+ }
+ }
+ onChange(arr, item)
+ }
+ },
+ [value],
+ )
+
+ return (
+
+ {options?.map((item) => (
+ ac == item[mergedFieldMaps.value]) ? activeStyle : ''
+ }
+ className={classNames('check-item', `check-item-row-${columnNum}`, {
+ 'item-active': value.length && value.some((ac) => ac == item[mergedFieldMaps.value]),
+ })}
+ onClick={() => itemClick(item)}
+ >
+ {item[mergedFieldMaps.label]}
+
+ ))}
+
+ )
+}
+
+export default CheckBox
diff --git a/packages/src/components/config-provider/index.tsx b/packages/src/components/config-provider/index.tsx
new file mode 100644
index 0000000..f1455ff
--- /dev/null
+++ b/packages/src/components/config-provider/index.tsx
@@ -0,0 +1,21 @@
+import { createContext, FC } from 'react'
+
+export type ConfigProviderProps = {
+ uploadOptions?: {
+ getTokens?: (...arg) => Promise
+ getPrivateUrls?: (...arg) => Promise
+ getBciscmPrivateUrl?: (...arg) => Promise
+ }
+ [key: string]: any
+}
+
+export const ConfigContext = createContext({})
+
+ConfigContext.displayName = 'ConfigContext'
+
+const ConfigProvider: FC = (props) => {
+ const { children, ...rest } = props
+ return {children}
+}
+
+export default ConfigProvider
diff --git a/packages/src/components/date-picker/index.tsx b/packages/src/components/date-picker/index.tsx
new file mode 100644
index 0000000..774f52d
--- /dev/null
+++ b/packages/src/components/date-picker/index.tsx
@@ -0,0 +1,281 @@
+import Taro from '@tarojs/taro'
+import { View, PickerView, PickerViewColumn } from '@tarojs/components'
+import { FC, useCallback, useEffect, useState, useMemo } from 'react'
+import Button from '../button'
+import FooterArea from '../footer-area'
+import Popup from '../popup'
+import Tabs, { TabItemType } from '../tabs'
+import Utils from '../../utils/date'
+import type { Props as PopupProps } from '../popup'
+import { isNumber } from '../../utils/is'
+
+type dateType = string | Date
+
+export interface DatePickerProps extends Omit {
+ /** 选中的数据 */
+ value?: dateType | dateType[]
+ /** 是否数据改变自动关闭弹窗 */
+ autoClose?: boolean
+ /** 时间选择的类型,单选或范围选择, 默认为单个 */
+ mode?: 'one' | 'range'
+ /** 数据改变的回调 */
+ onChange?: (date: string | string[]) => void
+ /** 点击确认的回调事件 */
+ onConfirm?: (date: string | string[]) => void
+}
+
+enum RangeDateEnum {
+ start,
+ end,
+}
+
+const DatePicker: FC = (props) => {
+ const { value, height = '50%', mode = 'one', autoClose, onClose, onConfirm, onChange } = props
+ const [date, setDate] = useState(undefined)
+ const [activeKey, setActivekey] = useState(RangeDateEnum.start)
+
+ const years = useMemo(() => {
+ const years: number[] = []
+ // 获取当前年份
+ let currentYear = new Date().getFullYear()
+ // 十年前
+ let minYear = currentYear - 10
+ // 十年后
+ let maxYear = currentYear + 10
+ while (minYear < maxYear) {
+ years.push(minYear)
+ minYear++
+ }
+ return years
+ }, [])
+
+ const months = useMemo(() => Array.from({ length: 12 }).map((_, index) => index + 1), [])
+
+ const getDays = useCallback(
+ (type?) => {
+ let curYear
+ let curMonth
+ if (mode === 'range') {
+ if (date?.[type]) {
+ curYear = years[date[type][0]]
+ curMonth = months[date[type][1]]
+ } else {
+ curYear = years[0]
+ curMonth = months[0]
+ }
+ } else {
+ if (date) {
+ curYear = years[(date as number[])[0]]
+ curMonth = months[(date as number[])[1]]
+ } else {
+ curYear = years[0]
+ curMonth = months[0]
+ }
+ }
+
+ const monthDay = Utils.getMonthDays(curYear + '', curMonth + '')
+ return Array.from({ length: monthDay }).map((_, index) => index + 1)
+ },
+ [date],
+ )
+
+ useEffect(() => {
+ if (value) {
+ if (mode === 'range') {
+ let start = date2Index(value[RangeDateEnum.start])
+ let end = date2Index(value[RangeDateEnum.end])
+ setDate([start, end])
+ setActivekey(RangeDateEnum.start)
+ } else {
+ setDate(date2Index(value))
+ }
+ } else {
+ setDate(undefined)
+ setActivekey(RangeDateEnum.start)
+ }
+ }, [value])
+
+ const date2Index = useCallback((date) => {
+ const newDate = new Date(date)
+ const yearIdx = years.findIndex((item) => item === newDate.getFullYear())
+ const monthIdx = months.findIndex((item) => item === newDate.getMonth() + 1)
+ const dayIdx = getDays().findIndex((item) => item === newDate.getDate())
+ return [yearIdx, monthIdx, dayIdx]
+ }, [])
+
+ const index2Str = useCallback((index: number[], type?) => {
+ return `${years[index[0]]}-${Utils.getNumTwoBit(months[index[1]])}-${Utils.getNumTwoBit(
+ getDays(type)[index[2]],
+ )}`
+ }, [])
+
+ const confirm = useCallback(() => {
+ let datestr
+ if (date) {
+ datestr = index2Str(date as number[])
+ } else {
+ datestr = date
+ }
+ onConfirm?.(datestr)
+ onChange?.(datestr)
+ autoClose && onClose?.()
+ }, [date])
+
+ const rangeConfirm = useCallback(() => {
+ if (activeKey === RangeDateEnum.start) {
+ if (date?.[RangeDateEnum.start]) {
+ setActivekey(RangeDateEnum.end)
+ } else {
+ Taro.showToast({
+ title: '请选择开始时间',
+ icon: 'none',
+ })
+ }
+ } else {
+ // 如果没选择开始时间, 回到选择开始时间
+ if (!date?.[RangeDateEnum.start]) {
+ setActivekey(RangeDateEnum.start)
+ return
+ }
+ if (date?.[RangeDateEnum.end]) {
+ let startDate = index2Str(date[RangeDateEnum.start] as number[], RangeDateEnum.start)
+ let endDate = index2Str(date[RangeDateEnum.end] as number[], RangeDateEnum.end)
+ if (new Date(startDate).valueOf() > new Date(endDate).valueOf()) {
+ Taro.showToast({
+ title: '结束时间不能小于开始时间',
+ icon: 'none',
+ })
+ } else {
+ onConfirm?.([startDate, endDate])
+ onChange?.([startDate, endDate])
+ autoClose && onClose?.()
+ }
+ } else {
+ Taro.showToast({
+ title: '请选择结束时间',
+ icon: 'none',
+ })
+ }
+ }
+ }, [date, activeKey])
+
+ const onPickerChange = useCallback(
+ (e, type) => {
+ if (isNumber(type)) {
+ if (type === RangeDateEnum.start) {
+ setDate([e.detail.value])
+ }
+ if (type === RangeDateEnum.end) {
+ setDate([date?.[RangeDateEnum.start], e.detail.value])
+ }
+ } else {
+ setDate(e.detail.value)
+ }
+ },
+ [date],
+ )
+
+ const renderPickerView = useCallback(
+ (type?) => {
+ const value = (mode === 'one' ? date : date?.[type]) as number[]
+ const days = getDays(type)
+ return (
+
+ {
+ onPickerChange(e, type)
+ }}
+ >
+ <>
+ {/* 年 */}
+
+ {years?.map((item) => (
+
+ {item}年
+
+ ))}
+
+ {/* 月 */}
+
+ {months?.map((item) => (
+
+ {item}月
+
+ ))}
+
+
+ {days?.map((item) => (
+
+ {item}日
+
+ ))}
+
+ >
+
+
+ )
+ },
+ [years, months, date],
+ )
+
+ const tabItems: TabItemType[] = useMemo(() => {
+ return [
+ {
+ label: date?.[RangeDateEnum.start]
+ ? index2Str(date?.[RangeDateEnum.start] as number[], RangeDateEnum.start)
+ : '开始时间',
+ key: RangeDateEnum.start,
+ children: renderPickerView(RangeDateEnum.start),
+ },
+ {
+ label: date?.[RangeDateEnum.end]
+ ? index2Str(date?.[RangeDateEnum.end] as number[], RangeDateEnum.end)
+ : '结束时间',
+ key: RangeDateEnum.end,
+ children: renderPickerView(RangeDateEnum.end),
+ },
+ ]
+ }, [date])
+
+ const onTabChange = useCallback((e) => {
+ setActivekey(e.key)
+ }, [])
+
+ return (
+
+
+ {mode === 'one' ? (
+ renderPickerView()
+ ) : (
+
+ )}
+
+
+ {mode === 'one' && (
+
+ )}
+ {mode === 'range' && (
+
+ )}
+
+
+
+ )
+}
+
+DatePicker.displayName = 'DatePicker'
+
+export default DatePicker
diff --git a/packages/src/components/field/Field.tsx b/packages/src/components/field/Field.tsx
new file mode 100644
index 0000000..7d6ccd9
--- /dev/null
+++ b/packages/src/components/field/Field.tsx
@@ -0,0 +1,116 @@
+import { Input, InputProps, CommonEventFunction } from '@tarojs/components'
+import { FC } from '@tarojs/taro'
+import { CSSProperties, ReactElement, ReactNode } from 'react'
+import { FieldLayout, ValueAlign } from '../form/context'
+import { isNumber } from '../../utils/is'
+import FieldWrap from './FieldWrap'
+
+export interface FieldProps extends InputProps {
+ type?: any
+ /** 自定义className */
+ className?: string
+ /** label文案 */
+ label?: string
+ /** label宽度 */
+ labelWidth?: number
+ /** label对齐方式 */
+ labelAlign?: ValueAlign
+ /** value对齐方式 */
+ valueAlign?: ValueAlign
+ /** 只读状态 */
+ readonly?: boolean
+ /** 是否需要清除功能 */
+ clear?: boolean
+ /** 是否必填 */
+ required?: boolean
+ /** 如果为true,则渲染可点击区域和箭头 */
+ isLink?: boolean
+ /** 数据是否出错 */
+ error?: boolean
+ /** 错误文案 */
+ errorMsg?: string
+ /** label插槽 */
+ labelSlot?: ReactNode
+ /** 右边插槽 */
+ rightSlot?: ReactNode
+ /** value区域插槽 */
+ valueSlot?: ReactNode
+ /** 自定义样式 */
+ style?: CSSProperties
+ /** 数字精度, type为number或digit时才生效 */
+ precision?: number
+ /** 最小值, type为number或digit时才生效 */
+ min?: number
+ /** 最大值, type为number或digit时才生效 */
+ max?: number
+ /** 布局,默认:horizontal */
+ layout?: FieldLayout
+ /** 是否需要点击hover,默认:true */
+ clickHover?: boolean
+ /** 自定义点击hover样式 */
+ hoverClass?: string
+ /** 点击hover展示到消失的持续时间(单位毫秒),默认600ms*/
+ hoverSustainTime?: number
+ /** 字段间的下边距, 默认false*/
+ gap?: boolean
+ /** value改变时的回调 */
+ // onChange?: CommonEventFunction
+ // TODO 根据类型推导 先解决报错 后面再处理
+ onChange?: Function
+ /** 点击清除按钮的回调 */
+ onClear?: CommonEventFunction
+ /** 点击field区域的回调 */
+ onClick?: CommonEventFunction
+}
+
+const Field: FC = (props): ReactElement => {
+ const { type, min, max, precision, readonly } = props
+ return (
+
+ {(fieldProps) => {
+ const { value, onChange, onBlur, ...restFieldProps } = fieldProps
+ const handelBlur = (e) => {
+ // 如果是数字类型
+ if (e.detail.value && (type === 'number' || type === 'digit')) {
+ const num = Number(e.detail.value)
+ if (Number.isFinite(num)) {
+ if (isNumber(min) && num < min) {
+ e.detail.value = min + ''
+ } else if (isNumber(max) && num > max) {
+ e.detail.value = max + ''
+ } else {
+ if (isNumber(precision)) {
+ e.detail.value = Number(num.toFixed(precision))
+ } else {
+ e.detail.value = num
+ }
+ }
+ } else {
+ e.detail.value = undefined
+ }
+ onChange?.(e)
+ }
+ onBlur?.(e)
+ }
+ return (
+ <>
+ {readonly ? (
+ value
+ ) : (
+
+ )}
+ >
+ )
+ }}
+
+ )
+}
+
+export default Field
diff --git a/packages/src/components/field/FieldWrap.tsx b/packages/src/components/field/FieldWrap.tsx
new file mode 100644
index 0000000..590f21c
--- /dev/null
+++ b/packages/src/components/field/FieldWrap.tsx
@@ -0,0 +1,293 @@
+import {
+ FC,
+ ReactElement,
+ ReactNode,
+ useCallback,
+ useMemo,
+ isValidElement,
+ cloneElement,
+ useState,
+} from 'react'
+import { View, Label, Image } from '@tarojs/components'
+import classNames from 'classnames'
+import { isArray, isFunction, isDate, isNullOrUnDef } from '../../utils/is'
+import { FieldProps } from './Field'
+import Utils from '../../utils/date'
+
+interface FieldWrapType extends Omit {
+ options?: any[]
+ renderLinkChild?: boolean
+ onChange?: Function
+ [key: string]: any
+}
+
+const FieldWrap: FC = (props) => {
+ const {
+ value,
+ label,
+ className,
+ required,
+ readonly,
+ labelWidth = 80,
+ isLink,
+ error,
+ errorMsg,
+ placeholder,
+ labelSlot,
+ rightSlot,
+ valueSlot,
+ style,
+ layout = 'horizontal',
+ labelAlign = 'left',
+ valueAlign = layout === 'horizontal' ? 'right' : 'left',
+ clickHover = true,
+ hoverClass,
+ hoverSustainTime = 300,
+ children,
+ clear = true,
+ onChange,
+ onClear,
+ onClick,
+ // picker相关
+ options,
+ renderLinkChild,
+ fieldMaps,
+ mode,
+ gap,
+ ...restFieldProps
+ } = props
+
+ const [visible, setVisible] = useState(false)
+
+ const mergedFieldMaps = useMemo(() => {
+ return {
+ label: 'label',
+ value: 'value',
+ children: 'children',
+ ...fieldMaps,
+ }
+ }, [fieldMaps])
+
+ const fieldClassName = useMemo(() => {
+ return classNames(
+ 'ygp-field',
+ {
+ 'ygp-field-error': error,
+ 'ygp-field-required': required,
+ 'ygp-field-gap': gap,
+ },
+ className,
+ )
+ }, [className, error, required])
+
+ const labelStyle = useMemo(() => {
+ return {
+ width: `${labelWidth}px`,
+ textAlign: labelAlign,
+ }
+ }, [labelAlign, labelWidth])
+
+ const valueStyle = useMemo(() => {
+ return {
+ textAlign: valueAlign,
+ }
+ }, [valueAlign])
+
+ const rightContent = useMemo(() => {
+ if (!rightSlot) {
+ return false
+ }
+ if (isFunction(rightSlot)) {
+ return rightSlot()
+ }
+ return rightSlot
+ }, [rightSlot])
+
+ const valueContent = useMemo(() => {
+ if (valueSlot) {
+ if (isFunction(valueSlot)) {
+ return valueSlot(value)
+ } else {
+ return valueSlot
+ }
+ } else {
+ return false
+ }
+ }, [valueSlot, value])
+
+ const labelCotent = useMemo(() => {
+ if (labelSlot) {
+ if (isFunction(labelSlot)) {
+ return labelSlot()
+ } else {
+ return labelSlot
+ }
+ } else {
+ return label
+ }
+ }, [label, labelSlot])
+
+ const handleClick = useCallback(
+ (e) => {
+ if (!readonly) {
+ onClick?.(e)
+ setVisible(true)
+ }
+ },
+ [readonly, children],
+ )
+
+ const handleClear = useCallback((e) => {
+ // @ts-ignore
+ onChange?.(undefined)
+ onClear?.(e)
+ }, [])
+
+ const getCascaderValue = useCallback((val, data) => {
+ let text = ''
+ isArray(val) &&
+ val?.forEach((el) => {
+ let option = data.find((item) => item[mergedFieldMaps.value] === el)
+ if (option) {
+ text += option[mergedFieldMaps.label]
+ if (option[mergedFieldMaps.children]) data = option[mergedFieldMaps.children]
+ }
+ })
+ return text
+ }, [])
+
+ const getValue = useCallback(() => {
+ let val: any = value
+ // 如果有options,匹配name
+ if (!isNullOrUnDef(val)) {
+ // @ts-ignore
+ switch (children.type.displayName) {
+ case 'Cascader':
+ val = getCascaderValue(value, options)
+ break
+ case 'Picker':
+ let option = options?.find((item) => item[mergedFieldMaps.value] === value)
+ val = option?.[mergedFieldMaps.label]
+ break
+ case 'DatePicker':
+ // 时间范围选择
+ if (mode === 'range') {
+ let start = val[0]
+ let end = val[1]
+ val = [
+ isDate(start) ? Utils.date2Str(start) : start,
+ isDate(end) ? Utils.date2Str(end) : end,
+ ].join('~')
+ } else {
+ // 判断是否是日期类型, 如果是日期类型转为字符串
+ if (isDate(value)) {
+ val = Utils.date2Str(value)
+ }
+ }
+ break
+ }
+ }
+ return readonly ? val : val || placeholder
+ }, [readonly, options, value, mode])
+
+ const renderLink = useCallback<() => ReactNode>(
+ () => (
+
+
+ {getValue()}
+
+ {!readonly && (
+
+ )}
+
+ ),
+ [value, placeholder, readonly, renderLinkChild, visible, options],
+ )
+
+ const renderChild = () => {
+ const childProps = {
+ value,
+ placeholder,
+ readonly,
+ onChange,
+ options,
+ visible,
+ onClose: () => {
+ setVisible(false)
+ },
+ fieldMaps,
+ mode,
+ ...restFieldProps,
+ }
+ if (isFunction(children)) {
+ return children(childProps)
+ } else if (isValidElement(children)) {
+ return cloneElement(children as ReactElement, { ...childProps, ...children.props })
+ } else {
+ console.warn('children of FieldWrap is not valid ReactElement.')
+ }
+ }
+
+ const renderFieldContent = () => {
+ let childNode: ReactNode
+ // 如果是link状态, 直接渲染link样式
+ if (isLink) {
+ childNode = renderLink()
+ } else {
+ childNode = renderChild()
+ }
+ return childNode
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {valueContent ? valueContent : renderFieldContent()}
+
+ {clear && !readonly && !isNullOrUnDef(value) && (
+
+ )}
+ {rightContent && (
+ {rightContent}
+ )}
+
+ {error && {errorMsg || ''}}
+
+
+ {renderLinkChild && renderChild()}
+
+ )
+}
+
+export default FieldWrap
diff --git a/packages/src/components/field/index.tsx b/packages/src/components/field/index.tsx
new file mode 100644
index 0000000..9ee13d5
--- /dev/null
+++ b/packages/src/components/field/index.tsx
@@ -0,0 +1,6 @@
+import Field from './Field'
+import FieldWrap from './FieldWrap'
+import type { FieldProps } from './Field'
+
+export { FieldProps, FieldWrap }
+export default Field
diff --git a/packages/src/components/footer-area/index.tsx b/packages/src/components/footer-area/index.tsx
new file mode 100644
index 0000000..cd10f87
--- /dev/null
+++ b/packages/src/components/footer-area/index.tsx
@@ -0,0 +1,44 @@
+import { CSSProperties, FC, useMemo } from 'react'
+import { View } from '@tarojs/components'
+import classNames from 'classnames'
+import { useSafeBottom } from '../../hooks'
+
+type FooterButton = {
+ // id
+ id?: string
+ // 自定义class
+ className?: string
+ // 背景是否透明, 默认false
+ transparent?: boolean
+ // 自定义style
+ style?: CSSProperties
+}
+
+const FooterArea: FC = (props) => {
+ const { children, className, transparent = false, style, id } = props
+ const { safeAreaPadding: paddingBottom, safeBottom } = useSafeBottom()
+
+ const classes = useMemo(
+ () =>
+ classNames('ygp-footer-area', {
+ 'ygp-footer-area-transparent': transparent,
+ }),
+ [transparent],
+ )
+
+ const fixedClasses = useMemo(() => classNames('ygp-footer-area-fixed', className), [])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export default FooterArea
diff --git a/packages/src/components/form/Form.tsx b/packages/src/components/form/Form.tsx
new file mode 100644
index 0000000..036c246
--- /dev/null
+++ b/packages/src/components/form/Form.tsx
@@ -0,0 +1,266 @@
+import { forwardRef, ForwardRefRenderFunction, Fragment, useImperativeHandle, useMemo } from 'react'
+import RcForm, { FormInstance, useForm } from 'rc-field-form'
+import type { FormProps as RcFormProps } from 'rc-field-form/es/Form'
+import { FieldProps as RcFieldProps } from 'rc-field-form/es/Field'
+import { InputProps } from '@tarojs/components'
+import classNames from 'classnames'
+import FieldWrap from '../field/FieldWrap'
+import FormItem, { FormItemProps } from './FormItem'
+import { FieldLayout, FormContext, ValueAlign } from './context'
+import Field, { FieldProps } from '../field'
+import Textarea, { TextareaProps } from '../textarea'
+import Swtich from '../switch'
+import Picker, { PickerProps } from '../picker'
+import Checkbox, { CheckboxProps } from '../checkbox'
+import Cascader, { CascaderProps } from '../cascader'
+import DatePicker, { DatePickerProps } from '../date-picker'
+
+export type BaseFormItem = InputProps &
+ RcFieldProps &
+ FieldProps &
+ FormItemProps &
+ TextareaProps &
+ Partial &
+ Partial &
+ Partial &
+ Partial
+export interface FormItemType extends Omit {
+ type?:
+ | InputProps['type']
+ | 'textarea'
+ | 'switch'
+ | 'upload'
+ | 'picker'
+ | 'cascader'
+ | 'checkbox'
+ | 'date'
+ //TODO 根据组件类型去推导onChange的类型
+ onChange?: Function
+}
+export interface FormProps extends Omit, 'form'> {
+ name?: string
+ /** 自定义className */
+ className?: string
+ /** 只读状态 */
+ readonly?: boolean
+ /** label宽度 */
+ labelWidth?: number
+ /** value对齐方式 */
+ valueAlign?: ValueAlign
+ /** useForm的返回值 */
+ form?: FormInstance
+ /** 表单配置 */
+ items?: FormItemType[]
+ /** 布局,默认:horizontal */
+ layout?: FieldLayout
+ /** 是否需要点击hover,默认:true */
+ clickHover?: boolean
+ /** 自定义点击hover样式*/
+ hoverClass?: string
+ /** 点击hover展示到消失的持续时间(单位毫秒),默认600ms*/
+ hoverSustainTime?: number
+}
+
+const InternalForm: ForwardRefRenderFunction = (props, ref) => {
+ const {
+ className = '',
+ readonly,
+ name,
+ labelWidth,
+ valueAlign,
+ layout,
+ clickHover,
+ hoverClass,
+ hoverSustainTime,
+ form,
+ items,
+ children,
+ // rc-field-form默认容器是form标签,
+ // taro某些环境没有form模版导致form整体渲染失败,具体还不清楚为什么某些环境没有form模版
+ // 把容器改为view做兼容处理
+ component = 'view',
+ ...restFormProps
+ } = props
+
+ const [wrapForm] = useForm(form)
+
+ useImperativeHandle(ref, () => wrapForm)
+
+ const formClassName = classNames('ygp-form', className)
+
+ const formContextValue = useMemo(() => {
+ return {
+ name,
+ readonly,
+ valueAlign,
+ labelWidth,
+ form: wrapForm,
+ layout,
+ clickHover,
+ hoverClass,
+ hoverSustainTime,
+ items,
+ }
+ }, [
+ name,
+ readonly,
+ layout,
+ clickHover,
+ hoverClass,
+ hoverSustainTime,
+ valueAlign,
+ labelWidth,
+ wrapForm,
+ items,
+ ])
+
+ // 通过配置渲染表单
+ const renderFormItem = () => {
+ return (
+
+ {items?.map((item) => {
+ const {
+ name: formItemName,
+ type,
+ trigger,
+ required,
+ isLink,
+ validateTrigger,
+ ...restItemProps
+ } = item
+ let { rules, placeholder } = item
+ let msgPrefix = '请输入'
+
+ switch (type) {
+ case 'checkbox':
+ case 'switch':
+ case 'cascader':
+ case 'picker':
+ case 'date':
+ msgPrefix = '请选择'
+ break
+ case 'upload':
+ msgPrefix = '请上传'
+ break
+ }
+
+ if (!placeholder) {
+ placeholder = msgPrefix
+ }
+
+ if (required) {
+ if (
+ rules &&
+ !rules.every(
+ (rule) => rule && typeof rule === 'object' && rule.required && !rule.warningOnly,
+ )
+ ) {
+ // @ts-ignore
+ rules = rules.concat([{ required: true, message: `${msgPrefix}${item.label || ''}` }])
+ } else {
+ rules = [{ required: true, message: `${msgPrefix}${item.label || ''}` }]
+ }
+ }
+ let formItemChild
+ const formItemChildProp = {
+ ...restItemProps,
+ placeholder,
+ isLink,
+ type,
+ }
+
+ switch (type) {
+ case 'date':
+ formItemChild = (
+
+ {/* @ts-ignore */}
+
+
+ )
+ break
+ case 'checkbox':
+ formItemChild = (
+
+ {/* @ts-ignore */}
+
+
+ )
+ break
+ case 'switch':
+ formItemChild = (
+
+ {(feildProps) => (
+
+ )}
+
+ )
+ break
+ case 'cascader':
+ formItemChild = (
+
+ {/* @ts-ignore */}
+
+
+ )
+ break
+ case 'picker':
+ formItemChild = (
+
+ {/* @ts-ignore */}
+
+
+ )
+ break
+ case 'textarea':
+ formItemChild = (
+
+
+
+ )
+ break
+ default:
+ formItemChild =
+ }
+
+ return (
+
+ {formItemChild}
+
+ )
+ })}
+
+ )
+ }
+
+ return (
+
+
+ {items ? renderFormItem() : children}
+
+
+ )
+}
+
+const Form = forwardRef(InternalForm)
+
+export { useForm }
+
+export default Form
diff --git a/packages/src/components/form/FormItem.tsx b/packages/src/components/form/FormItem.tsx
new file mode 100644
index 0000000..9eb84ab
--- /dev/null
+++ b/packages/src/components/form/FormItem.tsx
@@ -0,0 +1,83 @@
+import { View } from '@tarojs/components'
+import classNames from 'classnames'
+import { Field as RcField } from 'rc-field-form'
+import type { FieldProps } from 'rc-field-form/es/Field'
+import { cloneElement, FC, ReactElement, useContext, useMemo } from 'react'
+import { FormContext, FormContextProps } from './context'
+
+export interface FormItemProps extends FieldProps {
+ label?: string
+ className?: string
+ required?: boolean
+ children?: ReactElement
+}
+
+const InternalFormItem: FC = (props): ReactElement => {
+ const {
+ className,
+ required,
+ children,
+ rules,
+ trigger = 'onChange',
+ label,
+ ...formItemProps
+ } = props
+ const { valueAlign, labelWidth, layout, clickHover, hoverClass, hoverSustainTime, readonly } =
+ useContext(FormContext)
+
+ const formItemClassName = useMemo(() => classNames('ygp-form-item', className), [className])
+
+ return (
+
+ {(control, renderMeta, context) => {
+ const isRequired =
+ required !== undefined
+ ? required
+ : !!(
+ rules &&
+ rules.some((rule) => {
+ if (rule && typeof rule === 'object' && rule.required && !rule.warningOnly) {
+ return true
+ }
+ if (typeof rule === 'function') {
+ const ruleEntity = rule(context)
+ return ruleEntity && ruleEntity.required && !ruleEntity.warningOnly
+ }
+ return false
+ })
+ )
+
+ // 联合onChange事件
+ const onChange = (...arg) => {
+ control.onChange(...arg)
+ children && children.props.onChange && children.props.onChange(...arg)
+ }
+
+ if (children) {
+ const isError = !!renderMeta.errors.length
+ const childProps = {
+ ...control,
+ ...formItemProps,
+ label,
+ valueAlign,
+ labelWidth,
+ readonly,
+ layout,
+ clickHover,
+ hoverClass,
+ hoverSustainTime,
+ required: isRequired,
+ error: isError,
+ errorMsg: isError ? renderMeta.errors[0] : '',
+ ...children.props, // children.props优先
+ onChange,
+ }
+
+ return {cloneElement(children, childProps)}
+ }
+ }}
+
+ )
+}
+
+export default InternalFormItem
diff --git a/packages/src/components/form/context.ts b/packages/src/components/form/context.ts
new file mode 100644
index 0000000..98b1d56
--- /dev/null
+++ b/packages/src/components/form/context.ts
@@ -0,0 +1,26 @@
+import { createContext } from 'react'
+import { FormInstance } from 'rc-field-form'
+
+export type ValueAlign = 'left' | 'right'
+
+export type FieldLayout = 'vertical' | 'horizontal'
+export interface FormContextProps {
+ name?: string
+ valueAlign?: ValueAlign
+ labelWidth?: number
+ readonly?: boolean
+ form?: FormInstance
+ /** 布局,默认:horizontal */
+ layout?: FieldLayout
+ /** 是否需要点击hover,默认:true */
+ clickHover?: boolean
+ /** 自定义点击hover样式 */
+ hoverClass?: string
+ /** 点击hover展示到消失的持续时间(单位毫秒),默认600ms*/
+ hoverSustainTime?: number
+}
+
+export const FormContext = createContext({
+ valueAlign: 'right',
+ labelWidth: 80,
+})
diff --git a/packages/src/components/form/index.tsx b/packages/src/components/form/index.tsx
new file mode 100644
index 0000000..1714464
--- /dev/null
+++ b/packages/src/components/form/index.tsx
@@ -0,0 +1,16 @@
+import InternalForm, { useForm } from './Form'
+import InternalFormItem from './FormItem'
+
+type InternalFormType = typeof InternalForm
+type InternalFormItemType = typeof InternalFormItem
+interface FormInterface extends InternalFormType {
+ useForm: typeof useForm
+ Item: InternalFormItemType
+}
+
+const Form = InternalForm as FormInterface
+
+Form.useForm = useForm
+Form.Item = InternalFormItem
+
+export default Form
diff --git a/packages/src/components/picker/index.tsx b/packages/src/components/picker/index.tsx
new file mode 100644
index 0000000..d0edd77
--- /dev/null
+++ b/packages/src/components/picker/index.tsx
@@ -0,0 +1,101 @@
+import { ScrollView, View } from '@tarojs/components'
+import { FC, useCallback, useEffect, useState, useMemo } from 'react'
+import classNames from 'classnames'
+import Taro from '@tarojs/taro'
+import useUUID from '../../hooks/useUUID'
+import Popup from '../popup'
+import type { Props as PopupProps } from '../popup'
+
+export type PickerOptionType = { label?; value?; [key: string]: any }
+export interface PickerProps extends Omit {
+ /** 选中的数据 */
+ value: number | string
+ options: PickerOptionType[]
+ autoClose?: boolean
+ /** 字段映射关系, 可部分覆盖 */
+ fieldMaps?: Partial
+ onChange: (value: string | number, option: PickerOptionType) => void
+}
+
+const Picker: FC = (props) => {
+ const { options, value, height = '50%', visible, autoClose, fieldMaps, onClose, onChange } = props
+
+ const uuid = useUUID()
+ const [paickerId, setPaickerId] = useState('')
+
+ const mergedFieldMaps = useMemo(() => {
+ return {
+ label: 'label',
+ value: 'value',
+ ...fieldMaps,
+ }
+ }, [fieldMaps])
+
+ useEffect(() => {
+ if (visible) {
+ Taro.nextTick(() => {
+ for (let i = 0; i < options.length; i++) {
+ const item = options[i]
+ if (process.env.TARO_ENV !== 'h5') {
+ if (value || value === 0) {
+ if (item[mergedFieldMaps.value] === value) {
+ if (i > 1) {
+ setPaickerId('P' + uuid + options[i - 1][mergedFieldMaps.value])
+ } else {
+ setPaickerId('P' + uuid + options[0][mergedFieldMaps.value])
+ }
+ }
+ } else {
+ setPaickerId('P' + uuid + options[0][mergedFieldMaps.value])
+ }
+ } else {
+ setPaickerId('P' + uuid + value)
+ }
+ }
+ })
+ } else {
+ setPaickerId('')
+ }
+ }, [uuid, value, options, visible, mergedFieldMaps])
+
+ const tabClick = useCallback(
+ (val: number | string, index: number) => {
+ if (val === value) return
+ onChange(options[index][mergedFieldMaps.value], options[index])
+ autoClose && onClose()
+ },
+ [value, autoClose, options, mergedFieldMaps],
+ )
+
+ return (
+
+
+
+ {options?.map((item, index) => (
+ tabClick(item[mergedFieldMaps.value], index)}
+ >
+ {item[mergedFieldMaps.label]}
+
+ ))}
+
+
+
+ )
+}
+
+Picker.displayName = 'Picker'
+
+export default Picker
diff --git a/packages/src/components/popup/index.tsx b/packages/src/components/popup/index.tsx
new file mode 100644
index 0000000..0fa484b
--- /dev/null
+++ b/packages/src/components/popup/index.tsx
@@ -0,0 +1,116 @@
+import { View, Image } from '@tarojs/components'
+import { CSSProperties, FC, ReactNode, useCallback, useEffect, useState } from 'react'
+import classNames from 'classnames'
+import Taro from '@tarojs/taro'
+
+export type Props = {
+ /** 头部标题 */
+ title?: string
+ /** 点击阴影遮罩是否关闭,默认为true */
+ closeOnclickOverlay?: boolean
+ /** 标题对齐位置, 默认:center*/
+ titleAlign?: 'center' | 'left'
+ /** 弹框位置:默认为bottom*/
+ placement?: 'bottom' | 'right' | 'top'
+ /** 是否显示,默认为false */
+ visible: boolean
+ /** 自定义头部节点 */
+ headerSlot?: ReactNode
+ /**弹框高度,仅在placement为bottom时生效,默认为 60% */
+ height?: string
+ /**弹框宽度,仅在placement为right时生效,默认为 80% */
+ width?: string
+ /** 弹框过度动画时间(单位秒),默认为0.3S */
+ transitionTime?: number
+ /** 头部左边自定义按钮 */
+ headerLeft?: ReactNode
+ /** 关闭回调事件 */
+ onClose: (...p: any) => any
+ style?: CSSProperties
+ className?: string
+}
+const Popup: FC = (props) => {
+ const {
+ title,
+ closeOnclickOverlay = true,
+ titleAlign = 'center',
+ className,
+ placement = 'bottom',
+ height = '60%',
+ width = '80%',
+ transitionTime = 0.3,
+ visible,
+ headerLeft,
+ onClose,
+ style = {},
+ children,
+ headerSlot,
+ } = props
+ const closeOnclickOverlayClick = useCallback(() => {
+ closeOnclickOverlay && onClose && onClose()
+ }, [])
+ const [popupStyle, setPopupStyle] = useState({})
+ const [isShow, setIsShow] = useState(false)
+ useEffect(() => {
+ const pStyle: CSSProperties = { transition: `transform ${transitionTime}s` }
+ if (placement === 'top' || placement === 'bottom') {
+ pStyle.height = height
+ } else if (placement === 'right') {
+ pStyle.width = width
+ }
+ if (visible) {
+ setIsShow(visible)
+ Taro.nextTick(() => {
+ Taro.nextTick(() => {
+ pStyle.transform = 'translate(0,0)'
+ setPopupStyle(pStyle)
+ })
+ })
+ } else {
+ if (placement === 'bottom') {
+ pStyle.transform = 'translate(0,100%)'
+ } else if (placement === 'top') {
+ pStyle.transform = 'translate(0,-100%)'
+ } else if (placement == 'right') {
+ pStyle.transform = 'translate(100%,0)'
+ }
+ setPopupStyle(pStyle)
+ setTimeout(() => {
+ setIsShow(visible)
+ }, transitionTime * 1000)
+ }
+ }, [visible])
+
+ return (
+
+ e.stopPropagation()}
+ >
+ {headerSlot || (
+
+ {titleAlign == 'center' && {headerLeft}}
+ {title}
+
+
+
+
+ )}
+ {children}
+
+
+ )
+}
+
+export default Popup
diff --git a/packages/src/components/switch/index.tsx b/packages/src/components/switch/index.tsx
new file mode 100644
index 0000000..59c5371
--- /dev/null
+++ b/packages/src/components/switch/index.tsx
@@ -0,0 +1,41 @@
+import { FC, useState, useEffect, useMemo } from 'react'
+import { ITouchEvent, View } from '@tarojs/components'
+import classNames from 'classnames'
+
+export interface SwitchProps {
+ checked: boolean
+ disabled: boolean
+ className: string
+ onChange: (val: boolean, event: ITouchEvent) => void
+}
+
+const Switch: FC> = (props) => {
+ const { checked = false, disabled = false, onChange, className } = props
+ const [value, setValue] = useState(false)
+
+ useEffect(() => {
+ setValue(checked)
+ }, [checked])
+
+ const SwitchClassName = useMemo(
+ () =>
+ classNames('ygp-switch ygp-switch-base', className, {
+ 'switch-open': value,
+ 'switch-close': !value,
+ 'ygp-switch-disabled': disabled,
+ }),
+ [value],
+ )
+
+ const onClick = (event: ITouchEvent) => {
+ if (disabled) return
+ onChange?.(!value, event)
+ }
+ return (
+
+
+
+ )
+}
+
+export default Switch
diff --git a/packages/src/components/tabs/index.tsx b/packages/src/components/tabs/index.tsx
new file mode 100644
index 0000000..ad8b2c6
--- /dev/null
+++ b/packages/src/components/tabs/index.tsx
@@ -0,0 +1,134 @@
+import { ScrollView, Swiper, SwiperItem, View } from '@tarojs/components'
+import { CSSProperties, FC, ReactNode, useCallback, useEffect, useState } from 'react'
+import classNames from 'classnames'
+import useUUID from '../../hooks/useUUID'
+
+export type TabItemType = {
+ /** 标题,可自定义节点 */
+ label: string | ReactNode
+ /** 激活key */
+ key: number | string
+ /** 展示内容 */
+ children: ReactNode
+}
+
+type Props = {
+ /** 当前激活的key */
+ activeKey: number | string
+ /** item配置 */
+ items: TabItemType[]
+ /** tab左边自定义按钮 */
+ tabLeftBtn?: ReactNode | string
+ /** tab右边自定义按钮 */
+ tabRightBtn?: ReactNode | string
+ /** tabItme样式 */
+ tabItemClass?: string
+ /** 由于组件宽度无法确定,需要添加滚动显示向前索引数,如tabItem显示5个,则索引向前2,则当前选择值是第三个则会显示在中间,默认值3 */
+ scrollIntoViewNumber?: number
+ className?: string
+ style?: CSSProperties | string
+ onChange: (item: TabItemType) => void
+}
+const Tabs: FC = (props) => {
+ const {
+ activeKey,
+ items = [],
+ className = '',
+ tabItemClass,
+ scrollIntoViewNumber = 2,
+ style,
+ tabLeftBtn,
+ tabRightBtn,
+ onChange,
+ } = props
+ const uuid = useUUID()
+ const [tabID, setTabID] = useState('T' + uuid + items[0].key)
+ const [childIndex, setChildIndex] = useState(0)
+
+ useEffect(() => {
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i]
+ if (item.key === activeKey) {
+ if (i > scrollIntoViewNumber) {
+ setTabID('T' + uuid + items[i - scrollIntoViewNumber].key)
+ } else {
+ setTabID('T' + uuid + items[0].key)
+ }
+ setChildIndex(i)
+ return
+ }
+ }
+ }, [activeKey, uuid])
+
+ const tabClick = useCallback(
+ (key: number | string, index: number) => {
+ if (key === activeKey) return
+ onChange(items[index])
+ },
+ [activeKey],
+ )
+
+ const onSwiperChange = useCallback(
+ (e) => {
+ if (items[e.detail.current].key === activeKey) return
+ onChange(items[e.detail.current])
+ },
+ [activeKey],
+ )
+ return (
+
+ {/* tab */}
+
+ {tabLeftBtn}
+
+
+ {items.map((item, index) => (
+
+ tabClick(item.key, index)}
+ >
+ {item.label}
+
+
+ ))}
+
+
+ {tabRightBtn}
+
+ {/* children */}
+
+ {items.map((item) => (
+
+ {item.children}
+
+ ))}
+
+
+ )
+}
+
+export default Tabs
diff --git a/packages/src/components/textarea/index.tsx b/packages/src/components/textarea/index.tsx
new file mode 100644
index 0000000..a89c99a
--- /dev/null
+++ b/packages/src/components/textarea/index.tsx
@@ -0,0 +1,40 @@
+import {
+ View,
+ Textarea as TaroTextarea,
+ TextareaProps as TaroTextareaProps,
+ CommonEventFunction,
+} from '@tarojs/components'
+import classNames from 'classnames'
+import { FC, useCallback } from 'react'
+
+export interface TextareaProps extends TaroTextareaProps {
+ className?: string
+ showCount?: boolean
+ readonly?: boolean
+ onChange?: CommonEventFunction
+}
+
+const Textarea: FC = (props) => {
+ const { value, maxlength = 140, className, showCount, readonly = false, onChange } = props
+
+ const TextareaClassname = classNames('ygp-textarea', className)
+
+ const handleInput = useCallback((e) => {
+ onChange?.(e)
+ }, [])
+
+ return readonly ? (
+ {value}
+ ) : (
+
+
+ {showCount && (
+
+ {value ? value.length : 0}/{maxlength}
+
+ )}
+
+ )
+}
+
+export default Textarea
diff --git a/packages/src/hooks/index.ts b/packages/src/hooks/index.ts
new file mode 100644
index 0000000..b4a5acc
--- /dev/null
+++ b/packages/src/hooks/index.ts
@@ -0,0 +1,7 @@
+export { default as useBoolean } from './useBoolean'
+export { default as useFetch } from './useFetch'
+export { default as useSystemInfo } from './useSystemInfo'
+export { default as useSafeBottom } from './useSafeBottom'
+export { default as useSafeTop } from './useSafeTop'
+export { default as useAvailableViewHeight } from './useAvailableViewHeight'
+export { default as useUUID } from './useUUID'
diff --git a/packages/src/hooks/tsconfig.json b/packages/src/hooks/tsconfig.json
new file mode 100644
index 0000000..89a03e7
--- /dev/null
+++ b/packages/src/hooks/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "."
+ },
+ "include": ["index.ts"]
+}
diff --git a/packages/src/hooks/useAvailableViewHeight.ts b/packages/src/hooks/useAvailableViewHeight.ts
new file mode 100644
index 0000000..5d9b8e9
--- /dev/null
+++ b/packages/src/hooks/useAvailableViewHeight.ts
@@ -0,0 +1,39 @@
+import Taro from '@tarojs/taro'
+import { useEffect, useMemo, useState } from 'react'
+import useSystemInfo from './useSystemInfo'
+
+export type DeepArray = Array>
+/**
+ * 返回排除了传入查询节点后的视窗可用高度
+ * @param arg 多维字符串数组,最大深度为10
+ * @returns number
+ */
+const useAvailableViewHeight = (...arg: DeepArray) => {
+ const systemInfo = useSystemInfo()
+ const [extraHeight, setExtraHeight] = useState(0)
+ useEffect(() => {
+ Taro.nextTick(() => {
+ const query = Taro.createSelectorQuery()
+ arg.flat(10).forEach((item) => {
+ if (item instanceof Array) {
+ throw Error(
+ 'The useAvailableViewHeight parameter is incorrect, the array depth cannot be greater than 10',
+ )
+ } else {
+ query.select(item).boundingClientRect()
+ }
+ })
+
+ query.exec((res) => {
+ res.length &&
+ setExtraHeight(res.map((dom) => (dom ? dom.height : 0)).reduce((a, b) => a + b))
+ })
+ })
+ }, [arg])
+
+ return useMemo(() => {
+ return systemInfo ? systemInfo.windowHeight - extraHeight : 0
+ }, [systemInfo, extraHeight])
+}
+
+export default useAvailableViewHeight
diff --git a/packages/src/hooks/useBoolean.ts b/packages/src/hooks/useBoolean.ts
new file mode 100644
index 0000000..5c4dd9b
--- /dev/null
+++ b/packages/src/hooks/useBoolean.ts
@@ -0,0 +1,28 @@
+import { useState, useMemo } from 'react'
+
+export interface Actions {
+ setTrue: () => void
+ setFalse: () => void
+ set: (value: boolean) => void
+ toggle: () => void
+}
+
+const useBoolean = (defaultValue = false): [boolean, Actions] => {
+ const [state, setState] = useState(defaultValue)
+
+ const actions: Actions = useMemo(() => {
+ const setTrue = () => setState(true)
+ const setFalse = () => setState(false)
+ const toggle = () => setState(!state)
+ return {
+ toggle,
+ set: (v) => setState(!!v),
+ setTrue,
+ setFalse,
+ }
+ }, [state])
+
+ return [state, actions]
+}
+
+export default useBoolean
diff --git a/packages/src/hooks/useFetch.ts b/packages/src/hooks/useFetch.ts
new file mode 100644
index 0000000..0487b0f
--- /dev/null
+++ b/packages/src/hooks/useFetch.ts
@@ -0,0 +1,52 @@
+/* eslint-disable */
+import { useState, useRef, useEffect } from 'react'
+
+type Option = {
+ immediate?: boolean
+ initParams?: any
+ defaultValue?: any
+}
+
+export type FunctionArgs = (
+ ...args: Args
+) => Promise
+
+export type ResolveType = Awaited>>
+
+export default function useFetch(
+ fn: T,
+ opt?: Option,
+): [ResolveType, boolean, T]
+export default function useFetch(fn, opt?) {
+ const option: Option = {
+ immediate: true,
+ defaultValue: [],
+ ...(opt || {}),
+ }
+ const [data, setData] = useState()
+ const loading = useRef(false)
+
+ const handle = async (...args: any[]) => {
+ if (fn && typeof fn === 'function') {
+ loading.current = true
+ try {
+ const res = await fn(...args)
+ if (res === void 0 || res === null) {
+ setData(option.defaultValue)
+ }
+ setData(res)
+ return data
+ } finally {
+ loading.current = false
+ }
+ }
+ }
+
+ useEffect(() => {
+ if (option.immediate) {
+ handle()
+ }
+ }, [])
+
+ return [data, loading, handle]
+}
diff --git a/packages/src/hooks/useSafeBottom.ts b/packages/src/hooks/useSafeBottom.ts
new file mode 100644
index 0000000..24e23dd
--- /dev/null
+++ b/packages/src/hooks/useSafeBottom.ts
@@ -0,0 +1,33 @@
+import { useMemo } from 'react'
+import useSystemInfo from './useSystemInfo'
+
+type SafeBottomReturnType = {
+ safeBottom: number
+ safeAreaPadding: {
+ paddingBottom: string
+ }
+ safeAreaMargin: {
+ marginBottom: string
+ }
+}
+
+const useSafeBottom = (height = 0): SafeBottomReturnType => {
+ const systemInfo = useSystemInfo()
+ return useMemo(() => {
+ let safeAreaPadding = { paddingBottom: '0px' }
+ let safeAreaMargin = { marginBottom: '0px' }
+ let safeBottom = 0
+ if (systemInfo) {
+ const { safeArea, screenHeight } = systemInfo
+ if (safeArea) {
+ const { bottom } = safeArea
+ safeBottom = screenHeight - bottom
+ safeAreaPadding.paddingBottom = `${safeBottom + height}px`
+ safeAreaMargin.marginBottom = `${safeBottom + height}px`
+ }
+ }
+ return { safeBottom, safeAreaPadding, safeAreaMargin }
+ }, [systemInfo])
+}
+
+export default useSafeBottom
diff --git a/packages/src/hooks/useSafeTop.ts b/packages/src/hooks/useSafeTop.ts
new file mode 100644
index 0000000..e9c1381
--- /dev/null
+++ b/packages/src/hooks/useSafeTop.ts
@@ -0,0 +1,43 @@
+import { useMemo } from 'react'
+import useSystemInfo from './useSystemInfo'
+
+type SafeTopReturnType = {
+ /** 状态栏高度 */
+ safeTop: number
+ /** 导航栏高度 */
+ statusBarHeight: number
+ safeAreaPadding: {
+ paddingTop: string
+ }
+ safeAreaMargin: {
+ marginTop: string
+ }
+}
+
+const useSafeTop = (): SafeTopReturnType => {
+ const systemInfo = useSystemInfo()
+ return useMemo(() => {
+ let safeAreaPadding = { paddingTop: '0px' }
+ let safeAreaMargin = { marginTop: '0px' }
+ let safeTop = 0
+ let barHeight = 0
+ if (systemInfo) {
+ const { safeArea, statusBarHeight } = systemInfo
+ if (statusBarHeight) barHeight = statusBarHeight
+ if (safeArea) {
+ const { top } = safeArea
+ safeTop = top
+ safeAreaPadding.paddingTop = `${safeTop}px`
+ safeAreaMargin.marginTop = `${safeTop}px`
+ }
+ }
+ return {
+ safeTop,
+ statusBarHeight: barHeight,
+ safeAreaPadding,
+ safeAreaMargin,
+ }
+ }, [systemInfo])
+}
+
+export default useSafeTop
diff --git a/packages/src/hooks/useSystemInfo.ts b/packages/src/hooks/useSystemInfo.ts
new file mode 100644
index 0000000..ec71d5e
--- /dev/null
+++ b/packages/src/hooks/useSystemInfo.ts
@@ -0,0 +1,27 @@
+import { getSystemInfo } from '@tarojs/taro'
+import { useCallback, useEffect, useState } from 'react'
+
+export type Result = Taro.getSystemInfo.Result | undefined
+
+function useSystemInfo(): Result {
+ const [systemInfo, setSystemInfo] = useState()
+
+ const getSystemInfoSync = useCallback(() => {
+ try {
+ getSystemInfo({
+ success: setSystemInfo,
+ fail: () => console.error({ errMsg: 'getSystemInfo: fail' }),
+ })
+ } catch (e) {
+ console.error({ errMsg: 'getSystemInfo: fail', data: e })
+ }
+ }, [])
+
+ useEffect(() => {
+ getSystemInfoSync()
+ }, [])
+
+ return systemInfo
+}
+
+export default useSystemInfo
diff --git a/packages/src/hooks/useUUID.ts b/packages/src/hooks/useUUID.ts
new file mode 100644
index 0000000..856a7bb
--- /dev/null
+++ b/packages/src/hooks/useUUID.ts
@@ -0,0 +1,40 @@
+import { useMemo } from 'react'
+
+/**
+ * 生成uuid
+ * @param len 长度
+ * @param radix 基位
+ * @returns string
+ */
+const useUUID = (len: number = 10, radix: number = 16) => {
+ let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
+ let uuid: string[] = [],
+ i
+ radix = radix || chars.length || 10
+ return useMemo(() => {
+ if (len) {
+ // Compact form
+ for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]
+ } else {
+ // rfc4122, version 4 form
+ let r
+
+ // rfc4122 requires these characters
+ uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
+ uuid[14] = '4'
+
+ // Fill in random data. At i==19 set the high bits of clock sequence as
+ // per rfc4122, sec. 4.1.5
+ for (i = 0; i < 36; i++) {
+ if (!uuid[i]) {
+ r = 0 | (Math.random() * 16)
+ uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r]
+ }
+ }
+ }
+
+ return uuid.join('')
+ }, [])
+}
+
+export default useUUID
diff --git a/packages/src/index.ts b/packages/src/index.ts
new file mode 100644
index 0000000..71520bd
--- /dev/null
+++ b/packages/src/index.ts
@@ -0,0 +1,37 @@
+import Form from './components/form'
+import type { FormItemType } from './components/form/Form'
+import Popup from './components/popup'
+import Checkbox from './components/checkbox'
+import type { CheckOptionType } from './components/checkbox'
+import Field from './components/field'
+import Textarea from './components/textarea'
+import Switch from './components/switch'
+import ConfigProvider from './components/config-provider'
+import Picker from './components/picker'
+import type { PickerOptionType } from './components/picker'
+import Cascader from './components/cascader'
+import DatePicker from './components/date-picker'
+import Cell from './components/cell'
+import Button from './components/button'
+import Tabs from './components/tabs'
+import FooterArea from './components/footer-area'
+
+export default Form
+export {
+ Cell,
+ Popup,
+ Checkbox,
+ Field,
+ FormItemType,
+ CheckOptionType,
+ PickerOptionType,
+ Textarea,
+ Switch,
+ ConfigProvider,
+ Picker,
+ Cascader,
+ DatePicker,
+ Button,
+ Tabs,
+ FooterArea,
+}
diff --git a/packages/src/styles/common.less b/packages/src/styles/common.less
new file mode 100644
index 0000000..e2c598f
--- /dev/null
+++ b/packages/src/styles/common.less
@@ -0,0 +1,9 @@
+page {
+ box-sizing: border-box;
+}
+
+.input-placeholder,
+.textarea-placeholder {
+ font-weight: 400;
+ color: #cacaca;
+}
diff --git a/packages/src/styles/components/button.less b/packages/src/styles/components/button.less
new file mode 100644
index 0000000..61220b9
--- /dev/null
+++ b/packages/src/styles/components/button.less
@@ -0,0 +1,199 @@
+.ygp-web-button {
+ padding: 0;
+ border: 0;
+ outline: none;
+ box-shadow: none;
+ background: transparent;
+}
+
+.ygp-button {
+ position: relative;
+ display: inline-block;
+ box-sizing: border-box;
+ // margin: 0 auto;
+ border-radius: var(--button-radius);
+ font-size: var(--font-size-base);
+ color: var(--color-white);
+ background: #ffffff;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ &:active {
+ opacity: var(--opacity-active);
+ }
+
+ /* elements */
+
+ &__text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ min-width: var(--button-min-width);
+ height: var(--button-height);
+ padding: 0 var(--button-default-v-padding);
+ line-height: var(--button-height);
+ }
+
+ &__wxbutton {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ box-sizing: border-box;
+ min-width: var(--button-min-width);
+ height: 100%;
+ padding: 0;
+ padding: 0 var(--button-default-v-padding);
+ border: none;
+ outline: none;
+ background-color: transparent;
+ opacity: 0;
+
+ &::after {
+ display: none;
+ }
+ }
+
+ /* modifiers */
+ &--active {
+ opacity: var(--opacity-active);
+ }
+
+ &--disabled {
+ opacity: 0.6;
+
+ &:active {
+ opacity: 0.6;
+ }
+ }
+
+ &--default {
+ border: 2px solid var(--color-border-base);
+ color: var(--color-text);
+ }
+
+ &--primary {
+ background: var(--color-primary);
+ }
+
+ &--info {
+ background: var(--color-info);
+ }
+
+ &--danger {
+ background: var(--color-error);
+ }
+
+ &--warning {
+ background: var(--color-warning);
+ }
+
+ &--success {
+ background: var(--color-success);
+ }
+
+ &--primary--border {
+ box-sizing: border-box;
+ border: 2px solid var(--color-primary);
+ color: var(--color-primary);
+ background: none;
+ }
+
+ &--info--border {
+ box-sizing: border-box;
+ border: 2px solid var(--color-info);
+ color: var(--color-info);
+ background: none;
+ }
+
+ &--warning--border {
+ box-sizing: border-box;
+ border: 2px solid var(--color-warning);
+ color: var(--color-warning);
+ background: none;
+ }
+
+ &--danger--border {
+ box-sizing: border-box;
+ border: 2px solid var(--color-error);
+ color: var(--color-error);
+ background: none;
+ }
+
+ &--success--border {
+ box-sizing: border-box;
+ border: 2px solid var(--color-success);
+ color: var(--color-success);
+ background: none;
+ }
+
+ &--circle {
+ border-radius: var(--color-warning);
+ }
+
+ &--rect {
+ border-radius: 0;
+ }
+
+ &--large {
+ height: var(--button-large-height);
+ font-size: var(--font-size-base);
+
+ .ygp-button__wxbutton,
+ .ygp-button__text {
+ box-sizing: border-box;
+ min-width: var(--button-min-large-width);
+ height: var(--button-large-height);
+ padding: 0 var(--button-large-v-padding);
+ font-size: var(--button-large-text-size);
+ line-height: var(--button-large-height);
+ }
+ }
+
+ &--small {
+ height: var(--button-height-small);
+ font-size: var(--font-size-base);
+
+ .ygp-button__wxbutton,
+ .ygp-button__text {
+ box-sizing: border-box;
+ min-width: var(--button-min-width-small);
+ height: var(--button-height-small);
+ padding: 0 var(--button-small-v-padding);
+ font-size: var(--button-small-text-size);
+ line-height: var(--button-height-small);
+ }
+ }
+
+ &--mini {
+ box-sizing: border-box;
+ min-width: var(--button-min-width-mini);
+ height: var(--button-height-mini);
+ line-height: var(--button-height-mini);
+
+ .ygp-button__wxbutton,
+ .ygp-button__text {
+ box-sizing: border-box;
+ min-width: var(--button-min-width-mini);
+ height: var(--button-height-mini);
+ padding: 0 var(--button-mini-v-padding);
+ font-size: var(--button-mini-text-size);
+ line-height: var(--button-height-mini);
+ }
+ }
+
+ &--fill {
+ color: #ffffff;
+ background: var(--color-primary);
+ }
+
+ &__no-border {
+ border: none;
+ }
+
+ &--full {
+ width: 100%;
+ max-width: 100%;
+ }
+}
diff --git a/packages/src/styles/components/cascader.less b/packages/src/styles/components/cascader.less
new file mode 100644
index 0000000..98e5ab6
--- /dev/null
+++ b/packages/src/styles/components/cascader.less
@@ -0,0 +1,32 @@
+.ygp-cascader {
+ min-height: 100%;
+ position: relative;
+ background-color: #fff;
+ &__tab {
+ display: flex;
+ justify-content: flex-start;
+ padding-left: 32px;
+ &-item {
+ font-size: 30px;
+ font-weight: bold;
+ padding: 30px 0;
+ margin-right: 60px;
+ color: #1c1d21;
+ line-height: 28px;
+ border-bottom: 4px solid transparent;
+ &.active {
+ color: #ff6720;
+ border-color: #ff6720;
+ }
+ }
+ }
+ &__panel {
+ padding-left: 32px;
+ &-option {
+ padding: 30px 0;
+ font-size: 30px;
+ font-weight: 400;
+ color: #1c1d21;
+ }
+ }
+}
diff --git a/packages/src/styles/components/cell.less b/packages/src/styles/components/cell.less
new file mode 100644
index 0000000..a3db65d
--- /dev/null
+++ b/packages/src/styles/components/cell.less
@@ -0,0 +1,50 @@
+.ygp-cell {
+ position: relative;
+ font-size: 30px;
+ line-height: 1.5;
+ transition: background-color 0.3s;
+ padding: 0 24px;
+ overflow: hidden;
+ &-border {
+ border-bottom: 1px solid #ebebeb;
+ }
+ &:last-child &-border {
+ border-bottom: none;
+ }
+
+ &__container {
+ min-height: 104px;
+
+ display: flex;
+ align-items: center;
+ }
+ &__content {
+ flex: 1;
+ overflow: hidden;
+ color: #6b6b6b;
+ &-info {
+ color: #1c1d21;
+ font-weight: 400;
+ }
+ }
+ &__extra {
+ position: relative;
+ text-align: right;
+ color: #262626;
+ &-info {
+ overflow: hidden;
+ max-width: 100%;
+ padding-right: 40px;
+ vertical-align: middle;
+ }
+ &-icon {
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ position: absolute;
+ display: inline-block;
+ height: 24px;
+ width: 24px;
+ }
+ }
+}
diff --git a/packages/src/styles/components/checkbox.less b/packages/src/styles/components/checkbox.less
new file mode 100644
index 0000000..f96e40a
--- /dev/null
+++ b/packages/src/styles/components/checkbox.less
@@ -0,0 +1,44 @@
+.btn-check-box {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+
+ .check-item {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: var(--button-height-small);
+ border-radius: var(--border-radius-md);
+ font-size: var(--font-size-sm);
+ color: #262626;
+ margin-right: 16px;
+ margin-bottom: 16px;
+ background: #f6f6f6;
+ border: 2px solid #f6f6f6;
+ }
+
+ .check-item-row-2 {
+ flex: 0 0 calc(49.9999% - 16px);
+ &:nth-child(2) {
+ margin-right: 0;
+ }
+ }
+ .check-item-row-3 {
+ flex: 0 0 calc(33.333333% - 16px);
+ &:nth-child(3) {
+ margin-right: 0;
+ }
+ }
+ .check-item-row-4 {
+ flex: 0 0 calc(24.9999999% - 16px);
+ &:nth-child(4) {
+ margin-right: 0;
+ }
+ }
+
+ .item-active {
+ background: rgba(254, 95, 35, 0.1);
+ border-color: var(--color-primary);
+ color: var(--color-primary);
+ }
+}
diff --git a/packages/src/styles/components/date-picker.less b/packages/src/styles/components/date-picker.less
new file mode 100644
index 0000000..ab99032
--- /dev/null
+++ b/packages/src/styles/components/date-picker.less
@@ -0,0 +1,17 @@
+.ygp-date-picker {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ .ygp-picker-view {
+ width: 100%;
+ height: 440px;
+ }
+ .picker-column {
+ height: 76px;
+ text-align: center;
+ }
+ .ygp-picker-item {
+ line-height: 76px;
+ text-align: center;
+ }
+}
diff --git a/packages/src/styles/components/field.less b/packages/src/styles/components/field.less
new file mode 100644
index 0000000..57a1249
--- /dev/null
+++ b/packages/src/styles/components/field.less
@@ -0,0 +1,117 @@
+.ygp-form-item {
+ &:last-child {
+ .ygp-field-content {
+ border-color: transparent;
+ }
+ }
+}
+
+.ygp-field {
+ position: relative;
+ padding: 0 32px;
+ background-color: #fff;
+ &&-gap {
+ margin-bottom: 16px;
+ .ygp-field-content {
+ border-color: transparent;
+ }
+ }
+ &&-error {
+ .ygp-field-content {
+ border-color: #ff4949;
+ }
+ }
+ &&-required {
+ .ygp-field-content__label {
+ label::after {
+ content: '*';
+ position: absolute;
+ left: -16px;
+ top: 0;
+ color: #ff3b00;
+ font-size: 28px;
+ }
+ }
+ }
+ &-content {
+ display: flex;
+ border-bottom: 2px solid #ececec;
+ padding: 16px 0;
+ font-size: 30px;
+ &-horizontal {
+ justify-content: space-between;
+ }
+
+ &-vertical {
+ flex-direction: column;
+ .ygp-field-content__label {
+ margin-bottom: 16px;
+ width: 100% !important;
+ }
+ }
+
+ &__label {
+ color: #1c1d21;
+ margin-top: 16px;
+ label {
+ position: relative;
+ }
+ }
+ &__value {
+ flex: 1;
+ &-inner {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ min-height: 32px;
+ &__control {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ min-height: 68px;
+ input {
+ padding: 12px 0;
+ }
+ &-right {
+ justify-content: flex-end;
+ }
+ &-left {
+ justify-content: flex-start;
+ }
+ &-link {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ &-content {
+ flex: 1;
+ &.placeholder {
+ color: #666;
+ }
+ }
+ &-arrow {
+ width: 24px;
+ height: 24px;
+ margin-left: 4px;
+ }
+ }
+ &-clear {
+ width: 32px;
+ height: 32px;
+ padding: 12px 0 12px 12px;
+ }
+ }
+ &__right {
+ margin-left: 24px;
+ }
+ }
+ &-error {
+ color: #ff4949;
+ margin-bottom: -16px;
+ }
+ }
+ }
+}
+
+.hoverClass {
+ background: #ffeae0;
+}
diff --git a/packages/src/styles/components/footer-area.less b/packages/src/styles/components/footer-area.less
new file mode 100644
index 0000000..f21d1cd
--- /dev/null
+++ b/packages/src/styles/components/footer-area.less
@@ -0,0 +1,24 @@
+.ygp-footer-area {
+ height: 112px;
+ box-sizing: content-box;
+ position: relative;
+ z-index: 2;
+ &-fixed {
+ position: fixed;
+ background-color: white;
+ width: 100%;
+ bottom: 0;
+ padding: 16px 24px;
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ }
+}
+
+.ygp-footer-area-transparent {
+ .ygp-footer-area-fixed {
+ background-color: transparent;
+ box-shadow: none;
+ }
+}
diff --git a/packages/src/styles/components/picker.less b/packages/src/styles/components/picker.less
new file mode 100644
index 0000000..0424350
--- /dev/null
+++ b/packages/src/styles/components/picker.less
@@ -0,0 +1,18 @@
+.ygp-picker {
+ height: 100%;
+ width: 100%;
+ position: relative;
+
+ .item-content {
+ font-size: 30px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 88px;
+ }
+
+ &-item-underline {
+ color: #ff6720;
+ box-sizing: content-box;
+ }
+}
diff --git a/packages/src/styles/components/popup.less b/packages/src/styles/components/popup.less
new file mode 100644
index 0000000..bae4134
--- /dev/null
+++ b/packages/src/styles/components/popup.less
@@ -0,0 +1,71 @@
+.popup-box {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(0, 0, 0, 0.4);
+ z-index: 100;
+
+ .popup-main {
+ position: absolute;
+ background: #ffffff;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .placement-top {
+ right: 0;
+ left: 0;
+ top: 0;
+ border-radius: 0px 0px 16px 16px;
+ }
+
+ .placement-right {
+ right: 0;
+ height: 100%;
+ }
+
+ .placement-bottom {
+ right: 0;
+ left: 0;
+ bottom: 0;
+ border-radius: 16px 16px 0px 0px;
+ }
+
+ .popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ min-height: 42px;
+ padding: 24px;
+ border-bottom: 1px solid #ececec;
+
+ &-title {
+ font-size: 28px;
+ font-weight: bold;
+ }
+
+ .close-icon {
+ width: 32px;
+ height: 32px;
+ }
+
+ &-operation {
+ flex: 1;
+
+ &:last-child {
+ display: flex;
+ justify-content: flex-end;
+ }
+ }
+ }
+
+ .popup-content {
+ flex: 1;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ }
+}
diff --git a/packages/src/styles/components/switch.less b/packages/src/styles/components/switch.less
new file mode 100644
index 0000000..44d1d7e
--- /dev/null
+++ b/packages/src/styles/components/switch.less
@@ -0,0 +1,46 @@
+.ygp-switch {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ background-color: var(--color-primary);
+ border-radius: 21px;
+ background-size: 100% 100%;
+ background-repeat: no-repeat;
+ background-position: center center;
+ flex: 0 0 auto; // 防止被压缩
+ &.switch-close {
+ background-color: var(--color-grey-2);
+ }
+ .switch-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background: var(--color-white);
+ transition: transform 0.3s;
+ box-shadow: 0 0 24px rgba(0, 0, 0, 0.1);
+ }
+ &-disabled {
+ opacity: var(--opacity-disabled);
+ }
+ &-base {
+ width: 72px;
+ height: 42px;
+ line-height: 42px;
+ .switch-button {
+ height: 42px;
+ width: 42px;
+ }
+ &.switch-open {
+ .switch-button {
+ transform: translateX(73%);
+ }
+ }
+ &.switch-close {
+ .close-line {
+ width: 8px;
+ height: 2px;
+ }
+ }
+ }
+}
diff --git a/packages/src/styles/components/tabs.less b/packages/src/styles/components/tabs.less
new file mode 100644
index 0000000..235381b
--- /dev/null
+++ b/packages/src/styles/components/tabs.less
@@ -0,0 +1,57 @@
+.ygp-tab {
+ display: flex;
+ flex-direction: column;
+
+ &-btn {
+ width: 100%;
+ height: 88px;
+ position: relative;
+
+ &-items {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ width: max-content;
+ color: #76777b;
+ }
+
+ &-item {
+ padding: 0 24px;
+ height: 100%;
+ transition: background-color 1s;
+ font-size: 28px;
+
+ .item-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ &-underline {
+ color: #ff6720;
+ box-sizing: content-box;
+ background: linear-gradient(
+ to top,
+ transparent 0px,
+ #ff6720 0px,
+ #ff6720 4px,
+ transparent 4px
+ );
+ }
+ }
+
+ &-children-scroll-view {
+ width: 100%;
+ height: calc(100% - 88px);
+ }
+
+ &-children-box {
+ height: 100%;
+ }
+
+ &-children {
+ width: 100%;
+ height: 100%;
+ }
+}
diff --git a/packages/src/styles/components/textarea.less b/packages/src/styles/components/textarea.less
new file mode 100644
index 0000000..bc97090
--- /dev/null
+++ b/packages/src/styles/components/textarea.less
@@ -0,0 +1,12 @@
+.ygp-textarea {
+ position: relative;
+ width: 100%;
+ padding-bottom: 32px;
+ &-count {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ color: #999;
+ font-size: 26rpx;
+ }
+}
diff --git a/packages/src/styles/index.less b/packages/src/styles/index.less
new file mode 100644
index 0000000..e463a0c
--- /dev/null
+++ b/packages/src/styles/index.less
@@ -0,0 +1,14 @@
+@import './variable.less';
+@import './common.less';
+@import './components/cell.less';
+@import './components/field.less';
+@import './components/popup.less';
+@import './components/tabs.less';
+@import './components/checkbox.less';
+@import './components/textarea.less';
+@import './components/switch.less';
+@import './components/picker.less';
+@import './components/cascader.less';
+@import './components/button.less';
+@import './components/date-picker.less';
+@import './components/footer-area.less';
diff --git a/packages/src/styles/variable.less b/packages/src/styles/variable.less
new file mode 100644
index 0000000..5574b5e
--- /dev/null
+++ b/packages/src/styles/variable.less
@@ -0,0 +1,124 @@
+/* stylelint-disable declaration-block-no-duplicate-custom-properties */
+// 定义前缀
+:root,
+page {
+ // color base
+ // 品牌色,用于常规商品价格、功能按钮、促销活动等
+ --color-primary: #ff6720;
+
+ // color function
+ // 功能色,用于成功/通过等正向反馈
+ --color-success: #13ce66;
+ // 功能色,用于失败/警告等负向反馈
+ --color-error: #ff4949;
+ // 功能色,用于提示/警示等需要引起用户注意的场景
+ --color-warning: #ffc82c;
+ // 功能色,用于标识其他场景
+ --color-info: #78a4fa;
+ // 非常规用色,用于页面遮罩等
+ --color-black: #000000;
+ // 主内容用色,用于常规标题内容、细文浏览、按钮文字及图标引导
+ --color-grey-0: #333333;
+ // 次要内容用色,用于次级标题内容、属性标示、非主要信息引导及常规按钮边框等
+ --color-grey-1: #76777b;
+ // 特殊内容用色,用于无货标签文字、特殊不可点击按钮等
+ --color-grey-2: #cccccc;
+ // 辅助内容用色,用于页面分割线、分割底色、选项按钮常规底色等 #EBEBEB
+ --color-grey-3: #efefef;
+ // 非常规用色,用于文字反白等
+ --color-white: #ffffff;
+
+ // Text Color
+ // 文字的基本色
+ --color-text: var(--color-grey-0);
+
+ // 辅助色
+ --color-text-secondary: var(--color-grey-1);
+ --color-text-placeholder: var(--color-grey-2);
+ --color-text-disabled: var(--color-grey-2);
+
+ // overlay(@overlay-bg-color)
+ --color-overlay: rgba(0, 0, 0, 0.65);
+
+ // 边框颜色
+ --color-border-base: var(--color-grey-3);
+ --color-border-light: mix(#ffffff, #efefef, 30%);
+
+ // 图标颜色
+ --color-icon-base: var(--color-grey-2);
+
+ // opacity
+ --opacity-active: 0.8;
+ --opacity-disabled: 0.4;
+ --zindex-common: 1000;
+
+ // ease
+ --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ --ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
+
+ // Font
+ --font-size-xs: 20px; // 非常用字号,用于标签
+ --font-size-s: 22px;
+ --font-size-sm: 24px; // 用于辅助信息
+ --font-size-base: 28px; // 常用字号
+ --font-size-lg: 32px; // 常规标题
+ --font-size-xl: 36px; // 大标题
+ --font-size-xxl: 40px; // 用于大号的数字
+ --font-size-xxxl: 48px;
+ --font-size-xxxxl: 60px;
+ --font-size-max: 72px;
+
+ // 水平间距
+ --spacing-h-sm: 6px;
+ --spacing-h-md: 16px;
+ --spacing-h-lg: 24px;
+ --spacing-h-xl: 36px;
+
+ // 垂直间距
+ --spacing-v-xs: 6px;
+ --spacing-v-sm: 12px;
+ --spacing-v-md: 18px;
+ --spacing-v-lg: 24px;
+ --spacing-v-xl: 30px;
+
+ // Border Radius
+ --border-radius-sm: 4px;
+ --border-radius-md: 8px;
+ --border-radius-lg: 12px;
+ --border-radius-hg: 20px;
+ --border-radius-circle: 50%;
+
+ // Line Height
+ --line-height-base: 1; // 单行
+ --line-height-: 1.2;
+ --line-height-en: 1.3; // 英文多行
+ --line-height-zh: 1.5; // 中文多行
+ --line-height-lg: 2;
+
+ // font family
+ // 基本字体
+ --family-base: -apple-system, blinkmacsystemfont, 'Helvetica Neue', helvetica, segoe ui, arial,
+ roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
+ // 数字字体
+ --family-integer: arial, helvetica, sans-senif, microsoft yahei;
+
+ // overlay
+ --overlay-bg-color: rgba(0, 0, 0, 0.65);
+
+ --button-height: 80px;
+ --button-default-v-padding: 40px;
+ --button-min-width: 192px;
+ --button-min-width-mini: 120px;
+ --button-height-mini: 32px;
+ --button-mini-text-size: 24px;
+ --button-mini-v-padding: 6px;
+ --button-min-width-small: 144px;
+ --button-height-small: 64px;
+ --button-small-text-size: var(--font-size-base);
+ --button-small-v-padding: 24px;
+ --button-min-large-width: 360px;
+ --button-large-height: 96px;
+ --button-large-text-size: var(--font-size-lg);
+ --button-large-v-padding: 48px;
+ --button-radius: var(--border-radius-md);
+}
diff --git a/packages/src/utils/date.ts b/packages/src/utils/date.ts
new file mode 100644
index 0000000..8b020a4
--- /dev/null
+++ b/packages/src/utils/date.ts
@@ -0,0 +1,122 @@
+const Utils = {
+ /**
+ * 是否为闫年
+ * @return {Boolse} true|false
+ */
+ isLeapYear(y: number): boolean {
+ return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
+ },
+
+ /**
+ * 返回星期数
+ * @return {String}
+ */
+ getWhatDay(year: number, month: number, day: number): string {
+ const date = new Date(`${year}/${month}/${day}`)
+ const index = date.getDay()
+ const dayNames = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
+ return dayNames[index]
+ },
+
+ /**
+ * 返回星期数
+ * @return {Number}
+ */
+ getMonthPreDay(year: number, month: number): number {
+ const date = new Date(`${year}/${month}/01`)
+ let day = date.getDay()
+ if (day == 0) {
+ day = 7
+ }
+ return day
+ },
+
+ /**
+ * 返回月份天数
+ * @return {Number}
+ */
+ getMonthDays(year: string, month: string): number {
+ if (/^0/.test(month)) {
+ month = month.split('')[1]
+ }
+ return (
+ [
+ 0,
+ 31,
+ this.isLeapYear(Number(year)) ? 29 : 28,
+ 31,
+ 30,
+ 31,
+ 30,
+ 31,
+ 31,
+ 30,
+ 31,
+ 30,
+ 31,
+ ] as number[]
+ )[month as any]
+ },
+
+ /**
+ * 补齐数字位数
+ * @return {string}
+ */
+ getNumTwoBit(n: number): string {
+ n = Number(n)
+ return (n > 9 ? '' : '0') + n
+ },
+
+ /**
+ * 日期对象转成字符串
+ * @return {string}
+ */
+ date2Str(date: Date, split?: string): string {
+ split = split || '-'
+ const y = date.getFullYear()
+ const m = this.getNumTwoBit(date.getMonth() + 1)
+ const d = this.getNumTwoBit(date.getDate())
+ return [y, m, d].join(split)
+ },
+
+ /**
+ * 返回日期格式字符串
+ * @param {Number} 0返回今天的日期、1返回明天的日期,2返回后天得日期,依次类推
+ * @return {string} '2014-12-31'
+ */
+ getDay(i: number): string {
+ i = i || 0
+ let date = new Date()
+ const diff = i * (1000 * 60 * 60 * 24)
+ date = new Date(date.getTime() + diff)
+ return this.date2Str(date)
+ },
+
+ /**
+ * 时间比较
+ * @return {Boolean}
+ */
+ compareDate(date1: string, date2: string): boolean {
+ const startTime = new Date(date1.replace('-', '/').replace('-', '/'))
+ const endTime = new Date(date2.replace('-', '/').replace('-', '/'))
+ if (startTime >= endTime) {
+ return false
+ }
+ return true
+ },
+
+ /**
+ * 时间是否相等
+ * @return {Boolean}
+ */
+ isEqual(date1: string, date2: string): boolean {
+ const startTime = new Date(date1).getTime()
+ const endTime = new Date(date2).getTime()
+ if (startTime == endTime) {
+ return true
+ }
+ return false
+ },
+}
+
+export default Utils
diff --git a/packages/src/utils/debounce.ts b/packages/src/utils/debounce.ts
new file mode 100644
index 0000000..f76693a
--- /dev/null
+++ b/packages/src/utils/debounce.ts
@@ -0,0 +1,191 @@
+import { isObject } from './is'
+
+/** Error message constants. */
+var FUNC_ERROR_TEXT = 'Expected a function'
+
+/* Built-in method references for those with the same name as other `lodash` methods. */
+var nativeMax = Math.max,
+ nativeMin = Math.min
+
+/**
+ * Creates a debounced function that delays invoking `func` until after `wait`
+ * milliseconds have elapsed since the last time the debounced function was
+ * invoked. The debounced function comes with a `cancel` method to cancel
+ * delayed `func` invocations and a `flush` method to immediately invoke them.
+ * Provide `options` to indicate whether `func` should be invoked on the
+ * leading and/or trailing edge of the `wait` timeout. The `func` is invoked
+ * with the last arguments provided to the debounced function. Subsequent
+ * calls to the debounced function return the result of the last `func`
+ * invocation.
+ *
+ * **Note:** If `leading` and `trailing` options are `true`, `func` is
+ * invoked on the trailing edge of the timeout only if the debounced function
+ * is invoked more than once during the `wait` timeout.
+ *
+ * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
+ * until to the next tick, similar to `setTimeout` with a timeout of `0`.
+ *
+ * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
+ * for details over the differences between `_.debounce` and `_.throttle`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to debounce.
+ * @param {number} [wait=0] The number of milliseconds to delay.
+ * @param {Object} [options={}] The options object.
+ * @param {boolean} [options.leading=false]
+ * Specify invoking on the leading edge of the timeout.
+ * @param {number} [options.maxWait]
+ * The maximum time `func` is allowed to be delayed before it's invoked.
+ * @param {boolean} [options.trailing=true]
+ * Specify invoking on the trailing edge of the timeout.
+ * @returns {Function} Returns the new debounced function.
+ * @example
+ *
+ * // Avoid costly calculations while the window size is in flux.
+ * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
+ *
+ * // Invoke `sendMail` when clicked, debouncing subsequent calls.
+ * jQuery(element).on('click', _.debounce(sendMail, 300, {
+ * 'leading': true,
+ * 'trailing': false
+ * }));
+ *
+ * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
+ * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
+ * var source = new EventSource('/stream');
+ * jQuery(source).on('message', debounced);
+ *
+ * // Cancel the trailing debounced invocation.
+ * jQuery(window).on('popstate', debounced.cancel);
+ */
+function debounce(func, wait, options) {
+ var lastArgs,
+ lastThis,
+ maxWait,
+ result,
+ timerId,
+ lastCallTime,
+ lastInvokeTime = 0,
+ leading = false,
+ maxing = false,
+ trailing = true
+
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT)
+ }
+ wait = Number(wait) || 0
+ if (isObject(options)) {
+ leading = !!options.leading
+ maxing = 'maxWait' in options
+ maxWait = maxing ? nativeMax(Number(options.maxWait) || 0, wait) : maxWait
+ trailing = 'trailing' in options ? !!options.trailing : trailing
+ }
+
+ function invokeFunc(time) {
+ var args = lastArgs,
+ thisArg = lastThis
+
+ lastArgs = lastThis = undefined
+ lastInvokeTime = time
+ result = func.apply(thisArg, args)
+ return result
+ }
+
+ function leadingEdge(time) {
+ // Reset any `maxWait` timer.
+ lastInvokeTime = time
+ // Start the timer for the trailing edge.
+ timerId = setTimeout(timerExpired, wait)
+ // Invoke the leading edge.
+ return leading ? invokeFunc(time) : result
+ }
+
+ function remainingWait(time) {
+ var timeSinceLastCall = time - lastCallTime,
+ timeSinceLastInvoke = time - lastInvokeTime,
+ timeWaiting = wait - timeSinceLastCall
+
+ return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting
+ }
+
+ function shouldInvoke(time) {
+ var timeSinceLastCall = time - lastCallTime,
+ timeSinceLastInvoke = time - lastInvokeTime
+
+ // Either this is the first call, activity has stopped and we're at the
+ // trailing edge, the system time has gone backwards and we're treating
+ // it as the trailing edge, or we've hit the `maxWait` limit.
+ return (
+ lastCallTime === undefined ||
+ timeSinceLastCall >= wait ||
+ timeSinceLastCall < 0 ||
+ (maxing && timeSinceLastInvoke >= maxWait)
+ )
+ }
+
+ function timerExpired() {
+ var time = Date.now()
+ if (shouldInvoke(time)) {
+ return trailingEdge(time)
+ }
+ // Restart the timer.
+ timerId = setTimeout(timerExpired, remainingWait(time))
+ }
+
+ function trailingEdge(time) {
+ timerId = undefined
+
+ // Only invoke if we have `lastArgs` which means `func` has been
+ // debounced at least once.
+ if (trailing && lastArgs) {
+ return invokeFunc(time)
+ }
+ lastArgs = lastThis = undefined
+ return result
+ }
+
+ function cancel() {
+ if (timerId !== undefined) {
+ clearTimeout(timerId)
+ }
+ lastInvokeTime = 0
+ lastArgs = lastCallTime = lastThis = timerId = undefined
+ }
+
+ function flush() {
+ return timerId === undefined ? result : trailingEdge(Date.now())
+ }
+
+ function debounced(this: any) {
+ var time = Date.now(),
+ isInvoking = shouldInvoke(time)
+
+ lastArgs = arguments
+ lastThis = this
+ lastCallTime = time
+
+ if (isInvoking) {
+ if (timerId === undefined) {
+ return leadingEdge(lastCallTime)
+ }
+ if (maxing) {
+ // Handle invocations in a tight loop.
+ clearTimeout(timerId)
+ timerId = setTimeout(timerExpired, wait)
+ return invokeFunc(lastCallTime)
+ }
+ }
+ if (timerId === undefined) {
+ timerId = setTimeout(timerExpired, wait)
+ }
+ return result
+ }
+ debounced.cancel = cancel
+ debounced.flush = flush
+ return debounced
+}
+
+export default debounce
diff --git a/packages/src/utils/http/request.ts b/packages/src/utils/http/request.ts
new file mode 100644
index 0000000..250685a
--- /dev/null
+++ b/packages/src/utils/http/request.ts
@@ -0,0 +1,174 @@
+import Taro from '@tarojs/taro'
+import { RequestConfig, RequestStatus } from './types'
+
+class httpRequest {
+ private readonly defaultConfig
+
+ constructor(conf: RequestConfig) {
+ this.defaultConfig = conf
+ }
+ get(options: Taro.request.Option, config?: RequestConfig) {
+ return this.request(options, config)
+ }
+ post(options: Taro.request.Option, config?: RequestConfig) {
+ return this.request({ ...options, method: 'POST' }, config)
+ }
+ put(options: Taro.request.Option, config?: RequestConfig) {
+ return this.request({ ...options, method: 'PUT' }, config)
+ }
+ delete(options: Taro.request.Option, config?: RequestConfig) {
+ return this.request({ ...options, method: 'DELETE' }, config)
+ }
+ request(options: Taro.request.Option, config?: RequestConfig): Promise {
+ const conf = Object.assign({}, this.defaultConfig, config)
+ if (conf.loading) {
+ Taro.showLoading()
+ }
+ const opt = requestInterceptor(options)
+ return new Promise((resolve) => {
+ Taro.request({
+ timeout: 8000,
+ ...opt,
+ success(res) {
+ if (conf.loading) Taro.hideLoading()
+ const ret = transformResponse(res, conf)
+ resolve(ret)
+ },
+ fail(err) {
+ if (conf.loading) Taro.hideLoading()
+ responseInterceptorsCatch(err)
+ },
+ })
+ })
+ }
+}
+
+/**
+ * @description: 请求拦截
+ */
+function requestInterceptor(options: Taro.request.Option): Taro.request.Option {
+ const { header } = options
+ const token = ''
+ // 设置token、appName
+ options.header = {
+ ...header,
+ token,
+ }
+ return options
+}
+
+function transformResponse(res, config) {
+ const { isTransformResponse, isReturnNativeResponse } = config
+ if (isReturnNativeResponse) {
+ return res
+ }
+
+ if (!isTransformResponse) {
+ return res.data
+ }
+
+ const { data } = res
+ if (!data) {
+ Taro.showToast({
+ title: '请求出错,请稍候重试',
+ icon: 'none',
+ })
+ }
+
+ const { code, data: result } = data
+ if (code === 0) {
+ return result
+ }
+ return responseInterceptorsCatch(res)
+}
+
+/**
+ * @description: 响应错误处理
+ */
+function responseInterceptorsCatch(error: any) {
+ const { data } = error || {}
+ const msg: string = data?.msg ?? ''
+ const err: string = error?.errMsg?.toString?.() ?? ''
+ let errMessage = ''
+ try {
+ if (err?.includes('request:fail')) {
+ errMessage = '请求出错,请稍后重试'
+ }
+ if (err?.includes('request:fail timeout')) {
+ errMessage = '网络异常,请检查您的网络连接是否正常!'
+ }
+ if (errMessage) {
+ Taro.showToast({
+ title: errMessage,
+ icon: 'none',
+ })
+ return Promise.reject(error)
+ }
+ } catch (captureErr) {
+ throw new Error(captureErr as unknown as string)
+ }
+ checkStatus(data?.code || error?.statusCode, msg)
+ return Promise.reject(error)
+}
+
+function checkStatus(status: RequestStatus, msg: string): void {
+ let errMessage = ''
+
+ switch (status) {
+ case 400:
+ errMessage = `${msg}`
+ break
+ // 401: 无接口权限
+ case 401:
+ errMessage = msg || '没有权限访问'
+ break
+ // 403 一般为token过期出现
+ case 403:
+ errMessage = msg || '登录过期,请重新登录!'
+ break
+ // 404请求不存在
+ case 404:
+ errMessage = msg || '网络请求错误,未找到该资源!'
+ break
+ case 405:
+ errMessage = msg || '网络请求错误,请求方法未允许!'
+ break
+ case 408:
+ errMessage = msg || '网络请求超时!'
+ break
+ case 500:
+ errMessage = msg || '服务器错误,请联系管理员!'
+ break
+ case 501:
+ errMessage = msg || '网络未实现!'
+ break
+ case 502:
+ errMessage = msg || '网络错误!'
+ break
+ case 503:
+ errMessage = msg || '服务不可用,服务器暂时过载或维护!'
+ break
+ case 504:
+ errMessage = msg || '网络超时!'
+ break
+ case 505:
+ errMessage = msg || 'http版本不支持该请求!'
+ break
+ default:
+ errMessage = msg || '请求出错,请稍候重试'
+ }
+ Taro.showToast({
+ title: errMessage,
+ icon: 'none',
+ })
+}
+
+function createHttpRequest() {
+ return new httpRequest({
+ loading: true,
+ isTransformResponse: true,
+ isReturnNativeResponse: false,
+ })
+}
+
+export default createHttpRequest()
diff --git a/packages/src/utils/http/types.ts b/packages/src/utils/http/types.ts
new file mode 100644
index 0000000..d0ab06f
--- /dev/null
+++ b/packages/src/utils/http/types.ts
@@ -0,0 +1,30 @@
+import Taro from '@tarojs/taro'
+
+export type RequestConfig = {
+ loading?: boolean
+ isTransformResponse?: boolean
+ isReturnNativeResponse?: boolean
+}
+
+export interface http {
+ /**
+ * @description: 请求拦截
+ */
+ requestInterceptor: (options: Taro.request.Option) => Taro.request.Option
+}
+
+export type RequestStatus =
+ | 0
+ | 400
+ | 401
+ | 402
+ | 403
+ | 404
+ | 405
+ | 408
+ | 500
+ | 501
+ | 502
+ | 503
+ | 504
+ | 505
diff --git a/packages/src/utils/index.ts b/packages/src/utils/index.ts
new file mode 100644
index 0000000..6d6c6a9
--- /dev/null
+++ b/packages/src/utils/index.ts
@@ -0,0 +1,3 @@
+import http from './http/request'
+
+export { http }
diff --git a/packages/src/utils/is.ts b/packages/src/utils/is.ts
new file mode 100644
index 0000000..9f51236
--- /dev/null
+++ b/packages/src/utils/is.ts
@@ -0,0 +1,80 @@
+const toString = Object.prototype.toString
+
+export function is(val: unknown, type: string) {
+ return toString.call(val) === `[object ${type}]`
+}
+
+export function isDef(val?: T): val is T {
+ return typeof val !== 'undefined'
+}
+
+export function isUnDef(val?: T): val is T {
+ return !isDef(val)
+}
+
+export function isObject(val: any): val is Record {
+ return val !== null && is(val, 'Object')
+}
+
+export function isEmpty(val: T): val is T {
+ if (isArray(val) || isString(val)) {
+ return val.length === 0
+ }
+
+ if (val instanceof Map || val instanceof Set) {
+ return val.size === 0
+ }
+
+ if (isObject(val)) {
+ return Object.keys(val).length === 0
+ }
+
+ return false
+}
+
+export function isDate(val: unknown): val is Date {
+ return is(val, 'Date')
+}
+
+export function isNull(val: unknown): val is null {
+ return val === null
+}
+
+export function isNullAndUnDef(val: unknown): val is null | undefined {
+ return isUnDef(val) && isNull(val)
+}
+
+export function isNullOrUnDef(val: unknown): val is null | undefined {
+ return isUnDef(val) || isNull(val)
+}
+
+export function isNumber(val: unknown): val is number {
+ return is(val, 'Number')
+}
+
+export function isPromise(val: unknown): val is Promise {
+ return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
+}
+
+export function isString(val: unknown): val is string {
+ return is(val, 'String')
+}
+
+export function isFunction(val: unknown): val is Function {
+ return typeof val === 'function'
+}
+
+export function isBoolean(val: unknown): val is boolean {
+ return is(val, 'Boolean')
+}
+
+export function isRegExp(val: unknown): val is RegExp {
+ return is(val, 'RegExp')
+}
+
+export function isArray(val: any): val is Array {
+ return val && Array.isArray(val)
+}
+export function isMap(val: unknown): val is Map {
+ return is(val, 'Map')
+}
diff --git a/packages/src/utils/throttle.ts b/packages/src/utils/throttle.ts
new file mode 100644
index 0000000..2659f94
--- /dev/null
+++ b/packages/src/utils/throttle.ts
@@ -0,0 +1,69 @@
+import debounce from './debounce'
+import { isObject } from './is'
+
+/** Error message constants. */
+var FUNC_ERROR_TEXT = 'Expected a function'
+
+/**
+ * Creates a throttled function that only invokes `func` at most once per
+ * every `wait` milliseconds. The throttled function comes with a `cancel`
+ * method to cancel delayed `func` invocations and a `flush` method to
+ * immediately invoke them. Provide `options` to indicate whether `func`
+ * should be invoked on the leading and/or trailing edge of the `wait`
+ * timeout. The `func` is invoked with the last arguments provided to the
+ * throttled function. Subsequent calls to the throttled function return the
+ * result of the last `func` invocation.
+ *
+ * **Note:** If `leading` and `trailing` options are `true`, `func` is
+ * invoked on the trailing edge of the timeout only if the throttled function
+ * is invoked more than once during the `wait` timeout.
+ *
+ * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
+ * until to the next tick, similar to `setTimeout` with a timeout of `0`.
+ *
+ * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
+ * for details over the differences between `_.throttle` and `_.debounce`.
+ *
+ * @static
+ * @memberOf _
+ * @since 0.1.0
+ * @category Function
+ * @param {Function} func The function to throttle.
+ * @param {number} [wait=0] The number of milliseconds to throttle invocations to.
+ * @param {Object} [options={}] The options object.
+ * @param {boolean} [options.leading=true]
+ * Specify invoking on the leading edge of the timeout.
+ * @param {boolean} [options.trailing=true]
+ * Specify invoking on the trailing edge of the timeout.
+ * @returns {Function} Returns the new throttled function.
+ * @example
+ *
+ * // Avoid excessively updating the position while scrolling.
+ * jQuery(window).on('scroll', _.throttle(updatePosition, 100));
+ *
+ * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
+ * var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
+ * jQuery(element).on('click', throttled);
+ *
+ * // Cancel the trailing throttled invocation.
+ * jQuery(window).on('popstate', throttled.cancel);
+ */
+function throttle(func, wait, options?) {
+ var leading = true,
+ trailing = true
+
+ if (typeof func != 'function') {
+ throw new TypeError(FUNC_ERROR_TEXT)
+ }
+ if (isObject(options)) {
+ leading = 'leading' in options ? !!options.leading : leading
+ trailing = 'trailing' in options ? !!options.trailing : trailing
+ }
+ return debounce(func, wait, {
+ leading: leading,
+ maxWait: wait,
+ trailing: trailing,
+ })
+}
+
+export default throttle
diff --git a/packages/tsconfig.json b/packages/tsconfig.json
new file mode 100644
index 0000000..0804525
--- /dev/null
+++ b/packages/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src"
+ },
+ "include": ["src/index.ts"]
+}
diff --git a/taro-demo/babel.config.js b/taro-demo/babel.config.js
new file mode 100644
index 0000000..8829a94
--- /dev/null
+++ b/taro-demo/babel.config.js
@@ -0,0 +1,13 @@
+// babel-preset-taro 更多选项和默认值:
+// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
+module.exports = {
+ presets: [
+ [
+ 'taro',
+ {
+ framework: 'react',
+ ts: true,
+ },
+ ],
+ ],
+}
diff --git a/taro-demo/config/dev.js b/taro-demo/config/dev.js
new file mode 100644
index 0000000..f3768e6
--- /dev/null
+++ b/taro-demo/config/dev.js
@@ -0,0 +1,8 @@
+module.exports = {
+ env: {
+ NODE_ENV: '"development"',
+ },
+ defineConstants: {},
+ mini: {},
+ h5: {},
+}
diff --git a/taro-demo/config/index.js b/taro-demo/config/index.js
new file mode 100644
index 0000000..c759956
--- /dev/null
+++ b/taro-demo/config/index.js
@@ -0,0 +1,69 @@
+const path = require('path')
+const config = {
+ projectName: 'taro-demo',
+ date: '2022-10-10',
+ designWidth: 750,
+ deviceRatio: {
+ 640: 2.34 / 2,
+ 750: 1,
+ 828: 1.81 / 2,
+ },
+ sourceRoot: 'src',
+ outputRoot: 'dist',
+ plugins: [],
+ defineConstants: {},
+ copy: {
+ patterns: [],
+ options: {},
+ },
+ framework: 'react',
+ mini: {
+ postcss: {
+ pxtransform: {
+ enable: true,
+ config: {},
+ },
+ url: {
+ enable: true,
+ config: {
+ limit: 1024, // 设定转换尺寸上限
+ },
+ },
+ cssModules: {
+ enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+ config: {
+ namingPattern: 'module', // 转换模式,取值为 global/module
+ generateScopedName: '[name]__[local]___[hash:base64:5]',
+ },
+ },
+ },
+ },
+ h5: {
+ publicPath: '/',
+ staticDirectory: 'static',
+ postcss: {
+ autoprefixer: {
+ enable: true,
+ config: {},
+ },
+ cssModules: {
+ enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
+ config: {
+ namingPattern: 'module', // 转换模式,取值为 global/module
+ generateScopedName: '[name]__[local]___[hash:base64:5]',
+ },
+ },
+ },
+ },
+ alias: {
+ '@': path.resolve(__dirname, '../../'),
+ 'taro-react-form': path.resolve(__dirname, '../../packages/src'),
+ },
+}
+
+module.exports = function (merge) {
+ if (process.env.NODE_ENV === 'development') {
+ return merge({}, config, require('./dev'))
+ }
+ return merge({}, config, require('./prod'))
+}
diff --git a/taro-demo/config/prod.js b/taro-demo/config/prod.js
new file mode 100644
index 0000000..a799d1f
--- /dev/null
+++ b/taro-demo/config/prod.js
@@ -0,0 +1,35 @@
+module.exports = {
+ env: {
+ NODE_ENV: '"production"',
+ },
+ defineConstants: {},
+ mini: {},
+ h5: {
+ /**
+ * WebpackChain 插件配置
+ * @docs https://github.com/neutrinojs/webpack-chain
+ */
+ // webpackChain (chain) {
+ // /**
+ // * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
+ // * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
+ // */
+ // chain.plugin('analyzer')
+ // .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
+ // /**
+ // * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
+ // * @docs https://github.com/chrisvfritz/prerender-spa-plugin
+ // */
+ // const path = require('path')
+ // const Prerender = require('prerender-spa-plugin')
+ // const staticDir = path.join(__dirname, '..', 'dist')
+ // chain
+ // .plugin('prerender')
+ // .use(new Prerender({
+ // staticDir,
+ // routes: [ '/pages/index/index' ],
+ // postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
+ // }))
+ // }
+ },
+}
diff --git a/taro-demo/global.d.ts b/taro-demo/global.d.ts
new file mode 100644
index 0000000..0512466
--- /dev/null
+++ b/taro-demo/global.d.ts
@@ -0,0 +1,18 @@
+///
+
+declare module '*.png'
+declare module '*.gif'
+declare module '*.jpg'
+declare module '*.jpeg'
+declare module '*.svg'
+declare module '*.css'
+declare module '*.less'
+declare module '*.scss'
+declare module '*.sass'
+declare module '*.styl'
+
+declare namespace NodeJS {
+ interface ProcessEnv {
+ TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'
+ }
+}
diff --git a/taro-demo/package.json b/taro-demo/package.json
new file mode 100644
index 0000000..604e2b8
--- /dev/null
+++ b/taro-demo/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "taro-demo",
+ "version": "1.0.0",
+ "private": true,
+ "description": "ygp-taro-demo",
+ "templateInfo": {
+ "name": "default",
+ "typescript": true,
+ "css": "less"
+ },
+ "scripts": {
+ "build:weapp": "taro build --type weapp",
+ "build:swan": "taro build --type swan",
+ "build:alipay": "taro build --type alipay",
+ "build:tt": "taro build --type tt",
+ "build:h5": "taro build --type h5",
+ "build:rn": "taro build --type rn",
+ "build:qq": "taro build --type qq",
+ "build:jd": "taro build --type jd",
+ "build:quickapp": "taro build --type quickapp",
+ "dev:weapp": "npm run build:weapp -- --watch",
+ "dev:swan": "npm run build:swan -- --watch",
+ "dev:alipay": "npm run build:alipay -- --watch",
+ "dev:tt": "npm run build:tt -- --watch",
+ "dev:h5": "npm run build:h5 -- --watch",
+ "dev:rn": "npm run build:rn -- --watch",
+ "dev:qq": "npm run build:qq -- --watch",
+ "dev:jd": "npm run build:jd -- --watch",
+ "dev:quickapp": "npm run build:quickapp -- --watch"
+ },
+ "browserslist": [
+ "last 3 versions",
+ "Android >= 4.1",
+ "ios >= 8"
+ ],
+ "author": "",
+ "dependencies": {
+ "@babel/runtime": "^7.7.7",
+ "@tarojs/components": "3.4.3",
+ "@tarojs/plugin-framework-react": "3.4.3",
+ "@tarojs/react": "3.4.3",
+ "@tarojs/runtime": "3.4.3",
+ "@tarojs/taro": "3.4.3",
+ "async-validator": "^4.2.5",
+ "classnames": "^2.3.1",
+ "rc-field-form": "^1.27.1",
+ "react": "^17.0.0",
+ "react-dom": "^17.0.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.8.0",
+ "@tarojs/mini-runner": "3.4.3",
+ "@tarojs/webpack-runner": "3.4.3",
+ "@types/react": "^17.0.2",
+ "@types/webpack-env": "^1.13.6",
+ "@typescript-eslint/eslint-plugin": "^4.15.1",
+ "@typescript-eslint/parser": "^4.15.1",
+ "babel-preset-taro": "3.4.3",
+ "eslint": "^6.8.0",
+ "eslint-config-taro": "3.4.3",
+ "eslint-plugin-import": "^2.12.0",
+ "eslint-plugin-react": "^7.8.2",
+ "eslint-plugin-react-hooks": "^4.2.0",
+ "stylelint": "^14.4.0",
+ "typescript": "^4.1.0"
+ },
+ "parserOptions": {
+ "parser": "@babel/eslint-parser",
+ "requireConfigFile": false
+ }
+}
diff --git a/taro-demo/project.config.json b/taro-demo/project.config.json
new file mode 100644
index 0000000..2912fad
--- /dev/null
+++ b/taro-demo/project.config.json
@@ -0,0 +1,13 @@
+{
+ "miniprogramRoot": "./dist",
+ "projectname": "taro-demo",
+ "description": "ygp-taro-demo",
+ "appid": "wx23ed7b618c38efd8",
+ "setting": {
+ "urlCheck": true,
+ "es6": false,
+ "postcss": false,
+ "minified": false
+ },
+ "compileType": "miniprogram"
+}
diff --git a/taro-demo/project.tt.json b/taro-demo/project.tt.json
new file mode 100644
index 0000000..482f19d
--- /dev/null
+++ b/taro-demo/project.tt.json
@@ -0,0 +1,9 @@
+{
+ "miniprogramRoot": "./",
+ "projectname": "taro-demo",
+ "appid": "testAppId",
+ "setting": {
+ "es6": false,
+ "minified": false
+ }
+}
diff --git a/taro-demo/src/app.config.ts b/taro-demo/src/app.config.ts
new file mode 100644
index 0000000..deecc07
--- /dev/null
+++ b/taro-demo/src/app.config.ts
@@ -0,0 +1,20 @@
+export default defineAppConfig({
+ pages: [
+ 'pages/index/index',
+ 'pages/form/index',
+ 'pages/popup/index',
+ 'pages/checkbox/index',
+ 'pages/field/index',
+ 'pages/textarea/index',
+ 'pages/switch/index',
+ 'pages/picker/index',
+ 'pages/cascader/index',
+ 'pages/date-picker/index',
+ ],
+ window: {
+ backgroundTextStyle: 'light',
+ navigationBarBackgroundColor: '#fff',
+ navigationBarTitleText: 'WeChat',
+ navigationBarTextStyle: 'black',
+ },
+})
diff --git a/taro-demo/src/app.less b/taro-demo/src/app.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/app.tsx b/taro-demo/src/app.tsx
new file mode 100644
index 0000000..87c5e52
--- /dev/null
+++ b/taro-demo/src/app.tsx
@@ -0,0 +1,9 @@
+import { ConfigProvider } from 'taro-react-form'
+import 'taro-react-form/styles/index.less'
+import './app.less'
+
+const App = (props) => {
+ return {props.children}
+}
+
+export default App
diff --git a/taro-demo/src/index.html b/taro-demo/src/index.html
new file mode 100644
index 0000000..ec38302
--- /dev/null
+++ b/taro-demo/src/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/taro-demo/src/pages/cascader/index.config.ts b/taro-demo/src/pages/cascader/index.config.ts
new file mode 100644
index 0000000..6a0408d
--- /dev/null
+++ b/taro-demo/src/pages/cascader/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Cascader',
+})
diff --git a/taro-demo/src/pages/cascader/index.less b/taro-demo/src/pages/cascader/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/cascader/index.tsx b/taro-demo/src/pages/cascader/index.tsx
new file mode 100644
index 0000000..2ca02da
--- /dev/null
+++ b/taro-demo/src/pages/cascader/index.tsx
@@ -0,0 +1,137 @@
+import { Cascader } from 'taro-react-form'
+import { View } from '@tarojs/components'
+import { useCallback, useState } from 'react'
+
+import './index.less'
+
+export default () => {
+ const [visible, setVisible] = useState(false)
+ const [address, setAddress] = useState([])
+ const options = [
+ {
+ value: 'zhejiang',
+ label: '浙江省',
+ children: [
+ {
+ value: 'hangzhou',
+ label: '杭州市',
+ children: [
+ {
+ value: 'xihu',
+ label: '西湖区',
+ },
+ {
+ value: 'xiasha',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha1',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha2',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha3',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha4',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha5',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha6',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha7',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha8',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha9',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha10',
+ label: '下沙区',
+ },
+
+ {
+ value: 'xiasha11',
+ label: '下沙区',
+ },
+ {
+ value: 'xiasha12',
+ label: '下沙区',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ value: 'guangdong',
+ label: '广东省',
+ children: [
+ {
+ value: 'guangzhou',
+ label: '广州市',
+ children: [
+ {
+ value: 'haizhu',
+ label: '海珠区',
+ },
+ ],
+ },
+ {
+ value: 'shenzheng',
+ label: '深圳市',
+ children: [
+ {
+ value: 'qianhai',
+ label: '前海区',
+ },
+ {
+ value: 'baoan',
+ label: '宝安区',
+ },
+ ],
+ },
+ ],
+ },
+ ]
+
+ const onClose = useCallback(() => {
+ setVisible(false)
+ }, [])
+
+ const onChange = useCallback((value, selectedOptions, currentOption) => {
+ console.log(value, selectedOptions, currentOption)
+ setAddress(value)
+ }, [])
+
+ return (
+
+ setVisible(true)}>点击展示
+
+
+ )
+}
diff --git a/taro-demo/src/pages/checkbox/index.config.ts b/taro-demo/src/pages/checkbox/index.config.ts
new file mode 100644
index 0000000..22c3f91
--- /dev/null
+++ b/taro-demo/src/pages/checkbox/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Checkbox',
+})
diff --git a/taro-demo/src/pages/checkbox/index.less b/taro-demo/src/pages/checkbox/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/checkbox/index.tsx b/taro-demo/src/pages/checkbox/index.tsx
new file mode 100644
index 0000000..5eb44a4
--- /dev/null
+++ b/taro-demo/src/pages/checkbox/index.tsx
@@ -0,0 +1,40 @@
+import { Checkbox, CheckOptionType } from 'taro-react-form'
+import { View } from '@tarojs/components'
+import { useCallback, useState } from 'react'
+import './index.less'
+
+export default () => {
+ const [value, setValue] = useState([])
+ const [activeKeysT, setActiveKeysT] = useState([])
+
+ const options: CheckOptionType[] = [
+ { label: '报价中', value: '1' },
+ { label: '报价完成', value: '2' },
+ { label: '待确认', value: '3' },
+ { label: '内部完善中', value: '4' },
+ ]
+
+ const checkOnChange = useCallback((activeKey, item) => {
+ console.log('item', item)
+ setValue(activeKey)
+ }, [])
+
+ const checkOnChangeT = useCallback((activeKey, item) => {
+ console.log('item', item)
+ setActiveKeysT(activeKey)
+ }, [])
+
+ return (
+
+ 单选
+
+ 多选
+
+
+ )
+}
diff --git a/taro-demo/src/pages/date-picker/index.config.ts b/taro-demo/src/pages/date-picker/index.config.ts
new file mode 100644
index 0000000..37166b8
--- /dev/null
+++ b/taro-demo/src/pages/date-picker/index.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: 'DatePicker',
+ disableScroll: true,
+})
diff --git a/taro-demo/src/pages/date-picker/index.less b/taro-demo/src/pages/date-picker/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/date-picker/index.tsx b/taro-demo/src/pages/date-picker/index.tsx
new file mode 100644
index 0000000..7deed0c
--- /dev/null
+++ b/taro-demo/src/pages/date-picker/index.tsx
@@ -0,0 +1,57 @@
+import { DatePicker, Cell } from 'taro-react-form'
+import { useState } from 'react'
+
+import './index.less'
+
+export default () => {
+ const [value, setValue] = useState('2022-03-09')
+ const [value1, setValue1] = useState(['2022-03-09', '2023-01-09'])
+ const [visible, setVisible] = useState(false)
+ const [visible1, setVisible1] = useState(false)
+
+ const onChange = (date: string) => {
+ setValue(date)
+ }
+
+ const onChange1 = (date) => {
+ setValue1(date)
+ }
+
+ return (
+ <>
+ {
+ setVisible(true)
+ }}
+ />
+ {
+ setVisible1(true)
+ }}
+ />
+ setVisible(false)}
+ >
+ setVisible1(false)}
+ >
+ >
+ )
+}
diff --git a/taro-demo/src/pages/field/index.config.ts b/taro-demo/src/pages/field/index.config.ts
new file mode 100644
index 0000000..23b72ba
--- /dev/null
+++ b/taro-demo/src/pages/field/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Field',
+})
diff --git a/taro-demo/src/pages/field/index.less b/taro-demo/src/pages/field/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/field/index.tsx b/taro-demo/src/pages/field/index.tsx
new file mode 100644
index 0000000..ba113bd
--- /dev/null
+++ b/taro-demo/src/pages/field/index.tsx
@@ -0,0 +1,28 @@
+import { Field } from 'taro-react-form'
+import { View } from '@tarojs/components'
+import { useCallback, useState } from 'react'
+import './index.less'
+
+export default () => {
+ const [value, setValue] = useState('123')
+ const fieldChange = useCallback(
+ (e) => {
+ setValue(e ? e.detail.value : '')
+ },
+ [value],
+ )
+ const clear = useCallback(() => {
+ console.log('清除')
+ }, [])
+ return (
+
+
+
+ )
+}
diff --git a/taro-demo/src/pages/form/index.config.ts b/taro-demo/src/pages/form/index.config.ts
new file mode 100644
index 0000000..a19cf3f
--- /dev/null
+++ b/taro-demo/src/pages/form/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Form',
+})
diff --git a/taro-demo/src/pages/form/index.less b/taro-demo/src/pages/form/index.less
new file mode 100644
index 0000000..c189e02
--- /dev/null
+++ b/taro-demo/src/pages/form/index.less
@@ -0,0 +1,3 @@
+page {
+ background-color: #eee;
+}
diff --git a/taro-demo/src/pages/form/index.tsx b/taro-demo/src/pages/form/index.tsx
new file mode 100644
index 0000000..5ece5a2
--- /dev/null
+++ b/taro-demo/src/pages/form/index.tsx
@@ -0,0 +1,284 @@
+import { View } from '@tarojs/components'
+import Form, { FormItemType, Button, FooterArea } from 'taro-react-form'
+
+import './index.less'
+
+const items: FormItemType[] = [
+ {
+ name: 'name',
+ label: '姓名',
+ type: 'text',
+ placeholder: '请输入',
+ clear: true,
+ required: true,
+ onChange: (e) => {
+ console.log('e123213', e)
+ },
+ onFocus: () => {
+ console.log('onFocus')
+ },
+ },
+ {
+ name: 'number',
+ label: '电话',
+ required: true,
+ validateTrigger: 'onBlur',
+ rules: [
+ {
+ validator(_, value) {
+ console.log('value111', value)
+ if (value && value.length > 6 && value.length < 20) {
+ return Promise.resolve()
+ }
+ return Promise.reject('长度6到20位')
+ },
+ },
+ ],
+ // onClear(){
+ // form.validateFields(['number'])
+ // }
+ },
+ {
+ name: 'textarea',
+ label: '备注',
+ type: 'textarea',
+ showCount: true,
+ },
+ {
+ name: 'product',
+ labelSlot: 新增商品,
+ label: '新增商品',
+ required: true,
+ layout: 'vertical',
+ valueAlign: 'left',
+ type: 'textarea',
+ autoHeight: true,
+ showCount: true,
+ className: 'product',
+ gap: true,
+ },
+ {
+ name: 'switch',
+ label: '开关',
+ valueAlign: 'right',
+ type: 'switch',
+ required: true,
+ clear: false,
+ },
+ {
+ name: 'picker',
+ label: '弹窗选择(有默认值)',
+ type: 'picker',
+ labelWidth: 200,
+ required: true,
+ title: '弹窗选择',
+ options: [
+ { name: '测试1', code: 0 },
+ { name: '测试2', code: 1 },
+ { name: '测试3', code: 2 },
+ { name: '测试4', code: 3 },
+ { name: '测试5', code: 4 },
+ { name: '测试6', code: 5 },
+ ],
+ fieldMaps: { label: 'name', value: 'code' },
+ },
+ {
+ name: 'picker2',
+ label: '弹窗选择2',
+ type: 'picker',
+ required: true,
+ title: '弹窗选择2',
+ options: [
+ { label: '测试1', value: 1 },
+ { label: '测试2', value: 2 },
+ { label: '测试3', value: 3 },
+ { label: '测试4', value: '4' },
+ { label: '测试5', value: '5' },
+ { label: '测试6', value: '6' },
+ { label: '测试7', value: '7' },
+ { label: '测试8', value: '8' },
+ { label: '测试9', value: '9' },
+ { label: '测试10', value: '10' },
+ ],
+ },
+ {
+ name: 'checkbox',
+ label: '复选按钮',
+ type: 'checkbox',
+ required: true,
+ layout: 'vertical',
+ options: [
+ { name: '报价中', code: 0 },
+ { name: '报价完成', code: 1 },
+ { name: '待确认', code: 2 },
+ { name: '内部完善中', code: 3 },
+ ],
+ fieldMaps: { label: 'name', value: 'code' },
+ isMultiple: true,
+ },
+ {
+ name: 'checkbox2',
+ label: '复选按钮(默认值)',
+ type: 'checkbox',
+ required: true,
+ columnNum: 3,
+ layout: 'vertical',
+ options: [
+ { label: '报价中', value: 1 },
+ { label: '报价完成', value: 2 },
+ { label: '待确认', value: 3 },
+ ],
+ onChange: (e, item) => {
+ console.log('e, item', e, item)
+ },
+ isMultiple: true,
+ },
+ {
+ name: 'cascader',
+ label: '地址',
+ type: 'cascader',
+ title: '选择地址',
+ required: true,
+ onChange: (value, selectedOptions, currentOption) => {
+ console.log('value, selectedOptions, currentOption', value, selectedOptions, currentOption)
+ },
+ options: [
+ {
+ value: 'zhejiang',
+ label: '浙江省',
+ children: [
+ {
+ value: 'hangzhou',
+ label: '杭州市',
+ children: [
+ {
+ value: 'xihu',
+ label: '西湖区',
+ },
+ {
+ value: 'xiasha',
+ label: '下沙区',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ value: 'guangdong',
+ label: '广东省',
+ children: [
+ {
+ value: 'guangzhou',
+ label: '广州市',
+ children: [
+ {
+ value: 'haizhu',
+ label: '海珠区',
+ },
+ ],
+ },
+ {
+ value: 'shenzheng',
+ label: '深圳市',
+ children: [
+ {
+ value: 'qianhai',
+ label: '前海区',
+ },
+ {
+ value: 'baoan',
+ label: '宝安区',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'date',
+ label: '日期',
+ type: 'date',
+ title: '选择时间',
+ required: true,
+ },
+ {
+ name: 'date1',
+ label: '日期区间',
+ type: 'date',
+ mode: 'range',
+ title: '选择时间区间',
+ required: true,
+ gap: true,
+ },
+ {
+ name: 'custom',
+ label: '自定义value区域内容',
+ type: 'date',
+ mode: 'range',
+ layout: 'vertical',
+ valueAlign: 'left',
+ required: true,
+ valueSlot: (value) => {
+ return (
+
+ )
+ },
+ },
+]
+
+export default () => {
+ const [form] = Form.useForm()
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/taro-demo/src/pages/index/index.config.ts b/taro-demo/src/pages/index/index.config.ts
new file mode 100644
index 0000000..d160de6
--- /dev/null
+++ b/taro-demo/src/pages/index/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'taro-react-form',
+})
diff --git a/taro-demo/src/pages/index/index.less b/taro-demo/src/pages/index/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/index/index.tsx b/taro-demo/src/pages/index/index.tsx
new file mode 100644
index 0000000..861c8ad
--- /dev/null
+++ b/taro-demo/src/pages/index/index.tsx
@@ -0,0 +1,64 @@
+import Taro from '@tarojs/taro'
+import { useCallback } from 'react'
+import { View } from '@tarojs/components'
+import { Cell } from 'taro-react-form'
+
+import './index.less'
+
+const routes = [
+ {
+ title: 'Form',
+ url: '/pages/form/index',
+ },
+ {
+ title: 'Checkbox',
+ url: '/pages/checkbox/index',
+ },
+ {
+ title: 'Field',
+ url: '/pages/field/index',
+ },
+ {
+ title: 'Textarea',
+ url: '/pages/textarea/index',
+ },
+ {
+ title: 'Switch',
+ url: '/pages/switch/index',
+ },
+ {
+ title: 'Picker',
+ url: '/pages/picker/index',
+ },
+ {
+ title: 'Cascader',
+ url: '/pages/cascader/index',
+ },
+ {
+ title: 'DatePicker',
+ url: '/pages/date-picker/index',
+ },
+]
+
+export default () => {
+ const push = useCallback((url) => {
+ Taro.navigateTo({ url })
+ }, [])
+
+ return (
+
+ {routes.map((r, index) => {
+ return (
+ | {
+ push(r.url)
+ }}
+ > |
+ )
+ })}
+
+ )
+}
diff --git a/taro-demo/src/pages/listview/index.config.ts b/taro-demo/src/pages/listview/index.config.ts
new file mode 100644
index 0000000..f780f95
--- /dev/null
+++ b/taro-demo/src/pages/listview/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'ListView',
+})
diff --git a/taro-demo/src/pages/listview/index.less b/taro-demo/src/pages/listview/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/listview/index.tsx b/taro-demo/src/pages/listview/index.tsx
new file mode 100644
index 0000000..4b48315
--- /dev/null
+++ b/taro-demo/src/pages/listview/index.tsx
@@ -0,0 +1,47 @@
+import { View } from '@tarojs/components'
+import { ListView } from 'taro-react-form'
+
+import './index.less'
+
+const data = Array(100).fill({})
+
+const fetcher = async (params) => {
+ console.log('params', params)
+ const { page, limit } = params
+ let arr = await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(data.slice((page - 1) * limit, page * limit))
+ }, 1000)
+ })
+ console.log('arr', arr)
+ return { list: arr, total: 111 }
+}
+
+export default () => {
+ const { listData, listViewProps } = ListView.useListView(fetcher as any, {
+ selector: '#fltter',
+ limit: 50,
+ })
+
+ return (
+
+
+ total:{listViewProps.pagination.total}
+
+ {listData.map((_, index) => (
+
+ {index}
+
+ ))}
+
+
+ )
+}
diff --git a/taro-demo/src/pages/picker/index.config.ts b/taro-demo/src/pages/picker/index.config.ts
new file mode 100644
index 0000000..9b27868
--- /dev/null
+++ b/taro-demo/src/pages/picker/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Picker',
+})
diff --git a/taro-demo/src/pages/picker/index.less b/taro-demo/src/pages/picker/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/picker/index.tsx b/taro-demo/src/pages/picker/index.tsx
new file mode 100644
index 0000000..ca3e897
--- /dev/null
+++ b/taro-demo/src/pages/picker/index.tsx
@@ -0,0 +1,53 @@
+import { Picker } from 'taro-react-form'
+import { View } from '@tarojs/components'
+import { useCallback, useEffect, useState } from 'react'
+import './index.less'
+
+export default () => {
+ const [visible, setVisible] = useState(false)
+ const [value, setValue] = useState('004')
+ const [options, setOptions] = useState([])
+ const onClose = useCallback(() => {
+ setVisible(false)
+ }, [visible])
+
+ useEffect(() => {
+ setTimeout(() => {
+ console.log('执行了')
+ setOptions([
+ { label: '测试1', value: 1 },
+ { label: '测试2', value: 2 },
+ { label: '测试3', value: 3 },
+ { label: '测试4', value: '4' },
+ { label: '测试5', value: '5' },
+ { label: '测试6', value: '6' },
+ { label: '测试7', value: '7' },
+ { label: '测试8', value: '8' },
+ { label: '测试9', value: '9' },
+ { label: '测试10', value: '10' },
+ ])
+ }, 1000)
+ }, [])
+
+ const onChange = useCallback(
+ (val) => {
+ console.log(val)
+ setValue(val)
+ },
+ [value],
+ )
+ return (
+
+ setVisible(true)}>点击展示
+
+
+ )
+}
diff --git a/taro-demo/src/pages/popup/index.config.ts b/taro-demo/src/pages/popup/index.config.ts
new file mode 100644
index 0000000..2ea5510
--- /dev/null
+++ b/taro-demo/src/pages/popup/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Popup',
+})
diff --git a/taro-demo/src/pages/popup/index.less b/taro-demo/src/pages/popup/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/taro-demo/src/pages/popup/index.tsx b/taro-demo/src/pages/popup/index.tsx
new file mode 100644
index 0000000..cb6b446
--- /dev/null
+++ b/taro-demo/src/pages/popup/index.tsx
@@ -0,0 +1,44 @@
+import { Popup } from 'taro-react-form'
+import { View } from '@tarojs/components'
+import { useCallback, useState } from 'react'
+import './index.less'
+
+export default () => {
+ const [visible, setVisible] = useState(false)
+ const onClose = useCallback(() => {
+ setVisible(false)
+ }, [visible])
+ return (
+
+ setVisible(true)}>点击展示
+
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+ 内容
+
+
+ )
+}
diff --git a/taro-demo/src/pages/switch/index.config.ts b/taro-demo/src/pages/switch/index.config.ts
new file mode 100644
index 0000000..a2048b3
--- /dev/null
+++ b/taro-demo/src/pages/switch/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Switch',
+})
diff --git a/taro-demo/src/pages/switch/index.tsx b/taro-demo/src/pages/switch/index.tsx
new file mode 100644
index 0000000..268dd0c
--- /dev/null
+++ b/taro-demo/src/pages/switch/index.tsx
@@ -0,0 +1,12 @@
+import { View } from '@tarojs/components'
+import { Switch } from 'taro-react-form'
+import { useState } from 'react'
+
+export default () => {
+ const [checked, setChecked] = useState(false)
+ return (
+
+
+
+ )
+}
diff --git a/taro-demo/src/pages/textarea/index.config.ts b/taro-demo/src/pages/textarea/index.config.ts
new file mode 100644
index 0000000..524a28c
--- /dev/null
+++ b/taro-demo/src/pages/textarea/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: 'Textarea',
+})
diff --git a/taro-demo/src/pages/textarea/index.tsx b/taro-demo/src/pages/textarea/index.tsx
new file mode 100644
index 0000000..884bf7c
--- /dev/null
+++ b/taro-demo/src/pages/textarea/index.tsx
@@ -0,0 +1,5 @@
+import { Textarea } from 'taro-react-form'
+
+export default () => {
+ return
+}
diff --git a/taro-demo/tsconfig.json b/taro-demo/tsconfig.json
new file mode 100644
index 0000000..0d46fdf
--- /dev/null
+++ b/taro-demo/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es2017",
+ "module": "commonjs",
+ "removeComments": false,
+ "preserveConstEnums": true,
+ "moduleResolution": "node",
+ "experimentalDecorators": true,
+ "noImplicitAny": false,
+ "allowSyntheticDefaultImports": true,
+ "outDir": "lib",
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "strictNullChecks": true,
+ "sourceMap": true,
+ "baseUrl": ".",
+ "jsx": "react-jsx",
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "typeRoots": ["node_modules/@types"],
+ "paths": {
+ "@/*": ["../*"],
+ "taro-react-form": ["../packages/src/index.ts"],
+ }
+ },
+ "include": ["./src", "global.d.ts"],
+ "compileOnSave": false
+}
diff --git a/test.text b/test.text
new file mode 100644
index 0000000..d800886
--- /dev/null
+++ b/test.text
@@ -0,0 +1 @@
+123
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0b33abc
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "noImplicitAny": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "preserveConstEnums": true,
+ "skipLibCheck": true,
+ "sourceMap": false,
+ "resolveJsonModule": true,
+ "downlevelIteration": true,
+ "allowJs": false,
+ "strict": true,
+ "esModuleInterop": false,
+ "removeComments": false,
+ "declaration": true,
+ "declarationMap": false,
+ "baseUrl": ".",
+ "rootDir": ".",
+ "types": ["node"],
+ "typeRoots": ["node_modules/@types"]
+ },
+ "compileOnSave": false,
+ "exclude": ["node_modules/**/*"],
+ "include": ["packages/src/**/*"]
+}
| |