├── .gitignore ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_zh-CN.md ├── config ├── env.js ├── getHttpsConfig.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── test.loader.js ├── webpack.config.js └── webpackDevServer.config.js ├── docs ├── README.md ├── README_zh-CN.md ├── annotation │ ├── README.md │ ├── common.md │ ├── format │ │ └── yolo │ │ │ ├── yolo.md │ │ │ └── yolo格式.jpg │ ├── lineTool.md │ ├── pointTool.md │ ├── polygonTool.md │ ├── rectTool.md │ ├── tagTool.md │ └── textTool.md ├── assets │ ├── annotation-detection-segmentation.gif │ ├── annotation-line-point-text.gif │ ├── annotation.png │ ├── common-config.png │ ├── config-attribute.png │ ├── config-textAttribute.png │ ├── create-project.png │ ├── export-rect-format.png │ ├── hotkey.png │ ├── main.png │ ├── project-face-detection.png │ ├── project-folder.png │ ├── project-step.png │ ├── projectPlatform.png │ └── rectTool-common-config.png └── develop │ └── README.md ├── electron ├── file.ts ├── ipcEvent.ts ├── main.ts └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── hm.js ├── icon.icns ├── icon.ico ├── icon.png ├── icon │ ├── icon_512x512@2x.icns │ ├── label-app-icon-256.ico │ ├── label-app-icon-256.png │ └── label-app-icon-512.png ├── icon256.png ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── sensebee.ico ├── scripts ├── build.js ├── dataTransfer.py ├── hm │ ├── hm-origin.js │ └── update.js ├── start.js ├── static.js ├── test.js └── util │ ├── content.js │ ├── dir.js │ ├── file.js │ ├── mimes.js │ └── walk.js ├── src ├── Annotation │ └── index.tsx ├── App.css ├── App.test.tsx ├── App.tsx ├── ProjectPlatform │ ├── CreateProjectModal │ │ ├── MultiStep │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── SelectFolder.tsx │ │ ├── SelectTool │ │ │ └── index.tsx │ │ ├── TaskStep │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── ToolConfig │ │ │ ├── DefaultConfig.tsx │ │ │ ├── PointConfig │ │ │ │ └── index.tsx │ │ │ ├── PolygonToolConfig │ │ │ │ ├── GraphicsPointLimitInput │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── RectConfig │ │ │ │ ├── AttributeConfig │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── TagConfig │ │ │ │ ├── TagInput │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── TextConfigurable │ │ │ │ └── index.tsx │ │ │ ├── TextToolConfig │ │ │ │ ├── TextConfig │ │ │ │ │ └── index.tsx │ │ │ │ ├── TextList │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── ToolCommonFiled │ │ │ │ └── index.tsx │ │ │ ├── index.module.scss │ │ │ └── publicConfig.ts │ │ ├── Tools │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ ├── ProjectList │ │ ├── ExportData │ │ │ └── index.tsx │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── assets │ ├── color.json │ ├── editStep │ │ ├── auditors.svg │ │ ├── datePicker.svg │ │ ├── editStep.svg │ │ ├── icon_lessPoint.svg │ │ ├── icon_morePoint.svg │ │ ├── segment_robot.svg │ │ └── textToolIcons │ │ │ ├── icon_must.svg │ │ │ ├── icon_must_a.svg │ │ │ ├── icon_textSet.svg │ │ │ └── icon_textSet_a.svg │ ├── icon_doubleClick.png │ ├── inside_nodata.svg │ ├── loading.gif │ ├── logo.svg │ └── toolIcon │ │ ├── icon_line.svg │ │ ├── icon_point.svg │ │ ├── icon_polygon.svg │ │ ├── icon_rect.svg │ │ ├── icon_step.svg │ │ ├── icon_tag.svg │ │ └── icon_text.svg ├── components │ └── DataLoading │ │ └── index.tsx ├── constant │ ├── event.ts │ ├── file.ts │ ├── store.ts │ └── style.ts ├── i18n.ts ├── index.css ├── index.tsx ├── logo.svg ├── mock │ ├── images │ │ ├── 100000031654.jpg │ │ ├── 100000031706.jpg │ │ ├── 100000031783.jpeg │ │ ├── 100000031830.jpeg │ │ ├── 100000031901.jpg │ │ ├── 100000031909.jpg │ │ ├── 100000031937.jpg │ │ ├── 100000032770.jpg │ │ ├── 100000032774.jpeg │ │ ├── 100000032780.png │ │ ├── 100000032792.jpeg │ │ ├── 100000032799.jpg │ │ ├── 100000032804.jpg │ │ ├── 100000032810.jpg │ │ ├── 100000032814.jpg │ │ ├── 100000032816.jpg │ │ ├── 100000032820.jpg │ │ ├── 100000032826.jpeg │ │ ├── 500001884.jpg │ │ ├── 500001885.jpg │ │ └── 500001886.jpg │ ├── index.ts │ └── taskConfig.ts ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts ├── store │ ├── index.tsx │ └── locale.tsx └── utils │ ├── DataTransfer.ts │ └── tool │ ├── common.ts │ ├── editTool.ts │ ├── task.ts │ └── uuid.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .idea 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | package-lock.json 26 | /yarn.lock 27 | yarn.lock 28 | /electron/dist 29 | /dist 30 | /out -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | electron_mirror=https://npm.taobao.org/mirrors/electron/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "jsxSingleQuote": true, 5 | "trailingComma": "all", 6 | "semicolons": true, 7 | "tabWidth": 2 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.2.0](https://github.com/open-mmlab/labelbee-client/compare/v0.1.2...v0.2.0) (2022-05-24) 6 | 7 | 8 | ### Features 9 | 10 | * Add Path in annotation ([85480f2](https://github.com/open-mmlab/labelbee-client/commit/85480f2397f5c4bb1ea2258d8c3e18c00254d536)) 11 | * Change the value of "file_name" from path to file name in the coco result ([c15b6a5](https://github.com/open-mmlab/labelbee-client/commit/c15b6a5591c80bb89ac7cb640311b91c203ef1bc)) 12 | * Check whether there are pictures in the path ([23956e5](https://github.com/open-mmlab/labelbee-client/commit/23956e5b12dc4cd75d49bdfa0472465271b89080)) 13 | * Path allows and name editing ([731b5ca](https://github.com/open-mmlab/labelbee-client/commit/731b5caf7d7110ce3e6b30655e87c61e263ddd0a)) 14 | * Save page and step to local ([25219da](https://github.com/open-mmlab/labelbee-client/commit/25219daff2dfd250f96e564a11cc83612ad314d0)) 15 | * Supplementary submenu ([531ed42](https://github.com/open-mmlab/labelbee-client/commit/531ed420a76e0e64f3b1fe66c7f5207e694181be)) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * The params named projectList is getted error. ([63de7d1](https://github.com/open-mmlab/labelbee-client/commit/63de7d1f40f547a1262e0d7e7d398ba5789a5fd9)) 21 | * TrainIDs is not the same with the same config in different project ([369ffc7](https://github.com/open-mmlab/labelbee-client/commit/369ffc7809c652585ab086d8101767400069fd0b)) 22 | * Update project error tips ([ffc4793](https://github.com/open-mmlab/labelbee-client/commit/ffc47936a6c9cea9c31423a5f7decf3bf19563c5)) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 |

LabelBee-Client

5 |

6 | Releases 7 | · 8 | Getting Started 9 | · 10 | 简体中文 11 |

12 |
13 | 14 | 15 |
16 | 17 | ## Features 18 | 19 | - 📦 Out of the Box, built-in six annotation tools, simple configurations 20 | - 🪵 Flexible combinations, multiple tools can directly rely on each other 21 | - 💻 Multiple operating systems: Mac / Linux / Windows 22 | - 🏁 Support Data Formats 23 | 24 | | | General Data | COCO | Semantic Segmentation Mask | 25 | | ------ | ------------ | ---- | -------------------------- | 26 | | Export | ✔️ | ✔️ | ✔️ | 27 | | Import | ✔️ | ✖ | ✖ | 28 | 29 | ## Download 30 | 31 | [Mac & Windows & Linux](https://github.com/open-mmlab/labelbee-client/releases) 32 | 33 | ## Support Scenes 34 | 35 | - Detection: Detection scenes for vehicles, license plates, pedestrians, faces, industrial parts, etc. 36 | - Classification: Detection of object classification, target characteristics, right and wrong judgments, and other classification scenarios 37 | - Semantic segmentation: Human body segmentation, panoramic segmentation, drivable area segmentation, vehicle segmentation, etc. 38 | - Text transcription: Text detection and recognition of license plates, invoices, insurance policies, signs, etc. 39 | - Contour detection: positioning line scenes such as human contour lines, lane lines, etc. 40 | - Key point detection: positioning scenes such as human face key points, vehicle key points, road edge key points, etc. 41 | 42 |
43 | 44 | 45 | Detection / Segmentation 46 | 47 | 48 | 49 | Line / Point / Text 50 |

51 |
52 | 53 | ## Usage 54 | 55 | - [Getting Started](./docs/README.md) 56 | 57 | ## Annotation Format 58 | 59 | ```json 60 | { 61 | "width": 4368, 62 | "height": 2912, 63 | "valid": true, 64 | "rotate": 0, 65 | "step_1": { 66 | "toolName": "rectTool", 67 | "result": [ 68 | { 69 | "x": 530.7826086956522, 70 | "y": 1149.217391304348, 71 | "width": 1314.7826086956522, 72 | "height": 1655.6521739130435, 73 | "attribute": "", 74 | "valid": true, 75 | "id": "Rp1x6bZs", 76 | "sourceID": "", 77 | "textAttribute": "", 78 | "order": 1 79 | } 80 | ] 81 | } 82 | } 83 | ``` 84 | For details, click to view [LabelBee Annotation Format](./docs/annotation/README.md) 85 | 86 | ## Important 87 | js can't export 8bit unsign Int image, so a python script is provided to solve this problem 88 | - [Script] (https://github.com/open-mmlab/labelbee-client/blob/main/scripts/dataTransfer.py) 89 | 90 | ## Links 91 | 92 | - [LabelBee](https://github.com/open-mmlab/labelbee)(Powered by LabelBee) 93 | 94 | ## LICENSE 95 | 96 | This project is released under the [Apache 2.0 license](./LICENSE). 97 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

5 |

LabelBee-Client

6 |

7 | Releases 8 | · 9 | Usage 10 | · 11 | English 12 |

13 |
14 | 15 | 16 |
17 | 18 | ## 特性 19 | 20 | - 📦 开箱即用,内置 6 款通用的标注工具,仅需简单配置即可标注 21 | - 🪵 任意搭配,多种工具可直接互相依赖使用$$ 22 | - 🏁 支持通用数据,COCO 格式导出,语义分割 Mask 导出 23 | - 💻 全平台支持:Mac / Linux / Windows 24 | 25 | ## 客户端下载 26 | 27 | [Mac & Windows & Linux](https://github.com/open-mmlab/labelbee-client/releases) 28 | 29 | ## 支持场景 30 | 31 | 32 | - 目标检测:车辆、车牌、行人、人脸、工业部件等检测场景 33 | - 目标分类:检测对象分类、目标特征、是非判断等分类场景 34 | - 语义分割:人体分割、全景分割、可行驶区域分割、车辆分割等 35 | - 文本转写:车牌、发票、保单、招牌等文本检测和识别 36 | - 轮廓线检测:人体轮廓线、车道线等定位线场景 37 | - 关键点检测:人脸人体关键点、车辆关键点、路沿关键点等定位场景 38 | 39 | 40 |
41 | 42 | 43 | Detection / Segmentation 44 | 45 | 46 | 47 | Line / Point / Text 48 |

49 |
50 | 51 | ## 快速上手 52 | 53 | [基础流程创建](./docs/README.md) 54 | 55 | ## 标注格式说明 56 | 57 | ```json 58 | { 59 | "width": 4368, 60 | "height": 2912, 61 | "valid": true, 62 | "rotate": 0, 63 | "step_1": { 64 | "toolName": "rectTool", 65 | "result": [ 66 | { 67 | "x": 530.7826086956522, 68 | "y": 1149.217391304348, 69 | "width": 1314.7826086956522, 70 | "height": 1655.6521739130435, 71 | "attribute": "", 72 | "valid": true, 73 | "id": "Rp1x6bZs", 74 | "sourceID": "", 75 | "textAttribute": "", 76 | "order": 1 77 | } 78 | ] 79 | } 80 | ``` 81 | 详细细节请查看 [LabelBee 标注格式](./docs/annotation/README.md) 82 | 83 | ## 友情链接 84 | 85 | - [LabelBee](https://github.com/open-mmlab/labelbee)(本工具都是通过 LabelBee 进行开发) 86 | 87 | ## 开源许可证 88 | 89 | 该项目使用 [Apache 2.0 license](./LICENSE). 90 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | const dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | // We support configuring the sockjs pathname during development. 81 | // These settings let a developer run multiple simultaneous projects. 82 | // They are used as the connection `hostname`, `pathname` and `port` 83 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` 84 | // and `sockPort` options in webpack-dev-server. 85 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, 86 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, 87 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, 88 | } 89 | ); 90 | // Stringify all values so we can feed into webpack DefinePlugin 91 | const stringified = { 92 | 'process.env': Object.keys(raw).reduce((env, key) => { 93 | env[key] = JSON.stringify(raw[key]); 94 | return env; 95 | }, {}), 96 | }; 97 | 98 | return { raw, stringified }; 99 | } 100 | 101 | module.exports = getClientEnvironment; 102 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get additional module paths based on the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | // We need to explicitly check for null and undefined (and not a falsy value) because 18 | // TypeScript treats an empty string as `.`. 19 | if (baseUrl == null) { 20 | // If there's no baseUrl set we respect NODE_PATH 21 | // Note that NODE_PATH is deprecated and will be removed 22 | // in the next major release of create-react-app. 23 | 24 | const nodePath = process.env.NODE_PATH || ''; 25 | return nodePath.split(path.delimiter).filter(Boolean); 26 | } 27 | 28 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 29 | 30 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 31 | // the default behavior. 32 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 33 | return null; 34 | } 35 | 36 | // Allow the user set the `baseUrl` to `appSrc`. 37 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 38 | return [paths.appSrc]; 39 | } 40 | 41 | // If the path is equal to the root directory we ignore it here. 42 | // We don't want to allow importing from the root directly as source files are 43 | // not transpiled outside of `src`. We do allow importing them with the 44 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 45 | // an alias. 46 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 47 | return null; 48 | } 49 | 50 | // Otherwise, throw an error. 51 | throw new Error( 52 | chalk.red.bold( 53 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 54 | ' Create React App does not support other values at this time.' 55 | ) 56 | ); 57 | } 58 | 59 | /** 60 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 61 | * 62 | * @param {*} options 63 | */ 64 | function getWebpackAliases(options = {}) { 65 | const baseUrl = options.baseUrl; 66 | 67 | if (!baseUrl) { 68 | return {}; 69 | } 70 | 71 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 72 | 73 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 74 | return { 75 | src: paths.appSrc, 76 | }; 77 | } 78 | } 79 | 80 | /** 81 | * Get jest aliases based on the baseUrl of a compilerOptions object. 82 | * 83 | * @param {*} options 84 | */ 85 | function getJestAliases(options = {}) { 86 | const baseUrl = options.baseUrl; 87 | 88 | if (!baseUrl) { 89 | return {}; 90 | } 91 | 92 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 93 | 94 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 95 | return { 96 | '^src/(.*)$': '/src/$1', 97 | }; 98 | } 99 | } 100 | 101 | function getModules() { 102 | // Check if TypeScript is setup 103 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 104 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 105 | 106 | if (hasTsConfig && hasJsConfig) { 107 | throw new Error( 108 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 109 | ); 110 | } 111 | 112 | let config; 113 | 114 | // If there's a tsconfig.json we assume it's a 115 | // TypeScript project and set up the config 116 | // based on tsconfig.json 117 | if (hasTsConfig) { 118 | const ts = require(resolve.sync('typescript', { 119 | basedir: paths.appNodeModules, 120 | })); 121 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 122 | // Otherwise we'll check if there is jsconfig.json 123 | // for non TS projects. 124 | } else if (hasJsConfig) { 125 | config = require(paths.appJsConfig); 126 | } 127 | 128 | config = config || {}; 129 | const options = config.compilerOptions || {}; 130 | 131 | const additionalModulePaths = getAdditionalModulePaths(options); 132 | 133 | return { 134 | additionalModulePaths: additionalModulePaths, 135 | webpackAliases: getWebpackAliases(options), 136 | jestAliases: getJestAliases(options), 137 | hasTsConfig, 138 | }; 139 | } 140 | 141 | module.exports = getModules(); 142 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right 33 | LabelBee-Client 34 | 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-mmlab/labelbee-client/438ebd262c546b70f97062744d9076c7ba9d519c/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-mmlab/labelbee-client/438ebd262c546b70f97062744d9076c7ba9d519c/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/sensebee.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-mmlab/labelbee-client/438ebd262c546b70f97062744d9076c7ba9d519c/public/sensebee.ico -------------------------------------------------------------------------------- /scripts/dataTransfer.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from pathlib import Path, PurePath 3 | 4 | folder_path = './img/' # The folder of your exported data 5 | 6 | p = Path(folder_path) 7 | files = [x for x in p.iterdir() if PurePath(x).match('*_labelTrainIds.png')] 8 | 9 | for file in files: 10 | p_path = file 11 | p = Image.open(p_path).convert('L') # Transfer to L mode (8-bit pixels, black and white) 12 | p.save(file.name + '_labelTrainIds.png') -------------------------------------------------------------------------------- /scripts/hm/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 更改 hm 内的版本信息,用于 Electron 内版本跟 package.json 的同步 3 | */ 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const package = require('../../package.json'); 8 | 9 | try { 10 | const hmString = fs.readFileSync(path.resolve(__dirname, './hm-origin.js'), 'utf-8'); 11 | let href = "window.location.href.split('/').slice(-1)[0]"; 12 | let host = `"labelbee-Client-${package.version.replace(/\./g, '_')}.sensebee.xyz"`; 13 | 14 | const hmFormat = hmString 15 | .replace(/document.location.hostname/g, `${host}`) // file和chrome-extension等协议不存在document.location.hostname,直接使用新增网站时的网站域名来替代 16 | .replace(/window.location.host|document.location.host/g, `${host}`) // file和chrome-extension等协议不存在document.location.host,直接使用新增网站时的网站域名来替代 17 | .replace( 18 | /window.location.href|document.location.href/g, 19 | `"https://${host.replace(/"/gi, '')}/" + ` + href, 20 | ) // file和chrome-extension等协议的访问路径过长,直接使用url中最后的一个字符"/"后面的路径替换 21 | .replace('/https?:/.test(document.location.protocol)', `true`); // 不校验协议 22 | 23 | fs.writeFileSync('./public/hm.js', hmFormat); 24 | console.log(`HM 打点版本更改至 ${package.version}`); 25 | } catch (err) { 26 | console.error(err); 27 | } 28 | -------------------------------------------------------------------------------- /scripts/static.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const path = require('path') 3 | const content = require('./util/content') 4 | const mimes = require('./util/mimes') 5 | 6 | const app = new Koa() 7 | 8 | // 静态资源目录对于相对入口文件index.js的路径 9 | const staticPath = '../src/mock/images' 10 | 11 | // 解析资源类型 12 | function parseMime( url ) { 13 | let extName = path.extname( url ) 14 | extName = extName ? extName.slice(1) : 'unknown' 15 | return mimes[ extName ] 16 | } 17 | 18 | app.use( async ( ctx ) => { 19 | // 静态资源目录在本地的绝对路径 20 | let fullStaticPath = path.join(__dirname, staticPath) 21 | 22 | // 获取静态资源内容,有可能是文件内容,目录,或404 23 | let _content = await content( ctx, fullStaticPath ) 24 | 25 | // 解析请求内容的类型 26 | let _mime = parseMime( ctx.url ) 27 | 28 | // 如果有对应的文件类型,就配置上下文的类型 29 | if ( _mime ) { 30 | ctx.type = _mime 31 | } 32 | 33 | // 输出静态资源内容 34 | if ( _mime && _mime.indexOf('image/') >= 0 ) { 35 | // 如果是图片,则用node原生res,输出二进制数据 36 | ctx.res.writeHead(200) 37 | ctx.res.write(_content, 'binary') 38 | ctx.res.end() 39 | } else { 40 | // 其他则输出文本 41 | ctx.body = _content 42 | } 43 | 44 | 45 | }) 46 | 47 | app.listen(7001, () => { 48 | console.log('[demo] static-server is starting at port 7001') 49 | }) 50 | 51 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /scripts/util/content.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | // 封装读取目录内容方法 5 | const dir = require('./dir') 6 | 7 | // 封装读取文件内容方法 8 | const file = require('./file') 9 | 10 | 11 | /** 12 | * 获取静态资源内容 13 | * @param {object} ctx koa上下文 14 | * @param {string} 静态资源目录在本地的绝对路径 15 | * @return {string} 请求获取到的本地内容 16 | */ 17 | async function content( ctx, fullStaticPath ) { 18 | 19 | // 封装请求资源的完绝对径 20 | let reqPath = path.join(fullStaticPath, ctx.url) 21 | 22 | // 判断请求路径是否为存在目录或者文件 23 | let exist = fs.existsSync( reqPath ) 24 | 25 | // 返回请求内容, 默认为空 26 | let content = '' 27 | 28 | if( !exist ) { 29 | //如果请求路径不存在,返回404 30 | content = '404 Not Found! o(╯□╰)o!' 31 | } else { 32 | //判断访问地址是文件夹还是文件 33 | let stat = fs.statSync( reqPath ) 34 | 35 | if( stat.isDirectory() ) { 36 | //如果为目录,则渲读取目录内容 37 | content = dir( ctx.url, reqPath ) 38 | 39 | } else { 40 | // 如果请求为文件,则读取文件内容 41 | content = file( reqPath ) 42 | } 43 | } 44 | 45 | return content 46 | } 47 | 48 | module.exports = content 49 | -------------------------------------------------------------------------------- /scripts/util/dir.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | // 遍历读取目录内容方法 6 | const walk = require('./walk') 7 | 8 | /** 9 | * 封装目录内容 10 | * @param {string} url 当前请求的上下文中的url,即ctx.url 11 | * @param {string} reqPath 请求静态资源的完整本地路径 12 | * @return {string} 返回目录内容,封装成HTML 13 | */ 14 | function dir ( url, reqPath ) { 15 | 16 | // 遍历读取当前目录下的文件、子目录 17 | let contentList = walk( reqPath ) 18 | 19 | let html = `` 24 | 25 | return html 26 | } 27 | 28 | module.exports = dir -------------------------------------------------------------------------------- /scripts/util/file.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | /** 4 | * 读取文件方法 5 | * @param {string} 文件本地的绝对路径 6 | * @return {string|binary} 7 | */ 8 | function file ( filePath ) { 9 | 10 | let content = fs.readFileSync(filePath, 'binary' ) 11 | return content 12 | } 13 | 14 | module.exports = file 15 | -------------------------------------------------------------------------------- /scripts/util/mimes.js: -------------------------------------------------------------------------------- 1 | let mimes = { 2 | 'css': 'text/css', 3 | 'less': 'text/css', 4 | 'gif': 'image/gif', 5 | 'html': 'text/html', 6 | 'ico': 'image/x-icon', 7 | 'jpeg': 'image/jpeg', 8 | 'jpg': 'image/jpeg', 9 | 'js': 'text/javascript', 10 | 'json': 'application/json', 11 | 'pdf': 'application/pdf', 12 | 'png': 'image/png', 13 | 'svg': 'image/svg+xml', 14 | 'swf': 'application/x-shockwave-flash', 15 | 'tiff': 'image/tiff', 16 | 'txt': 'text/plain', 17 | 'wav': 'audio/x-wav', 18 | 'wma': 'audio/x-ms-wma', 19 | 'wmv': 'video/x-ms-wmv', 20 | 'xml': 'text/xml' 21 | } 22 | 23 | module.exports = mimes 24 | -------------------------------------------------------------------------------- /scripts/util/walk.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const mimes = require('./mimes') 3 | 4 | /** 5 | * 遍历读取目录内容(子目录,文件名) 6 | * @param {string} reqPath 请求资源的绝对路径 7 | * @return {array} 目录内容列表 8 | */ 9 | function walk( reqPath ){ 10 | 11 | let files = fs.readdirSync( reqPath ); 12 | 13 | let dirList = [], fileList = []; 14 | for( let i=0, len=files.length; i 1 ) ? itemArr[ itemArr.length - 1 ] : "undefined"; 18 | 19 | if( typeof mimes[ itemMime ] === "undefined" ) { 20 | dirList.push( files[i] ); 21 | } else { 22 | fileList.push( files[i] ); 23 | } 24 | } 25 | 26 | 27 | let result = dirList.concat( fileList ); 28 | 29 | return result; 30 | }; 31 | 32 | module.exports = walk; 33 | -------------------------------------------------------------------------------- /src/Annotation/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Annotation Component 3 | * @Author: Laoluo luozefeng@sensetime.com 4 | * @Date: 2022-05-23 19:15:58 5 | * @LastEditors: Laoluo luozefeng@sensetime.com 6 | * @LastEditTime: 2022-05-24 10:43:53 7 | */ 8 | 9 | import React, { useContext, useRef } from 'react'; 10 | import AnnotationOperation from '@labelbee/lb-components'; 11 | import '@labelbee/lb-components/dist/index.css'; 12 | import { EIpcEvent } from '../constant/event'; 13 | import { AnnotationContext } from '../store'; 14 | import i18n from '@/i18n'; 15 | 16 | const electron = window.require && window.require('electron'); 17 | const ipcRenderer = electron?.ipcRenderer; 18 | 19 | const Annotation = (props: any) => { 20 | const { 21 | dispatch, 22 | state: { currentProjectInfo, projectList, fileList }, 23 | } = useContext(AnnotationContext); 24 | const cacheProjectList = useRef(projectList); // TODO: I will rewrite by custom hook later 25 | 26 | const onSubmit = (data: any[], submitType: any, i: number) => { 27 | // 翻页时触发当前页面数据的输出 28 | if (ipcRenderer) { 29 | // 翻页时触发数据保存 30 | ipcRenderer.send( 31 | EIpcEvent.SaveResult, 32 | data, 33 | currentProjectInfo?.path, 34 | currentProjectInfo?.resultPath, 35 | ); 36 | } 37 | }; 38 | 39 | const goBack = (imgList: any[]) => { 40 | dispatch({ 41 | type: 'UPDATE_CURRENT_PROJECTINFO', 42 | payload: { 43 | projectInfo: undefined, 44 | }, 45 | }); 46 | 47 | // 清空默认操作 48 | dispatch({ 49 | type: 'UPDATE_FILE_LIST', 50 | payload: { 51 | fileList: [], 52 | }, 53 | }); 54 | }; 55 | 56 | const updateProjectInfo = (info: { imgIndex?: number; step?: number }) => { 57 | // Notice: The value of context(e.g. projectList) is not updated 58 | const newProjectList = cacheProjectList.current.map((item) => { 59 | return item.id === currentProjectInfo?.id ? { ...item, ...info } : item; 60 | }); 61 | cacheProjectList.current = newProjectList; // need to update 62 | dispatch({ 63 | type: 'UPDATE_PROJECT_LIST', 64 | payload: { projectList: newProjectList }, 65 | }); 66 | }; 67 | 68 | const onPageChange = (imgIndex: number) => { 69 | // 保存当前页数到本地 70 | updateProjectInfo({ imgIndex }); 71 | }; 72 | 73 | const onStepChange = (step: number) => { 74 | // 保存当前步骤到本地 75 | updateProjectInfo({ step }); 76 | }; 77 | 78 | const loadFileList = (page: number, size: number) => { 79 | return new Promise((resolve) => { 80 | const currentList = fileList.slice(page * size, (page + 1) * size); 81 | ipcRenderer.send( 82 | EIpcEvent.GetFileListResult, 83 | currentList, 84 | currentProjectInfo?.path, 85 | currentProjectInfo?.resultPath, 86 | ); 87 | ipcRenderer.once(EIpcEvent.GetFileListResultReply, (event: any, newFileList: any[]) => { 88 | resolve({ 89 | fileList: newFileList.map((file: any) => ({ ...file, url: 'file:///' + file.url })), 90 | total: fileList.length, 91 | }); 92 | }); 93 | }); 94 | }; 95 | 96 | return ( 97 |
98 | 111 |
112 | ); 113 | }; 114 | export default Annotation; 115 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | color: #333; 3 | } 4 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from 'react'; 2 | import './App.css'; 3 | import 'antd/dist/antd.less'; 4 | import Annotation from './Annotation'; 5 | import ProjectPlatform from './ProjectPlatform'; 6 | import { AnnotationContext } from './store'; 7 | import { EStore } from './constant/store'; 8 | import { useLocale } from './store/locale'; 9 | import { ConfigProvider } from 'antd'; 10 | import i18n from './i18n'; 11 | import zhCN from 'antd/lib/locale/zh_CN'; 12 | import enUS from 'antd/lib/locale/en_US'; 13 | 14 | const App = () => { 15 | const { 16 | state: { fileList, currentProjectInfo }, 17 | dispatch, 18 | } = useContext(AnnotationContext); 19 | const locale = useLocale(); 20 | 21 | useEffect(() => { 22 | i18n.on('languageChanged', function (lang: string) { 23 | // 更改内部 antd 的国际化 24 | switch (lang) { 25 | case 'cn': 26 | locale.dispatch({ 27 | type: 'UPDATE_LOCALE', 28 | payload: { 29 | locale: zhCN, 30 | }, 31 | }); 32 | break; 33 | 34 | case 'en': 35 | locale.dispatch({ 36 | type: 'UPDATE_LOCALE', 37 | payload: { 38 | locale: enUS, 39 | }, 40 | }); 41 | break; 42 | } 43 | }); 44 | }, []); 45 | 46 | useEffect(() => { 47 | try { 48 | const projectListString = localStorage.getItem(EStore.LOCAL_PROJECT_LIST) || '[]'; 49 | const projectList = JSON.parse(projectListString); 50 | if (projectList.length > 0) { 51 | dispatch({ 52 | type: 'UPDATE_PROJECT_LIST', 53 | payload: { 54 | projectList, 55 | }, 56 | }); 57 | } 58 | } catch {} 59 | }, [dispatch]); 60 | 61 | if (currentProjectInfo && fileList.length > 0) { 62 | return ; 63 | } 64 | 65 | return ( 66 | 67 |
68 | 69 |
70 |
71 | ); 72 | }; 73 | 74 | export default App; 75 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/MultiStep/index.module.scss: -------------------------------------------------------------------------------- 1 | .multistep { 2 | max-height: 500px; 3 | overflow-x: hidden; 4 | overflow-y: auto; 5 | 6 | .mb_24 { 7 | margin-bottom: 24px; 8 | } 9 | 10 | .selectedName{ 11 | text-align: start; 12 | height: 30px; 13 | line-height: 30px; 14 | color: rgba(51, 51, 51, 1); 15 | margin-bottom: 13px; 16 | font-weight: 600; 17 | font-size: 14px; 18 | color: rgba(51, 51, 51, 1); 19 | } 20 | } -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/SelectFolder.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { Input } from 'antd'; 3 | import { FolderOpenOutlined } from '@ant-design/icons'; 4 | import { EIpcEvent } from '../../constant/event'; 5 | import styles from './index.module.scss'; 6 | const electron = window.require && window.require('electron'); 7 | 8 | interface IProps { 9 | value?: string; 10 | key: string; 11 | onChange?: (value: any) => void; 12 | } 13 | 14 | const SelectFolder: React.FC = ({ value, onChange, key }) => { 15 | const [path, setPath] = useState(''); 16 | const pathRef = useRef(null); 17 | 18 | const openDir = () => { 19 | const ipcRenderer = electron && electron.ipcRenderer; 20 | if (ipcRenderer) { 21 | // 防止监听多个 22 | ipcRenderer.removeAllListeners([EIpcEvent.SelectedDirectory], () => {}); 23 | ipcRenderer.send(EIpcEvent.SelectDirectory); 24 | ipcRenderer.once(EIpcEvent.SelectedDirectory, function (event: any, paths: any) { 25 | setPath(paths[0]); 26 | 27 | if (pathRef.current !== null) { 28 | pathRef.current.value = paths[0]; 29 | } 30 | onChange?.(paths[0]); 31 | }); 32 | } 33 | // 具体的图片 34 | }; 35 | return ( 36 |
37 | } 40 | value={value || path} 41 | /> 42 |
43 | ); 44 | }; 45 | 46 | export default SelectFolder; 47 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/SelectTool/index.tsx: -------------------------------------------------------------------------------- 1 | // cl 2021/9/10 18:29 2 | import React, { Fragment } from 'react'; 3 | import { Menu, Select } from 'antd'; 4 | import styles from '@/ProjectPlatform/CreateProjectModal/index.module.scss'; 5 | import { EToolName, TOOL_NAME } from '@/constant/store'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | const { Option } = Select; 9 | 10 | export const annotationTypeList = [ 11 | { 12 | name: TOOL_NAME[EToolName.Rect], 13 | key: EToolName.Rect, 14 | }, 15 | { 16 | name: TOOL_NAME[EToolName.Tag], 17 | key: EToolName.Tag, 18 | }, 19 | { 20 | name: TOOL_NAME[EToolName.Polygon], 21 | key: EToolName.Polygon, 22 | }, 23 | { 24 | name: TOOL_NAME[EToolName.Line], 25 | key: EToolName.Line, 26 | }, 27 | { 28 | name: TOOL_NAME[EToolName.Point], 29 | key: EToolName.Point, 30 | }, 31 | { 32 | name: TOOL_NAME[EToolName.Text], 33 | key: EToolName.Text, 34 | }, 35 | ]; 36 | export type Itool = typeof annotationTypeList[0]; 37 | interface IProps { 38 | disabled?: boolean; 39 | type?: 'menu' | 'select'; 40 | tools?: Itool[]; 41 | toolName?: EToolName; 42 | onChange: (text: EToolName) => void; 43 | } 44 | 45 | const SelectTool: React.FC = ({ disabled, type = 'menu', toolName, tools, onChange }) => { 46 | const { t } = useTranslation(); 47 | return ( 48 | 49 | {type === 'menu' ? ( 50 | { 55 | onChange(info.key as EToolName); 56 | }} 57 | > 58 | {annotationTypeList.map((annotationType) => ( 59 | 60 | {t(annotationType.name)} 61 | 62 | ))} 63 | 64 | ) : ( 65 | 79 | )} 80 | 81 | ); 82 | }; 83 | 84 | export default SelectTool; 85 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/TaskStep/index.scss: -------------------------------------------------------------------------------- 1 | .task-step-list { 2 | .ant-list-item-action { 3 | display: none; 4 | } 5 | .ant-list-item { 6 | &:hover { 7 | .ant-list-item-action { 8 | display: block; 9 | cursor: pointer; 10 | } 11 | } 12 | } 13 | .ant-list-item-icon { 14 | display: flex; 15 | align-items: center; 16 | 17 | .icon { 18 | width: 18px; 19 | margin: 0 4px; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/TaskStep/index.tsx: -------------------------------------------------------------------------------- 1 | // cl 2021/9/13 14:51 2 | import React, { ReactNode } from 'react'; 3 | import { IStepInfo, useAnnotation } from '@/store'; 4 | import { List, Modal } from 'antd'; 5 | import { CloseCircleOutlined, EditOutlined } from '@ant-design/icons'; 6 | import { TOOL_NAME } from '@/constant/store'; 7 | import { deleteStep } from '@/utils/tool/task'; 8 | import { icon } from '@/ProjectPlatform/ProjectList'; 9 | import './index.scss'; 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | interface IProps { 13 | footer: ReactNode; 14 | stepList: IStepInfo[]; 15 | setStepId: (id: string) => void; 16 | changeTaskVisible: () => void; 17 | setStepLIst: (stepInfos: IStepInfo[]) => void; 18 | } 19 | 20 | const TaskStep: React.FC = ({ 21 | stepList, 22 | footer, 23 | setStepId, 24 | changeTaskVisible, 25 | setStepLIst, 26 | }) => { 27 | const { 28 | state: { currentProjectInfo }, 29 | } = useAnnotation(); 30 | const { t } = useTranslation(); 31 | // 删除步骤, step为步骤 32 | const delStep = (step: number) => { 33 | const list = stepList.slice(); 34 | const { deleteStepList, newStepList } = deleteStep(step, list); 35 | Modal.confirm({ 36 | title: t('ConfirmToDeleteSteps'), 37 | content: deleteStepList.map((item: any, index: number) => ( 38 |
39 | {item} 40 |
41 | )), 42 | okButtonProps: { type: 'primary' }, 43 | okText: t('Confirm'), 44 | onOk: () => { 45 | setStepLIst(newStepList); 46 | }, 47 | cancelText: t('Cancel'), 48 | autoFocusButton: null, 49 | }); 50 | }; 51 | const edit = (id: string) => { 52 | setStepId(id); 53 | changeTaskVisible(); 54 | }; 55 | 56 | return ( 57 | {footer}} 62 | bordered 63 | dataSource={stepList} 64 | renderItem={(item) => ( 65 | { 69 | edit(item.id); 70 | }} 71 | />, 72 | !currentProjectInfo && delStep(item.step)} />, 73 | ].filter(Boolean)} 74 | > 75 |
76 | {item.step} - 77 | 78 | {TOOL_NAME[item.tool]} 79 |
80 |
81 | )} 82 | /> 83 | ); 84 | }; 85 | 86 | export default TaskStep; 87 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/DefaultConfig.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Input, Tooltip } from 'antd'; 3 | import styles from './index.module.scss'; 4 | import SelectFolder from '../SelectFolder'; 5 | import { ExclamationCircleOutlined } from '@ant-design/icons'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | const isRequired = true; 9 | const DefaultConfig = () => { 10 | const { t } = useTranslation(); 11 | 12 | return ( 13 | <> 14 | {t('ProjectName')}} 17 | rules={[{ required: isRequired, message: t('Required') }]} 18 | > 19 | 20 | 21 | {t('SelectImageFolder')}} 24 | rules={[{ required: isRequired, message: t('Required') }]} 25 | > 26 | 27 | 28 | 32 | {t('SelectResultFolder')} 33 | 34 | 35 | 36 | 37 | } 38 | rules={[{ required: isRequired, message: t('Required') }]} 39 | > 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default DefaultConfig; 47 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/PointConfig/index.tsx: -------------------------------------------------------------------------------- 1 | // import SenseInput from '@/components/customAntd/Input'; 2 | import { Switch, InputNumber, Form, FormInstance } from 'antd'; 3 | import React from 'react'; 4 | import styles from '../index.module.scss'; 5 | import { MapStateJSONTab } from '@/ProjectPlatform/CreateProjectModal/ToolConfig/RectConfig/AttributeConfig'; 6 | import TextConfigurable from '../TextConfigurable'; 7 | import ToolCommonFiled from '../ToolCommonFiled'; 8 | import { ETextType, EToolName } from '@/constant/store'; 9 | import { ToolConfigIProps } from '../../Tools'; 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | const isAllReadOnly = false; 13 | 14 | const PointConfig = (props: ToolConfigIProps) => { 15 | const { t } = useTranslation(); 16 | 17 | return ( 18 | 19 | {t('UpperLimitPoints')}} 23 | > 24 | 25 | 26 | 27 | {t('TextAnnotation')}} 29 | name='textConfigurableContext' 30 | initialValue={{ 31 | textConfigurable: false, 32 | textCheckType: ETextType.AnyString, 33 | customFormat: '', 34 | }} 35 | > 36 | 37 | 38 | {t('AttributeAnnotation')}} 41 | name='attributeConfigurable' 42 | initialValue={false} 43 | > 44 | 45 | 46 | 47 | 48 | {() => { 49 | return ( 50 | props.form?.getFieldValue('attributeConfigurable') && ( 51 | 61 | 62 | 63 | ) 64 | ); 65 | }} 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default PointConfig; 72 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/PolygonToolConfig/GraphicsPointLimitInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { checkNumber } from '@/utils/tool/common'; 2 | import { message, Row, Col, Input as SenseInput } from 'antd'; 3 | import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; 4 | import React, { useState } from 'react'; 5 | import { useTranslation } from 'react-i18next'; 6 | // const iconMorePoint = require('@/assets/editStep/icon_morePoint.svg'); 7 | // const iconLessPoint = require('@/assets/editStep/icon_lessPoint.svg'); 8 | 9 | interface GraphicsValue { 10 | lowerLimitPointNum?: string; 11 | upperLimitPointNum?: string | undefined; 12 | } 13 | 14 | interface IProps { 15 | value?: GraphicsValue; 16 | minLowerLimitPointNum?: number; 17 | onChange?: (value: GraphicsValue) => void; 18 | } 19 | 20 | /** 21 | * 图形工具标注顶点数限制输入控件 22 | * @param props 23 | */ 24 | const GraphicsPointLimitInput: React.FC = ({ 25 | value = {}, 26 | minLowerLimitPointNum = 3, 27 | onChange, 28 | }) => { 29 | const [lowerLimitPointNum, setLowerLimitPointNum] = useState(''); 30 | const [upperLimitPointNum, setUpperLimitPointNum] = useState(''); 31 | const { t } = useTranslation(); 32 | 33 | const triggerChange = (changeValue: GraphicsValue) => { 34 | onChange?.({ lowerLimitPointNum, upperLimitPointNum, ...value, ...changeValue }); 35 | }; 36 | 37 | const confirmScopeChange = (e: any, pattern: 'lowerLimitPointNum' | 'upperLimitPointNum') => { 38 | let newValue = e.target.value; 39 | const isLowerLim = pattern === 'lowerLimitPointNum'; 40 | const isUpperLim = pattern === 'upperLimitPointNum'; 41 | const DEFAULT_VALUE = ''; 42 | 43 | // 数字检查 44 | if (!checkNumber(newValue)) { 45 | let showError = true; 46 | if (isLowerLim) { 47 | newValue = minLowerLimitPointNum.toString(); 48 | } 49 | 50 | if (isUpperLim) { 51 | // 上限值允许为空 52 | if (newValue === DEFAULT_VALUE) { 53 | showError = false; 54 | } 55 | 56 | newValue = DEFAULT_VALUE; 57 | } 58 | 59 | if (showError) { 60 | message.error(t('PointsZeroLimitNotify')); 61 | } 62 | } 63 | 64 | // 下限值检查 65 | if (isLowerLim && newValue < minLowerLimitPointNum) { 66 | newValue = minLowerLimitPointNum; 67 | message.error(t('PointLowLimitNotify', { num: minLowerLimitPointNum })); 68 | } 69 | 70 | // 上限值检查 71 | if ( 72 | isUpperLim && 73 | newValue && 74 | ~~newValue < ~~(value?.lowerLimitPointNum ?? lowerLimitPointNum) 75 | ) { 76 | newValue = DEFAULT_VALUE; 77 | message.error(t('LowLimitMustGreaterThanUpNotify')); 78 | } 79 | 80 | if (!(pattern in value)) { 81 | isLowerLim && setLowerLimitPointNum(newValue); 82 | isUpperLim && setUpperLimitPointNum(newValue); 83 | } 84 | triggerChange({ [pattern]: newValue }); 85 | }; 86 | 87 | return ( 88 | 89 | 90 | } 93 | value={value.lowerLimitPointNum || lowerLimitPointNum} 94 | placeholder={t('LowerLimitPoints')} 95 | onChange={(e) => triggerChange({ lowerLimitPointNum: e.target.value })} 96 | onBlur={(e) => confirmScopeChange(e, 'lowerLimitPointNum')} 97 | /> 98 | 99 | 100 | } 104 | value={value.upperLimitPointNum || upperLimitPointNum} 105 | onChange={(e) => triggerChange({ upperLimitPointNum: e.target.value })} 106 | onBlur={(e) => confirmScopeChange(e, 'upperLimitPointNum')} 107 | /> 108 | 109 | 110 | ); 111 | }; 112 | 113 | export default GraphicsPointLimitInput; 114 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/PolygonToolConfig/index.tsx: -------------------------------------------------------------------------------- 1 | // cl 2021/8/5 09:49 2 | import React, { useEffect } from 'react'; 3 | import { Form, FormInstance, Select, Switch } from 'antd'; 4 | import { ELineTypes, ELineColor, ETextType, EToolName } from '@/constant/store'; 5 | import GraphicsPointLimitInput from './GraphicsPointLimitInput'; 6 | import TextConfigurable from '../../ToolConfig/TextConfigurable'; 7 | import ToolCommonFiled from '../ToolCommonFiled'; 8 | import { MapStateJSONTab } from '@/ProjectPlatform/CreateProjectModal/ToolConfig/RectConfig/AttributeConfig'; 9 | import { ToolConfigIProps } from '../../Tools'; 10 | 11 | import styles from '../index.module.scss'; 12 | import { useTranslation } from 'react-i18next'; 13 | const { Option } = Select; 14 | 15 | const initLowerLimitPoint: any = { 16 | [EToolName.Polygon]: { num: 3, text: 'ClosedPoints' }, 17 | [EToolName.Line]: { num: 2, text: 'PointsLimit' }, 18 | }; 19 | 20 | const selectList = [ 21 | { 22 | label: 'LineType', 23 | value: ELineTypes.Line, 24 | name: 'lineType', 25 | select: [ 26 | { 27 | key: 'StraightLine', 28 | value: ELineTypes.Line, 29 | }, 30 | { 31 | key: 'CurveLine', 32 | value: ELineTypes.Curve, 33 | }, 34 | ], 35 | }, 36 | // { 37 | // label: '线条颜色', 38 | // name: 'lineColor', 39 | // value: ELineColor.SingleColor, 40 | // select: [ 41 | // { key: '单色', value: ELineColor.SingleColor }, 42 | // { key: '多色', value: ELineColor.MultiColor }, 43 | // ], 44 | // }, 45 | ]; 46 | 47 | const PolygonToolConfig: React.FC = ({ dataSourceStep, toolName, form }) => { 48 | const { t } = useTranslation(); 49 | const setEdgeAdsorption = (val: ELineTypes) => { 50 | if (val === ELineTypes.Curve) { 51 | form?.setFieldsValue({ edgeAdsorption: false }); 52 | } 53 | }; 54 | useEffect(() => { 55 | form?.setFieldsValue({ 56 | toolGraphicsPoint: { lowerLimitPointNum: initLowerLimitPoint[toolName].num }, 57 | }); 58 | }, [toolName]); 59 | return ( 60 | 61 | {selectList.map((item) => ( 62 | {t(item.label)}} 66 | key={item.label} 67 | > 68 | 75 | 76 | ))} 77 | {t(initLowerLimitPoint[toolName].text)}} 80 | > 81 | 82 | 83 | 84 | {() => { 85 | return ( 86 | {t('EdgeAdsorption')}} 88 | name='edgeAdsorption' 89 | valuePropName='checked' 90 | initialValue={false} 91 | > 92 | 93 | 94 | ); 95 | }} 96 | 97 | 98 | {t('TextAnnotation')}} 100 | name='textConfigurableContext' 101 | initialValue={{ 102 | textConfigurable: false, 103 | textCheckType: ETextType.AnyString, 104 | customFormat: '', 105 | }} 106 | > 107 | 108 | 109 | 110 | {t('AttributeAnnotation')}} 113 | name='attributeConfigurable' 114 | initialValue={false} 115 | > 116 | 117 | 118 | 119 | 120 | {() => { 121 | return ( 122 | form?.getFieldValue('attributeConfigurable') && ( 123 | 133 | 134 | 135 | ) 136 | ); 137 | }} 138 | 139 | 140 | ); 141 | }; 142 | 143 | export default PolygonToolConfig; 144 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/RectConfig/AttributeConfig/index.scss: -------------------------------------------------------------------------------- 1 | $input-border: 1px solid #ccc; 2 | $input-border_activated: 1px solid #6371ff; 3 | 4 | %clearInput { 5 | border: 0; 6 | border-radius: 0; 7 | border-bottom: $input-border; 8 | } 9 | .sensebee-input-wrap, { 10 | .ant-input,.ant-input-group-addon { 11 | @extend %clearInput; 12 | 13 | &:focus, 14 | &:hover { 15 | border-bottom: $input-border_activated; 16 | } 17 | } 18 | .ant-input-group-addon { 19 | background-color: transparent; 20 | } 21 | } -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/RectConfig/AttributeConfig/index.tsx: -------------------------------------------------------------------------------- 1 | import { COLORS_ARRAY } from '@/constant/style'; 2 | import { addInputList, changeInputList, deleteInputList } from '@/utils/tool/editTool'; 3 | import { CloseCircleFilled } from '@ant-design/icons'; 4 | import { Button, Tabs, Input as SenseInput, message as SenseMessage } from 'antd'; 5 | import React, { useEffect, useRef, useState } from 'react'; 6 | import MonacoEditor from 'react-monaco-editor'; 7 | import styles from '../../index.module.scss'; 8 | import './index.scss'; 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | interface IJsonTabProps { 12 | value?: any[]; 13 | readonly: boolean; 14 | onChange?: (value: any[]) => void; 15 | /** 是否为属性列表 */ 16 | isAttributeList: boolean; 17 | } 18 | const EDIT_SUBSELECTED = false; 19 | const { TabPane } = Tabs; 20 | 21 | export const ColorTag = ({ color, style }: any) => { 22 | return ( 23 | 33 | ); 34 | }; 35 | 36 | const JSONTab = (props: IJsonTabProps) => { 37 | const [jsonCode, setJsonCode] = useState(''); 38 | const attributeListDom = useRef(null); 39 | const { t } = useTranslation(); 40 | const { 41 | value = [ 42 | { 43 | key: '类别1', 44 | value: '类别1', 45 | }, 46 | ], 47 | readonly, 48 | onChange, 49 | isAttributeList, 50 | } = props; 51 | 52 | useEffect(() => { 53 | setJsonCode(JSON.stringify(value, null, 2)); 54 | const inputListLastDom: any = attributeListDom?.current; 55 | if (inputListLastDom) { 56 | inputListLastDom?.scrollIntoView({ 57 | behavior: 'smooth', 58 | block: 'end', 59 | }); 60 | } 61 | }, [value]); 62 | 63 | const addInputInfo = () => { 64 | onChange?.(addInputList(value, EDIT_SUBSELECTED)); 65 | }; 66 | 67 | const changeTagType = (v: any) => {}; 68 | 69 | // 更改标签工具里面的对应值 70 | const changeInputInfo = (e: any, target: 'key' | 'value', index: number) => { 71 | onChange?.(changeInputList(e, target, value, index)); 72 | }; 73 | 74 | // 删除对应输入 75 | const deleteInputInfo = (i: number) => { 76 | onChange?.(deleteInputList(value, i)); 77 | }; 78 | 79 | const editorChange = (v: string) => { 80 | try { 81 | const newInputList = JSON.parse(v); 82 | if (value?.constructor !== newInputList.constructor) { 83 | SenseMessage.error(t('ConfigurationFormatErrorNotify')); 84 | return; 85 | } 86 | onChange?.(newInputList); 87 | } catch (e) { 88 | // message.error('JSON 格式错误'); 89 | } 90 | }; 91 | 92 | // 限定质检 93 | const options = { 94 | selectOnLineNumbers: true, 95 | renderSideBySide: false, 96 | }; 97 | 98 | return ( 99 | 100 | 101 | {value?.map((info, i) => ( 102 |
103 |
104 | {i + 1} 105 | changeInputInfo(e, 'key', i)} 110 | disabled={readonly} 111 | addonBefore={isAttributeList && } 112 | /> 113 | changeInputInfo(e, 'value', i)} 118 | disabled={readonly} 119 | /> 120 | 121 | {i > 0 && !readonly && ( 122 | deleteInputInfo(i)}> 123 | 124 | 125 | )} 126 |
127 |
128 | ))} 129 | 130 | {!readonly && ( 131 | 138 | )} 139 |
140 | 141 | 142 | 151 | 152 |
153 | ); 154 | }; 155 | 156 | export const MapStateJSONTab = JSONTab; 157 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/RectConfig/index.tsx: -------------------------------------------------------------------------------- 1 | // import SenseInput from '@/components/customAntd/Input'; 2 | import { Col, Row, Switch, Input as SenseInput, Form, FormInstance } from 'antd'; 3 | import React from 'react'; 4 | import styles from '../index.module.scss'; 5 | import { MapStateJSONTab } from './AttributeConfig'; 6 | import TextConfigurable from '../TextConfigurable'; 7 | import { ETextType, EToolName } from '@/constant/store'; 8 | import ToolCommonFiled from '../ToolCommonFiled'; 9 | import { ToolConfigIProps } from '../../Tools'; 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | function checkNumber(v: string) { 13 | const reg = /^[1-9]\d*$/g; 14 | if (reg.test(v)) { 15 | return true; 16 | } 17 | return false; 18 | } 19 | 20 | export const rectScopeChange = (value: string) => { 21 | if (value.length === 0) { 22 | return undefined; 23 | } 24 | if (!checkNumber(value)) { 25 | return; 26 | } 27 | return ~~value; 28 | }; 29 | 30 | const minWidth = 1, 31 | minHeight = 1; 32 | const isAllReadOnly = false; 33 | 34 | const RectConfig = (props: ToolConfigIProps) => { 35 | const { t } = useTranslation(); 36 | 37 | return ( 38 | 39 |
40 | 41 | 42 |
{t('SmallestSize')}
43 | 44 | 45 | 46 | W
} disabled={isAllReadOnly} /> 47 | 48 | 49 | 50 | 51 | 52 | H} disabled={isAllReadOnly} /> 53 | 54 | 55 | 56 | 57 | 58 | {t('TextAnnotation')}
} 60 | name='textConfigurableContext' 61 | initialValue={{ 62 | textConfigurable: false, 63 | textCheckType: ETextType.AnyString, 64 | customFormat: '', 65 | }} 66 | > 67 | 68 | 69 | {t('AttributeAnnotation')}} 72 | name='attributeConfigurable' 73 | initialValue={false} 74 | > 75 | 76 | 77 | 78 | 79 | {() => { 80 | return ( 81 | props.form?.getFieldValue('attributeConfigurable') && ( 82 | 92 | 93 | 94 | ) 95 | ); 96 | }} 97 | 98 | 99 | ); 100 | }; 101 | 102 | export default RectConfig; 103 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/TagConfig/TagInput/index.module.scss: -------------------------------------------------------------------------------- 1 | .select { 2 | display: flex; 3 | align-items: center; 4 | 5 | a { 6 | display: none; 7 | width: 5%; 8 | color: #cccccc; 9 | position: relative; 10 | } 11 | 12 | .inputSerial { 13 | width: 10%; 14 | display: inline-block; 15 | } 16 | 17 | .inputGroup { 18 | width: 100%; 19 | display: inline-block; 20 | padding-bottom: 13px; 21 | padding-top: 13px; 22 | 23 | .input_single { 24 | width: 45%; 25 | margin-right: 5px; 26 | padding: 4px 8px; 27 | border: 0px; 28 | border-bottom: 1px solid #cccccc; 29 | border-radius: 0; 30 | } 31 | } 32 | 33 | .addIcon { 34 | margin-right: 14px; 35 | } 36 | 37 | .deleteIcon { 38 | margin-right: 10px; 39 | } 40 | 41 | .checkboxUnselected { 42 | display: block; 43 | 44 | :global { 45 | .ant-checkbox-checked .ant-checkbox-inner { 46 | background-color: rgba(153, 153, 153, 1); 47 | border-color: rgba(153, 153, 153, 1); 48 | } 49 | .ant-checkbox-checked::after { 50 | border-color: rgba(153, 153, 153, 1); 51 | } 52 | } 53 | } 54 | .checkboxSelected { 55 | display: block; 56 | :global { 57 | .ant-checkbox-checked .ant-checkbox-inner { 58 | background-color: rgba(255, 179, 102, 1); 59 | border-color: rgba(255, 179, 102, 1); 60 | } 61 | .ant-checkbox-checked::after { 62 | border-color: rgba(255, 179, 102, 1); 63 | } 64 | } 65 | } 66 | } 67 | 68 | .select:hover { 69 | a { 70 | display: inline-block; 71 | } 72 | } 73 | 74 | .headerOption { 75 | display: flex; 76 | justify-content: flex-start; 77 | height: 20px; 78 | 79 | :global { 80 | .ant-checkbox-checked::after { 81 | display: none; 82 | } 83 | } 84 | .icon { 85 | margin-right: 9px; 86 | } 87 | .star { 88 | display: block; 89 | margin-right: 6px; 90 | cursor: pointer; 91 | } 92 | .starSelected { 93 | color: rgba(255, 179, 102, 1); 94 | } 95 | .starUnselected { 96 | color: rgba(153, 153, 153, 1); 97 | } 98 | } 99 | 100 | .subMain { 101 | //margin-left: 26px; 102 | //width: 90%; 103 | 104 | .subSelect { 105 | margin-top: 2px; 106 | padding-bottom: 10px; 107 | display: inline-block; 108 | position: relative; 109 | display: flex; 110 | align-items: center; 111 | 112 | a { 113 | display: none; 114 | } 115 | .sub_input_box { 116 | width: 100%; 117 | } 118 | .sub_input { 119 | width: 45%; 120 | margin-right: 5px; 121 | padding: 4px 8px; 122 | border: 0px; 123 | border-bottom: 1px solid #cccccc; 124 | border-radius: 0; 125 | } 126 | .firstItem { 127 | display: flex; 128 | align-items: center; 129 | } 130 | } 131 | 132 | .subSelect:hover { 133 | a { 134 | display: inline-block; 135 | } 136 | } 137 | } 138 | 139 | .firstItem::before { 140 | content: ''; 141 | margin-left: 10px; 142 | border-left: 1px solid #ccc; 143 | border-bottom: 1px solid #ccc; 144 | width: 6px; 145 | height: 25px; 146 | } 147 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/TagConfig/index.tsx: -------------------------------------------------------------------------------- 1 | // cl 2021/8/4 10:45 2 | import React, { useEffect } from 'react'; 3 | import { Button, FormInstance, Tabs, Form, Checkbox } from 'antd'; 4 | import TagInput from './TagInput'; 5 | import { 6 | addInputList, 7 | changeInputList, 8 | deleteInputList, 9 | judgeIsTagConfig, 10 | } from '@/utils/tool/editTool'; 11 | import MonacoEditor from 'react-monaco-editor'; 12 | import { useTranslation } from 'react-i18next'; 13 | import i18n from '@/i18n'; 14 | 15 | const { TabPane } = Tabs; 16 | interface IProps { 17 | form?: FormInstance; 18 | } 19 | 20 | const EDIT_SUBSELECTED = true; 21 | export interface IInputList { 22 | isMulti?: Boolean; 23 | key: string; 24 | value: string; 25 | subSelected?: IInfoList[]; 26 | } 27 | 28 | interface IInfoList { 29 | isDefault?: any; 30 | key: string; 31 | value: string; 32 | } 33 | 34 | const initInputList = [ 35 | { 36 | key: '类别1', 37 | value: 'class-1', 38 | isMulti: false, 39 | subSelected: [{ key: '选项1-1', value: 'option1-1', isDefault: false }], 40 | }, 41 | ]; 42 | 43 | const initInputList_EN = [ 44 | { 45 | key: 'className1', 46 | value: 'class-1', 47 | isMulti: false, 48 | subSelected: [{ key: 'optionName1-1', value: 'option1-1', isDefault: false }], 49 | }, 50 | ]; 51 | 52 | // 限定质检 53 | const options = { 54 | selectOnLineNumbers: true, 55 | renderSideBySide: false, 56 | }; 57 | 58 | const ToolConfig: React.FC = ({ form }) => { 59 | const { t } = useTranslation(); 60 | // 更改标签工具里面的对应值 61 | const changeInputInfo = ( 62 | e: any, 63 | target: 'key' | 'value' | 'isMulti' | 'isDefault', 64 | index: number, 65 | subIndex?: number, 66 | ) => { 67 | // 这个是什么情况才有 ? 68 | if (e?.target?.value?.indexOf('@') > -1 && !['isMulti', 'isDefault'].includes(target)) { 69 | return; 70 | } 71 | const inputList = form?.getFieldValue('inputList'); 72 | form?.setFieldsValue({ inputList: changeInputList(e, target, inputList, index, subIndex) }); 73 | }; 74 | // add inputList 75 | const addInputInfo = (i?: number) => { 76 | const inputList = form?.getFieldValue('inputList'); 77 | form?.setFieldsValue({ 78 | inputList: addInputList(inputList, EDIT_SUBSELECTED, i, { 79 | isMulti: true, 80 | lang: i18n.language, 81 | }), 82 | }); 83 | }; 84 | // 删除对应输入 85 | const deleteInputInfo = (i: number, subIndex?: number) => { 86 | const inputList = form?.getFieldValue('inputList'); 87 | form?.setFieldsValue({ inputList: deleteInputList(inputList, i, subIndex) }); 88 | }; 89 | 90 | // 编辑器更改 91 | const editorChange = (v: string) => { 92 | try { 93 | const newInputList = JSON.parse(v); 94 | // 做编辑步骤的格式验证 95 | if (judgeIsTagConfig(newInputList)) { 96 | form?.setFieldsValue({ inputList: newInputList }); 97 | } 98 | } catch (e) { 99 | // message.error('JSON 格式错误'); 100 | } 101 | }; 102 | useEffect(() => { 103 | // 通过 form 来管理数据 后面有异步的话也可以通过这里管理 104 | let inputList = initInputList; 105 | if (i18n.language === 'en') { 106 | inputList = initInputList_EN; 107 | } 108 | 109 | form?.setFieldsValue({ inputList }); 110 | // eslint-disable-next-line react-hooks/exhaustive-deps 111 | }, []); 112 | return ( 113 | 114 | {/* 两个switch 站位用的 便于 form 拿数据 */} 115 | 121 | 122 | 123 | 129 | 130 | 131 | 132 | {() => { 133 | let inputList: IInputList[] = form?.getFieldValue('inputList'); 134 | return ( 135 | 136 | 137 |
138 | {inputList?.map((info, i) => ( 139 | 148 | ))} 149 | 150 | 153 |
154 |
155 | 156 | 165 | 166 |
167 | ); 168 | }} 169 |
170 |
171 | ); 172 | }; 173 | 174 | export default ToolConfig; 175 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/TextConfigurable/index.tsx: -------------------------------------------------------------------------------- 1 | import { ETextType, TEXT_TYPE } from '@/constant/store'; 2 | import { Select as SenseSelect, Input as SenseInput } from 'antd'; 3 | import { Select, Switch } from 'antd'; 4 | import React, { useState } from 'react'; 5 | import styles from '../../index.module.scss'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | interface ITextConfigurableValue { 9 | textConfigurable?: boolean; 10 | textCheckType?: string; 11 | customFormat?: string; 12 | } 13 | interface IProps { 14 | value?: ITextConfigurableValue; 15 | onChange?: (value: ITextConfigurableValue) => void; 16 | } 17 | 18 | const TextConfigurable: React.FC = ({ value = {}, onChange }) => { 19 | // const { textConfigurable, textCheckType, customFormat, isAllReadOnly, updateData } = props; 20 | const [textConfigurable, setTextConfigurable] = useState(false); 21 | const [textCheckType, setTextCheckType] = useState(ETextType.AnyString); 22 | const [customFormat, setCustomFormat] = useState(''); 23 | const { t } = useTranslation(); 24 | 25 | const triggerChange = (changeValue: any) => { 26 | onChange?.({ textConfigurable, textCheckType, customFormat, ...value, ...changeValue }); 27 | }; 28 | 29 | const onSwitchChange = (newTextConfigurable: boolean) => { 30 | if (!('textConfigurable' in value)) { 31 | setTextConfigurable(newTextConfigurable); 32 | } 33 | triggerChange({ textConfigurable: newTextConfigurable }); 34 | }; 35 | const onSelectChange = (newTextCheckType: keyof typeof TEXT_TYPE) => { 36 | if (!('textCheckType' in value)) { 37 | setTextCheckType(newTextCheckType); 38 | } 39 | triggerChange({ textCheckType: newTextCheckType }); 40 | }; 41 | const onInputChange = (e: React.ChangeEvent) => { 42 | const newCustomFormat = e.target.value; 43 | if (!('customFormat' in value)) { 44 | setCustomFormat(newCustomFormat); 45 | } 46 | triggerChange({ customFormat: newCustomFormat }); 47 | }; 48 | return ( 49 | <> 50 |
51 | 52 |
53 | {(value.textConfigurable || textConfigurable) && ( 54 | 59 | {Object.entries(TEXT_TYPE).map((item) => ( 60 | 61 | {t(item[1])} 62 | 63 | ))} 64 | 65 | )} 66 | {(value.textCheckType || textCheckType) === ETextType.CustomFormat && ( 67 | 73 | )} 74 | 75 | ); 76 | }; 77 | export default TextConfigurable; 78 | -------------------------------------------------------------------------------- /src/ProjectPlatform/CreateProjectModal/ToolConfig/TextToolConfig/TextConfig/index.tsx: -------------------------------------------------------------------------------- 1 | // cl 2021/9/8 14:17 2 | import React from 'react'; 3 | import { Form, FormInstance, InputNumber, Input } from 'antd'; 4 | import { ItextConfig } from '../TextList'; 5 | import { useTranslation } from 'react-i18next'; 6 | const { TextArea } = Input; 7 | 8 | interface IProps { 9 | form: FormInstance; 10 | } 11 | 12 | const Index: React.FC = ({ 13 | label, 14 | maxLength, 15 | default: defaultValue, 16 | form, 17 | }) => { 18 | const { t } = useTranslation(); 19 | return ( 20 |
28 | {label} 29 | 35 | 36 | 37 | 38 | {() => { 39 | const len = form.getFieldValue('maxLength'); 40 | return ( 41 | 47 |