├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .huskyrc.js ├── .lintstagedrc.js ├── .postcssrc.js ├── .prettierignore ├── .vscode └── setting.json ├── LICENSE ├── README.md ├── jsconfig.json ├── package.json ├── packages ├── fm-components │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .storybook │ │ ├── addons.js │ │ ├── config.js │ │ └── webpack.config.js │ ├── .vscode │ │ └── setting.json │ ├── README.md │ ├── babel.config.js │ ├── example │ │ ├── Simple │ │ │ ├── dummyFileSystem.ts │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── types.d.ts │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── scripts │ │ ├── jest │ │ │ └── jest.config.js │ │ └── webpack │ │ │ ├── webpack.config.dev.js │ │ │ └── webpack.config.umd.js │ ├── src │ │ ├── components │ │ │ ├── Add │ │ │ │ └── index.tsx │ │ │ ├── FileCreator │ │ │ │ ├── index.tsx │ │ │ │ └── styles.tsx │ │ │ ├── FileGrid │ │ │ │ ├── FileIconList.tsx │ │ │ │ └── index.tsx │ │ │ ├── FileIcon │ │ │ │ ├── index.tsx │ │ │ │ └── styles.tsx │ │ │ ├── FileInfo │ │ │ │ ├── index.tsx │ │ │ │ └── styles.tsx │ │ │ ├── Menu │ │ │ │ └── index.tsx │ │ │ ├── Navigation │ │ │ │ ├── GoBack.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.tsx │ │ │ ├── SEO │ │ │ │ └── index.tsx │ │ │ ├── SearchBar │ │ │ │ ├── Filter.tsx │ │ │ │ ├── MagnifyIcon.tsx │ │ │ │ ├── SearchResults.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.tsx │ │ │ ├── Sidebar │ │ │ │ ├── Collapse.tsx │ │ │ │ ├── SideMenu.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.tsx │ │ │ ├── ToggleSwitch │ │ │ │ └── index.tsx │ │ │ ├── UfFileManager │ │ │ │ └── index.tsx │ │ │ └── index.ts │ │ ├── context │ │ │ └── NavContext.tsx │ │ ├── elements │ │ │ ├── SvgIcon.tsx │ │ │ └── withModal.tsx │ │ ├── i18n │ │ │ └── index.ts │ │ ├── index.less │ │ ├── index.ts │ │ └── types │ │ │ ├── FileType.ts │ │ │ ├── common.ts │ │ │ ├── constants.ts │ │ │ ├── datetime.ts │ │ │ ├── index.ts │ │ │ ├── path.tsx │ │ │ └── utils.ts │ ├── tsconfig.cjs.json │ ├── tsconfig.es.json │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock └── fm-core │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .vscode │ └── setting.json │ ├── babel.config.js │ ├── package.json │ ├── scripts │ ├── jest │ │ └── jest.config.js │ └── webpack │ │ └── webpack.config.umd.js │ ├── src │ ├── constant │ │ └── types.ts │ ├── func │ │ ├── __test__ │ │ │ └── sum.test.ts │ │ └── sum.ts │ ├── index.ts │ ├── registry │ │ └── Registry.ts │ └── utils │ │ └── log.ts │ ├── tsconfig.cjs.json │ ├── tsconfig.es.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── tslint.json ├── scripts ├── jest │ └── jest.config.js ├── tools │ ├── publish_pkgs.sh │ ├── release_pkgs.sh │ ├── start_micro.sh │ ├── start_mono.sh │ └── upgrade_pkgs.sh └── webpack │ └── webpack.config.js ├── tsconfig.json ├── tsconfig.test.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !/.*.js 2 | *.min.* 3 | *.production.* 4 | *.md 5 | *.js 6 | *.json 7 | 8 | coverage 9 | dist 10 | node_modules 11 | build 12 | scripts 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@wx-fc/eslint-config/base', 3 | parserOptions: { 4 | ecmaVersion: 2018, 5 | sourceType: 'module', 6 | project: './tsconfig.json', 7 | tsconfigRootDir: __dirname, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node files 2 | node_modules 3 | 4 | # OS junk files 5 | .DS_Store 6 | .stylelintcache 7 | .eslintcache 8 | 9 | # Project specific stuff 10 | .cache-loader 11 | @coverage 12 | *.log 13 | dist 14 | build 15 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@wx-fc/husky-config'); 2 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@wx-fc/lint-staged'); 2 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@wx-fc/postcss-config'); 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | !/.vscode/settings.json 2 | *.min.* 3 | *.production.* 4 | coverage 5 | dist 6 | node_modules 7 | -------------------------------------------------------------------------------- /.vscode/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | { 6 | "language": "typescript", 7 | "autoFix": true 8 | }, 9 | { 10 | "language": "typescriptreact", 11 | "autoFix": true 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 王下邀月熊 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fc-file-manager 2 | 3 | ![文件截图](https://s2.ax1x.com/2020/03/02/32cUbR.md.png) 4 | 5 | ## Feature 6 | 7 | - 能提供列表、网格等多种视图。 8 | - 提供拖拽放入、排序等功能。 9 | - 提供扩展接口:视图渲染回调、右击工具菜单渲染回调。 10 | 11 | ## Usage 12 | 13 | ```tsx 14 | import { dummyFileSystem } from './dummyFileSystem'; 15 | 16 | export default function Simple() { 17 | return ; 18 | } 19 | ``` 20 | 21 | # About 22 | 23 | ## Roadmap 24 | 25 | - [ ] 允许传入自定义的渲染函数 26 | - [ ] 使用 react-beautiful-dnd 添加拖拽功能 27 | 28 | ## Motivation & Credits 29 | 30 | ### File Manager 31 | 32 | - [el-file #Project#](https://github.com/mflorence99/el-file): The Elf file manager is the latest. My motivation throughout has partly been that few Linux tools have the kind of rich UI you can build today with tools like Angular and Material Design. 33 | 34 | - [electron-quasar-file-explorer #Project#](https://github.com/hawkeye64/electron-quasar-file-explorer): A Simple File Explorer using Vue/Quasar/Electron 35 | 36 | - [JumpFm #Project#](https://jumpfm.org/#Builtin%20Super-Powers): A file manager that lets you jump. 37 | 38 | - [filemanager #Project#](https://github.com/OpusCapita/filemanager): React based FileManager for browser ( + FS REST API for Node.js and Express) 39 | 40 | - [file-system-react #Project#](https://github.com/imshubhamsingh/file-system-react): File System UI in Web using react. 41 | 42 | ### File Preview 43 | 44 | - [wopihost #Project#](https://github.com/ethendev/wopihost): 基于 wopi 协议开发的 WopiHost , 支持 word, excel,ppt, pdf(仅支持预览)等文档的预览和编辑。 45 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@wx-fc/tsconfig/jsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uf-file-manager", 3 | "version": "0.0.1", 4 | "description": "Micro-Frontend Libs, with React & TS & Webpack", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/wx-chevalier/m-fe-libs" 8 | }, 9 | "author": "wx-chevalier@github", 10 | "license": "MIT", 11 | "private": true, 12 | "workspaces": [ 13 | "packages/react-lib" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "redux", 18 | "mobx", 19 | "webpack", 20 | "typescript" 21 | ], 22 | "scripts": { 23 | "bootstrap": "yarn install && yarn run build", 24 | "build": "npm run clean && yarn workspaces run build && cp -r ./packages/fm-components/build ./build", 25 | "clean": "yarn workspaces run clean", 26 | "clean:cov": "yarn workspaces run clean:cov", 27 | "dev": "npm start", 28 | "lint": "yarn workspaces run lint", 29 | "lint-staged": "lint-staged", 30 | "postinstall": "node ./node_modules/husky/lib/installer/bin install", 31 | "prettier-all": "prettier --write 'packages/**/src/**/*.{ts,tsx}' '!src/{assets,datas}/**'", 32 | "start": "(cd packages/fm-components && npm start)", 33 | "start:m-fe": "./scripts/tools/start_micro.sh", 34 | "test": "yarn workspaces run test", 35 | "test:cov": "yarn workspaces run test:cov", 36 | "test:watch": "yarn workspaces run test:watch", 37 | "upgrade": "./scripts/tools/upgrade_pkgs.sh" 38 | }, 39 | "devDependencies": { 40 | "@svgr/webpack": "^5.1.0", 41 | "@wx-fc/app-config": "^0.3.3" 42 | }, 43 | "browserslist": [ 44 | "extends @wx-fc/browserslist-config/modern" 45 | ], 46 | "commitlint": { 47 | "extends": [ 48 | "@wx-fc" 49 | ] 50 | }, 51 | "prettier": "@wx-fc/prettier-config/semi", 52 | "remarkConfig": { 53 | "plugins": [ 54 | "@wx-fc/remark-config" 55 | ] 56 | }, 57 | "stylelint": { 58 | "extends": [ 59 | "@wx-fc/stylelint-config", 60 | "@wx-fc/stylelint-config/modules" 61 | ], 62 | "rules": { 63 | "font-family-no-missing-generic-family-keyword": null, 64 | "declaration-empty-line-before": null, 65 | "no-descending-specificity": null, 66 | "plugin/no-unsupported-browser-features": null, 67 | "plugin/no-low-performance-animation-properties": null 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/fm-components/.eslintignore: -------------------------------------------------------------------------------- 1 | !/.*.js 2 | *.min.* 3 | *.production.* 4 | *.md 5 | *.js 6 | *.json 7 | 8 | coverage 9 | dist 10 | node_modules 11 | build 12 | scripts 13 | -------------------------------------------------------------------------------- /packages/fm-components/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../../.eslintrc.js") -------------------------------------------------------------------------------- /packages/fm-components/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /packages/fm-components/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context('../stories', true, /\.stories\.tsx?$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /packages/fm-components/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | use: [ 5 | { 6 | loader: require.resolve('awesome-typescript-loader') 7 | }, 8 | // Optional 9 | { 10 | loader: require.resolve('react-docgen-typescript-loader') 11 | } 12 | ] 13 | }); 14 | config.resolve.extensions.push('.ts', '.tsx'); 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/fm-components/.vscode/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | { "language": "typescript", "autoFix": true }, 6 | { "language": "typescriptreact", "autoFix": true } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/fm-components/README.md: -------------------------------------------------------------------------------- 1 | # rtw-components 2 | 3 | rtw-components 是对应用开发过程中的常见组件进行抽取,以 CJS/UMD 形式发布。 4 | -------------------------------------------------------------------------------- /packages/fm-components/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@wx-fc', 5 | { 6 | import: true, 7 | react: true, 8 | typescript: true 9 | } 10 | ] 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /packages/fm-components/example/Simple/dummyFileSystem.ts: -------------------------------------------------------------------------------- 1 | import { FileType } from '../../src'; 2 | 3 | export const dummyFileSystem: Record = { 4 | '0': { id: '0', name: '根目录', path: '/', isDir: true }, 5 | '31258': { 6 | id: '31258', 7 | name: '6.stl.zlib', 8 | isDir: false, 9 | parentId: '0' 10 | }, 11 | '31259': { 12 | id: '31259', 13 | name: '1.stl.zlibdfasdfadfasfasdfsdfasdfasdfasdfasdfasdfa', 14 | isDir: true, 15 | parentId: '0', 16 | path: '/1.stl.zlibdfasdfadfasfasdfsdfasdfasdfasdfasdfasdfa' 17 | }, 18 | '31260': { 19 | id: '31260', 20 | name: '2.netfabb.stl.zlib', 21 | isDir: false, 22 | parentId: '0' 23 | }, 24 | '31267': { 25 | id: '31267', 26 | name: '07-07-唐司军发送到发送到发送到发送到发送到发送到发送到发顺丰', 27 | isDir: true, 28 | parentId: '0', 29 | path: '/1.stl.zlib/test1/07-07-唐司军发送到发送到发送到发送到发送到发送到发送到发顺丰' 30 | }, 31 | '31261': { 32 | id: '31261', 33 | name: '2.stl.zlib', 34 | isDir: false, 35 | parentId: '0' 36 | }, 37 | '31262': { 38 | id: '31262', 39 | name: '2.netfabb.stl.zlib', 40 | isDir: false, 41 | parentId: '0' 42 | }, 43 | '31263': { 44 | id: '31263', 45 | name: '2.stl.zlib', 46 | isDir: false, 47 | parentId: '0' 48 | }, 49 | '31264': { 50 | id: '31264', 51 | name: 'test1', 52 | isDir: true, 53 | parentId: '31259', 54 | path: '/1.stl.zlib/test1' 55 | }, 56 | '31265': { 57 | id: '31265', 58 | name: 'test2', 59 | isDir: true, 60 | parentId: '31264', 61 | path: '/1.stl.zlib/test1/test2' 62 | }, 63 | '31266': { 64 | id: '31266', 65 | name: '07-07-唐司军', 66 | isDir: true, 67 | parentId: '31264', 68 | path: '/1.stl.zlib/test1/07-07-唐司军' 69 | }, 70 | 71 | '31268': { 72 | id: '31268', 73 | name: '07-07-唐司军发送到发送到发送到发送到发送到发送到发送到发顺丰dfasdfasdfas', 74 | isDir: true, 75 | parentId: '0', 76 | path: 77 | '/1.stl.zlib/test1/07-07-唐司军发送到发送到发送到发送到发送到发送到发送到发顺丰dfasdfasdfas' 78 | } 79 | }; 80 | // { 81 | // '1382b6993e9f270cb1c29833be3f5750': { 82 | // isDir: true, 83 | // name: 'root', 84 | // path: '/', 85 | // size: 0, 86 | // createdAt: '2019-04-07', 87 | // creatorName: 'admin', 88 | // parentId: null 89 | // }, 90 | // '9b6739960c1ac83251046da2c718019b': { 91 | // isDir: true, 92 | // name: 'apps', 93 | // creatorName: 'Shubham Singh', 94 | // size: 223, 95 | // createdAt: '2019-04-29', 96 | // parentId: '1382b6993e9f270cb1c29833be3f5750', 97 | // path: '/apps' 98 | // }, 99 | // '147d0ef33fe657ce53a83de6a630473d': { 100 | // isDir: true, 101 | // name: 'pictures', 102 | // creatorName: 'Shubham Singh', 103 | // size: 23, 104 | // createdAt: '2019-04-29', 105 | // parentId: '1382b6993e9f270cb1c29833be3f5750', 106 | // path: '/pictures' 107 | // }, 108 | // a55cfa9e1bf87138edd25c4b1553104d: { 109 | // isDir: true, 110 | // name: 'videos', 111 | // creatorName: 'Shubham Singh', 112 | // size: 0, 113 | // createdAt: '2019-04-29', 114 | // parentId: '1382b6993e9f270cb1c29833be3f5750', 115 | // path: '/videos' 116 | // }, 117 | // '5f2b4d35489a8617e574060b19b7cad9': { 118 | // isDir: true, 119 | // name: 'docs', 120 | // creatorName: 'Shubham Singh', 121 | // size: 233, 122 | // createdAt: '2019-04-29', 123 | // parentId: '1382b6993e9f270cb1c29833be3f5750', 124 | // path: '/docs' 125 | // }, 126 | // ab7e413a3798155e37a9361140522e39: { 127 | // isDir: false, 128 | // name: 'a.pdf', 129 | // creatorName: 'Shubham Singh', 130 | // size: 234, 131 | // createdAt: '2019-04-29', 132 | // parentId: '1382b6993e9f270cb1c29833be3f5750', 133 | // path: '/a.pdf' 134 | // }, 135 | // '891debd77d0bc40d30ff7d7e6c628e9f': { 136 | // isDir: false, 137 | // name: 'b.jpg', 138 | // creatorName: 'Shubham Singh', 139 | // size: 234, 140 | // createdAt: '2019-04-29', 141 | // parentId: '1382b6993e9f270cb1c29833be3f5750', 142 | // path: '/b.jpg' 143 | // }, 144 | // '2d03459789f153918dfc0be413fe9987': { 145 | // isDir: true, 146 | // name: 'work', 147 | // creatorName: 'Shubham Singh', 148 | // size: 200, 149 | // createdAt: '2019-04-29', 150 | // parentId: '5f2b4d35489a8617e574060b19b7cad9', 151 | // path: '/docs/work' 152 | // }, 153 | // '2d03459789f153918dfc0be413fe9986': { 154 | // isDir: true, 155 | // name: 'study', 156 | // creatorName: 'Shubham Singh', 157 | // size: 200, 158 | // createdAt: '2019-04-29', 159 | // parentId: '5f2b4d35489a8617e574060b19b7cad9', 160 | // path: '/docs/work/study' 161 | // }, 162 | // '8f7c5959dbb088c0aef8b145dbdf6e43': { 163 | // isDir: false, 164 | // name: 'c.pdf', 165 | // creatorName: 'Shubham Singh', 166 | // size: 200, 167 | // createdAt: '2019-04-29', 168 | // parentId: '5f2b4d35489a8617e574060b19b7cad9', 169 | // path: '/docs/c.pdf' 170 | // }, 171 | // '579c51eec02e43b4dfad314e05365fe4': { 172 | // isDir: false, 173 | // name: 'd.docx', 174 | // creatorName: 'Shubham Singh', 175 | // size: 235, 176 | // createdAt: '2019-04-29', 177 | // parentId: '5f2b4d35489a8617e574060b19b7cad9', 178 | // path: '/docs/d.docx' 179 | // }, 180 | // b42eff45517edc2e543b3d2750bd08c3: { 181 | // isDir: false, 182 | // name: 'e.pdf', 183 | // creatorName: 'Shubham Singh', 184 | // size: 0, 185 | // createdAt: '2019-04-29', 186 | // parentId: '2d03459789f153918dfc0be413fe9987', 187 | // path: '/docs/work/e.pdf' 188 | // }, 189 | // '00ce12a7746322ce403e17992674f81b': { 190 | // isDir: false, 191 | // name: 'f.ts', 192 | // creatorName: 'Shubham Singh', 193 | // size: 235, 194 | // createdAt: '2019-04-29', 195 | // parentId: '2d03459789f153918dfc0be413fe9987', 196 | // path: '/docs/work/f.ts' 197 | // } 198 | // }; 199 | 200 | Object.keys(dummyFileSystem).forEach(id => { 201 | dummyFileSystem[id] = { id, ...dummyFileSystem[id] }; 202 | }); 203 | -------------------------------------------------------------------------------- /packages/fm-components/example/Simple/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { FileType, UfFileManager } from '../../src'; 4 | 5 | import { dummyFileSystem } from './dummyFileSystem'; 6 | 7 | const consoleEntry = (param?: any) => { 8 | console.log('entry', param); 9 | }; 10 | 11 | export default function Simple() { 12 | return ( 13 |
点击上传
} 17 | onAdd={file => { 18 | console.log(file); 19 | }} 20 | onDelete={(id, isDir) => console.log(id, isDir)} 21 | isCombineEnabled={true} 22 | onEnter={consoleEntry} 23 | onToggleSwitch={checked => console.log(checked)} 24 | onClickPreview={(entry: FileType) => alert(`preview ${entry.name}`)} 25 | onMoveTo={({ ids, targetCategoryId, showModal }) => 26 | console.log('ids', ids, 'target', targetCategoryId, showModal) 27 | } 28 | /> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/fm-components/example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import Simple from './Simple'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /packages/fm-components/example/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less' { 2 | const styles: Record; 3 | export = styles; 4 | } 5 | -------------------------------------------------------------------------------- /packages/fm-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unionfab/fm-components", 3 | "version": "0.0.24", 4 | "description": "Unionfab File Manager", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Unionfab/uf-file-manager" 8 | }, 9 | "author": "wx-chevalier@github", 10 | "license": "MIT", 11 | "main": "dist/cjs/index.js", 12 | "module": "dist/es/index.js", 13 | "types": "dist/types/index.d.ts", 14 | "files": [ 15 | "dist/" 16 | ], 17 | "keywords": [ 18 | "webpack", 19 | "react" 20 | ], 21 | "scripts": { 22 | "build": "npm run build:es && npm run build:cjs && npm run build:umd && npm run clean:r", 23 | "build-storybook": "build-storybook", 24 | "build:cjs": "tsc --project ./tsconfig.cjs.json", 25 | "build:es": "tsc --project ./tsconfig.es.json", 26 | "build:umd": "NODE_ENV=production webpack --config ./scripts/webpack/webpack.config.umd.js -p", 27 | "clean": "rimraf dist", 28 | "clean:r": "rimraf ./dist/*.map && rimraf ./dist/**/*.map && rimraf ./dist/**/*.tsbuildinfo && rimraf ./dist/**/precache-*", 29 | "dev": "webpack-dev-server --config ./scripts/webpack/webpack.config.dev.js --hot", 30 | "lint": "run-p lint:*", 31 | "lint:es": "cross-env PARSER_NO_WATCH=true eslint . --cache --ext js,md,ts,tsx -f friendly --max-warnings 10", 32 | "lint:style": "stylelint **/*.less --cache", 33 | "lint:ts": "tslint -p . -t stylish", 34 | "lint:tsc": "tsc -p tsconfig.json --incremental false --noEmit", 35 | "prepublish": "npm run clean:r", 36 | "start": "npm run dev", 37 | "storybook": "start-storybook -p 6006" 38 | }, 39 | "dependencies": { 40 | "@types/react-beautiful-dnd": "^12.1.1", 41 | "formik": "^2.1.4", 42 | "lodash": "^4.17.15", 43 | "react": "^16.8.6", 44 | "react-beautiful-dnd": "^13.0.0", 45 | "react-dom": "^16.8.6", 46 | "react-helmet": "^5.2.1", 47 | "styled-components": "^5.0.1", 48 | "with-context": "^0.0.3" 49 | }, 50 | "devDependencies": { 51 | "@storybook/addon-actions": "^5.1.11", 52 | "@storybook/addon-info": "^5.1.11", 53 | "@storybook/addon-links": "^5.1.11", 54 | "@storybook/addons": "^5.1.11", 55 | "@storybook/react": "^5.1.11", 56 | "@types/react-helmet": "^5.0.15", 57 | "@types/styled-components": "^5.0.1", 58 | "@wx-fc/app-config": "^0.3.3", 59 | "cross-env": "^6.0.3", 60 | "npm-run-all": "^4.1.5", 61 | "webpack": "^4.41.6", 62 | "webpack-dev-server": "^3.1.14" 63 | }, 64 | "lint-staged": { 65 | "*.{ts,tsx,scss,less,md}": [ 66 | "prettier --write", 67 | "git add" 68 | ] 69 | }, 70 | "prettier": { 71 | "printWidth": 100, 72 | "singleQuote": true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/fm-components/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /packages/fm-components/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/web-file-manager/4b2e2cea85dc9ca2496036701a2116c1dcfd46bd/packages/fm-components/public/favicon.ico -------------------------------------------------------------------------------- /packages/fm-components/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @m-fe/react-lib 7 | 8 | 9 | 10 | 11 | 12 |
13 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/fm-components/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "RTW", 3 | "name": "Micro Frontend Boilerplate", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#fff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/fm-components/scripts/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../../../scripts/jest/jest.config'); 2 | 3 | module.exports = baseConfig; 4 | -------------------------------------------------------------------------------- /packages/fm-components/scripts/webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | const devConfig = require('../../../../scripts/webpack/webpack.config').devConfig; 5 | 6 | module.exports = merge(devConfig, { 7 | entry: { 8 | index: path.resolve(__dirname, '../../example') 9 | }, 10 | devServer: { 11 | contentBase: path.resolve(__dirname, '../../public') 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /packages/fm-components/scripts/webpack/webpack.config.umd.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | const umdConfig = require('../../../../scripts/webpack/webpack.config').umdConfig; 5 | 6 | const rootPath = process.cwd(); 7 | 8 | module.exports = merge(umdConfig, { 9 | output: { 10 | library: 'rtwComponents' 11 | }, 12 | entry: { 13 | index: path.resolve(rootPath, './src/index.ts') 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Add/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, memo, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { FileType } from '../../types'; 5 | import { FileCreator } from '../FileCreator'; 6 | 7 | interface IProps { 8 | onAdd: (file: FileType) => void; 9 | } 10 | 11 | export function AddComp(props: IProps) { 12 | const [open, handleOpen] = useState(false); 13 | return ( 14 | 15 | handleOpen(true)}>+ 16 | {open ? ( 17 | props.onAdd(value)} 20 | onClose={() => handleOpen(false)} 21 | /> 22 | ) : ( 23 | '' 24 | )} 25 | 26 | ); 27 | } 28 | 29 | export const Add = memo(AddComp); 30 | 31 | const Container = styled.div` 32 | height: 109px; 33 | width: 96px; 34 | border: 3px dashed #dee0e4; 35 | display: flex; 36 | justify-content: center; 37 | border-radius: 8px; 38 | align-items: center; 39 | font-size: 30px; 40 | color: #dee0e4; 41 | margin: 0 16px; 42 | cursor: copy; 43 | `; 44 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileCreator/index.tsx: -------------------------------------------------------------------------------- 1 | import { Field, Form as FForm, Formik } from 'formik'; 2 | import React, { useState } from 'react'; 3 | 4 | import { NavContextProps, withContext } from '../../context/NavContext'; 5 | import { ModalProps, withModal } from '../../elements/withModal'; 6 | import { FileType, getTodayDate } from '../../types'; 7 | 8 | import { Container, Error, Form, Toggle, Top } from './styles'; 9 | 10 | interface IProps extends ModalProps, NavContextProps { 11 | children?: JSX.Element; 12 | isSubmitting?: boolean; 13 | onAdd?: (file: FileType) => void; 14 | onClose?: Function; 15 | } 16 | 17 | function FileCreatorComp(_props: IProps) { 18 | const [isDir, setIsDir] = useState(false); 19 | 20 | return ( 21 | 22 | 23 | 24 | setIsDir(false)}> 25 | 文件 26 | 27 | setIsDir(true)}> 28 | 文件夹 29 | 30 | 31 | 32 | 33 | {!isDir && _props.renderAddFileElement ? ( 34 | _props.renderAddFileElement({ onClose: _props.onClose }) 35 | ) : ( 36 | { 44 | const errors: Record = {}; 45 | if (!values.name) { 46 | errors.name = 'Name is Required'; 47 | } 48 | return errors; 49 | }} 50 | onSubmit={values => { 51 | _props.onAdd({ 52 | ...values, 53 | isDir 54 | }); 55 | 56 | _props.onClose(); 57 | }} 58 | > 59 | {(props: any) => ( 60 | 61 | 62 |
63 | 71 | {props.errors.name && props.touched.name ? ( 72 | {props.errors.name} 73 | ) : ( 74 | '' 75 | )} 76 |
77 | 78 |
79 | 87 | {props.errors.creatorName && props.touched.creatorName ? ( 88 | {props.errors.creatorName} 89 | ) : ( 90 | '' 91 | )} 92 |
93 | 94 |
95 | 103 |
104 | 105 | 110 | 创建 111 | 112 |
113 |
114 | )} 115 |
116 | )} 117 |
118 | ); 119 | } 120 | 121 | export const FileCreator = withModal(withContext(FileCreatorComp))({}); 122 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileCreator/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | position: relative; 5 | `; 6 | 7 | export const Top = styled.div` 8 | width: 100%; 9 | display: flex; 10 | justify-content: center; 11 | padding: 16px 0 32px; 12 | `; 13 | 14 | export const Toggle = { 15 | Container: styled.div` 16 | height: 32px; 17 | width: 128px; 18 | border: 1px solid #dde0e4; 19 | border-radius: 8px; 20 | overflow: hidden; 21 | display: flex; 22 | user-select: none; 23 | `, 24 | 25 | Option: styled.div` 26 | font-family: Lato, sans-serif; 27 | font-size: 14px; 28 | color: #535b62; 29 | width: 50%; 30 | cursor: pointer; 31 | display: flex; 32 | text-align: center; 33 | justify-content: center; 34 | align-items: center; 35 | letter-spacing: 0; 36 | &.selected { 37 | background: #4ab7ff; 38 | color: white; 39 | } 40 | ` 41 | }; 42 | 43 | export const Form = { 44 | Container: styled.div` 45 | padding: 1px 48px; 46 | & .formField { 47 | position: relative; 48 | & .field { 49 | width: 100%; 50 | background: #fff; 51 | border: 1px solid #dde0e4; 52 | border-radius: 8px; 53 | font-family: Lato, sans-serif; 54 | font-size: 14px; 55 | padding: 10px; 56 | position: relative; 57 | margin-bottom: 20px; 58 | &::placeholder { 59 | color: #afb2b6; 60 | } 61 | &[type='date']::placeholder { 62 | color: #afb2b6; 63 | } 64 | } 65 | } 66 | `, 67 | 68 | Submit: styled.button` 69 | background: #4ab7ff; 70 | border-radius: 8px; 71 | color: white; 72 | font-size: 14px; 73 | justify-content: center; 74 | font-family: Lato, sans-serif; 75 | display: flex; 76 | align-items: center; 77 | border: none; 78 | cursor: pointer; 79 | width: 100%; 80 | height: 40px; 81 | transition: opacity 250ms ease-in; 82 | &.disabled { 83 | cursor: not-allowed; 84 | opacity: 0.5; 85 | } 86 | ` 87 | }; 88 | 89 | export const Error = styled.div` 90 | color: red; 91 | position: absolute; 92 | top: 42px; 93 | left: 5px; 94 | font-size: 12px; 95 | font-family: Lato, sans-serif; 96 | `; 97 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileGrid/FileIconList.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as React from 'react'; 3 | import { Draggable, Droppable, DroppableProvided } from 'react-beautiful-dnd'; 4 | import styled from 'styled-components'; 5 | 6 | import { FileType } from '../../types'; 7 | import { FileIcon } from '../FileIcon'; 8 | 9 | interface FileIconListProps { 10 | files: FileType[]; 11 | listType?: string; 12 | internalScroll?: boolean; 13 | isCombineEnabled?: boolean; 14 | extraEle?: JSX.Element; 15 | isMultiple?: boolean; 16 | 17 | onDelete: (id: string, isDir: boolean) => void; 18 | } 19 | 20 | interface FileIconListState { 21 | multipledEntryIds: { entryId: string; isDir: boolean }[]; 22 | } 23 | 24 | export class FileIconList extends React.Component { 25 | constructor(props: FileIconListProps) { 26 | super(props); 27 | this.state = { 28 | multipledEntryIds: [] 29 | }; 30 | } 31 | 32 | onMultipleSelect = (entryId: string, isDir: boolean, isAdd: boolean) => { 33 | const { multipledEntryIds } = this.state; 34 | 35 | isAdd 36 | ? multipledEntryIds.push({ entryId, isDir }) 37 | : _.remove(multipledEntryIds, s => s.entryId === entryId); 38 | 39 | this.setState({ multipledEntryIds }); 40 | }; 41 | 42 | renderFileIcon = (dropProvided: DroppableProvided) => { 43 | const { multipledEntryIds } = this.state; 44 | const { isMultiple, files, extraEle, onDelete } = this.props; 45 | 46 | return ( 47 | 48 | 49 | {(files || []).map((entry, i) => ( 50 | 51 | {(dragProvided, _dragSnapshot) => ( 52 |
57 | onDelete(entry.id, entry.isDir)} 65 | /> 66 |
67 | )} 68 |
69 | ))} 70 | {extraEle} 71 |
72 |
73 | ); 74 | }; 75 | 76 | public render() { 77 | const { listType, isCombineEnabled } = this.props; 78 | return ( 79 | 80 | 86 | {(dropProvided, _dropSnapshot) => ( 87 | {this.renderFileIcon(dropProvided)} 88 | )} 89 | 90 | 91 | ); 92 | } 93 | } 94 | 95 | const Wrapper = styled.div` 96 | display: flex; 97 | flex-direction: column; 98 | padding: 8px; 99 | user-select: none; 100 | transition: background-color 0.1s ease; 101 | margin: 8px 0; 102 | `; 103 | 104 | const DropZone = styled.div` 105 | display: flex; 106 | align-items: start; 107 | min-height: 60px; 108 | flex-wrap: wrap; 109 | `; 110 | 111 | const Container = styled.div` 112 | flex-grow: 0; 113 | display: inline-flex; 114 | `; 115 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileGrid/index.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import { DragDropContext, DragUpdate, DropResult } from 'react-beautiful-dnd'; 4 | import styled from 'styled-components'; 5 | 6 | import { NavContextProps, withContext } from '../../context/NavContext'; 7 | import { FileType, getDirFiles } from '../../types'; 8 | import { Add } from '../Add'; 9 | 10 | import { FileIconList } from './FileIconList'; 11 | 12 | interface IProps extends NavContextProps { 13 | isMultiple?: boolean; 14 | 15 | onAdd: (file: FileType) => void; 16 | onDelete: (id: string, isDir: boolean) => void; 17 | onMoveTo?: ({ 18 | ids, 19 | targetCategoryId, 20 | showModal 21 | }: { 22 | showModal?: boolean; 23 | targetCategoryId?: string; 24 | ids?: { entryId: string; isDir: boolean }[]; 25 | }) => void; 26 | } 27 | 28 | interface IState { 29 | files: FileType[]; 30 | isDisableCombine: boolean; 31 | 32 | isDir?: boolean; 33 | } 34 | 35 | class FileGridComp extends Component { 36 | constructor(props: IProps) { 37 | super(props); 38 | 39 | this.state = { 40 | files: [], 41 | isDisableCombine: false 42 | }; 43 | } 44 | // 判断路径是否准确,不准确则跳转到根路径 45 | componentDidMount() { 46 | if (this.props.currentDirId && this.props.fileMap) { 47 | this.onRefresh(this.props); 48 | } 49 | } 50 | 51 | componentWillReceiveProps(nextProps: IProps) { 52 | if ( 53 | nextProps.currentDirId !== this.props.currentDirId || 54 | nextProps.fileMap !== this.props.fileMap 55 | ) { 56 | this.onRefresh(nextProps); 57 | } 58 | } 59 | 60 | onRefresh(props: IProps) { 61 | const { currentDirId, fileMap } = props; 62 | 63 | const files = getDirFiles(currentDirId, fileMap); 64 | 65 | this.setState({ files }); 66 | } 67 | 68 | // shouldComponentUpdate(nextProps, nextState) { 69 | // if (this.props.location.pathname === nextProps.location.pathname) { 70 | // if (entriesAreSame(this.props.entry, nextProps.entry)) { 71 | // return false; 72 | // } 73 | // return true; 74 | // } 75 | // return true; 76 | // } 77 | 78 | onDragEnd = (result: DropResult) => { 79 | this.setState({ isDisableCombine: false }); 80 | // super simple, just removing the dragging item 81 | if (result.combine) { 82 | const files: FileType[] = [...this.state.files]; 83 | files.splice(result.source.index, 1); 84 | this.setState({ files }); 85 | 86 | const { onMoveTo } = this.props; 87 | 88 | onMoveTo && 89 | onMoveTo({ 90 | ids: [ 91 | { 92 | isDir: this.state.isDir, 93 | entryId: _.trimStart(result.draggableId, 'fileIconList-drag-') 94 | } 95 | ], 96 | targetCategoryId: _.trimStart(result.combine.draggableId, 'fileIconList-drag-') 97 | }); 98 | return; 99 | } 100 | 101 | // dropped outside the list 102 | if (!result.destination) { 103 | return; 104 | } 105 | 106 | if (result.destination.index === result.source.index) { 107 | return; 108 | } 109 | 110 | if (result.destination.index !== result.source.index) { 111 | const { files } = this.state; 112 | const startIndex = result.source.index; 113 | const endIndex = result.destination.index; 114 | 115 | const resp = Array.from(files); 116 | const [removed] = resp.splice(startIndex, 1); 117 | resp.splice(endIndex, 0, removed); 118 | 119 | this.setState({ files: resp }); 120 | } 121 | }; 122 | 123 | onDragUpdate = (result: DragUpdate) => { 124 | const { files } = this.state; 125 | const { isMultiple } = this.props; 126 | const { combine } = result; 127 | 128 | // 要移动的目标文件或目录 129 | const resp = (files || []).find(f => _.endsWith(result.draggableId, f.id)); 130 | 131 | if (!isMultiple) { 132 | // 单选模式时获取需要移动目标的 isDir 133 | this.setState({ isDir: resp.isDir }); 134 | } 135 | 136 | if (combine) { 137 | // 要移入的目标文件或目录 138 | const target = (files || []).find(f => _.endsWith(combine.draggableId, f.id)); 139 | 140 | if ((target.isDir && !resp.isDir) || (target.isDir && resp.isDir)) { 141 | this.setState({ isDisableCombine: false }); 142 | } else { 143 | this.setState({ isDisableCombine: true }); 144 | } 145 | } 146 | }; 147 | 148 | render() { 149 | const { isDisableCombine, files } = this.state; 150 | const { isMultiple, currentDirId, isCombineEnabled, onDelete } = this.props; 151 | 152 | return ( 153 | 154 | 155 | { 163 | this.props.onAdd({ 164 | ...value, 165 | parentId: currentDirId 166 | }); 167 | }} 168 | /> 169 | } 170 | onDelete={onDelete} 171 | /> 172 | 173 | 174 | ); 175 | } 176 | } 177 | 178 | export const FileGrid = withContext(FileGridComp); 179 | 180 | const Container = styled.div` 181 | display: flex; 182 | flex-wrap: wrap; 183 | justify-content: flex-start; 184 | padding: 40px 0; 185 | `; 186 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; 3 | 4 | import { NavContextProps, withContext } from '../../context/NavContext'; 5 | import { SvgIcon } from '../../elements/SvgIcon'; 6 | import { FileType, StyleObject, getFileExt } from '../../types'; 7 | import { FileInfo } from '../FileInfo'; 8 | import { Menu } from '../Menu'; 9 | 10 | import { Container, Logo, Name } from './styles'; 11 | 12 | interface IProps extends NavContextProps { 13 | index: number | string; 14 | entry: FileType; 15 | 16 | isMultiple?: boolean; 17 | provided?: DraggableProvided; 18 | snapshot?: DraggableStateSnapshot; 19 | multipledEntryIds?: { entryId: string; isDir: boolean }[]; 20 | 21 | onDelete: Function; 22 | 23 | onMultipleSelect?: (entryId: string, isDir: boolean, isChecked: boolean) => void; 24 | } 25 | 26 | interface IState { 27 | visible: boolean; 28 | showInfo: boolean; 29 | style: StyleObject; 30 | isChecked?: boolean; 31 | prevStyle?: StyleObject; 32 | } 33 | 34 | class FileIconComp extends Component { 35 | nodeRef = createRef(); 36 | 37 | constructor(props: IProps) { 38 | super(props); 39 | this.state = { 40 | visible: false, 41 | showInfo: false, 42 | style: { 43 | right: 0, 44 | left: 0 45 | }, 46 | prevStyle: {}, 47 | isChecked: false 48 | }; 49 | } 50 | 51 | componentDidMount() { 52 | document.addEventListener('contextmenu', this._handleContextMenu); 53 | document.addEventListener('click', this._handleMouseLeave); 54 | } 55 | 56 | componentWillUnmount() { 57 | document.removeEventListener('contextmenu', this._handleContextMenu); 58 | document.removeEventListener('click', this._handleMouseLeave); 59 | } 60 | 61 | componentDidUpdate(prevProps: IProps) { 62 | const { isMultiple } = this.props; 63 | // 从多选模式切回单选时取消 文件或文件夹的选中状态 64 | if (prevProps.isMultiple !== isMultiple && !isMultiple) { 65 | this.setState({ isChecked: false }); 66 | } 67 | } 68 | 69 | _handleContextMenu = (event: any) => { 70 | event.preventDefault(); 71 | 72 | const path = event.composedPath(); 73 | 74 | const wasOutside = !path.includes(this.nodeRef.current) || false; 75 | 76 | if (wasOutside && !this.props.isMultiple) { 77 | this.setState({ 78 | visible: false, 79 | style: { 80 | right: 0, 81 | left: 0 82 | }, 83 | prevStyle: { 84 | right: 0, 85 | left: 0 86 | } 87 | }); 88 | return; 89 | } 90 | 91 | const clickX = event.clientX; 92 | const clickY = event.clientY; 93 | const screenW = window.innerWidth; 94 | const screenH = window.innerHeight; 95 | const rootW = this.nodeRef.current.offsetWidth; 96 | const rootH = this.nodeRef.current.offsetHeight; 97 | 98 | const right = screenW - clickX > rootW; 99 | const left = !right; 100 | const top = screenH - clickY > rootH; 101 | const bottom = !top; 102 | 103 | const style: StyleObject = { 104 | left: 0, 105 | top: 0 106 | }; 107 | 108 | if (right) { 109 | style.left = `${clickX + 5}px`; 110 | } 111 | 112 | if (left) { 113 | style.left = `${clickX - rootW - 5}px`; 114 | } 115 | 116 | if (top) { 117 | style.top = `${clickY + 5}px`; 118 | } 119 | 120 | if (bottom) { 121 | style.top = `${clickY - rootH - 5}px`; 122 | } 123 | 124 | const prevStyle = { 125 | top: style.top, 126 | left: style.left 127 | }; 128 | 129 | this.setState({ 130 | style, 131 | prevStyle, 132 | visible: true 133 | }); 134 | }; 135 | 136 | _handleMouseLeave = (event: any) => { 137 | const { visible } = this.state; 138 | const wasOutside = !(event.target && this.nodeRef.current.contains(event.target)); 139 | 140 | if (wasOutside && visible) 141 | this.setState({ 142 | visible: false, 143 | style: { 144 | right: 0, 145 | left: 0 146 | } 147 | }); 148 | }; 149 | 150 | _handleClick = (event: any) => { 151 | const { visible } = this.state; 152 | const wasOutside = !(event.target.contains === this.nodeRef); 153 | 154 | if (wasOutside && visible) 155 | this.setState({ 156 | visible: false, 157 | style: { 158 | right: 0, 159 | left: 0 160 | } 161 | }); 162 | }; 163 | 164 | _handleScroll = () => { 165 | const { visible } = this.state; 166 | 167 | if (visible) 168 | this.setState({ 169 | visible: false, 170 | style: { 171 | right: 0, 172 | left: 0 173 | } 174 | }); 175 | }; 176 | 177 | handleDelete = async () => { 178 | await this.props.onDelete(); 179 | this.setState({ visible: false }); 180 | }; 181 | 182 | enterFolder = () => { 183 | if (this.props.entry.isDir) { 184 | const { onUpdateCurrentDir } = this.props; 185 | 186 | onUpdateCurrentDir(this.props.entry.id); 187 | } else { 188 | // 对于文件夹,直接打开 189 | this.setState({ 190 | style: { 191 | top: 192 | this.nodeRef.current.offsetTop + 193 | this.nodeRef.current.getBoundingClientRect().height / 2, 194 | left: 195 | this.nodeRef.current.offsetLeft + this.nodeRef.current.getBoundingClientRect().width / 2 196 | }, 197 | prevStyle: this.state.style, 198 | visible: true 199 | }); 200 | } 201 | }; 202 | 203 | render() { 204 | const { isChecked } = this.state; 205 | const { isMultiple, multipledEntryIds, entry, onMoveTo, onMultipleSelect } = this.props; 206 | 207 | const ext = getFileExt(entry); 208 | 209 | return ( 210 | 214 | { 220 | this.setState({ isChecked: !isChecked }, () => { 221 | onMultipleSelect(entry.id, entry.isDir, this.state.isChecked); 222 | }); 223 | } 224 | : null 225 | } 226 | > 227 | {entry.isDir ? : } 228 | {!entry.isDir ? {`.${ext}`} : ''} 229 | 230 | {entry.name} 231 | {this.state.visible && ( 232 | { 238 | entry.isDir 239 | ? this.props.onUpdateCurrentDir(this.props.entry.id) 240 | : this.props.onClickPreview 241 | ? this.props.onClickPreview(this.props.entry) 242 | : this.setState({ 243 | showInfo: true 244 | }); 245 | } 246 | }, 247 | !isMultiple && { 248 | info: '详情', 249 | onClick: () => 250 | this.setState({ 251 | showInfo: true 252 | }) 253 | }, 254 | { 255 | info: '移动/复制', 256 | onClick: () => 257 | onMoveTo({ 258 | showModal: true, 259 | ids: isMultiple 260 | ? multipledEntryIds 261 | : [{ entryId: entry.id, isDir: entry.isDir }] 262 | }) 263 | }, 264 | { 265 | info: '删除', 266 | style: { color: 'red' }, 267 | onClick: () => { 268 | this.handleDelete(); 269 | } 270 | } 271 | ]} 272 | /> 273 | )} 274 | {this.state.showInfo ? ( 275 | 279 | this.setState({ 280 | showInfo: false 281 | }) 282 | } 283 | entry={{ 284 | isDir: entry.isDir, 285 | name: entry.name, 286 | path: '/', 287 | ext: ext, 288 | size: entry.size, 289 | createdAt: entry.createdAt, 290 | creatorName: entry.creatorName 291 | }} 292 | /> 293 | ) : ( 294 | '' 295 | )} 296 | 297 | ); 298 | } 299 | } 300 | 301 | export const FileIcon = withContext(FileIconComp); 302 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileIcon/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | width: 100px; 5 | display: flex; 6 | cursor: pointer; 7 | flex-direction: column; 8 | justify-content: space-between; 9 | align-items: center; 10 | font-family: Lato, sans-serif; 11 | font-weight: lighter; 12 | color: #535b62; 13 | padding: 8px 0 10px 0; 14 | border-radius: 8px; 15 | margin: 0 10px 22px 7px; 16 | transition: background 230ms ease-in; 17 | &:hover { 18 | background: #e6f5ff; 19 | } 20 | `; 21 | 22 | export const Logo = styled.div` 23 | position: relative; 24 | & span { 25 | position: absolute; 26 | bottom: 7px; 27 | left: 4px; 28 | width: 96%; 29 | font-weight: bold; 30 | color: white; 31 | font-size: 12px; 32 | text-overflow: ellipsis; 33 | white-space: nowrap; 34 | overflow: hidden; 35 | } 36 | `; 37 | 38 | export const Img = styled.img` 39 | position: relative; 40 | `; 41 | 42 | export const Name = styled.div` 43 | width: 100%; 44 | text-align: center; 45 | word-break: break-all; 46 | position: relative; 47 | `; 48 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | 3 | import { ModalProps, withModal } from '../../elements/withModal'; 4 | import { SvgIcon } from '../../elements/SvgIcon'; 5 | import { FileType, formatDate, getFileExt } from '../../types'; 6 | 7 | import { Details, Icon, Logo } from './styles'; 8 | 9 | interface IProps extends ModalProps { 10 | entry: FileType; 11 | } 12 | 13 | class FileInfoComp extends Component { 14 | render() { 15 | const { entry } = this.props; 16 | const ext = getFileExt(entry); 17 | 18 | return ( 19 | 20 | 21 | 22 | {entry.isDir ? : } 23 | {!entry.isDir ? {`.${ext}`} : ''} 24 | 25 | 26 | 27 | 28 | 29 | Name: 30 | {entry.name} 31 | 32 | 33 | Size: 34 | {entry.size}kb 35 | 36 | 37 | Creator Name: 38 | {entry.creatorName} 39 | 40 | 41 | Created Date: 42 | {formatDate(entry.createdAt)} 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export const FileInfo = withModal(FileInfoComp)({ 51 | style: { 52 | position: 'absolute', 53 | zIndex: 4000 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/FileInfo/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Icon = styled.div` 4 | width: 100%; 5 | display: flex; 6 | justify-content: center; 7 | padding: 44px 0 32px; 8 | `; 9 | 10 | export const Logo = styled.div` 11 | position: relative; 12 | font-family: Lato, sans-serif; 13 | & span { 14 | position: absolute; 15 | bottom: 7px; 16 | left: 4px; 17 | width: 96%; 18 | font-weight: bold; 19 | color: white; 20 | font-size: 12px; 21 | text-overflow: ellipsis; 22 | white-space: nowrap; 23 | overflow: hidden; 24 | } 25 | `; 26 | 27 | export const Img = styled.img` 28 | position: relative; 29 | `; 30 | 31 | export const Details = { 32 | Container: styled.div` 33 | position: relative; 34 | `, 35 | Info: styled.div` 36 | display: flex; 37 | font-family: Lato, sans-serif; 38 | width: 100%; 39 | padding: 10px 0; 40 | justify-content: center; 41 | `, 42 | Label: styled.div` 43 | margin-right: 5px; 44 | width: 50%; 45 | color: #2f363f; 46 | text-align: right; 47 | `, 48 | Value: styled.div` 49 | margin-left: 5px; 50 | color: #81878c; 51 | width: 50%; 52 | text-align: left; 53 | ` 54 | }; 55 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { StyleObject } from '../../types'; 6 | 7 | interface IProps { 8 | style?: StyleObject; 9 | content: { 10 | info?: string; 11 | style?: StyleObject; 12 | onClick?: React.MouseEventHandler; 13 | }[]; 14 | } 15 | 16 | interface IState { 17 | [key: string]: boolean; 18 | } 19 | 20 | export class Menu extends Component { 21 | state = {}; 22 | 23 | render() { 24 | const { style, content } = this.props; 25 | 26 | return ( 27 | 28 | {_.compact(content).map(c => ( 29 |
30 | {c.info} 31 |
32 | ))} 33 |
34 | ); 35 | } 36 | } 37 | 38 | const Container = styled.div` 39 | position: absolute; 40 | background: white; 41 | width: 145px; 42 | z-index: 1000; 43 | border: 1px solid rgba(221, 224, 228, 0.5); 44 | box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.05); 45 | border-radius: 8px; 46 | & .content { 47 | padding: 15px; 48 | transition: background 250ms ease-in; 49 | &:hover { 50 | background: #eeeff1; 51 | } 52 | } 53 | & .items { 54 | top: 0; 55 | left: 145px; 56 | position: absolute; 57 | background: white; 58 | width: 145px; 59 | border: 1px solid rgba(221, 224, 228, 0.5); 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Navigation/GoBack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SvgIconProps } from '../../elements/SvgIcon'; 4 | 5 | export default (props: SvgIconProps) => ( 6 | 7 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | 4 | import { NavContextProps, withContext } from '../../context/NavContext'; 5 | import { getRootDirId } from '../../types'; 6 | 7 | import { Container, Path } from './styles'; 8 | import GoBack from './GoBack'; 9 | 10 | export const renderPath = (path = '') => { 11 | const pathArr = path.split('/').filter(p => p); 12 | const len = pathArr.length; 13 | const arr = [{` root `}]; 14 | 15 | pathArr.map((p, i) => { 16 | i === len - 1 17 | ? arr.push( 18 | 19 | / {p} 20 | 21 | ) 22 | : arr.push(/ {p}); 23 | }); 24 | 25 | return arr; 26 | }; 27 | 28 | interface IProps extends NavContextProps {} 29 | 30 | const NavigationComp = (props: IProps) => { 31 | // const onClickPath = (path: string) => { 32 | // props.onUpdateCurrentDir(getIdByPath(path, props.fileMap)); 33 | // }; 34 | 35 | const rootId = getRootDirId(props.fileMap); 36 | 37 | return ( 38 | 39 |
{ 42 | if (props.currentDirId !== rootId && props.fileMap[props.currentDirId]) { 43 | if (props.fileMap[props.currentDirId]) { 44 | props.onUpdateCurrentDir(props.fileMap[props.currentDirId].parentId); 45 | } 46 | } 47 | }} 48 | > 49 | 50 |
51 | 52 | {props.fileMap[props.currentDirId] && renderPath(props.fileMap[props.currentDirId].path)} 53 | 54 |
55 | ); 56 | }; 57 | 58 | export const Navigation = withContext(NavigationComp); 59 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Navigation/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | width: 60%; 5 | display: flex; 6 | @media screen and (max-width: 768px) { 7 | width: 100%; 8 | } 9 | `; 10 | 11 | export const Path = styled.p` 12 | font-family: 'Proxima Nova', sans-serif; 13 | font-size: 24px; 14 | color: #81878c; 15 | font-weight: lighter; 16 | margin: -6px 0 0 28px; 17 | word-spacing: 2px; 18 | white-space: nowrap; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | 22 | & .currentDirId { 23 | color: #001800; 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/SEO/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | interface IProps { 5 | url: string; 6 | title: string; 7 | image: string; 8 | description: string; 9 | } 10 | 11 | export class SEO extends Component { 12 | render() { 13 | const { url, title, image, description } = this.props; 14 | return ( 15 | 16 | {/* General tags */} 17 | {title} 18 | 19 | 20 | 21 | {/* OpenGraph tags */} 22 | 23 | 24 | 25 | 26 | 27 | {/* Twitter Card tags */} 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/SearchBar/Filter.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { SearchType } from '../../types'; 4 | 5 | import { FilterContainer, FilterContainerOptions } from './styles'; 6 | 7 | interface IProps { 8 | mode: SearchType; 9 | 10 | onModeChange: (mode: SearchType) => void; 11 | } 12 | 13 | export default class Filter extends Component { 14 | render() { 15 | return ( 16 | 17 | Search: 18 | 19 | this.props.onModeChange('LOCAL')} 22 | > 23 | Local 24 | 25 | this.props.onModeChange('GLOBAL')} 28 | > 29 | Global 30 | 31 | 32 | 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/SearchBar/MagnifyIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SvgIconProps } from '../../elements/SvgIcon'; 4 | 5 | export default (props: SvgIconProps) => ( 6 |
7 | 8 | 12 | 13 |
14 | ); 15 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/SearchBar/SearchResults.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | 3 | import { NavContextProps, withContext } from '../../context/NavContext'; 4 | import { SvgIcon } from '../../elements/SvgIcon'; 5 | import { FileType, StyleObject, classPrefix } from '../../types'; 6 | 7 | import { NoResult, Path, Result } from './styles'; 8 | 9 | interface IProps extends NavContextProps { 10 | data: FileType[]; 11 | term: string; 12 | style: StyleObject; 13 | 14 | onResultClose: Function; 15 | } 16 | 17 | class SearchResultsComp extends Component { 18 | onClick = (file: FileType) => { 19 | const dirId = !file.isDir ? file.parentId : file.id; 20 | 21 | this.props.onUpdateCurrentDir(dirId); 22 | this.props.onResultClose(); 23 | }; 24 | 25 | render() { 26 | const { style } = this.props; 27 | 28 | const data = this.props.data.filter(arr => arr.name.match(this.props.term) !== null); 29 | 30 | return ( 31 | 32 | {data.length > 0 ? ( 33 | data.map(file => ( 34 | this.onClick(file)} 39 | > 40 |
41 | 42 | {file.name} 43 |
44 | {file.path} 45 |
46 | )) 47 | ) : ( 48 | No Result 49 | )} 50 |
51 | ); 52 | } 53 | } 54 | 55 | export const SearchResults = withContext(SearchResultsComp); 56 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/SearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | 3 | import { NavContextProps, withContext } from '../../context/NavContext'; 4 | import { FileType, SearchType, getDirFiles } from '../../types'; 5 | 6 | import { Container, Input, Line } from './styles'; 7 | import Filter from './Filter'; 8 | import MagnifyIcon from './MagnifyIcon'; 9 | import { SearchResults } from './SearchResults'; 10 | 11 | interface IProps extends NavContextProps {} 12 | 13 | interface IState { 14 | term: string; 15 | width: string; 16 | mode: SearchType; 17 | data?: FileType[]; 18 | } 19 | 20 | class SearchBarComp extends Component { 21 | _ref = createRef(); 22 | state: IState = { 23 | term: '', 24 | width: '0', 25 | mode: 'LOCAL', 26 | data: null 27 | }; 28 | 29 | componentDidMount() { 30 | this.setState(() => { 31 | const { width } = getComputedStyle(this._ref.current); 32 | return { 33 | width 34 | }; 35 | }); 36 | } 37 | 38 | onModeChange = (mode: SearchType) => { 39 | this.setState({ 40 | mode 41 | }); 42 | }; 43 | 44 | render() { 45 | const { fileMap, currentDirId } = this.props; 46 | 47 | return ( 48 | 49 | 59 | this.setState({ term: event.target.value })} 63 | /> 64 | {this.state.term.length > 0 ? ( 65 | 66 | 67 | 68 | fileMap[id]) 75 | } 76 | onResultClose={() => this.setState({ term: '' })} 77 | /> 78 | 79 | ) : ( 80 | '' 81 | )} 82 | 83 | ); 84 | } 85 | } 86 | 87 | export const SearchBar = withContext(SearchBarComp); 88 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/SearchBar/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | position: absolute; 5 | left: -1px; 6 | top: 33px; 7 | z-index: 2000; 8 | background: #fff; 9 | border: 1px solid rgba(221, 224, 228, 0.7); 10 | box-shadow: 0 16px 64px 0 rgba(0, 0, 0, 0.08); 11 | border-radius: 8px; 12 | `; 13 | 14 | export const Line = styled.hr` 15 | background: #eeeff1; 16 | padding: 0; 17 | margin: -3px 0; 18 | opacity: 0.25; 19 | `; 20 | 21 | export const Input = styled.div` 22 | margin-top: -6px; 23 | width: 40%; 24 | position: relative; 25 | height: 32px; 26 | display: flex; 27 | border-radius: 8px; 28 | border: 1px solid #dde0e4; 29 | padding: 0 8px; 30 | font-family: Lato, sans-serif; 31 | outline: none; 32 | input { 33 | border: 0; 34 | width: 100%; 35 | font-size: 15px; 36 | padding: 0 0 0 23px; 37 | & ::placeholder { 38 | color: #afb2b6; 39 | } 40 | } 41 | @media screen and (max-width: 768px) { 42 | width: 100%; 43 | margin-top: 20px; 44 | } 45 | `; 46 | 47 | export const FilterContainer = styled.div` 48 | display: flex; 49 | padding: 10px; 50 | justify-content: flex-start; 51 | `; 52 | 53 | export const FilterContainerOptions = styled.div` 54 | border: none; 55 | & span { 56 | background: #82878b; 57 | border-radius: 8px; 58 | color: white; 59 | margin: 20px 5px; 60 | font-size: 15px; 61 | cursor: pointer; 62 | padding: 2px 10px; 63 | &.selected { 64 | background: #051702; 65 | } 66 | } 67 | `; 68 | 69 | export const Result = styled.div` 70 | padding: 10px; 71 | display: flex; 72 | white-space: nowrap; 73 | max-height: 100px; 74 | justify-content: space-between; 75 | align-items: center; 76 | border-bottom: 2px solid rgba(238, 239, 241, 0.5); 77 | &:hover { 78 | background: #eeeff1; 79 | } 80 | `; 81 | 82 | export const Path = styled.div` 83 | white-space: nowrap; 84 | overflow: hidden; 85 | text-overflow: ellipsis; 86 | color: #b1b1b1; 87 | margin-right: 32px; 88 | `; 89 | 90 | export const Img = styled.img` 91 | width: 23px; 92 | height: 20px; 93 | margin-bottom: -3px; 94 | margin-right: 6px; 95 | `; 96 | 97 | export const NoResult = styled.div` 98 | padding: 10px; 99 | display: flex; 100 | border-bottom: 2px solid rgba(238, 239, 241, 0.5); 101 | `; 102 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Sidebar/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { CollapseContainer } from './styles'; 4 | 5 | export function Collapse(props: { children: Function; index: number }) { 6 | const [visible, setVisible] = useState(false); 7 | 8 | return ( 9 | {props.children(visible, () => setVisible(!visible))} 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Sidebar/SideMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | 3 | import { NavContextProps, withContext } from '../../context/NavContext'; 4 | import { FileType } from '../../types'; 5 | 6 | import { DropDownIcon, LinkContainer } from './styles'; 7 | import { Collapse } from './Collapse'; 8 | 9 | interface IProps extends NavContextProps { 10 | fileTree: FileType[]; 11 | } 12 | 13 | interface IState { 14 | fileTree: FileType[] | null; 15 | } 16 | 17 | class SideMenuComp extends Component { 18 | state: IState = { 19 | fileTree: null 20 | }; 21 | 22 | static getDerivedStateFromProps(nextProps: IProps) { 23 | return { 24 | fileTree: nextProps.fileTree 25 | }; 26 | } 27 | 28 | // shouldComponentUpdate(nextProps, nextState) { 29 | // console.log( 30 | // entriesAreSame(nextProps.fileStructure, this.props.fileStructure) 31 | // ); 32 | // if (entriesAreSame(nextProps.fileStructure, this.props.fileStructure)) { 33 | // return false; 34 | // } 35 | // return true; 36 | // } 37 | 38 | renderTree = (children: FileType[], value: number) => { 39 | const i = value + 1; 40 | 41 | return children && children.length > 0 42 | ? children.map((entry, _) => { 43 | if (!entry.isDir) return; 44 | const flag = entry.children ? (entry.children.length ? true : false) : false; 45 | if (!flag) { 46 | return ( 47 | this.props.onUpdateCurrentDir(entry.id)} 50 | className={this.props.currentDirId === entry.id ? 'selected' : ''} 51 | > 52 |
53 | {entry.name} 54 |
55 |
56 | ); 57 | } 58 | return ( 59 | 60 | {(visible: boolean, setVisible: Function) => { 61 | return ( 62 | 63 | 67 |
this.props.onUpdateCurrentDir(entry.id)} 74 | > 75 | {entry.name} 76 |
77 |
setVisible()}> 78 | 79 |
80 |
81 |
82 | {visible ? this.renderTree(entry.children, i) : ''} 83 |
84 |
85 | ); 86 | }} 87 |
88 | ); 89 | }) 90 | : ''; 91 | }; 92 | 93 | render() { 94 | return {this.renderTree(this.state.fileTree, 0)}; 95 | } 96 | } 97 | 98 | export const SideMenu = withContext(SideMenuComp); 99 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { NavContextProps, withContext } from '../../context/NavContext'; 4 | import { FileType } from '../../types'; 5 | 6 | import { Root, ShowMenu, SideBarContainer } from './styles'; 7 | import { SideMenu } from './SideMenu'; 8 | 9 | interface IProps extends NavContextProps { 10 | fileTree: FileType[]; 11 | } 12 | 13 | const SidebarComp = ({ fileTree, onUpdateCurrentDir }: IProps) => { 14 | const children = fileTree[0].children; 15 | 16 | const [toggle, handleToggle] = useState(true); 17 | 18 | return ( 19 | 20 | handleToggle(!toggle)} /> 21 | { 24 | onUpdateCurrentDir('/'); 25 | }} 26 | > 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export const Sidebar = withContext(SidebarComp); 35 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/Sidebar/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SideBarContainer = styled.section<{ toggle: boolean }>` 4 | background: #f9fafc; 5 | width: 320px; 6 | height: 100%; 7 | position: absolute; 8 | z-index: 2000; 9 | & .rootLink { 10 | text-decoration: none; 11 | } 12 | transition: margin-left 250ms ease-in; 13 | @media screen and (max-width: 768px) { 14 | margin-left: ${props => (props.toggle ? '-320px' : '0')}; 15 | } 16 | `; 17 | 18 | export const ShowMenu = styled.div` 19 | width: 34px; 20 | height: 27px; 21 | position: absolute; 22 | top: 10px; 23 | right: -28px; 24 | background: linear-gradient(to right, #f9fafc, #acb9c3); 25 | border-radius: 3px; 26 | box-shadow: 5px 0 0 #888; 27 | display: none; 28 | z-index: 2000; 29 | cursor: pointer; 30 | @media screen and (max-width: 768px) { 31 | display: block; 32 | } 33 | `; 34 | 35 | export const Root = styled.div` 36 | font-family: Lato, sans-serif; 37 | font-weight: bolder; 38 | padding: 32px; 39 | text-decoration: none; 40 | color: #afb2b6; 41 | &:after { 42 | content: 'Root'; 43 | text-transform: uppercase; 44 | } 45 | `; 46 | 47 | export const LinkContainer = styled.div` 48 | width: 100%; 49 | position: relative; 50 | padding: 7px 32px; 51 | font-family: Lato, sans-serif; 52 | font-size: 16px; 53 | cursor: pointer; 54 | align-content: space-around; 55 | color: #2f363f; 56 | transition: background 250ms ease-in; 57 | & .dropdown { 58 | float: right; 59 | height: 20px; 60 | width: 20px; 61 | display: flex; 62 | margin-top: -22px; 63 | justify-content: center; 64 | align-items: center; 65 | } 66 | 67 | &.selected { 68 | background: #eeeff1; 69 | } 70 | 71 | &:hover { 72 | background: #eeeff1; 73 | } 74 | `; 75 | 76 | export const DropDownIcon = styled.div` 77 | width: 2px; 78 | height: 2px; 79 | border-left: 5px solid transparent; 80 | border-right: 5px solid transparent; 81 | border-bottom: 5px solid #30363e; 82 | transition: transform 150ms ease-in; 83 | &.clicked { 84 | transform: rotate(180deg); 85 | } 86 | `; 87 | 88 | export const Bar = styled.div` 89 | height: 20px; 90 | width: 5px; 91 | position: absolute; 92 | z-index: 20; 93 | background: red; 94 | `; 95 | 96 | export const CollapseContainer = styled.div` 97 | position: relative; 98 | &:after { 99 | content: ''; 100 | position: absolute; 101 | } 102 | `; 103 | 104 | export const Line = styled.div` 105 | &:after { 106 | content: ''; 107 | position: absolute; 108 | width: 2px; 109 | background: black; 110 | height: 17px; 111 | left: 40px; 112 | top: 8px; 113 | } 114 | `; 115 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/ToggleSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React, { ReactNode } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { StyleObject } from '../../types'; 6 | 7 | export interface ToggleSwitchProps { 8 | style?: StyleObject; 9 | checkedValue?: boolean; 10 | checkedChildren?: string | ReactNode; 11 | unCheckedChildren?: string | ReactNode; 12 | onChange?: (checked: boolean) => void; 13 | } 14 | 15 | export const ToggleSwitch = ({ 16 | style, 17 | checkedValue, 18 | checkedChildren, 19 | unCheckedChildren, 20 | onChange 21 | }: ToggleSwitchProps) => { 22 | const [isChecked, setChecked] = React.useState(false); 23 | return ( 24 |
25 | 26 | { 31 | if (!_.isUndefined(checkedValue) && onChange) { 32 | onChange(checked); 33 | return; 34 | } 35 | setChecked(checked); 36 | }} 37 | /> 38 | 39 | {checkedChildren && unCheckedChildren && ( 40 | {checkedValue || isChecked ? checkedChildren : unCheckedChildren} 41 | )} 42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | const CheckBoxWrapper = styled.div` 49 | position: relative; 50 | `; 51 | const CheckBoxLabel = styled.label` 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | width: 60px; 56 | height: 26px; 57 | border-radius: 15px; 58 | background: #bebebe; 59 | cursor: pointer; 60 | & > span { 61 | color: #fff; 62 | font-size: 14px; 63 | line-height: 26px; 64 | margin: 0 5px 0 25px; 65 | transition: margin 0.2s; 66 | } 67 | &:before { 68 | position: absolute; 69 | left: 4px; 70 | top: 50%; 71 | transform: translateY(-50%); 72 | content: ''; 73 | display: block; 74 | border-radius: 50%; 75 | width: 18px; 76 | height: 18px; 77 | background: #fff; 78 | box-shadow: 1px 3px 3px 1px rgba(0, 0, 0, 0.2); 79 | transition: left 0.2s; 80 | } 81 | `; 82 | const CheckBox = styled.input` 83 | opacity: 0; 84 | z-index: 1; 85 | width: 60px; 86 | height: 26px; 87 | border-radius: 15px; 88 | &:checked + ${CheckBoxLabel} { 89 | background: rgb(103, 182, 249); 90 | &:before { 91 | content: ''; 92 | display: block; 93 | border-radius: 50%; 94 | width: 18px; 95 | height: 18px; 96 | left: 37px; 97 | transition: left 0.2s; 98 | } 99 | & > span { 100 | color: #fff; 101 | font-size: 14px; 102 | margin: 0 25px 0 6px; 103 | transition: margin 0.2s; 104 | } 105 | } 106 | `; 107 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/UfFileManager/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { NavContext } from '../../context/NavContext'; 5 | import { SvgIcon } from '../../elements/SvgIcon'; 6 | import { FileType, classPrefix } from '../../types'; 7 | import { FileGrid } from '../FileGrid'; 8 | import { Navigation } from '../Navigation'; 9 | import { SearchBar } from '../SearchBar'; 10 | import { SEO } from '../SEO'; 11 | import { ToggleSwitch } from '../ToggleSwitch'; 12 | 13 | interface IProps { 14 | fileMap: Record; 15 | currentDirId?: string; 16 | withSEO?: boolean; 17 | isCombineEnabled?: boolean; 18 | 19 | onToggleSwitch?: (checked: boolean) => void; 20 | renderAddFileElement?: ({ onClose }: { onClose: Function }) => JSX.Element; 21 | onOpenMenu?: (id: string) => void; 22 | onAdd?: (file: FileType) => void; 23 | onDelete?: (id: string, isDir: boolean) => void; 24 | onEnter?: (id: string) => void; 25 | onCopyTo?: (file: FileType) => void; 26 | onClickPreview?: (file: FileType) => void; 27 | onMoveTo?: ({ 28 | ids, 29 | targetCategoryId, 30 | showModal 31 | }: { 32 | showModal?: boolean; 33 | targetCategoryId?: string; 34 | ids?: { entryId: string; isDir: boolean }[]; 35 | }) => void; 36 | } 37 | 38 | interface IState { 39 | isMultiple: boolean; 40 | currentDirId: string; 41 | } 42 | 43 | export class UfFileManager extends Component { 44 | static defaultProps = { 45 | withSEO: false 46 | }; 47 | 48 | state = { currentDirId: this.props.currentDirId, isMultiple: false }; 49 | 50 | componentWillReceiveProps(nextProps: IProps) { 51 | // 当外部传入的 Path 发生变化时候 52 | if (nextProps.currentDirId !== this.props.currentDirId) { 53 | this.onUpdateCurrentDir(nextProps.currentDirId); 54 | } 55 | } 56 | 57 | onUpdateCurrentDir = (currentDirId: string) => { 58 | this.setState({ currentDirId }); 59 | 60 | if (this.props.onEnter) { 61 | this.props.onEnter(currentDirId); 62 | } 63 | }; 64 | 65 | render() { 66 | const { 67 | fileMap, 68 | withSEO, 69 | isCombineEnabled, 70 | renderAddFileElement, 71 | onAdd, 72 | onDelete, 73 | onMoveTo, 74 | onClickPreview, 75 | onToggleSwitch 76 | } = this.props; 77 | const { isMultiple, currentDirId } = this.state; 78 | 79 | return ( 80 | 92 | {withSEO && ( 93 | ) as unknown) as string} 97 | description={currentDirId} 98 | /> 99 | )} 100 | 101 | 102 | 103 | 104 | { 110 | this.setState({ isMultiple }); 111 | onToggleSwitch(isMultiple); 112 | }} 113 | /> 114 | 115 | 116 | 117 | 118 | ); 119 | } 120 | } 121 | 122 | const Container = styled.div` 123 | padding: 41px; 124 | transition: margin-left 250ms ease-in; 125 | @media screen and (max-width: 768px) { 126 | margin-left: 0; 127 | padding: 55px 15px 15px 15px; 128 | } 129 | `; 130 | 131 | const TopBar = styled.div` 132 | display: flex; 133 | align-items: center; 134 | @media screen and (max-width: 768px) { 135 | display: block; 136 | } 137 | `; 138 | -------------------------------------------------------------------------------- /packages/fm-components/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Add'; 2 | export * from './FileCreator'; 3 | export * from './FileGrid'; 4 | export * from './FileIcon'; 5 | export * from './FileInfo'; 6 | export * from './Menu'; 7 | export * from './Navigation'; 8 | export * from './SearchBar'; 9 | export * from './SEO'; 10 | export * from './Sidebar'; 11 | export * from './UfFileManager'; 12 | -------------------------------------------------------------------------------- /packages/fm-components/src/context/NavContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react'; 2 | 3 | import { FileType } from '../types'; 4 | 5 | export interface NavContextProps { 6 | currentDirId?: string; 7 | fileMap?: Record; 8 | isCombineEnabled?: boolean; 9 | renderAddFileElement?: ({ onClose }: { onClose: Function }) => void; 10 | 11 | onToggleSwitch?: (checked: boolean) => void; 12 | onUpdateCurrentDir?: (newPath: string) => void; 13 | onClickPreview?: (file: FileType) => void; 14 | onMoveTo?: ({ 15 | ids, 16 | targetCategoryId, 17 | showModal 18 | }: { 19 | showModal?: boolean; 20 | targetCategoryId?: string; 21 | ids?: { entryId: string; isDir: boolean }[]; 22 | }) => void; 23 | } 24 | 25 | export const NavContext: React.Context = createContext({ 26 | currentDirId: '/' 27 | }); 28 | 29 | export const withContext =

( 30 | Component: React.ComponentType

31 | ): React.FC> => props => ( 32 | 33 | {({ 34 | fileMap, 35 | currentDirId, 36 | isCombineEnabled, 37 | onMoveTo, 38 | onClickPreview, 39 | onToggleSwitch, 40 | onUpdateCurrentDir, 41 | renderAddFileElement 42 | }) => ( 43 | 54 | )} 55 | 56 | ); 57 | -------------------------------------------------------------------------------- /packages/fm-components/src/elements/SvgIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { StyleObject } from '../types'; 4 | 5 | export interface SvgIconProps { 6 | name?: 'close' | 'file' | 'folder' | 'png-file'; 7 | size?: number; 8 | color?: string; 9 | 10 | style?: StyleObject; 11 | fill?: string; 12 | } 13 | 14 | export const SvgIcon = (props: SvgIconProps) => { 15 | const { size = 10, color } = props; 16 | 17 | switch (props.name) { 18 | case 'close': { 19 | return ( 20 | 21 | 25 | 26 | ); 27 | } 28 | case 'file': { 29 | return ( 30 | 31 | 35 | 39 | 40 | ); 41 | } 42 | case 'folder': { 43 | return ( 44 | 45 | 49 | 53 | 54 | ); 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /packages/fm-components/src/elements/withModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { StyleObject } from '../types'; 5 | 6 | import { SvgIcon } from './SvgIcon'; 7 | 8 | export interface ModalProps { 9 | style?: StyleObject; 10 | isDraggale?: boolean; 11 | title?: string; 12 | 13 | onClose?: Function; 14 | } 15 | 16 | export interface IModalState { 17 | originalX: number; 18 | originalY: number; 19 | lastTranslateX: number; 20 | lastTranslateY: number; 21 | translateX: number; 22 | translateY: number; 23 | isDragging: boolean; 24 | } 25 | 26 | export const withModal = (WrappedComponent: React.ComponentType) => ( 27 | params: ModalProps 28 | ) => 29 | class Modal extends Component { 30 | _ref = createRef(); 31 | state = { 32 | isDragging: false, 33 | originalX: 0, 34 | originalY: 0, 35 | translateX: 0, 36 | translateY: 0, 37 | lastTranslateX: 0, 38 | lastTranslateY: 0 39 | }; 40 | 41 | componentDidMount() {} 42 | 43 | componentWillUnmount() { 44 | this._ref.current.removeEventListener('mousemove', this.handleMouseMove); 45 | this._ref.current.removeEventListener('mouseup', this.handleMouseUp); 46 | } 47 | 48 | handleMouseDown = ({ clientX, clientY }: MouseEvent) => { 49 | if (this.props.isDraggale) { 50 | return; 51 | } 52 | 53 | this._ref.current.addEventListener('mousemove', this.handleMouseMove); 54 | this._ref.current.addEventListener('mouseup', this.handleMouseUp); 55 | 56 | this.setState({ 57 | originalX: clientX, 58 | originalY: clientY, 59 | isDragging: true 60 | }); 61 | }; 62 | 63 | handleMouseMove = ({ clientX, clientY }: MouseEvent) => { 64 | const { isDragging } = this.state; 65 | if (!isDragging) { 66 | return; 67 | } 68 | 69 | this.setState(prevState => ({ 70 | translateX: clientX - prevState.originalX + prevState.lastTranslateX, 71 | translateY: clientY - prevState.originalY + prevState.lastTranslateY 72 | })); 73 | }; 74 | 75 | handleMouseUp = () => { 76 | this._ref.current.removeEventListener('mousemove', this.handleMouseMove); 77 | this._ref.current.removeEventListener('mouseup', this.handleMouseUp); 78 | 79 | this.setState({ 80 | originalX: 0, 81 | originalY: 0, 82 | lastTranslateX: this.state.translateX, 83 | lastTranslateY: this.state.translateY, 84 | 85 | isDragging: false 86 | }); 87 | }; 88 | 89 | render() { 90 | const { translateX, translateY } = this.state; 91 | const style = params.style ? params.style : this.props.style ? this.props.style : {}; 92 | return ( 93 | 103 | 104 | {this.props.title} 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ); 113 | } 114 | }; 115 | 116 | const Container = styled.div` 117 | width: 316px; 118 | position: relative; 119 | z-index: 4000; 120 | padding: 20px 0 44px; 121 | background: #fff; 122 | border: 1px solid rgba(221, 224, 228, 0.7); 123 | box-shadow: 0 16px 64px 0 rgba(0, 0, 0, 0.08); 124 | border-radius: 8px; 125 | `; 126 | 127 | const Title = styled.div` 128 | font-family: Lato, sans-serif; 129 | font-size: 18px; 130 | color: #2f363f; 131 | letter-spacing: 0; 132 | `; 133 | 134 | const Heading = styled.div` 135 | width: 100%; 136 | display: flex; 137 | justify-content: center; 138 | `; 139 | 140 | const Close = styled.div` 141 | position: absolute; 142 | top: 10px; 143 | right: 24px; 144 | padding: 13px; 145 | cursor: pointer; 146 | `; 147 | -------------------------------------------------------------------------------- /packages/fm-components/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export function i18n(messageId: string) { 2 | return messageId; 3 | } 4 | -------------------------------------------------------------------------------- /packages/fm-components/src/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | } 4 | -------------------------------------------------------------------------------- /packages/fm-components/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './components'; 3 | -------------------------------------------------------------------------------- /packages/fm-components/src/types/FileType.ts: -------------------------------------------------------------------------------- 1 | export interface FileType { 2 | // ID 3 | id?: string; 4 | // 父文件夹 id 5 | parentId?: string | null; 6 | 7 | // 是否为目录 8 | isDir: boolean; 9 | // 文件类型 10 | ext?: string; 11 | 12 | // 名称 13 | name?: string; 14 | // 创建者 15 | creatorName?: string; 16 | // 路径,完整的文件路径 17 | path?: string; 18 | // 大小 in Bytes 19 | size?: number; 20 | 21 | // 创建、更新、删除的时间 22 | createdAt?: string; 23 | updatedAt?: string; 24 | deletedAt?: string; 25 | 26 | // 子路径,字符串或者文件类型 27 | childrenIds?: string[]; 28 | children?: FileType[]; 29 | 30 | // 用于记述原始对象 31 | originObj?: any; 32 | } 33 | -------------------------------------------------------------------------------- /packages/fm-components/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export type StyleObject = Record; 2 | -------------------------------------------------------------------------------- /packages/fm-components/src/types/constants.ts: -------------------------------------------------------------------------------- 1 | export const classPrefix = 'fmc'; 2 | export const LOCAL = '__local__'; 3 | export const GLOBAL = '__global__'; 4 | 5 | export type SearchType = 'LOCAL' | 'GLOBAL'; 6 | -------------------------------------------------------------------------------- /packages/fm-components/src/types/datetime.ts: -------------------------------------------------------------------------------- 1 | const monthNames = [ 2 | 'Jan', 3 | 'Feb', 4 | 'Mar', 5 | 'Apr', 6 | 'May', 7 | 'Jun', 8 | 'Jul', 9 | 'Aug', 10 | 'Sep', 11 | 'Oct', 12 | 'Nov', 13 | 'Dec' 14 | ]; 15 | 16 | const nth = (d: number) => { 17 | if (d > 3 && d < 21) return 'th'; 18 | switch (d % 10) { 19 | case 1: 20 | return 'st'; 21 | case 2: 22 | return 'nd'; 23 | case 3: 24 | return 'rd'; 25 | default: 26 | return 'th'; 27 | } 28 | }; 29 | 30 | /** 获取今日的时间字符串 */ 31 | export const getTodayDate = () => { 32 | const d = new Date(); 33 | let month = '' + (d.getMonth() + 1), 34 | day = '' + d.getDate(); 35 | const year = d.getFullYear(); 36 | 37 | if (month.length < 2) month = '0' + month; 38 | if (day.length < 2) day = '0' + day; 39 | 40 | return [year, month, day].join('-'); 41 | }; 42 | 43 | export const formatDate = (value: string | number) => { 44 | const date = new Date(value); 45 | const day = date.getDate(), 46 | monthIndex = date.getMonth(), 47 | year = date.getFullYear().toString(); 48 | return `${day}${nth(day)} ${monthNames[monthIndex]}, ${year}`; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/fm-components/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './constants'; 3 | export * from './datetime'; 4 | export * from './FileType'; 5 | export * from './path'; 6 | export * from './utils'; 7 | -------------------------------------------------------------------------------- /packages/fm-components/src/types/path.tsx: -------------------------------------------------------------------------------- 1 | import { FileType } from './FileType'; 2 | 3 | /** 获取全部路径的集合 */ 4 | export const getPathSet = (fileMap: Record) => { 5 | const pathSet = new Set(); 6 | 7 | Object.keys(fileMap).forEach(id => { 8 | pathSet.add(fileMap[id].path); 9 | pathSet.add(id); 10 | }); 11 | 12 | return pathSet; 13 | }; 14 | 15 | /** 从 fileMap 中获取到根文件夹 id */ 16 | export const getRootDirId = (fileMap: Record) => { 17 | let dirId: string | null = null; 18 | 19 | Object.keys(fileMap).forEach(id => { 20 | if (fileMap[id].path === '/') { 21 | dirId = id; 22 | } 23 | }); 24 | 25 | return dirId; 26 | }; 27 | 28 | export const getIdByPath = (path: string, fileMap: Record) => { 29 | let dirId: string | null = null; 30 | 31 | Object.keys(fileMap).forEach(id => { 32 | if (fileMap[id].path === path) { 33 | dirId = id; 34 | } 35 | }); 36 | 37 | return dirId; 38 | }; 39 | 40 | /** 获取返回的路径 */ 41 | export const getGoBackPath = (path: string) => { 42 | const newPath = path.split('/'); 43 | 44 | newPath.splice(newPath.length - 1, 1); 45 | 46 | return newPath.join('/') || '/'; 47 | }; 48 | 49 | /** 根据某个路径获取当前的文件列表 */ 50 | export function getDirFiles(dirId: string, fileMap: Record) { 51 | const files: FileType[] = []; 52 | 53 | Object.keys(fileMap).forEach(id => { 54 | if (fileMap[id].parentId === dirId) { 55 | files.push(fileMap[id]); 56 | } 57 | }); 58 | 59 | return files; 60 | } 61 | -------------------------------------------------------------------------------- /packages/fm-components/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | import { FileType } from './FileType'; 2 | 3 | /** 复制某个对象 */ 4 | export const cloneObj = (obj: T): T => { 5 | if (Object(obj) !== obj) return (obj as unknown) as T; 6 | else if (Array.isArray(obj)) return (obj.map(cloneObj) as unknown) as T; 7 | 8 | return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, cloneObj(v)])); 9 | }; 10 | 11 | /** 获取文件后缀 */ 12 | export const getFileExt = (file: FileType) => { 13 | const ext = file.name.split('.').filter(el => el); 14 | 15 | return ext.length >= 2 ? ext[ext.length - 1] : ''; 16 | }; 17 | 18 | /** 判断重名的文件数目 */ 19 | export const getSameNameCnt = (fileMap: Record, entry: FileType) => { 20 | let no = 0; 21 | 22 | fileMap[entry.parentId].childrenIds.forEach((elementId: string) => { 23 | if (fileMap[elementId].name.includes(entry.name) && fileMap[elementId].isDir === entry.isDir) { 24 | no++; 25 | } 26 | }); 27 | 28 | return no; 29 | }; 30 | 31 | /** 从 Map 当中生成 Tree */ 32 | export const generateTreeFromMap = (_fileMap: Record) => { 33 | const rootFiles: FileType[] = []; 34 | 35 | const fileMap: Record = cloneObj>(_fileMap); // create empty list to hold copy 36 | 37 | Object.keys(fileMap).forEach((nodeId: string) => { 38 | // 如果是根目录,则添加到根文件列表中 39 | if (!fileMap[nodeId].parentId) { 40 | return rootFiles.push(fileMap[nodeId]); 41 | } 42 | 43 | const parentId = fileMap[nodeId].parentId; 44 | 45 | if (fileMap[parentId]) { 46 | // 判断是否已存在父 47 | const index = fileMap[parentId].childrenIds.indexOf(nodeId); 48 | if (index > -1) { 49 | fileMap[parentId].children.splice(index, 1); 50 | } else if (fileMap[nodeId].isDir) { 51 | fileMap[parentId].children.push(fileMap[nodeId]); 52 | fileMap[parentId].childrenIds.push(nodeId); 53 | } 54 | } 55 | }); 56 | 57 | return { rootFiles, fileMap }; 58 | }; 59 | 60 | /** 判断文件是否相同 */ 61 | export const entriesAreSame = (x: any, y: any) => { 62 | for (const p in x) { 63 | if (x.hasOwnProperty(p) !== y.hasOwnProperty(p)) return false; 64 | 65 | if (x[p] === null && y[p] !== null) return false; 66 | if (x[p] === null && y[p] !== null) return false; 67 | 68 | if (typeof x[p] === 'object') { 69 | if (!entriesAreSame(x[p], y[p])) { 70 | return false; 71 | } 72 | } else if (x[p] != y[p]) return false; 73 | } 74 | 75 | return true; 76 | }; 77 | -------------------------------------------------------------------------------- /packages/fm-components/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist/cjs", 6 | "declaration": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/fm-components/tsconfig.es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es6", 5 | "outDir": "dist/es", 6 | "declaration": true, 7 | "declarationDir": "dist/types" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/fm-components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/fm-components/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../tslint.json"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/fm-core/.eslintignore: -------------------------------------------------------------------------------- 1 | !/.*.js 2 | *.min.* 3 | *.production.* 4 | *.md 5 | *.js 6 | *.json 7 | 8 | coverage 9 | dist 10 | node_modules 11 | build 12 | scripts 13 | -------------------------------------------------------------------------------- /packages/fm-core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../../.eslintrc.js") -------------------------------------------------------------------------------- /packages/fm-core/.vscode/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | { 6 | "language": "typescript", 7 | "autoFix": true 8 | }, 9 | { 10 | "language": "typescriptreact", 11 | "autoFix": true 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/fm-core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@wx-fc', 5 | { 6 | import: true, 7 | react: true, 8 | typescript: true, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/fm-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@m-fe/ts-lib", 3 | "version": "0.0.1", 4 | "description": "@m-fe/ts-lib", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/wx-chevalier/fe-boilerplates" 8 | }, 9 | "author": "wx-chevalier@github", 10 | "license": "MIT", 11 | "main": "dist/cjs/index.js", 12 | "module": "dist/es/index.js", 13 | "types": "dist/types/index.d.ts", 14 | "files": [ 15 | "dist/" 16 | ], 17 | "keywords": [ 18 | "webpack", 19 | "react" 20 | ], 21 | "scripts": { 22 | "build": "npm run build:es && npm run build:cjs && npm run build:umd", 23 | "build:cjs": "tsc --project ./tsconfig.cjs.json", 24 | "build:es": "tsc --project ./tsconfig.es.json", 25 | "build:umd": "cross-env NODE_ENV=production webpack -p --config ./scripts/webpack/webpack.config.umd.js", 26 | "clean": "rimraf dist", 27 | "dev": "tsc -w --project ./tsconfig.cjs.json", 28 | "lint": "run-p lint:*", 29 | "lint:es": "cross-env PARSER_NO_WATCH=true eslint . --cache --ext js,md,ts,tsx -f friendly --max-warnings 10", 30 | "lint:ts": "tslint -p . -t stylish", 31 | "lint:tsc": "tsc -p tsconfig.json --incremental false --noEmit", 32 | "test": "jest --config ./scripts/jest/jest.config.js", 33 | "test:cov": "npm run cleanCov && npm test -- --coverage", 34 | "test:watch": "npm test -- --watch" 35 | }, 36 | "dependencies": {}, 37 | "devDependencies": { 38 | "@wx-fc/app-config": "^0.3.3", 39 | "cross-env": "^6.0.3", 40 | "webpack": "^4.41.2" 41 | }, 42 | "browserslist": [ 43 | "extends @wx-fc/browserslist-config/modern" 44 | ], 45 | "commitlint": { 46 | "extends": [ 47 | "@wx-fc" 48 | ] 49 | }, 50 | "prettier": "@wx-fc/prettier-config/semi", 51 | "remarkConfig": { 52 | "plugins": [ 53 | "@wx-fc/remark-config" 54 | ] 55 | }, 56 | "stylelint": { 57 | "extends": [ 58 | "@wx-fc/stylelint-config", 59 | "@wx-fc/stylelint-config/modules" 60 | ], 61 | "rules": { 62 | "font-family-no-missing-generic-family-keyword": null, 63 | "no-descending-specificity": null, 64 | "plugin/no-unsupported-browser-features": null, 65 | "plugin/no-low-performance-animation-properties": null 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/fm-core/scripts/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('../../../../scripts/jest/jest.config'); 2 | 3 | module.exports = baseConfig; 4 | -------------------------------------------------------------------------------- /packages/fm-core/scripts/webpack/webpack.config.umd.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | const umdConfig = require('../../../../scripts/webpack/webpack.config') 5 | .umdConfig; 6 | 7 | const rootPath = process.cwd(); 8 | 9 | module.exports = merge(umdConfig, { 10 | output: { 11 | library: 'rtwCore', 12 | }, 13 | entry: { 14 | index: path.resolve(rootPath, './src/index.ts'), 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/fm-core/src/constant/types.ts: -------------------------------------------------------------------------------- 1 | export interface IBasicModule { 2 | // 模块编号 3 | id: string; 4 | // 模块的加载文件路径 5 | module: string; 6 | // 版本 7 | version?: string; 8 | } 9 | 10 | export interface IAppModule extends IBasicModule { 11 | // 模块标题 12 | name: string; 13 | // 引入的 CSS 路径 14 | css?: string | string[]; 15 | } 16 | 17 | /** 初始化参数 */ 18 | export interface IInitOption { 19 | apps: IAppModule[]; 20 | vendors?: IBasicModule[]; 21 | } 22 | -------------------------------------------------------------------------------- /packages/fm-core/src/func/__test__/sum.test.ts: -------------------------------------------------------------------------------- 1 | import { sum } from '../sum'; 2 | 3 | test('sum', () => { 4 | expect(sum(1, 2)).toBe(3); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/fm-core/src/func/sum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 计算两个值之和 3 | * @param num1 4 | * @param num2 5 | */ 6 | export function sum(num1: number, num2: number) { 7 | return num1 + num2; 8 | } 9 | -------------------------------------------------------------------------------- /packages/fm-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constant/types'; 2 | export { sum } from './func/sum'; 3 | export * from './utils/log'; 4 | 5 | export const library = 'rtwCore'; 6 | -------------------------------------------------------------------------------- /packages/fm-core/src/registry/Registry.ts: -------------------------------------------------------------------------------- 1 | /** 默认的核心注册类 */ 2 | export class Registry { 3 | localMap: Map = new Map(); 4 | 5 | set(key: string, value: object) { 6 | this.localMap.set(key, value); 7 | } 8 | } 9 | 10 | export default new Registry(); 11 | -------------------------------------------------------------------------------- /packages/fm-core/src/utils/log.ts: -------------------------------------------------------------------------------- 1 | export function logError(...args: object[]) { 2 | // tslint:disable-next-line 3 | console.log(...args); 4 | } 5 | -------------------------------------------------------------------------------- /packages/fm-core/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist/cjs", 6 | "declaration": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/fm-core/tsconfig.es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es6", 5 | "outDir": "dist/es", 6 | "declaration": true, 7 | "declarationDir": "dist/types" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/fm-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/fm-core/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/fm-core/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../tslint.json"] 3 | } 4 | -------------------------------------------------------------------------------- /scripts/jest/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@wx-fc/jest-config/jest.config'); 2 | -------------------------------------------------------------------------------- /scripts/tools/publish_pkgs.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/web-file-manager/4b2e2cea85dc9ca2496036701a2116c1dcfd46bd/scripts/tools/publish_pkgs.sh -------------------------------------------------------------------------------- /scripts/tools/release_pkgs.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/web-file-manager/4b2e2cea85dc9ca2496036701a2116c1dcfd46bd/scripts/tools/release_pkgs.sh -------------------------------------------------------------------------------- /scripts/tools/start_micro.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | (cd packages/rtw-host-app && npm start) -------------------------------------------------------------------------------- /scripts/tools/start_mono.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | (cd packages/rtw-host-app && npm start) -------------------------------------------------------------------------------- /scripts/tools/upgrade_pkgs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | ncu -u 5 | 6 | (cd ./packages/rtw-core && ncu -u) 7 | (cd ./packages/rtw-bootstrap && ncu -u) 8 | (cd ./packages/rtw-host-app && ncu -u) 9 | (cd ./packages/rtw-mobx-app && ncu -u) 10 | -------------------------------------------------------------------------------- /scripts/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@wx-fc/webpack-config')({ 2 | useCssModule: false, 3 | themeVars: { 4 | 'primary-color': '#5d4bff', 5 | }, 6 | extendedBaseConfig: { 7 | module: { 8 | rules: [ 9 | // svg 的加载交于应用自身决定 10 | { 11 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 12 | oneOf: [ 13 | { 14 | issuer: /\.[jt]sx?$/, 15 | use: [ 16 | { 17 | loader: '@svgr/webpack', 18 | // loader: 'svg-inline-loader', 19 | }, 20 | ], 21 | }, 22 | { 23 | loader: 'url-loader', 24 | }, 25 | ], 26 | }, 27 | ], 28 | }, 29 | resolve: { 30 | alias: { 31 | dayjs: 'dayjs/esm', 32 | moment$: 'dayjs/esm', 33 | systemjs$: 'systemjs/dist/system.js', 34 | }, 35 | }, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@wx-fc/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["packages/rtw-host-app/src/*"] 7 | }, 8 | "strictFunctionTypes": false, 9 | "strictNullChecks": false, 10 | "suppressImplicitAnyIndexErrors": true 11 | }, 12 | "include": ["**/*.ts", "**/*.tsx"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@wx-fc/tslint-config/react", "tslint-config-eslint/prettier"], 3 | "rules": { 4 | "jsx-no-lambda": false, 5 | "ordered-imports": [ 6 | true, 7 | { 8 | "grouped-imports": true, 9 | "import-sources-order": "lowercase-first", 10 | "named-imports-order": "lowercase-last", 11 | "groups": [ 12 | "^(?!rtw-)(@[^/]|[^@.])", 13 | "^(@/|rtw-)", 14 | "^(../)+", 15 | "^(?!\\./.*\\.less)" 16 | ] 17 | } 18 | ] 19 | }, 20 | "linterOptions": { 21 | "exclude": ["**/*.d.ts"] 22 | } 23 | } 24 | --------------------------------------------------------------------------------