From ae6553ba6312491b397e35742d1e98c1967d45e1 Mon Sep 17 00:00:00 2001 From: LavaCourage07 Date: Sun, 29 Jan 2023 16:51:31 +0800 Subject: [PATCH 1/2] modify --- .eslintignore | 17 + .eslintrc | 11 + .gitignore | 26 ++ .prettierignore | 13 + .prettierrc | 16 + README.md | 18 ++ commitlint.config.js | 27 ++ package.json | 83 +++++ packages/README.md | 3 + packages/build/after-build.js | 11 + packages/build/before-build.js | 11 + packages/build/scripts.js | 7 + packages/package.json | 31 ++ packages/rollup.config.js | 66 ++++ packages/src/components/button/index.tsx | 214 +++++++++++++ packages/src/components/cascader/index.tsx | 180 +++++++++++ packages/src/components/cell/index.tsx | 60 ++++ packages/src/components/checkbox/index.tsx | 88 ++++++ .../src/components/config-provider/index.tsx | 21 ++ packages/src/components/date-picker/index.tsx | 281 +++++++++++++++++ packages/src/components/field/Field.tsx | 116 +++++++ packages/src/components/field/FieldWrap.tsx | 293 ++++++++++++++++++ packages/src/components/field/index.tsx | 6 + packages/src/components/footer-area/index.tsx | 44 +++ packages/src/components/form/Form.tsx | 266 ++++++++++++++++ packages/src/components/form/FormItem.tsx | 83 +++++ packages/src/components/form/context.ts | 26 ++ packages/src/components/form/index.tsx | 16 + packages/src/components/picker/index.tsx | 101 ++++++ packages/src/components/popup/index.tsx | 116 +++++++ packages/src/components/switch/index.tsx | 41 +++ packages/src/components/tabs/index.tsx | 134 ++++++++ packages/src/components/textarea/index.tsx | 40 +++ packages/src/hooks/index.ts | 7 + packages/src/hooks/tsconfig.json | 7 + packages/src/hooks/useAvailableViewHeight.ts | 39 +++ packages/src/hooks/useBoolean.ts | 28 ++ packages/src/hooks/useFetch.ts | 52 ++++ packages/src/hooks/useSafeBottom.ts | 33 ++ packages/src/hooks/useSafeTop.ts | 43 +++ packages/src/hooks/useSystemInfo.ts | 27 ++ packages/src/hooks/useUUID.ts | 40 +++ packages/src/index.ts | 37 +++ packages/src/styles/common.less | 9 + packages/src/styles/components/button.less | 199 ++++++++++++ packages/src/styles/components/cascader.less | 32 ++ packages/src/styles/components/cell.less | 50 +++ packages/src/styles/components/checkbox.less | 44 +++ .../src/styles/components/date-picker.less | 17 + packages/src/styles/components/field.less | 117 +++++++ .../src/styles/components/footer-area.less | 24 ++ packages/src/styles/components/picker.less | 18 ++ packages/src/styles/components/popup.less | 71 +++++ packages/src/styles/components/switch.less | 46 +++ packages/src/styles/components/tabs.less | 57 ++++ packages/src/styles/components/textarea.less | 12 + packages/src/styles/index.less | 14 + packages/src/styles/variable.less | 124 ++++++++ packages/src/utils/date.ts | 122 ++++++++ packages/src/utils/debounce.ts | 191 ++++++++++++ packages/src/utils/http/request.ts | 174 +++++++++++ packages/src/utils/http/types.ts | 30 ++ packages/src/utils/index.ts | 3 + packages/src/utils/is.ts | 80 +++++ packages/src/utils/throttle.ts | 69 +++++ packages/tsconfig.json | 7 + taro-demo/babel.config.js | 13 + taro-demo/config/dev.js | 8 + taro-demo/config/index.js | 69 +++++ taro-demo/config/prod.js | 35 +++ taro-demo/global.d.ts | 18 ++ taro-demo/package.json | 71 +++++ taro-demo/project.config.json | 13 + taro-demo/project.tt.json | 9 + taro-demo/src/app.config.ts | 20 ++ taro-demo/src/app.less | 0 taro-demo/src/app.tsx | 9 + taro-demo/src/index.html | 19 ++ taro-demo/src/pages/cascader/index.config.ts | 3 + taro-demo/src/pages/cascader/index.less | 0 taro-demo/src/pages/cascader/index.tsx | 137 ++++++++ taro-demo/src/pages/checkbox/index.config.ts | 3 + taro-demo/src/pages/checkbox/index.less | 0 taro-demo/src/pages/checkbox/index.tsx | 40 +++ .../src/pages/date-picker/index.config.ts | 4 + taro-demo/src/pages/date-picker/index.less | 0 taro-demo/src/pages/date-picker/index.tsx | 57 ++++ taro-demo/src/pages/field/index.config.ts | 3 + taro-demo/src/pages/field/index.less | 0 taro-demo/src/pages/field/index.tsx | 28 ++ taro-demo/src/pages/form/index.config.ts | 3 + taro-demo/src/pages/form/index.less | 3 + taro-demo/src/pages/form/index.tsx | 284 +++++++++++++++++ taro-demo/src/pages/index/index.config.ts | 3 + taro-demo/src/pages/index/index.less | 0 taro-demo/src/pages/index/index.tsx | 64 ++++ taro-demo/src/pages/listview/index.config.ts | 3 + taro-demo/src/pages/listview/index.less | 0 taro-demo/src/pages/listview/index.tsx | 47 +++ taro-demo/src/pages/picker/index.config.ts | 3 + taro-demo/src/pages/picker/index.less | 0 taro-demo/src/pages/picker/index.tsx | 53 ++++ taro-demo/src/pages/popup/index.config.ts | 3 + taro-demo/src/pages/popup/index.less | 0 taro-demo/src/pages/popup/index.tsx | 44 +++ taro-demo/src/pages/switch/index.config.ts | 3 + taro-demo/src/pages/switch/index.tsx | 12 + taro-demo/src/pages/textarea/index.config.ts | 3 + taro-demo/src/pages/textarea/index.tsx | 5 + taro-demo/tsconfig.json | 28 ++ tsconfig.json | 31 ++ 111 files changed, 5440 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 commitlint.config.js create mode 100644 package.json create mode 100644 packages/README.md create mode 100644 packages/build/after-build.js create mode 100644 packages/build/before-build.js create mode 100644 packages/build/scripts.js create mode 100644 packages/package.json create mode 100644 packages/rollup.config.js create mode 100644 packages/src/components/button/index.tsx create mode 100644 packages/src/components/cascader/index.tsx create mode 100644 packages/src/components/cell/index.tsx create mode 100644 packages/src/components/checkbox/index.tsx create mode 100644 packages/src/components/config-provider/index.tsx create mode 100644 packages/src/components/date-picker/index.tsx create mode 100644 packages/src/components/field/Field.tsx create mode 100644 packages/src/components/field/FieldWrap.tsx create mode 100644 packages/src/components/field/index.tsx create mode 100644 packages/src/components/footer-area/index.tsx create mode 100644 packages/src/components/form/Form.tsx create mode 100644 packages/src/components/form/FormItem.tsx create mode 100644 packages/src/components/form/context.ts create mode 100644 packages/src/components/form/index.tsx create mode 100644 packages/src/components/picker/index.tsx create mode 100644 packages/src/components/popup/index.tsx create mode 100644 packages/src/components/switch/index.tsx create mode 100644 packages/src/components/tabs/index.tsx create mode 100644 packages/src/components/textarea/index.tsx create mode 100644 packages/src/hooks/index.ts create mode 100644 packages/src/hooks/tsconfig.json create mode 100644 packages/src/hooks/useAvailableViewHeight.ts create mode 100644 packages/src/hooks/useBoolean.ts create mode 100644 packages/src/hooks/useFetch.ts create mode 100644 packages/src/hooks/useSafeBottom.ts create mode 100644 packages/src/hooks/useSafeTop.ts create mode 100644 packages/src/hooks/useSystemInfo.ts create mode 100644 packages/src/hooks/useUUID.ts create mode 100644 packages/src/index.ts create mode 100644 packages/src/styles/common.less create mode 100644 packages/src/styles/components/button.less create mode 100644 packages/src/styles/components/cascader.less create mode 100644 packages/src/styles/components/cell.less create mode 100644 packages/src/styles/components/checkbox.less create mode 100644 packages/src/styles/components/date-picker.less create mode 100644 packages/src/styles/components/field.less create mode 100644 packages/src/styles/components/footer-area.less create mode 100644 packages/src/styles/components/picker.less create mode 100644 packages/src/styles/components/popup.less create mode 100644 packages/src/styles/components/switch.less create mode 100644 packages/src/styles/components/tabs.less create mode 100644 packages/src/styles/components/textarea.less create mode 100644 packages/src/styles/index.less create mode 100644 packages/src/styles/variable.less create mode 100644 packages/src/utils/date.ts create mode 100644 packages/src/utils/debounce.ts create mode 100644 packages/src/utils/http/request.ts create mode 100644 packages/src/utils/http/types.ts create mode 100644 packages/src/utils/index.ts create mode 100644 packages/src/utils/is.ts create mode 100644 packages/src/utils/throttle.ts create mode 100644 packages/tsconfig.json create mode 100644 taro-demo/babel.config.js create mode 100644 taro-demo/config/dev.js create mode 100644 taro-demo/config/index.js create mode 100644 taro-demo/config/prod.js create mode 100644 taro-demo/global.d.ts create mode 100644 taro-demo/package.json create mode 100644 taro-demo/project.config.json create mode 100644 taro-demo/project.tt.json create mode 100644 taro-demo/src/app.config.ts create mode 100644 taro-demo/src/app.less create mode 100644 taro-demo/src/app.tsx create mode 100644 taro-demo/src/index.html create mode 100644 taro-demo/src/pages/cascader/index.config.ts create mode 100644 taro-demo/src/pages/cascader/index.less create mode 100644 taro-demo/src/pages/cascader/index.tsx create mode 100644 taro-demo/src/pages/checkbox/index.config.ts create mode 100644 taro-demo/src/pages/checkbox/index.less create mode 100644 taro-demo/src/pages/checkbox/index.tsx create mode 100644 taro-demo/src/pages/date-picker/index.config.ts create mode 100644 taro-demo/src/pages/date-picker/index.less create mode 100644 taro-demo/src/pages/date-picker/index.tsx create mode 100644 taro-demo/src/pages/field/index.config.ts create mode 100644 taro-demo/src/pages/field/index.less create mode 100644 taro-demo/src/pages/field/index.tsx create mode 100644 taro-demo/src/pages/form/index.config.ts create mode 100644 taro-demo/src/pages/form/index.less create mode 100644 taro-demo/src/pages/form/index.tsx create mode 100644 taro-demo/src/pages/index/index.config.ts create mode 100644 taro-demo/src/pages/index/index.less create mode 100644 taro-demo/src/pages/index/index.tsx create mode 100644 taro-demo/src/pages/listview/index.config.ts create mode 100644 taro-demo/src/pages/listview/index.less create mode 100644 taro-demo/src/pages/listview/index.tsx create mode 100644 taro-demo/src/pages/picker/index.config.ts create mode 100644 taro-demo/src/pages/picker/index.less create mode 100644 taro-demo/src/pages/picker/index.tsx create mode 100644 taro-demo/src/pages/popup/index.config.ts create mode 100644 taro-demo/src/pages/popup/index.less create mode 100644 taro-demo/src/pages/popup/index.tsx create mode 100644 taro-demo/src/pages/switch/index.config.ts create mode 100644 taro-demo/src/pages/switch/index.tsx create mode 100644 taro-demo/src/pages/textarea/index.config.ts create mode 100644 taro-demo/src/pages/textarea/index.tsx create mode 100644 taro-demo/tsconfig.json create mode 100644 tsconfig.json 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 && ( +
+ {button} +
+ )} + {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 = ( + +