├── example ├── src │ ├── index.css │ └── index.js ├── assets │ ├── test1.png │ └── test2.png └── index.html ├── postcss.config.js ├── .babelrc ├── tsconfig.json ├── .eslintrc.json ├── webpack.config.js ├── .gitignore ├── LICENSE ├── package.json ├── webpack.build.config.js ├── README.md └── src └── index.tsx /example/src/index.css: -------------------------------------------------------------------------------- 1 | .vcode{ 2 | width: 300px; 3 | } -------------------------------------------------------------------------------- /example/assets/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-vcode/HEAD/example/assets/test1.png -------------------------------------------------------------------------------- /example/assets/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-vcode/HEAD/example/assets/test2.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** postcss-loader 解析器所需的配置文件 **/ 2 | module.exports = { 3 | plugins: [require('autoprefixer')()], 4 | }; 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime", 5 | ["@babel/plugin-proposal-decorators", { "legacy": true }] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "declaration": true, 8 | "pretty": true, 9 | "rootDir": "src", 10 | "sourceMap": false, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noImplicitAny": false, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "outDir": "dist", 20 | "lib": ["es2018", "dom"] 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "plugin:react-hooks/recommended", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier/@typescript-eslint" 9 | ], 10 | "plugins": ["react", "react-hooks", "prettier"], 11 | "rules": { 12 | "semi": "warn", 13 | "no-unused-vars": "off", 14 | "no-cond-assign": "error", 15 | "no-debugger": "warn", 16 | "no-dupe-args": "error", 17 | "no-caller": "error", 18 | "no-unmodified-loop-condition": "error", 19 | "no-with": "error", 20 | "no-catch-shadow": "error", 21 | 22 | "@typescript-eslint/camelcase": "off", 23 | "@typescript-eslint/no-unused-vars": "off", 24 | "@typescript-eslint/no-var-requires": "off", 25 | "@typescript-eslint/no-use-before-define": "off", 26 | "@typescript-eslint/explicit-function-return-type": "off", 27 | 28 | "prettier/prettier": "warn" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", 5 | entry: path.join(__dirname, "example", "src", "index.js"), 6 | output: { 7 | filename: "bundle.js", 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js?$/, 13 | use: ["babel-loader"], 14 | include: [path.join(__dirname, "example")], 15 | }, 16 | { 17 | test: /\.css?$/, 18 | use: ["style-loader", "css-loader", "postcss-loader"], 19 | include: [path.join(__dirname, "example")], 20 | }, 21 | { 22 | test: /\.(png|jpg|gif)$/, 23 | use: ["url-loader?limit=8132&name=images/[name].[ext]"], 24 | include: [path.join(__dirname, "example")], 25 | }, 26 | { 27 | test: /\.(eot|woff|svg|ttf|woff2|appcache|mp3|pdf|png)(\?|$)/, 28 | use: ["file-loader?name=files/[name].[ext]"], 29 | include: [path.join(__dirname, "example")], 30 | }, 31 | ], 32 | }, 33 | devServer: { 34 | contentBase: path.join(__dirname, "example"), 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # mac .DS_Store 2 | .DS_Store 3 | 4 | # Webstorm 5 | .idea 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules 37 | jspm_packages 38 | build 39 | 40 | # Optional npm cache directory 41 | .npm 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | 46 | # bower 47 | # bower_components 48 | 49 | # access log 50 | access.log 51 | 52 | # backup 53 | backup 54 | 55 | # config.json 56 | config.json 57 | 58 | # build 59 | .build 60 | 61 | # app config 62 | 63 | # tests 64 | mytest/* 65 | 66 | # build 67 | dist/* 68 | !dist/.gitkeep 69 | *.lock 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 java_luo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Vcode from "../../dist/index.js"; 3 | import ReactDom from "react-dom"; 4 | import ImgTest1 from "../assets/test1.png"; 5 | import ImgTest2 from "../assets/test2.png"; 6 | import './index.css'; 7 | 8 | class Test extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | img: 1, 13 | input2: "", // 第2个input的值 14 | vcode2: "-1", // 第2个vcode的值 15 | code: "", 16 | width: 100, 17 | }; 18 | } 19 | 20 | onInput2Change(e) { 21 | this.setState({ 22 | input2: e.target.value, 23 | }); 24 | } 25 | 26 | onVcode2Change(v) { 27 | console.log("触发回调onChange", v); 28 | if (v) { 29 | this.setState({ 30 | vcode2: v, 31 | }); 32 | } 33 | } 34 | 35 | onChangeImg() { 36 | const imgindex = this.state.img === 1 ? 2 : 1; 37 | this.setState({ 38 | img: imgindex, 39 | code: imgindex === 1 ? ImgTest1 : ImgTest2, 40 | vcode2: imgindex === 1 ? "wow1" : "helloworld", 41 | }); 42 | } 43 | onChangeStr() { 44 | const a = ["a", "b", "c", "d"]; 45 | const d = []; 46 | for (let i = 0; i < 5; i++) { 47 | d.push(a[Math.round(Math.random() * 3)]); 48 | } 49 | console.log("code:", d); 50 | this.setState({ 51 | code: d.join(""), 52 | }); 53 | } 54 | 55 | onVcodeClick() { 56 | this.onChangeStr(); 57 | } 58 | onChangeWidth() { 59 | const l = Math.round(Math.random() * 800 + 400); 60 | this.setState({ 61 | width: l, 62 | }); 63 | } 64 | render() { 65 | return ( 66 |
67 |
68 | this.onInput2Change(e)} maxLength={20} /> 69 | this.onVcode2Change(v)} onClick={() => console.log('触发onClick') } value={this.state.code} width={this.state.width} className={'vcode'}/> 70 | {this.state.input2 === this.state.vcode2 ? "输入正确" : "输入错误"} 71 |
72 |
73 | 74 | 75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | ReactDom.render(, document.getElementById("root")); 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-vcode", 3 | "version": "1.0.11", 4 | "description": "a react verification code component", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "scripts": { 10 | "dev": "webpack-dev-server --config webpack.config.js", 11 | "build:babel": "babel src -d lib", 12 | "build": "webpack --config webpack.build.config.js --progress --profile --colors", 13 | "prettier": "prettier --write \"src/index.tsx\"" 14 | }, 15 | "types": "dist/index.d.ts", 16 | "typings": "dist/index.d.ts", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/javaLuo/react-vcode.git" 20 | }, 21 | "keywords": [ 22 | "vcode", 23 | "react-vcode" 24 | ], 25 | "author": "Logic", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/javaLuo/react-vcode/issues" 29 | }, 30 | "homepage": "https://github.com/javaLuo/react-vcode#readme", 31 | "dependencies": { 32 | "es6-object-assign": "^1.1.0", 33 | "react": "^16.13.1", 34 | "react-dom": "^16.13.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.11.6", 38 | "@babel/core": "^7.11.6", 39 | "@babel/plugin-proposal-decorators": "^7.10.5", 40 | "@babel/plugin-transform-runtime": "^7.11.5", 41 | "@babel/preset-env": "^7.11.5", 42 | "@babel/preset-react": "^7.10.4", 43 | "@babel/runtime": "^7.11.2", 44 | "@types/react": "^16.9.49", 45 | "@types/react-dom": "^16.9.8", 46 | "@typescript-eslint/eslint-plugin": "^4.1.0", 47 | "@typescript-eslint/parser": "^4.1.0", 48 | "autoprefixer": "^9.8.6", 49 | "awesome-typescript-loader": "^5.2.1", 50 | "babel-loader": "^8.1.0", 51 | "css-loader": "^4.3.0", 52 | "eslint": "^7.8.1", 53 | "eslint-config-prettier": "^6.11.0", 54 | "eslint-loader": "^4.0.2", 55 | "eslint-plugin-prettier": "^3.1.4", 56 | "eslint-plugin-react": "^7.20.6", 57 | "eslint-plugin-react-hooks": "^4.1.0", 58 | "file-loader": "^6.1.0", 59 | "postcss-loader": "^4.0.2", 60 | "prettier": "^2.1.1", 61 | "source-map-loader": "^1.1.0", 62 | "style-loader": "^1.2.1", 63 | "typescript": "^4.0.2", 64 | "url-loader": "^4.1.0", 65 | "webpack": "^4.44.1", 66 | "webpack-bundle-analyzer": "^3.8.0", 67 | "webpack-cli": "^3.3.12", 68 | "webpack-dev-server": "^3.11.0" 69 | }, 70 | "browserslist": [ 71 | "iOS >= 8", 72 | "last 1 versions", 73 | "> 2%", 74 | "not dead", 75 | "not op_mini all" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const TerserPlugin = require("terser-webpack-plugin"); // 优化js 4 | const ROOT_PATH = path.resolve(__dirname); 5 | 6 | module.exports = { 7 | mode: "production", 8 | // 页面入口文件配置 9 | entry: { 10 | index: ["./src/index.tsx"], 11 | }, 12 | // 输出文件配置 13 | output: { 14 | path: path.resolve(ROOT_PATH, "dist"), 15 | filename: "[name].js", 16 | library: "vcode", 17 | libraryTarget: "umd", 18 | globalObject: 'this' 19 | //libraryExport: 'default', 20 | }, 21 | externals: { 22 | react: "react", 23 | "react-dom": "react-dom", 24 | }, 25 | optimization: { 26 | minimizer: [ 27 | new TerserPlugin({ 28 | parallel: true, // 多线程并行构建 29 | terserOptions: { 30 | // https://github.com/terser/terser#minify-options 31 | compress: { 32 | warnings: false, // 删除无用代码时是否给出警告 33 | drop_console: true, // 删除所有的console.* 34 | drop_debugger: true, // 删除所有的debugger 35 | // pure_funcs: ["console.log"], // 删除所有的console.log 36 | }, 37 | }, 38 | }), 39 | ], 40 | }, 41 | // 解析器配置 42 | module: { 43 | rules: [ 44 | { 45 | // 编译前通过eslint检查代码 (注释掉即可取消eslint检测) 46 | test: /\.(ts|tsx|js|jsx)?$/, 47 | enforce: "pre", 48 | use: ["source-map-loader", "eslint-loader"], 49 | include: path.join(__dirname, "src"), 50 | }, 51 | { 52 | // .tsx用typescript-loader解析解析 53 | test: /\.(ts|tsx|js|jsx)?$/, 54 | use: [ 55 | { 56 | loader: "awesome-typescript-loader", 57 | }, 58 | ], 59 | include: [path.join(__dirname, "src")], 60 | }, 61 | { 62 | test: /\.css?$/, 63 | use: ["style-loader", "css-loader", "postcss-loader"], 64 | include: [path.join(__dirname, "src")], 65 | }, 66 | { 67 | test: /\.(png|jpg|gif)$/, 68 | use: ["url-loader?limit=8192&name=images/[name].[ext]"], 69 | include: [path.join(__dirname, "src")], 70 | }, 71 | { 72 | test: /\.(eot|woff|svg|ttf|woff2|appcache|mp3|pdf)(\?|$)/, 73 | use: ["file-loader?name=files/[name].[ext]"], 74 | include: [path.join(__dirname, "src")], 75 | }, 76 | ], 77 | }, 78 | // 第3方插件配置 79 | plugins: [ 80 | // http://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin 81 | //用来优化生成的代码 chunk,合并相同的代码 82 | new webpack.optimize.AggressiveMergingPlugin(), 83 | ], 84 | // 其他解决方案配置 85 | resolve: { 86 | extensions: [".js", ".jsx", ".ts", ".tsx", ".less", ".css"], //后缀名自动补全 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-vcode [![npm](https://img.shields.io/npm/v/react-vcode.svg)](https://www.npmjs.com/package/react-vcode) [![codebeat badge](https://codebeat.co/badges/6b270f5e-c8d9-4f47-9f84-2833fcf897aa)](https://codebeat.co/projects/github-com-javaluo-react-vcode-master) [![npm downloads](https://img.shields.io/npm/dt/react-vcode.svg)](https://www.npmjs.com/package/react-vcode) 2 | 一个简单的React验证码组件 3 | 4 | ## 示例图 5 | 6 | ![image](example/assets/test1.png) 7 | 8 | ![image](example/assets/test2.png) 9 | 10 | 11 | ## 1. 安装 12 | 13 | ```` 14 | npm install react-vcode 15 | ```` 16 | 17 | ## 2. 使用 18 | 19 | ```` 20 | import Vcode from 'react-vcode'; 21 | 22 | 23 | 24 | ```` 25 | ## 3. 服务端渲染 26 | ``` 27 | import Vcode from 'react-vcode'; 28 | 29 | 30 | ``` 31 | 需要自己加个id, 不然服务端渲染和本地渲染,id变了会报错,因为Vcode内部使用了随机值 32 | 33 | ## 4. 自定义参数 34 | 35 | 可自行设置覆盖原有值 36 | 37 | ````javascript 38 | value // string 受控,不设置的话将随机生成验证码 (支持的值: 普通字符串/网络图片路径/import的本地图片/base64) 39 | onChange // func 回调,生成新的验证码时触发,将新的验证码字符串返回上级 (如果value字段被传入了图片,将返回null) 40 | onClick // func 回调,点击组件本身时触发,外部可通过此回调来更新需要传入的验证码 (如果没设置value,点击就会自动重新生成二维码) 41 | length: 4 // number 生成几位验证码(没设置value时有效) 42 | width: 150 // number 容器宽度(px) 43 | height: 40 // number 容器高度(px) 44 | className // string 自定义容器样式class 45 | style: { // object 容器默认样式 (注:如果在style中设置width和height,将覆盖上面通过属性设置的width和height) 46 |  position: 'relative', 47 | backgroundColor: '#fff', 48 | overflow: 'hidden', 49 | cursor: 'pointer', 50 | verticalAlign: 'middle', 51 | userSelect: 'none', 52 | } 53 | options:{ // 验证码相关自定义参数 54 | codes: [ // 所有可能出现的字符 55 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 56 | 'o', 'p', 'q', 'r', 's', 't', 'x', 'u', 'v', 'y', 'z', 'w', 'n', 57 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 58 | ], 59 |  fontSizeMin: 22, // 字体尺寸最小值 60 |  fontSizeMax: 26, // 字体尺寸最大值 61 |  colors: [         // 字可能的颜色 62 |    '#117cb3', 63 |    '#f47b06', 64 | '#202890', 65 | '#db1821', 66 | '#b812c2', 67 | ], 68 |  fonts: [         // 可能的字体 69 | 'Times New Roman', 70 | 'Georgia', 71 | 'Serif', 72 | 'sans-serif', 73 | 'arial', 74 | 'tahoma', 75 | 'Hiragino Sans GB', 76 | ], 77 |  lines: 8,         // 生成多少根干扰线 78 |  lineColors: [     // 线可能的颜色 79 | '#7999e1', 80 | '#383838', 81 | '#ec856d', 82 | '#008888', 83 | ], 84 |  lineHeightMin: 1, // 线的粗细最小值 85 |  lineHeightMax: 2, // 线的粗细最大值 86 |  lineWidthMin: 40, // 线的长度最小值 87 |  lineWidthMax: 100, // 线的长度最大值 88 | } 89 | 90 | // 例子: 91 | 92 | {console.log('当前的验证码值:', v)}} 95 |  options={{ codes: [ 'A', 'B', 'C' ] }} 96 | /> 97 | ```` 98 | 99 | ## 5. 手动刷新验证码 100 | ```javascript 101 | this.vcode = obj} /> 102 | 103 | this.vcode.onClick(); // 调用内部的onClick方法可刷新验证码 104 | ``` 105 | 106 | ## 6. 额外说明 107 | 108 | - 之前用过一个验证码插件叫 vcode.js, 不知道作者。 本react-vcode是通过vcode.js的源码进行修改加工,转成了react组件。感谢原作者。 109 | 110 | ## 更新日志 111 | 112 | 1.0.3 - 2019/03/11
113 | - 去掉UNSAFE_componentWillReceiveProps,使用componentDidUpdate 114 | - 修复动态改变width、height、style时不刷新的问题 115 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | require("es6-object-assign").polyfill(); 3 | 4 | interface Props { 5 | id?: string; // ID 6 | length?: number; // 生成几位字符串 7 | value?: string; // 由父级传入指定的字符串生成code 8 | width?: number; // 多宽 px 9 | height?: number; // 多高 px 10 | style?: { 11 | [propName: string]: any; 12 | }; // 自定义style 13 | className?: string; // 各种class 14 | options?: OptionsProps; // 自定义各参数 15 | onChange?: (p: string | null) => any; // 每次生成新的验证码时,将验证码的值传到上级 16 | onClick?: () => any; // 用户每次点击时触发 17 | } 18 | interface State { 19 | id: string; 20 | width: number; 21 | height: number; 22 | len: number; 23 | style: { 24 | [propName: string]: any; 25 | }; 26 | options: Options; 27 | } 28 | 29 | interface Options { 30 | codes: (string | number)[]; // 所有可能出现的字符 31 | fontSizeMin: number; // 字体尺寸最小值 32 | fontSizeMax: number; // 字体尺寸最大值 33 | colors: string[]; // 所有可能出现的颜色 34 | fonts: string[]; // 所有可能出现的字体 35 | lines: number; // 生成多少根线 36 | lineColors: string[]; // 线可能的颜色 37 | lineHeightMin: number; // 线的粗细最小值 38 | lineHeightMax: number; // 线的粗细最大值 39 | lineWidthMin: number; // 线的长度最小值 40 | lineWidthMax: number; // 线的长度最大值 41 | } 42 | 43 | type OptionsProps = { [P in keyof Options]?: Options[P] }; 44 | 45 | export default class Vcode extends React.PureComponent { 46 | constructor(props: Props) { 47 | super(props); 48 | this.state = { 49 | id: this.props.id || `${Date.now()}_${Math.random().toFixed(4)}`, // 需要一个唯一的ID,因为vcode要直接操作dom 50 | width: this.props.width || 150, // vcode宽度 51 | height: this.props.height || 40, // vcode高度 52 | len: this.props.length || 4, // 生成几位code 53 | style: (() => { 54 | // vcode容器样式 55 | const a = { 56 | position: "relative", 57 | backgroundColor: "#fff", 58 | overflow: "hidden", 59 | width: this.props.width ? `${this.props.width}px` : "150px", 60 | height: this.props.height ? `${this.props.height}px` : "40px", 61 | cursor: "pointer", 62 | verticalAlign: "middle", 63 | userSelect: "none", 64 | }; 65 | if (this.props.style) { 66 | return Object.assign({}, a, this.props.style); 67 | } 68 | return a; 69 | })(), 70 | options: (() => { 71 | // 初始化参数 72 | const a: Options = { 73 | codes: [ 74 | "a", 75 | "b", 76 | "c", 77 | "d", 78 | "e", 79 | "f", 80 | "g", 81 | "h", 82 | "i", 83 | "j", 84 | "k", 85 | "l", 86 | "m", 87 | "o", 88 | "p", 89 | "q", 90 | "r", 91 | "s", 92 | "t", 93 | "x", 94 | "u", 95 | "v", 96 | "y", 97 | "z", 98 | "w", 99 | "n", 100 | "0", 101 | "1", 102 | "2", 103 | "3", 104 | "4", 105 | "5", 106 | "6", 107 | "7", 108 | "8", 109 | "9", 110 | ], 111 | fontSizeMin: 22, // 字体尺寸最小值 112 | fontSizeMax: 26, // 字体尺寸最大值 113 | colors: [ 114 | // 字可能的颜色 115 | "#117cb3", 116 | "#f47b06", 117 | "#202890", 118 | "#db1821", 119 | "#b812c2", 120 | ], 121 | fonts: [ 122 | // 可能的字体 123 | "Times New Roman", 124 | "Georgia", 125 | "Serif", 126 | "sans-serif", 127 | "arial", 128 | "tahoma", 129 | "Hiragino Sans GB", 130 | ], 131 | lines: 8, // 生成多少根线 132 | lineColors: [ 133 | // 线可能的颜色 134 | "#7999e1", 135 | "#383838", 136 | "#ec856d", 137 | "#008888", 138 | ], 139 | lineHeightMin: 1, // 线的粗细最小值 140 | lineHeightMax: 2, // 线的粗细最大值 141 | lineWidthMin: 40, // 线的长度最小值 142 | lineWidthMax: 100, // 线的长度最大值 143 | }; 144 | if (this.props.options) { 145 | return Object.assign({}, a, this.props.options); 146 | } 147 | return a; 148 | })(), 149 | }; 150 | } 151 | 152 | /** 组件初始化完毕时触发 **/ 153 | componentDidMount(): void { 154 | this.onDraw(this.props.value); 155 | } 156 | 157 | /** 组件参数改变 **/ 158 | componentDidUpdate(prevP: Props): void { 159 | if (this.props.value !== prevP.value) { 160 | this.onDraw(this.props.value); 161 | } 162 | if ( 163 | this.props.width !== prevP.width || 164 | this.props.height !== prevP.height || 165 | this.props.style !== prevP.style 166 | ) { 167 | this.setState({ 168 | width: this.props.width || 150, 169 | height: this.props.height || 40, 170 | style: Object.assign({}, this.state.style, { 171 | width: this.props.width ? `${this.props.width}px` : "150px", 172 | height: this.props.height ? `${this.props.height}px` : "40px", 173 | }), 174 | }); 175 | } 176 | } 177 | 178 | /** 用户点击了验证码图片 **/ 179 | onClick(): void { 180 | // 如果用户没有设置值,就直接重新生成 181 | if (!this.props.value) { 182 | this.onDraw(this.props.value); 183 | } 184 | this.props.onClick && this.props.onClick(); // 触发外部的onClick,什么都不返回 185 | } 186 | 187 | /** 188 | * 随机生成一个Code的CSS样式 189 | * @param uW 每个字符所占的宽度 190 | * @param i 当前字符的下标 191 | * @param maxW 最大偏移值 192 | * @return CSS字符串 193 | */ 194 | codeCss(uW: number, i: number, maxW: number): string { 195 | const transStr = `rotate(${this.randint( 196 | -15, 197 | 15, 198 | true 199 | )}deg) translateY(${this.randint(-55, -45, true)}%)`; 200 | return [ 201 | `font-size:${this.randint( 202 | this.state.options.fontSizeMin, 203 | this.state.options.fontSizeMax 204 | )}px`, 205 | `color:${ 206 | this.state.options.colors[ 207 | this.randint(0, this.state.options.colors.length - 1) 208 | ] 209 | }`, 210 | "position: absolute", 211 | `left:${Math.max( 212 | Math.min(this.randint(uW * i, uW * i + uW / 2, true), maxW), 213 | uW / 4 214 | )}px`, 215 | "top:50%", 216 | `transform:${transStr};-o-transform:${transStr};-ms-transform:${transStr};-moz-transform:${transStr};-webkit-transform:${transStr}`, 217 | `font-family:${ 218 | this.state.options.fonts[ 219 | this.randint(0, this.state.options.fonts.length - 1) 220 | ] 221 | }`, 222 | "font-weight:bold", 223 | "z-index:2", 224 | ].join(";"); 225 | } 226 | 227 | /** 228 | * 随机生成一条线的CSS样式 229 | * @return CSS字符串 230 | */ 231 | lineCss(): string { 232 | const transStr = `rotate(${this.randint(-30, 30)}deg)`; 233 | return [ 234 | "position: absolute", 235 | `opacity:${this.randint(3, 8) / 10}`, 236 | `width:${this.randint( 237 | this.state.options.lineWidthMin, 238 | this.state.options.lineWidthMax 239 | )}px`, 240 | `height:${this.randint( 241 | this.state.options.lineHeightMin, 242 | this.state.options.lineHeightMax 243 | )}px`, 244 | `background:${ 245 | this.state.options.lineColors[ 246 | this.randint(0, this.state.options.lineColors.length - 1) 247 | ] 248 | }`, 249 | `left:${this.randint( 250 | -this.state.options.lineWidthMin / 2, 251 | this.state.width 252 | )}px`, 253 | `top:${this.randint(0, this.state.height)}px`, 254 | `transform:${transStr};-o-transform:${transStr};-ms-transform:${transStr};-moz-transform:${transStr};-webkit-transform:${transStr}`, 255 | ].join(";"); 256 | } 257 | 258 | /** 259 | * 绘制 260 | * @param value 需要生成的字符值,不传则随机生成 261 | * */ 262 | onDraw(value: string | undefined): string | null { 263 | let c = ""; // 存储生成的code 264 | const div = document.getElementById(this.state.id); 265 | 266 | const isImg: boolean = /^http[s]*:\/\/|\.jpg$|\.png$|\.jpeg$|\.gif$|\.bmp$|\.webp$|^data:image/.test( 267 | value || "" 268 | ); // 是否是图片 269 | if (div) { 270 | div.innerHTML = ""; 271 | } 272 | 273 | if (isImg) { 274 | // 用户传递了一张图片 275 | const dom = document.createElement("img"); 276 | dom.style.cssText = [ 277 | "display: block", 278 | "max-width:100%", 279 | "max-height:100%", 280 | ].join(";"); 281 | dom.src = value as string; 282 | div && div.appendChild(dom); 283 | this.props.onChange && this.props.onChange(null); 284 | return null; 285 | } 286 | 287 | // 不是图片而是普通字符串, 如果value存在说明是用户自定义的字符串 288 | const length = value ? value.length : this.state.len; // 字符的长度 289 | const uW: number = this.state.width / length; // 每个字符能够占据的范围宽度 290 | const maxW = this.state.width - uW / 4; // 最大可偏移距离 291 | 292 | for (let i = 0; i < length; i++) { 293 | const dom = document.createElement("span"); 294 | dom.style.cssText = this.codeCss(uW, i, maxW); 295 | const temp = value 296 | ? value[i] 297 | : this.state.options.codes[ 298 | Math.round(Math.random() * (this.state.options.codes.length - 1)) 299 | ]; 300 | dom.innerHTML = String(temp); 301 | c = `${c}${temp}`; 302 | div && div.appendChild(dom); 303 | } 304 | 305 | // 生成好看的线条 306 | for (let i = 0; i < this.state.options.lines; i++) { 307 | const dom = document.createElement("div"); 308 | dom.style.cssText = this.lineCss(); 309 | div && div.appendChild(dom); 310 | } 311 | this.props.onChange && this.props.onChange(c); // 触发回调 312 | return c; 313 | } 314 | 315 | /** 生成范围随机数 **/ 316 | randint(n: number, m: number, t?: boolean): number { 317 | const c = m - n + 1; 318 | const num = Math.random() * c + n; 319 | return t ? num : Math.floor(num); 320 | } 321 | 322 | render() { 323 | return ( 324 |
this.onClick()} 329 | /> 330 | ); 331 | } 332 | } 333 | --------------------------------------------------------------------------------