├── 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 [](https://www.npmjs.com/package/react-vcode) [](https://codebeat.co/projects/github-com-javaluo-react-vcode-master) [](https://www.npmjs.com/package/react-vcode)
2 | 一个简单的React验证码组件
3 |
4 | ## 示例图
5 |
6 | 
7 |
8 | 
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 |
--------------------------------------------------------------------------------