├── .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 |
201 | 202 | 203 |
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 | --------------------------------------------------------------------------------