├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── README.md
├── commitlint.config.js
├── package.json
├── pnpm-lock.yaml
├── public
└── index.html
├── src
├── App.tsx
├── index.module.scss
├── index.tsx
├── react-app-env.d.ts
└── utils
│ ├── compressBase64.ts
│ ├── getUserMediaStream.ts
│ └── toast.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*]
6 | indent_style = space
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,node,sass,webstorm
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,visualstudiocode,node,sass,webstorm
4 |
5 | ### macOS ###
6 | # General
7 | .DS_Store
8 | .AppleDouble
9 | .LSOverride
10 |
11 | # Icon must end with two \r
12 | Icon
13 |
14 | # Thumbnails
15 | ._*
16 |
17 | # Files that might appear in the root of a volume
18 | .DocumentRevisions-V100
19 | .fseventsd
20 | .Spotlight-V100
21 | .TemporaryItems
22 | .Trashes
23 | .VolumeIcon.icns
24 | .com.apple.timemachine.donotpresent
25 |
26 | # Directories potentially created on remote AFP share
27 | .AppleDB
28 | .AppleDesktop
29 | Network Trash Folder
30 | Temporary Items
31 | .apdisk
32 |
33 | ### Node ###
34 | # Logs
35 | logs
36 | *.log
37 | npm-debug.log*
38 | yarn-debug.log*
39 | yarn-error.log*
40 | lerna-debug.log*
41 |
42 | # Diagnostic reports (https://nodejs.org/api/report.html)
43 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
44 |
45 | # Runtime data
46 | pids
47 | *.pid
48 | *.seed
49 | *.pid.lock
50 |
51 | # Directory for instrumented libs generated by jscoverage/JSCover
52 | lib-cov
53 |
54 | # Coverage directory used by tools like istanbul
55 | coverage
56 | *.lcov
57 |
58 | # nyc test coverage
59 | .nyc_output
60 |
61 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
62 | .grunt
63 |
64 | # Bower dependency directory (https://bower.io/)
65 | bower_components
66 |
67 | # node-waf configuration
68 | .lock-wscript
69 |
70 | # Compiled binary addons (https://nodejs.org/api/addons.html)
71 | build/Release
72 |
73 | # Dependency directories
74 | node_modules/
75 | jspm_packages/
76 |
77 | # TypeScript v1 declaration files
78 | typings/
79 |
80 | # TypeScript cache
81 | *.tsbuildinfo
82 |
83 | # Optional npm cache directory
84 | .npm
85 |
86 | # Optional eslint cache
87 | .eslintcache
88 |
89 | # Microbundle cache
90 | .rpt2_cache/
91 | .rts2_cache_cjs/
92 | .rts2_cache_es/
93 | .rts2_cache_umd/
94 |
95 | # Optional REPL history
96 | .node_repl_history
97 |
98 | # Output of 'npm pack'
99 | *.tgz
100 |
101 | # Yarn Integrity file
102 | .yarn-integrity
103 |
104 | # dotenv environment variables file
105 | .env
106 | .env.test
107 |
108 | # parcel-bundler cache (https://parceljs.org/)
109 | .cache
110 |
111 | # Next.js build output
112 | .next
113 |
114 | # Nuxt.js build / generate output
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 | .cache/
120 | # Comment in the public line in if your project uses Gatsby and not Next.js
121 | # https://nextjs.org/blog/next-9-1#public-directory-support
122 | # public
123 |
124 | # vuepress build output
125 | .vuepress/dist
126 |
127 | # Serverless directories
128 | .serverless/
129 |
130 | # FuseBox cache
131 | .fusebox/
132 |
133 | # DynamoDB Local files
134 | .dynamodb/
135 |
136 | # TernJS port file
137 | .tern-port
138 |
139 | # Stores VSCode versions used for testing VSCode extensions
140 | .vscode-test
141 |
142 | ### Sass ###
143 | .sass-cache/
144 | *.css.map
145 | *.sass.map
146 | *.scss.map
147 |
148 | ### VisualStudioCode ###
149 | .vscode/*
150 | !.vscode/settings.json
151 | !.vscode/tasks.json
152 | !.vscode/launch.json
153 | !.vscode/extensions.json
154 | *.code-workspace
155 |
156 | ### VisualStudioCode Patch ###
157 | # Ignore all local history of files
158 | .history
159 |
160 | ### WebStorm ###
161 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
162 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
163 |
164 | # User-specific stuff
165 | .idea/**/workspace.xml
166 | .idea/**/tasks.xml
167 | .idea/**/usage.statistics.xml
168 | .idea/**/dictionaries
169 | .idea/**/shelf
170 |
171 | # Generated files
172 | .idea/**/contentModel.xml
173 |
174 | # Sensitive or high-churn files
175 | .idea/**/dataSources/
176 | .idea/**/dataSources.ids
177 | .idea/**/dataSources.local.xml
178 | .idea/**/sqlDataSources.xml
179 | .idea/**/dynamic.xml
180 | .idea/**/uiDesigner.xml
181 | .idea/**/dbnavigator.xml
182 |
183 | # Gradle
184 | .idea/**/gradle.xml
185 | .idea/**/libraries
186 |
187 | # Gradle and Maven with auto-import
188 | # When using Gradle or Maven with auto-import, you should exclude module files,
189 | # since they will be recreated, and may cause churn. Uncomment if using
190 | # auto-import.
191 | # .idea/artifacts
192 | # .idea/compiler.xml
193 | # .idea/jarRepositories.xml
194 | # .idea/modules.xml
195 | # .idea/*.iml
196 | # .idea/modules
197 | # *.iml
198 | # *.ipr
199 |
200 | # CMake
201 | cmake-build-*/
202 |
203 | # Mongo Explorer plugin
204 | .idea/**/mongoSettings.xml
205 |
206 | # File-based project format
207 | *.iws
208 |
209 | # IntelliJ
210 | out/
211 |
212 | # mpeltonen/sbt-idea plugin
213 | .idea_modules/
214 |
215 | # JIRA plugin
216 | atlassian-ide-plugin.xml
217 |
218 | # Cursive Clojure plugin
219 | .idea/replstate.xml
220 |
221 | # Crashlytics plugin (for Android Studio and IntelliJ)
222 | com_crashlytics_export_strings.xml
223 | crashlytics.properties
224 | crashlytics-build.properties
225 | fabric.properties
226 |
227 | # Editor-based Rest Client
228 | .idea/httpRequests
229 |
230 | # Android studio 3.1+ serialized cache file
231 | .idea/caches/build_file_checksums.ser
232 |
233 | ### WebStorm Patch ###
234 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
235 |
236 | # *.iml
237 | # modules.xml
238 | # .idea/misc.xml
239 | # *.ipr
240 |
241 | # Sonarlint plugin
242 | .idea/**/sonarlint/
243 |
244 | # SonarQube Plugin
245 | .idea/**/sonarIssues.xml
246 |
247 | # Markdown Navigator plugin
248 | .idea/**/markdown-navigator.xml
249 | .idea/**/markdown-navigator-enh.xml
250 | .idea/**/markdown-navigator/
251 |
252 | # Cache file creation bug
253 | # See https://youtrack.jetbrains.com/issue/JBR-2257
254 | .idea/$CACHE_FILE$
255 |
256 | # End of https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,node,sass,webstorm
257 |
258 | ### Custom config ###
259 | coverage
260 | sassdoc
261 | src/project.config.json
262 | src/miniprogram_npm
263 | src/services
264 | **/*.zip
265 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // I prefer using a JavaScript file for the .eslintrc file (instead of a JSON file) as it supports comments that can be used to better describe rules.
2 |
3 | module.exports = {
4 | parserOptions: {
5 | ecmaVersion: 2018,
6 | sourceType: 'module',
7 | ecmaFeatures: {
8 | // Doc of this config https://github.com/babel/babel-eslint/releases/tag/v9.0.0
9 | legacyDecorators: true,
10 | },
11 | },
12 | // Because need to use 'legacyDecorators', we have to choose 'babel-eslint' as parser.
13 | // https://github.com/babel/babel-eslint/releases/tag/v9.0.0
14 | parser: 'babel-eslint',
15 | env: {
16 | es2020: true,
17 | browser: true,
18 | node: true,
19 | jest: true,
20 | },
21 | extends: [
22 | 'eslint:recommended', // Use the recommended config for JavaScript.
23 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
24 | 'plugin:react-hooks/recommended',
25 | ],
26 | rules: {
27 | 'no-misleading-character-class': 'off',
28 | // 为了兼容旧代码
29 | 'no-unused-vars': 'off',
30 | 'no-debugger': 'off',
31 | 'no-useless-escape': 'off',
32 | 'no-empty': 'off',
33 | 'no-unreachable': 'off',
34 | 'react-hooks/rules-of-hooks': 'warn', // 检查 Hook 的规则
35 | 'react-hooks/exhaustive-deps': 'warn', // 检查 effect 的依赖
36 | 'no-undef': 'off',
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | tabWidth: 2,
4 | semi: true,
5 | singleQuote: true,
6 | trailingComma: 'all',
7 | bracketSpacing: true,
8 | arrowParens: 'avoid',
9 | };
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## h5身份证拍照最简 demo,如果觉得有帮助的话,欢迎点个star👏
2 |
3 | 注:请本地启动后,在移动端下进行食用
4 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "h5-id-card",
3 | "version": "1.1.0",
4 | "dependencies": {
5 | "@ant-design/icons": "^4.7.0",
6 | "antd": "^4.15.4",
7 | "antd-mobile": "^2.3.4",
8 | "lib-flexible": "^0.3.2",
9 | "react": "^17.0.1",
10 | "react-dom": "^17.0.1",
11 | "react-scripts": "4.0.3",
12 | "typescript": "^4.7.4"
13 | },
14 | "devDependencies": {
15 | "@commitlint/cli": "^12.1.1",
16 | "@commitlint/config-conventional": "^12.1.1",
17 | "@types/react": "^17.0.3",
18 | "@types/react-dom": "^17.0.3",
19 | "babel-eslint": "^10.1.0",
20 | "cz-conventional-changelog": "^3.3.0",
21 | "eslint": "^7.11.0",
22 | "eslint-config-prettier": "^8.1.0",
23 | "eslint-plugin-prettier": "^3.3.1",
24 | "eslint-plugin-react-hooks": "^4.2.0",
25 | "husky": "^6.0.0",
26 | "node-sass": "^5.0.0",
27 | "prettier": "^2.2.1",
28 | "sass-loader": "^11.0.1"
29 | },
30 | "eslintConfig": {
31 | "extends": [
32 | "react-app",
33 | "react-app/jest"
34 | ]
35 | },
36 | "scripts": {
37 | "dev": "HTTPS=true react-scripts start",
38 | "build": "react-scripts build",
39 | "eject": "react-scripts eject",
40 | "commit": "git-cz"
41 | },
42 | "browserslist": {
43 | "production": [
44 | ">0.2%",
45 | "not dead",
46 | "not op_mini all"
47 | ],
48 | "development": [
49 | "last 1 chrome version",
50 | "last 1 firefox version",
51 | "last 1 safari version"
52 | ]
53 | },
54 | "config": {
55 | "commitizen": {
56 | "path": "node_modules/cz-conventional-changelog"
57 | }
58 | },
59 | "husky": {
60 | "hooks": {
61 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 | h5-id-card
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 |
3 | import 'lib-flexible';
4 |
5 | import 'antd-mobile/dist/antd-mobile.css'; // or 'antd-mobile/dist/antd-mobile.less'
6 | import 'antd/dist/antd.css';
7 | import { showLoading, hideLoading, showFail, showSuccess } from './utils/toast';
8 | import styles from './index.module.scss';
9 | import { PictureOutlined } from '@ant-design/icons';
10 | import { startCompress } from './utils/compressBase64';
11 | import { getUserMediaStream } from './utils/getUserMediaStream';
12 |
13 | const App: React.FC<{}> = () => {
14 | const [videoHeight, setVideoHeight] = useState(0);
15 | const [fileList, setFileList] = useState([]);
16 | const ref = useRef();
17 |
18 | useEffect(() => {
19 | const v: any = document.getElementById('video');
20 | const rectangle = document.getElementById('capture-rectangle');
21 | const _canvas = document.createElement('canvas');
22 | _canvas.style.display = 'block';
23 |
24 | if (!v) {
25 | return;
26 | }
27 | const video: HTMLVideoElement = v;
28 |
29 | getUserMediaStream(video)
30 | .then(() => {
31 | setVideoHeight(video.offsetHeight);
32 | startCapture();
33 | })
34 | .catch(() => {
35 | showFail({
36 | text: '无法调起后置摄像头,请点击相册,手动上传身份证',
37 | duration: 6,
38 | });
39 | });
40 |
41 | /**
42 | * 获取video中对应的真实size
43 | */
44 | function getRealSize() {
45 | const { videoHeight: vh, videoWidth: vw, offsetHeight: oh, offsetWidth: ow } = video;
46 |
47 | return {
48 | getHeight: height => {
49 | return (vh / oh) * height;
50 | },
51 | getWidth: width => {
52 | return (vw / ow) * width;
53 | },
54 | };
55 | }
56 |
57 | function isChildOf(child, parent) {
58 | var parentNode;
59 | if (child && parent) {
60 | parentNode = child.parentNode;
61 | while (parentNode) {
62 | if (parent === parentNode) {
63 | return true;
64 | }
65 | parentNode = parentNode.parentNode;
66 | }
67 | }
68 | return false;
69 | }
70 |
71 | function startCapture() {
72 | ref.current = setInterval(() => {
73 | const { getHeight, getWidth } = getRealSize();
74 | if (!rectangle) {
75 | return;
76 | }
77 | /** 获取框的位置 */
78 | const { left, top, width, height } = rectangle.getBoundingClientRect();
79 |
80 | /** 测试时预览 */
81 | // if (isChildOf(_canvas, container)) {
82 | // container.removeChild(_canvas);
83 | // }
84 | // container.appendChild(_canvas);
85 |
86 | const context = _canvas.getContext('2d');
87 | _canvas.width = width;
88 | _canvas.height = height;
89 |
90 | context?.drawImage(
91 | video,
92 | getWidth(left + window.scrollX),
93 | getHeight(top + window.scrollY),
94 | getWidth(width),
95 | getHeight(height),
96 | 0,
97 | 0,
98 | width,
99 | height,
100 | );
101 |
102 | const base64 = _canvas.toDataURL('image/jpeg');
103 | // TODO 此处可以根据需要调用OCR识别接口
104 | }, 200);
105 | }
106 |
107 | /** 防止内存泄露 */
108 | return () => clearInterval(ref.current);
109 | }, []);
110 |
111 | /** 只支持1张图片 */
112 | function updateUploadFiles(url = '') {
113 | let files: any[] = [];
114 | if (url) {
115 | files = [{ url }];
116 | }
117 |
118 | setFileList(files);
119 | }
120 |
121 | const __formatUploadFile2base64AndCompress = file => {
122 | const handleImgFileBase64 = file => {
123 | return new Promise(resolve => {
124 | const reader = new FileReader();
125 | reader.readAsDataURL(file);
126 |
127 | reader.onloadend = function () {
128 | resolve(reader.result);
129 | };
130 | });
131 | };
132 |
133 | showLoading();
134 | handleImgFileBase64(file)
135 | .then(res => {
136 | if (file.size > 750 * 1334) {
137 | showLoading('图片压缩中...');
138 | return startCompress(res);
139 | } else {
140 | return res;
141 | }
142 | })
143 | .then(res => {
144 | hideLoading();
145 | updateUploadFiles();
146 | // TODO 上传
147 | showSuccess({
148 | text: '上传成功!',
149 | });
150 | })
151 | .catch(err => {
152 | console.error(err);
153 | hideLoading();
154 | showFail({
155 | text: '上传失败',
156 | });
157 | });
158 | };
159 |
160 | const onChangeFile = event => {
161 | const files = event.target.files;
162 | if (files?.[0]) {
163 | __formatUploadFile2base64AndCompress(files[0]);
164 | }
165 | };
166 |
167 | const customUploadProps = {
168 | onChange: onChangeFile,
169 | accept: 'image/jpeg,image/jpg,image/png',
170 | files: fileList,
171 | };
172 |
173 | /**
174 | * 从本地上传
175 | */
176 | const CustomUpload = customUploadProps => (
177 |
178 | );
179 |
180 | return (
181 |
182 |
191 |
192 |
193 |
194 |
195 |
hold-tips
196 |
197 |
198 |
tips
199 |
200 |
204 |
205 | );
206 | };
207 |
208 | export default App;
209 |
--------------------------------------------------------------------------------
/src/index.module.scss:
--------------------------------------------------------------------------------
1 | $base: #f43c59;
2 | $bg-color: #FAFAF9;
3 |
4 | @mixin flex-center($direction: row) {
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | @if $direction != row {
9 | flex-direction: $direction;
10 | }
11 | }
12 |
13 |
14 | /**
15 | 盒子px转rem
16 | */
17 | @function remB($px) {
18 | @return ($px/75) * 1rem;
19 | }
20 | /**
21 | 字号px转rem
22 | */
23 | @function remF($px) {
24 | @return ($px/64) * 0.853333333rem;
25 | }
26 |
27 | .container {
28 | background-color: #000;
29 | width: 100%;
30 | min-height:100%;
31 |
32 | .back {
33 | position: absolute;
34 | top: remB(40);
35 | left: remB(30);
36 | color: #fff;
37 | font-weight: bold;
38 | z-index: 100;
39 | }
40 |
41 | .shadow-layer {
42 | position: absolute;
43 | top: 0;
44 | left: 0;
45 | width: 100%;
46 | z-index: 1;
47 | overflow: hidden;
48 |
49 | .capture-rectangle {
50 | margin: remB(200) auto 0;
51 | width: remB(700);
52 | height: remB(450);
53 | // width: remB(350);
54 | // height: remB(250);
55 | border: 1px solid #fff;
56 | border-radius: remB(20);
57 | z-index: 2;
58 | box-shadow: 0 0 0 remB(1000) rgba(0, 0, 0, 0.7);
59 | }
60 |
61 | .hold-tips {
62 | background-color: rgba(0, 0, 0, 0.6);
63 | color: #e1e1e1;
64 | font-size: remF(24);
65 | display: flex;
66 | align-items: center;
67 | justify-content: center;
68 | width: remB(300);
69 | margin: remB(30) auto 0;
70 | border-radius: remB(20);
71 | }
72 | }
73 |
74 | .tips {
75 | background-color: #333;
76 | color: #fff;
77 | font-size: remF(24);
78 | display: flex;
79 | align-items: center;
80 | justify-content: center;
81 | width: remB(500);
82 | margin: remB(30) auto 0;
83 | border-radius: remB(20);
84 | }
85 |
86 | .gallery-container {
87 | position: relative;
88 | .input {
89 | position: absolute;
90 | top: remB(66);
91 | left: remB(40);
92 | width: remB(100);
93 | height: remB(100);
94 | opacity: 0;
95 | z-index: 2;
96 | }
97 | .icon {
98 | margin-top: remB(100);
99 | margin-left: remB(40);
100 | color: #fff;
101 | width: remB(100);
102 | height: remB(100);
103 | font-size: remF(40);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | ReactDOM.render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/utils/compressBase64.ts:
--------------------------------------------------------------------------------
1 | function startCompress(base64) {
2 | function compress(
3 | base64, // 源图片
4 | rate, // 缩放比例
5 | resolve,
6 | ) {
7 | //处理缩放,转格式
8 | var _img = new Image();
9 | _img.src = base64;
10 | _img.onload = function () {
11 | var _canvas = document.createElement('canvas');
12 | var w = _img.width / rate;
13 | var h = _img.height / rate;
14 | _canvas.setAttribute('width', w.toString());
15 | _canvas.setAttribute('height', h.toString());
16 | _canvas.getContext('2d')?.drawImage(_canvas, 0, 0, w, h);
17 | var base64 = _canvas.toDataURL('image/jpeg');
18 | _canvas.toBlob(function (blob) {
19 | // blob size单位为byte 1000byte = 1kb 1000kb = 1mb
20 | if (blob?.size ?? 0 > 750 * 1334) {
21 | //如果还大,继续压缩
22 | compress(base64, rate, resolve);
23 | } else {
24 | resolve(base64);
25 | }
26 | }, 'image/jpeg');
27 | };
28 | }
29 |
30 | return new Promise(resolve => {
31 | compress(base64, 1.5, resolve);
32 | });
33 | }
34 |
35 | export { startCompress };
36 |
--------------------------------------------------------------------------------
/src/utils/getUserMediaStream.ts:
--------------------------------------------------------------------------------
1 | //访问用户媒体设备的兼容方法
2 | function getUserMedia(constrains) {
3 | const navigator: any = window.navigator;
4 | if (navigator.mediaDevices?.getUserMedia) {
5 | //最新标准API
6 | return navigator.mediaDevices.getUserMedia(constrains);
7 | } else if (navigator.webkitGetUserMedia) {
8 | //webkit内核浏览器
9 | return navigator.webkitGetUserMedia(constrains);
10 | } else if (navigator.mozGetUserMedia) {
11 | //Firefox浏览器
12 | return navigator.mozGetUserMedia(constrains);
13 | } else if (navigator.getUserMedia) {
14 | //旧版API
15 | return navigator.getUserMedia(constrains);
16 | }
17 | }
18 |
19 | //成功的回调函数
20 | function success(stream, video) {
21 | return new Promise((resolve, reject) => {
22 | video.srcObject = stream;
23 |
24 | //播放视频
25 | video.onloadedmetadata = function (e) {
26 | video.play();
27 | resolve();
28 | };
29 | });
30 | }
31 |
32 | function getUserMediaStream(videoNode) {
33 | //调用用户媒体设备,访问摄像头
34 | return getUserMedia({
35 | audio: false,
36 | // video: { facingMode: { exact: 'environment' } },
37 | video: true,
38 | // video: { facingMode: { exact: 'environment', width: 1280, height: 720 } },
39 | })
40 | .then(res => {
41 | return success(res, videoNode);
42 | })
43 | .catch(error => {
44 | console.log('访问用户媒体设备失败:', error.name, error.message);
45 | return Promise.reject();
46 | });
47 | }
48 |
49 | export { getUserMediaStream };
50 |
--------------------------------------------------------------------------------
/src/utils/toast.ts:
--------------------------------------------------------------------------------
1 | import { Toast } from 'antd-mobile';
2 |
3 | function __hide() {
4 | Toast.hide();
5 | }
6 |
7 | function showToast(text = '', isError = true) {
8 | __hide();
9 | Toast.info(text || (isError && '出错了'), 2, undefined, false);
10 | }
11 |
12 | function hideToast() {
13 | __hide();
14 | }
15 |
16 | function hideLoading() {
17 | __hide();
18 | }
19 |
20 | function showLoading(text = 'Loading...') {
21 | __hide();
22 | Toast.loading(text, 0, () => {});
23 | }
24 |
25 | function showSuccess(config: Record = {}) {
26 | const { text = 'Success', mask = false, duration = 2, onClose = () => {} } = config;
27 | Toast.success(text, duration, onClose, mask);
28 | }
29 |
30 | function showFail(config: Record = {}) {
31 | const { text = 'fail', mask = false, duration = 2, onClose = () => {} } = config;
32 | Toast.fail(text, duration, onClose, mask);
33 | }
34 |
35 | export { showToast, hideToast, hideLoading, showLoading, showSuccess, showFail };
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "noImplicitAny": false,
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------