├── src ├── views │ ├── NoMatch.js │ ├── index.js │ ├── Layout.js │ ├── Share.js │ └── Home.js ├── styles │ ├── toolbar.scss │ ├── images │ │ ├── head.png │ │ ├── main_logo.png │ │ ├── EmptySession.png │ │ ├── PhoneEmpty.png │ │ └── fileType │ │ │ ├── Apps.png │ │ │ ├── ApkType.png │ │ │ ├── CADType.png │ │ │ ├── DocType.png │ │ │ ├── ExeType.png │ │ │ ├── ImgType.png │ │ │ ├── IpaType.png │ │ │ ├── PdfType.png │ │ │ ├── PptType.png │ │ │ ├── RarType.png │ │ │ ├── TxtType.png │ │ │ ├── VsdType.png │ │ │ ├── XlsType.png │ │ │ ├── FolderType.png │ │ │ ├── MusicType.png │ │ │ ├── OtherType.png │ │ │ ├── VideoType.png │ │ │ ├── MixFileType.png │ │ │ ├── PS_54_93523dc.png │ │ │ └── TorrentType.png │ ├── index.scss │ ├── _var.scss │ ├── footer.scss │ ├── share.scss │ ├── button.scss │ ├── modal.scss │ ├── taskbar.scss │ ├── menu.scss │ ├── header.scss │ ├── icon.scss │ └── layout.scss ├── stores │ ├── select.js │ ├── login.js │ ├── index.js │ ├── user.js │ ├── window.js │ ├── history.js │ ├── item.js │ └── files.js ├── components │ ├── list.js │ ├── thumb.js │ ├── titlebar.js │ ├── layout.js │ ├── sider.js │ ├── content.js │ ├── dropdown.js │ ├── index.js │ ├── footer.js │ ├── modal.js │ ├── button.js │ ├── toolbar.js │ ├── breadcrumb.js │ ├── menu.js │ ├── taskbar.js │ ├── icon.js │ └── header.js ├── constant │ └── index.js ├── index.js ├── index.html ├── container │ └── index.js ├── routes │ └── index.js └── utils │ └── index.js ├── jsconfig.json ├── .compilerc ├── package ├── resource │ ├── logo.ico │ ├── logo.png │ ├── logo@2x.png │ └── logo@1.5x.png ├── yarn.lock ├── README.md └── package.json ├── .gitignore ├── backend ├── controller │ ├── index.js │ ├── user.js │ └── file.js ├── config.js ├── nedbPromise.js ├── index.js └── utils.js ├── webpack.config.js ├── .editorconfig ├── .eslintrc.json ├── server.js ├── README.md ├── main.js └── package.json /src/views/NoMatch.js: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /src/styles/toolbar.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | -------------------------------------------------------------------------------- /src/stores/select.js: -------------------------------------------------------------------------------- 1 | // import { observable, action } from 'mobx'; 2 | 3 | 4 | export default {}; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } -------------------------------------------------------------------------------- /.compilerc: -------------------------------------------------------------------------------- 1 | { 2 | "application/javascript": { 3 | "plugins": ["react-hot-loader/babel"] 4 | } 5 | } -------------------------------------------------------------------------------- /package/resource/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/package/resource/logo.ico -------------------------------------------------------------------------------- /package/resource/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/package/resource/logo.png -------------------------------------------------------------------------------- /src/styles/images/head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/head.png -------------------------------------------------------------------------------- /package/resource/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/package/resource/logo@2x.png -------------------------------------------------------------------------------- /src/components/list.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | 4 | export default class List extends Component {} -------------------------------------------------------------------------------- /src/constant/index.js: -------------------------------------------------------------------------------- 1 | export const hostname = process.env.NODE_ENV === 'development' ? '' : 'http://localhost:10527'; -------------------------------------------------------------------------------- /package/resource/logo@1.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/package/resource/logo@1.5x.png -------------------------------------------------------------------------------- /package/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/thumb.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | 4 | export default class Thumb extends Component {} -------------------------------------------------------------------------------- /src/styles/images/main_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/main_logo.png -------------------------------------------------------------------------------- /src/components/titlebar.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | 4 | export default class Titlebar extends Component {} -------------------------------------------------------------------------------- /src/styles/images/EmptySession.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/EmptySession.png -------------------------------------------------------------------------------- /src/styles/images/PhoneEmpty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/PhoneEmpty.png -------------------------------------------------------------------------------- /src/styles/images/fileType/Apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/Apps.png -------------------------------------------------------------------------------- /src/styles/images/fileType/ApkType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/ApkType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/CADType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/CADType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/DocType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/DocType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/ExeType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/ExeType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/ImgType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/ImgType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/IpaType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/IpaType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/PdfType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/PdfType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/PptType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/PptType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/RarType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/RarType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/TxtType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/TxtType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/VsdType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/VsdType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/XlsType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/XlsType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/FolderType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/FolderType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/MusicType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/MusicType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/OtherType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/OtherType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/VideoType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/VideoType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/MixFileType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/MixFileType.png -------------------------------------------------------------------------------- /src/styles/images/fileType/PS_54_93523dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/PS_54_93523dc.png -------------------------------------------------------------------------------- /src/styles/images/fileType/TorrentType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zedwang/electron-bdcloud/HEAD/src/styles/images/fileType/TorrentType.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | package/dist 4 | package/release 5 | package/*.js 6 | package/electron-cloud-* 7 | db 8 | .vscode 9 | yarn-error.log -------------------------------------------------------------------------------- /backend/controller/index.js: -------------------------------------------------------------------------------- 1 | const user = require('./user'); 2 | const file = require('./file'); 3 | 4 | 5 | 6 | module.exports = { 7 | user, 8 | file 9 | }; -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 | ## 打包目录 2 | 3 | #### Why ? 4 | 为了打包的纯洁性,先分别把src的主业务逻辑进行编译打包到此目录下,然后把main.js给编译并把编译之后的文件生成到此目录下。当前目录下的package.json就是专门为打包使用,所有打包相关的参数配置都在这个json里面 -------------------------------------------------------------------------------- /src/views/index.js: -------------------------------------------------------------------------------- 1 | import Layout from './Layout'; 2 | import Home from './Home'; 3 | import Share from './Share'; 4 | 5 | export {Layout, Home, Share}; 6 | 7 | -------------------------------------------------------------------------------- /backend/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const config = new Map(); 4 | 5 | config.set('db_path', path.join(__dirname, '../db')); 6 | 7 | module.exports = config; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV || 'development'; 2 | console.log('begin '+ env + ' model...'); 3 | module.exports = require('./build/webpack.'+ env.trim()); -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newnewline = true -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'layout'; 2 | @import 'button'; 3 | @import 'footer'; 4 | @import 'header'; 5 | @import 'icon'; 6 | @import 'menu'; 7 | @import 'modal'; 8 | @import 'taskbar'; 9 | @import 'toolbar'; 10 | @import 'share'; -------------------------------------------------------------------------------- /src/stores/login.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class Login { 4 | @observable isLogin = false; 5 | 6 | 7 | @action login() { 8 | login.isLogin = true; 9 | } 10 | } 11 | 12 | const login = new Login(); 13 | 14 | export default login; -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import './layout.scss'; 3 | 4 | export default class Layout extends Component { 5 | render() { 6 | return ( 7 |
8 | {this.props.children} 9 |
); 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/sider.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import '../styles/sider.scss'; 3 | 4 | export default class Sider extends Component { 5 | render() { 6 | return ( 7 | ); 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/content.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import './Content.scss'; 3 | 4 | export default class Content extends Component { 5 | render() { 6 | return ( 7 |
8 | {this.props.children} 9 |
); 10 | } 11 | } -------------------------------------------------------------------------------- /backend/nedbPromise.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const np = require('nedb-promise'); 3 | const config = require('./config'); 4 | 5 | function db(dbname) { 6 | return np({ 7 | filename: path.join(config.get('db_path'), dbname), 8 | autoload: true 9 | }); 10 | } 11 | 12 | module.exports = db; -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | import user from './user'; 2 | import login from './login'; 3 | import files from './files'; 4 | import history from './history'; 5 | import select from './select'; 6 | import window from './window'; 7 | 8 | export default { 9 | user, 10 | login, 11 | files, 12 | history, 13 | select, 14 | window 15 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import Root from './container'; 4 | import '@fortawesome/fontawesome-free-solid'; 5 | import '@fortawesome/fontawesome-free-regular'; 6 | import 'normalize.css'; 7 | import './styles/index.scss'; 8 | 9 | render(, document.getElementById('root')); 10 | -------------------------------------------------------------------------------- /src/styles/_var.scss: -------------------------------------------------------------------------------- 1 | // global 2 | $background: #fff; 3 | $color: #6b6a6b; 4 | $color-light: #f6f5f5; 5 | $color-hover: #3a8cff; 6 | $color-disabled: #b3b1b1; 7 | 8 | // header 9 | $background-header: #eef0f6; 10 | 11 | // border 12 | $border-color: #d6d8dd; 13 | $border-color-light: #ededed; 14 | $border-width: 1px; 15 | $border-active-width: 2px; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title%> 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /src/views/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { Header } from '../components'; 4 | 5 | @observer 6 | export default class Layout extends Component { 7 | 8 | render() { 9 | return ( 10 |
11 |
12 | {this.props.children} 13 |
); 14 | } 15 | } -------------------------------------------------------------------------------- /src/stores/user.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { hostname } from '../constant'; 3 | 4 | class Store { 5 | @observable userInfo = {} 6 | 7 | async loadUser() { 8 | let user; 9 | user = await fetch(hostname + '/user'); 10 | user = await user.json(); 11 | this.userInfo = user.data; 12 | } 13 | } 14 | 15 | const user = new Store(); 16 | export default user; -------------------------------------------------------------------------------- /src/styles/footer.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | .footer { 4 | position: absolute; 5 | left: 0; 6 | bottom: 2px; 7 | width: 100%; 8 | height: 30px; 9 | border-top: 1px solid $border-color; 10 | font-size: 12px; 11 | line-height: 30px; 12 | padding-left: 15px; 13 | color: $color; 14 | 15 | .total { 16 | 17 | span { 18 | margin-right: 10px; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/views/Share.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | 4 | 5 | 6 | @inject(stores => ({ 7 | files: stores.files, 8 | window: stores.window 9 | })) 10 | @observer 11 | export default class Share extends Component { 12 | 13 | 14 | render() { 15 | return ( 16 |
17 | 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/dropdown.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | export default class Dropdown extends Component { 4 | 5 | constructor() { 6 | super(); 7 | } 8 | 9 | componentDidMount() { 10 | } 11 | 12 | onHover = (ev) => { 13 | console.log('hover', ev); 14 | } 15 | 16 | render() { 17 | return ( this.node = elm} onMouseOver={this.onHover}>{this.props.children}); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/share.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | .share-empty { 4 | height: calc(100vh - 75px); 5 | position: relative; 6 | 7 | &::after { 8 | content: ' '; 9 | display: block; 10 | position: absolute; 11 | width: 364px; 12 | height: 158px; 13 | background-image: url('./images/EmptySession.png'); 14 | top: 50%; 15 | left: 50%; 16 | transform: translate(-50%, -50%) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const app = new express(); 4 | const { user, file } = require('./controller'); 5 | 6 | app.use(bodyParser.json()); 7 | 8 | app.get('/files', file.search); 9 | app.post('/files/upload', file.upload); 10 | app.get('/files/rename/:id', file.rename); 11 | app.post('/files/moving', file.move); 12 | app.delete('/files/:id', file.delete); 13 | app.post('/files/delete', file.delete); 14 | app.post('/folder', file.createFolder); 15 | app.get('/user', user); 16 | 17 | module.exports = app; -------------------------------------------------------------------------------- /src/container/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'mobx-react'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | // import { ipcRenderer, remote, shell } from 'electron'; 5 | import routes from '../routes'; 6 | import stores from '../stores'; 7 | 8 | class App extends Component { 9 | componentDidMount() { 10 | 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | {routes} 17 | 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default App; -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | withRouter, 4 | Route, 5 | } from 'react-router-dom'; 6 | import { Layout, Home, Share } from '../views'; 7 | 8 | const Router = withRouter((props) => ); 9 | export default ( 10 | /* eslint-disable */ 11 | 12 | 13 | 14 | {/* */} 15 | {/* */} 16 | 17 | /* eslint-enable */ 18 | ); -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Button from './button'; 2 | import Dropdown from './dropdown'; 3 | import Footer from './footer'; 4 | import Header from './header'; 5 | import List from './list'; 6 | import Icon from './icon'; 7 | import Menu from './menu'; 8 | import Taskbar from './taskbar'; 9 | import Thumb from './thumb'; 10 | import Toolbar from './toolbar'; 11 | import Titlebar from './titlebar'; 12 | import Modal from './modal'; 13 | 14 | 15 | export { 16 | Button, 17 | Dropdown, 18 | Footer, 19 | Header, 20 | List, 21 | Icon, 22 | Menu, 23 | Taskbar, 24 | Thumb, 25 | Titlebar, 26 | Toolbar, 27 | Modal 28 | }; 29 | -------------------------------------------------------------------------------- /backend/controller/user.js: -------------------------------------------------------------------------------- 1 | const userModel = require('../nedbPromise')('user.db'); 2 | const os = require('os'); 3 | 4 | module.exports = async function searchController(req, res) { 5 | /** 6 | * 模拟用户信息 7 | * 保证每次都有且只有一个用户 8 | */ 9 | let users = await userModel.count({}); 10 | if (!users.length) { 11 | users = await userModel.insert({ 12 | niceName: 'Hi,' + os.hostname(), 13 | mail: 'wzd*****@sina.com', 14 | totalSize: '109951162777', 15 | isVip: true, 16 | used: '53687091200' 17 | }); 18 | } 19 | users = await userModel.find({}); 20 | res.json({ 21 | code: 0, 22 | data: users[0] 23 | }); 24 | 25 | }; -------------------------------------------------------------------------------- /backend/utils.js: -------------------------------------------------------------------------------- 1 | const category = { 2 | image: 1, 3 | ppt: 2, 4 | doc: 2, 5 | xls: 2, 6 | video: 3, 7 | mp3: 5, 8 | exe: 6, 9 | other: 7 10 | }; 11 | module.exports = { 12 | isNotEmpty: function(obj) { 13 | if (!obj) return false; 14 | if ( typeof obj === 'string') { 15 | return Boolean(obj.length); 16 | } 17 | if (Array.isArray(obj)) { 18 | return Boolean(Array.length); 19 | } 20 | return Boolean(Object.keys(obj).length); 21 | }, 22 | signType: function(type) { 23 | const res = type.match(/image|ppt|doc|xls|video|exe|mp3/); 24 | if (res) { 25 | return category[res[0]]; 26 | } 27 | return category['other']; 28 | } 29 | }; -------------------------------------------------------------------------------- /src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import PropTypes from 'prop-types'; 4 | import '../styles/footer.scss'; 5 | 6 | @inject('files') 7 | @observer 8 | export default class Footer extends Component { 9 | render() { 10 | return (
11 |
{ this.props.files.total } 项 12 | {this.props.files.selected.size ? 已选中 { this.props.files.selected.size } 个文件/文件夹 : null} 13 |
14 |
15 | 16 |
17 |
); 18 | } 19 | } 20 | 21 | Footer.propTypes = { 22 | files: PropTypes.object 23 | }; -------------------------------------------------------------------------------- /src/components/modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | // import { observer, inject } from 'mobx-react'; 4 | 5 | import '../styles/modal.scss'; 6 | // These two containers are siblings in the DOM 7 | const modalRoot = document.getElementsByTagName('body')[0]; 8 | 9 | class Modal extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.el = document.createElement('div'); 13 | this.el.className = 'modal-backend'; 14 | } 15 | 16 | componentDidMount() { 17 | modalRoot.appendChild(this.el); 18 | } 19 | 20 | componentWillUnmount() { 21 | modalRoot.removeChild(this.el); 22 | } 23 | 24 | render() { 25 | return ReactDOM.createPortal( 26 | this.props.children, 27 | this.el, 28 | ); 29 | } 30 | } 31 | 32 | export default Modal; 33 | 34 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node":true 6 | }, 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:react/recommended" 15 | ], 16 | "rules": { 17 | "react/prop-types": [2, {"ignore": ["children", "match", "location", "history"]}], 18 | "brace-style": [ 19 | "error", 20 | "1tbs", 21 | { "allowSingleLine": true } 22 | ], 23 | "space-before-blocks": "error", 24 | "indent": [ 25 | "error", 26 | 2 27 | ], 28 | "linebreak-style": [ 29 | "error", 30 | "unix" 31 | ], 32 | "quotes": [ 33 | "error", 34 | "single" 35 | ], 36 | "semi": [ 37 | "error", 38 | "always" 39 | ], 40 | "no-console": "off", 41 | "prefer-const": [ 42 | "error", 43 | { "destructuring": "all" } 44 | ] 45 | } 46 | } -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | 2 | export function formatSize(size) { 3 | let volume = Math.round(size / (1024*1024*1024)); 4 | let unit = 'G'; 5 | if (volume >= 1024) { 6 | volume = Math.round(volume / 1024); 7 | unit = 'T'; 8 | return volume + unit; 9 | } 10 | return volume + unit; 11 | } 12 | 13 | export function iconType(type) { 14 | const types = { 15 | video: /video|mp4|mkv/, 16 | image: /image|jpg|jpeg|gif|png/, 17 | zip: /zip/, 18 | word: /word/, 19 | xls: /xls/, 20 | app: /exe|ms|dmg/, 21 | folder: /folder/ 22 | }; 23 | for (const [key, value] of Object.entries(types)) { 24 | const res = type.match(value); 25 | if (res) { 26 | return key; 27 | } 28 | } 29 | return 'other'; 30 | } 31 | 32 | export function queryParams(params) { 33 | const keys = Object.keys(params); 34 | if (keys.length) { 35 | return '?' + keys 36 | .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) 37 | .join('&'); 38 | } 39 | return ''; 40 | 41 | } -------------------------------------------------------------------------------- /src/stores/window.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { remote, ipcRenderer } from 'electron'; 3 | 4 | 5 | class Window { 6 | window 7 | // normal max min 8 | @observable isMax 9 | @observable showLandingPoint = false; 10 | constructor() { 11 | this.window = remote.getCurrentWindow(); 12 | this.isMax = this.window.isMaximized(); 13 | } 14 | 15 | exit() { 16 | this.window.close(); 17 | } 18 | 19 | mini() { 20 | this.window.minimize(); 21 | } 22 | 23 | max() { 24 | this.isMax = !this.isMax; 25 | this.window.maximize(); 26 | } 27 | 28 | restore() { 29 | this.isMax = !this.isMax; 30 | this.window.restore(); 31 | } 32 | 33 | hiddenWindow() { 34 | ipcRenderer.send('hidden-window', 'hello'); 35 | 36 | } 37 | 38 | showLanding() { 39 | this.showLandingPoint = !this.showLandingPoint; 40 | } 41 | } 42 | 43 | const window = new Window(); 44 | 45 | export default window; 46 | 47 | export {Window}; -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-cloud", 3 | "version": "1.0.0", 4 | "description": "A project of electron practices copy the bd", 5 | "main": "./main.js", 6 | "author": { 7 | "name": "woox.wzd", 8 | "email": "woox.wzd@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/zedwang/electron-bdcloud.git" 13 | }, 14 | "license": "MIT", 15 | "build": { 16 | "productName": "electron-cloud", 17 | "appId": "zed.woox.icloud", 18 | "compression": "maximum", 19 | "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", 20 | "win": { 21 | "target": "nsis", 22 | "icon": "./resource/logo.ico" 23 | }, 24 | "nsis": { 25 | "oneClick": false, 26 | "allowToChangeInstallationDirectory": true, 27 | "artifactName": "${productName}-${version}-${os}-${arch}-setup.${ext}", 28 | "deleteAppDataOnUninstall": true 29 | }, 30 | "directories": { 31 | "buildResources": "./resource", 32 | "output": "release" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/stores/history.js: -------------------------------------------------------------------------------- 1 | import { observable ,toJS } from 'mobx'; 2 | 3 | class History { 4 | recorder = [{step: '/', breadcrumb: ['/']}]; 5 | index = 0; 6 | @observable first = true; 7 | @observable last = true; 8 | 9 | add(store) { 10 | const raw = toJS(store); 11 | const new_recorder = this.recorder.slice(0, this.index + 1); 12 | new_recorder.push({step: raw.dir, breadcrumb: raw.breadcrumb}); 13 | this.recorder = new_recorder; 14 | this.index = this.recorder.length - 1; 15 | } 16 | 17 | clear() { 18 | this.recorder = [{step: '/', breadcrumb: ['/']}]; 19 | } 20 | 21 | next() { 22 | this.index++; 23 | if (this.index === this.recorder.length) this.index = this.recorder.length - 1; 24 | return this.recorder[this.index]; 25 | } 26 | 27 | prev() { 28 | this.index-- ; 29 | if (this.index < 0) { 30 | this.index = 0; 31 | this.first = true; 32 | } 33 | return this.recorder[this.index]; 34 | } 35 | 36 | getCurrent() { 37 | return this.recorder[this.index]; 38 | } 39 | } 40 | 41 | const history = new History(); 42 | 43 | export default history; -------------------------------------------------------------------------------- /src/stores/item.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | export default class FileModel { 4 | store; 5 | id; 6 | size; 7 | type; 8 | lastModified; 9 | isEdit; 10 | @observable name; 11 | @observable selected; 12 | 13 | constructor(store, id, name, size, type, lastModified, selected) { 14 | this.store = store; 15 | this.id = id; 16 | this.name = name; 17 | this.size = size; 18 | this.type = type; 19 | this.lastModified = lastModified; 20 | this.selected = selected; 21 | } 22 | 23 | setName(title) { 24 | this.name = title; 25 | } 26 | 27 | setSelected(selected) { 28 | this.selected = selected; 29 | } 30 | 31 | destroy() { 32 | this.store.files.remove(this); 33 | } 34 | 35 | toJs() { 36 | return { 37 | id: this.id, 38 | size: this.size, 39 | type: this.type, 40 | name: this.name, 41 | lastModified: this.lastModified 42 | }; 43 | } 44 | 45 | static fromJS(store, object) { 46 | return new FileModel(store, object.id, object.name, object.size, object.type, object.lastModified, object.selected); 47 | } 48 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup and run the development server for Hot-Module-Replacement 3 | * https://webpack.github.io/docs/hot-module-replacement-with-webpack.html 4 | * @flow 5 | */ 6 | const express = require('express'); 7 | const webpack = require('webpack'); 8 | const webpackDevMiddleware = require('webpack-dev-middleware'); 9 | const webpackHotMiddleware = require('webpack-hot-middleware'); 10 | const proxy = require('http-proxy-middleware'); 11 | const config = require('./build/config'); 12 | const webpackConfig = require('./webpack.config'); 13 | 14 | const app = new express(); 15 | const compiler = webpack(webpackConfig); 16 | 17 | app.use( 18 | webpackDevMiddleware(compiler, { 19 | contentBase: webpackConfig.output.path, 20 | publicPath: config.get('webpack.public.path'), 21 | stats: { 22 | colors: true 23 | }, 24 | }) 25 | ); 26 | app.use(webpackHotMiddleware(compiler)); 27 | 28 | app.use('/', proxy({target: 'http://localhost:10527', changeOrigin: true})); 29 | 30 | app.listen(config.get('webpack.port'), config.get('webpack.host'), err => { 31 | if (err) { 32 | console.error(err); 33 | return; 34 | } 35 | 36 | console.log(`Server is running with port ${config.get('webpack.port')} 👏`); 37 | }); -------------------------------------------------------------------------------- /src/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import PropTypes from 'prop-types'; 4 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 5 | import '../styles/button.scss'; 6 | 7 | const defaultProps = { 8 | type: 'default', 9 | active: false, 10 | disabled: false, 11 | size: 'md' 12 | }; 13 | 14 | function prefix(...props) { 15 | const classes = props.map(value => { 16 | if (value) { 17 | return `btn-${value}`; 18 | } 19 | }); 20 | return classes.join(' '); 21 | } 22 | 23 | const Button = (props) => { 24 | const { type, size, disabled, icon, text, className, active, ...other } = props; 25 | 26 | return ( 27 | 30 | ); 31 | }; 32 | 33 | Button.propTypes = { 34 | className: PropTypes.string, 35 | active: PropTypes.bool, 36 | type: PropTypes.oneOf(['danger','default','success', 'link']), 37 | size: PropTypes.oneOf(['lg', 'md', 'sm']), 38 | disabled: PropTypes.bool, 39 | icon: PropTypes.string, 40 | text: PropTypes.string, 41 | href: PropTypes.string 42 | }; 43 | Button.defaultProps = defaultProps; 44 | 45 | export default Button; -------------------------------------------------------------------------------- /src/styles/button.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | .btn { 4 | display: inline-block; 5 | margin-bottom: 0; 6 | font-weight: 400; 7 | text-align: center; 8 | vertical-align: middle; 9 | touch-action: manipulation; 10 | cursor: pointer; 11 | background-image: none; 12 | border: 1px solid transparent; 13 | white-space: nowrap; 14 | padding: 6px 10px; 15 | font-size: 12px; 16 | line-height: 1.33; 17 | border-radius: 4px; 18 | -webkit-user-select: none; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | user-select: none; 22 | 23 | &:active, 24 | &:focus { 25 | outline: none; 26 | } 27 | 28 | &:hover { 29 | background: rgba(216, 216, 216, 0.6); 30 | } 31 | 32 | span { 33 | font-size: 1rem; 34 | position: relative; 35 | top: 1; 36 | } 37 | } 38 | 39 | .btn-default { 40 | border: transparent; 41 | background: transparent; 42 | } 43 | 44 | .btn-sm { 45 | padding: 3px; 46 | } 47 | 48 | button[disabled] { 49 | color: $color-disabled; 50 | cursor: not-allowed; 51 | 52 | &:hover { 53 | background: transparent; 54 | } 55 | } 56 | 57 | .btn-link, 58 | .btn-link:hover { 59 | background: transparent; 60 | border-color: transparent; 61 | outline-color: transparent; 62 | } 63 | $types: (danger #d43f3a, warning #eea236,success #4cae4c,link transparent); 64 | 65 | @each $key,$value in $types { 66 | .btn-#{$key} { 67 | border: 1px solid #{$value}; 68 | color: $value 69 | } 70 | } -------------------------------------------------------------------------------- /src/components/toolbar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import PropTypes from 'prop-types'; 4 | import Button from './button'; 5 | import '../styles/toolbar.scss'; 6 | import FileModel from '../stores/item'; 7 | 8 | @inject('files') 9 | @observer 10 | export default class Toolbar extends Component { 11 | handleCreateFolder = async () => { 12 | this.props.files.data.push(FileModel.fromJS(this.props, {type: 'folder', name: '新建文件夹'})); 13 | } 14 | 15 | handleMultiDelete = async () => { 16 | const selected = this.props.files.selected.keys(); 17 | await this.props.files.multiRemove(selected); 18 | const params = new URLSearchParams(); 19 | params.append('dir', this.props.files.dir); 20 | if (this.props.files.category) { 21 | params.append('category', this.props.files.category); 22 | } 23 | this.props.files.fetchFiles(params.toString()); 24 | this.props.files.selected.clear(); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 |
37 | ); 38 | } 39 | } 40 | Toolbar.propTypes = { 41 | files: PropTypes.object 42 | }; -------------------------------------------------------------------------------- /src/styles/modal.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | .modal-backend { 4 | position: absolute; 5 | top: 0; 6 | right: 0; 7 | left: 0; 8 | bottom: 0; 9 | background-color: rgba(71, 71, 71, 0.4); 10 | 11 | .modal { 12 | width: 300px; 13 | height: 200px; 14 | position: absolute; 15 | background: #fff; 16 | top: 50%; 17 | left: 50%; 18 | margin-top: -100px; 19 | margin-left: -150px; 20 | border: 1px solid $border-color; 21 | border-radius: 2px; 22 | // box-shadow: 0 0 1px 1px #e6e6e6; 23 | overflow: hidden; 24 | 25 | .modal-head { 26 | height: 32px; 27 | background: $background-header; 28 | color: $color; 29 | position: relative; 30 | padding: 0 10px; 31 | 32 | h3 { 33 | margin: 0; 34 | font-size: 14px; 35 | line-height: 32px; 36 | font-weight: normal; 37 | } 38 | 39 | > span { 40 | display: inline-block; 41 | width: 40px; 42 | height: 32px; 43 | text-align: center; 44 | line-height: 32px; 45 | position: absolute; 46 | top: 0; 47 | right: 0; 48 | 49 | &:hover { 50 | // background: $color-hover; 51 | cursor: pointer; 52 | } 53 | } 54 | } 55 | 56 | .modal-body { 57 | min-height: 100px; 58 | padding: 10px; 59 | font-size: 12px; 60 | } 61 | 62 | .modal-foot { 63 | padding: 10px; 64 | border-top: 1px solid $border-color; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 仿百度云盘客户端 2 | 3 | 基于`Electron`,`Webpack3`,`Babel7`,`React16`,`MobX` 4 | ![shortscreen](https://github.com/zedwang/iCloud/blob/master/screenshots/main.png?raw=true) 5 | 6 | > electron的官方脚手架只是一个demo,并没有完全的基于业务实现。所以,想通过此项目来系统的接触。其实不太需要真实的API,所以并不不会考虑文件的传输问题(当然要对接一些云也是可以的)。 7 | > 关于UI的细节也没有花太多精力,那些都是体力活,模仿的百度云仅用于学习。主要是强调生产的过程,比如项目的工程化,托盘处理,自动更新、跨平台打包、网络检测,发布到平台等等这样原生功能。 8 | 9 | ## Dev 10 | 11 | 1. fork仓库,安装依赖包 12 | 13 | ```shell 14 | yarn install 15 | ``` 16 | 17 | 1. 启动 18 | 19 | ```shell 20 | yarn start 21 | ``` 22 | 23 | 1. 打包调试 24 | 25 | ```shell 26 | yarn package:win 27 | ``` 28 | 29 | 1. 编译安装包 30 | 31 | ```shell 32 | yarn release 33 | ``` 34 | 35 | ## TODOS 36 | 37 | * [x] 拖拽窗口 38 | * [ ] 网络检测 39 | * [ ] 自动更新 40 | * [ ] 系统通知 41 | 42 | ## 注意事项 43 | 44 | 1. electron不同于普通的web程序,涉及到进程通信,所以在webpack编译的时候要配置 `target: 'electron-renderer'` 45 | 1. 比较常见的就是es6`class`this的绑定问题,一般有三种方式解决: 46 | * 在`constructor`手动bind,或者用`auto-bind`这个报来自动绑定 47 | * 在调用的地方bind,`` 48 | * 不定义方法类成员方法,定义成类的属性`handleClick = () => {....}` 49 | 1. `babel7`修饰符插件的问题。修饰符插件和类属性插件顺序很重要,`@babel/plugin-proposal-class-properties`插件必须开启`loose`模式,否则就会出现mobx不会刷新组件 50 | 1. `Not allowed load local resource`错误。 51 | * 检查文件是否存在,或者路劲是否正确 52 | * 可以在`BrowserWindow()`中关闭安全策略 53 | ```js 54 | { 55 | webPreferences: { 56 | webSecurity: false 57 | } 58 | } 59 | ``` 60 | * 因为运行的环境是nodejs,所以在Webpack配置中要保证`__dirname`行为输出的是常规的文件目录 61 | ```js 62 | node: { 63 | __dirname: false 64 | } 65 | ``` 66 | 1. 打包体积问题。electron打包的东西出来通常体积比较大,那是因为包含了一个chromuin浏览器和node在里面。但是我们开发的环境中node_modules的体积也是很庞大的。所以我们可以main.js也进行打包,这样就可以不出现冗余的node_modules包。注意的是,在编译main的时候webpack的target要配置为`electron-main` 67 | 1. 字体乱码的问题。这个问题比较2,就是缺少了``的声明 68 | 1. `electron-packager`适合打包调试,推荐`electron-builder` 69 | 1. 要自动更新必须要打nupkg类型的包,`linux`平台并不支持自动更新 70 | 1. 一般情况下在render进程中通过remote接口去拿main进程的模块,可以不用显示的发事件 71 | 72 | ## 体会 73 | 74 | 1. MobX vs Redux 75 | 2. Electron 76 | -------------------------------------------------------------------------------- /src/components/breadcrumb.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 4 | import PropTypes from 'prop-types'; 5 | 6 | @inject('files', 'history') 7 | @observer 8 | 9 | export default class Breadcrumb extends Component { 10 | 11 | handleJumpTo = (e, step) => { 12 | e.preventDefault(); 13 | const path = e.target.attributes.getNamedItem('title').value; 14 | const deep = e.target.attributes.getNamedItem('deep').value; 15 | const now = this.props.files.breadcrumb.splice(0, step + 1); 16 | this.props.files.breadcrumb = now; 17 | this.props.files.setDir(path); 18 | if (Number(deep) === 0) this.props.files.setCategory(0); 19 | this.props.history.add(this.props.files); 20 | const params = new URLSearchParams(); 21 | params.append('dir', this.props.files.dir); 22 | this.props.files.fetchFiles(params.toString()); 23 | } 24 | 25 | render() { 26 | const { files } = this.props; 27 | let absPath = '/'; 28 | const { breadcrumb } = files; 29 | const last = breadcrumb.length - 1; 30 | const nav = breadcrumb.map((item, index) => { 31 | if (index < 1) { 32 | return({this.handleJumpTo(e, index);}} deep={index} title="/"> 我的网盘 >); 33 | } 34 | if (last === index) { 35 | absPath += item + '/'; 36 | return( 37 | {item.replace(/^\//, '')} > 38 | ); 39 | } else { 40 | absPath += item + '/'; 41 | return( 42 | {this.handleJumpTo(e, index);}} title={absPath} deep={index}> {item.replace(/^\//, '')} > 43 | ); 44 | } 45 | }); 46 | return ( 47 |
48 | {nav} 49 |
50 | ); 51 | } 52 | } 53 | 54 | Breadcrumb.propTypes = { 55 | files: PropTypes.object, 56 | history: PropTypes.object 57 | }; -------------------------------------------------------------------------------- /src/styles/taskbar.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | .taskbar { 3 | padding: 5px 0 5px 15px; 4 | border-bottom: 1px solid #e6e3e3; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | align-content: center; 9 | .link-gray { 10 | svg { 11 | color: rgb(150, 146, 146); 12 | } 13 | } 14 | .link-gray:hover { 15 | svg { 16 | color: $color-hover; 17 | } 18 | } 19 | .history, 20 | .bread, 21 | .search, 22 | .view-mode { 23 | margin-left: 10px 24 | } 25 | .history { 26 | width: 68px; 27 | margin-left: 0; 28 | flex-shrink: 0; 29 | } 30 | .bread { 31 | flex-grow: 1; 32 | padding: 3px 10px; 33 | overflow: hidden; 34 | white-space: nowrap; 35 | text-overflow: ellipsis; 36 | a:last-child { 37 | span { 38 | display: none; 39 | } 40 | } 41 | a { 42 | display: inline-block; 43 | max-width: 150px; 44 | height: 20px; 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | white-space: nowrap; 48 | text-decoration: none; 49 | font-size: 12px; 50 | line-height: 22px; 51 | margin-right: 10px; 52 | color: $color-hover; 53 | &.disabled { 54 | color: $color 55 | } 56 | &.disabled:hover { 57 | text-decoration: none; 58 | } 59 | &:hover { 60 | text-decoration: underline; 61 | } 62 | svg { 63 | width: 18px; 64 | display: inline-block; 65 | padding-left: 5px; 66 | } 67 | >span { 68 | display: inline-block; 69 | margin-left: 2px; 70 | } 71 | } 72 | } 73 | .search { 74 | width: 200px; 75 | height: 31px; 76 | padding: 3px 10px; 77 | font-size: 12px; 78 | flex-shrink: 0; 79 | position: relative; 80 | input { 81 | border: none; 82 | width: 100%; 83 | padding-right: 20px; 84 | padding-left: 20px; 85 | height: 22px; 86 | color: rgb(150, 146, 146); 87 | border-left: 1px solid #e6e3e3; 88 | &:focus, 89 | &:active { 90 | outline: none; 91 | } 92 | } 93 | span { 94 | display: inline-block; 95 | position: absolute; 96 | right: 8px; 97 | top: 10px; 98 | color: rgb(150, 146, 146); 99 | } 100 | } 101 | .view-mode { 102 | width: 30px; 103 | flex-shrink: 0; 104 | } 105 | } -------------------------------------------------------------------------------- /src/components/menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import PropTypes from 'prop-types'; 4 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 5 | import cls from 'classnames'; 6 | import '../styles/menu.scss'; 7 | 8 | const categories = ['视频','音乐','图片','文档','应用','其他','种子']; 9 | 10 | @inject('files', 'history') 11 | @observer 12 | export default class Menu extends React.Component { 13 | 14 | toggleCategory = (type) => { 15 | this.props.files.setCategory(type); 16 | const params = new URLSearchParams(); 17 | if (type === 0) { 18 | params.append('dir', '/'); 19 | this.props.files.setBreadcrumb(['/']); 20 | this.props.files.setDir('/'); 21 | } else { 22 | params.append('category', this.props.files.category); 23 | this.props.files.setBreadcrumb(['/',categories[this.props.files.category - 1]]); 24 | } 25 | this.props.files.fetchFiles(params.toString()); 26 | } 27 | 28 | render() { 29 | return ( 30 | 45 | ); 46 | } 47 | } 48 | 49 | Menu.propTypes = { 50 | files: PropTypes.object 51 | }; -------------------------------------------------------------------------------- /src/styles/menu.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | .menu { 4 | padding: 10px 0; 5 | 6 | span { 7 | margin-right: 10px; 8 | } 9 | 10 | li > ul { 11 | a { 12 | padding-left: 20px; 13 | } 14 | } 15 | 16 | li a { 17 | display: block; 18 | text-align: center; 19 | padding: 10px 4px; 20 | font-size: 12px; 21 | cursor: default; 22 | 23 | &:hover:not(.active) { 24 | color: $color-hover; 25 | } 26 | } 27 | 28 | li a.active { 29 | background: #e6edff; 30 | color: $color-hover; 31 | position: relative; 32 | 33 | &::before { 34 | content: ''; 35 | display: block; 36 | width: 3px; 37 | background: $color-hover; 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | bottom: 0; 42 | } 43 | } 44 | } 45 | 46 | .react-contextmenu { 47 | background-color: #fff; 48 | background-clip: padding-box; 49 | border: 1px solid $border-color; 50 | border-radius: .25rem; 51 | color: $color; 52 | margin: 2px 0 0; 53 | outline: none; 54 | opacity: 0; 55 | padding: 0; 56 | pointer-events: none; 57 | text-align: left; 58 | transition: opacity 250ms ease !important; 59 | 60 | &.react-contextmenu--visible { 61 | opacity: 1; 62 | pointer-events: auto; 63 | z-index: 9999; 64 | } 65 | } 66 | 67 | .react-contextmenu-item { 68 | background: 0 0; 69 | border: 0; 70 | color: $color; 71 | cursor: pointer; 72 | font-size: 12px; 73 | font-weight: 400; 74 | line-height: 1.5; 75 | padding: 2px 20px; 76 | text-align: inherit; 77 | white-space: nowrap; 78 | } 79 | 80 | .react-contextmenu-item.react-contextmenu-item--active, 81 | .react-contextmenu-item.react-contextmenu-item--selected { 82 | color: #fff; 83 | background-color: $color-hover; 84 | border-color: $border-color; 85 | text-decoration: none; 86 | } 87 | 88 | .react-contextmenu-item.react-contextmenu-item--disabled, 89 | .react-contextmenu-item.react-contextmenu-item--disabled:hover { 90 | background-color: transparent; 91 | border-color: rgba(0,0,0,.15); 92 | color: $color-disabled; 93 | } 94 | 95 | .react-contextmenu-item--divider { 96 | border-bottom: 1px solid rgba(0,0,0,.15); 97 | cursor: inherit; 98 | margin-bottom: 3px; 99 | padding: 2px 0; 100 | } 101 | .react-contextmenu-item--divider:hover { 102 | background-color: transparent; 103 | border-color: rgba(0,0,0,.15); 104 | } 105 | 106 | .react-contextmenu-item.react-contextmenu-submenu { 107 | padding: 0; 108 | } 109 | 110 | .react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item:after { 111 | content: "▶"; 112 | display: inline-block; 113 | position: absolute; 114 | right: 7px; 115 | } 116 | -------------------------------------------------------------------------------- /src/components/taskbar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { observable } from 'mobx'; 4 | import PropTypes from 'prop-types'; 5 | import Button from './button'; 6 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 7 | import Breadcrumb from './breadcrumb'; 8 | import '../styles/taskbar.scss'; 9 | 10 | @inject('files', 'history') 11 | @observer 12 | export default class Taskbar extends Component { 13 | @observable q = ''; 14 | timer = null; 15 | handleChange = (event) => { 16 | this.q = event.target.value; 17 | clearTimeout(this.timer); 18 | this.timer = setTimeout(()=> { 19 | const params = new URLSearchParams(); 20 | params.append('q', this.q); 21 | this.props.files.fetchFiles(params.toString()); 22 | }, 5e2); 23 | } 24 | 25 | handleGoBack = () => { 26 | const prev = this.props.history.prev(); 27 | this.props.files.setBreadcrumb(prev.breadcrumb); 28 | this.props.files.setDir(prev.step); 29 | const params = new URLSearchParams(); 30 | params.append('dir', prev.step); 31 | this.props.files.fetchFiles(params.toString()); 32 | } 33 | 34 | handleGoto = () => { 35 | const next = this.props.history.next(); 36 | this.props.files.setBreadcrumb(next.breadcrumb); 37 | this.props.files.setDir(next.step); 38 | const params = new URLSearchParams(); 39 | params.append('dir', next.step); 40 | this.props.files.fetchFiles(params.toString()); 41 | } 42 | 43 | handleRefresh = () => { 44 | const params = new URLSearchParams(); 45 | params.append('dir', this.props.files.dir); 46 | if (this.props.files.category) { 47 | params.append('category', this.props.files.category); 48 | } 49 | this.props.files.fetchFiles(params.toString()); 50 | } 51 | 52 | render() { 53 | return ( 54 |
55 |
56 |
60 | 61 |
62 | 63 | 64 |
65 |
66 | {/*
70 |
71 | ); 72 | } 73 | } 74 | Taskbar.propTypes = { 75 | files: PropTypes.object, 76 | q: PropTypes.string, 77 | history: PropTypes.object 78 | }; -------------------------------------------------------------------------------- /src/stores/files.js: -------------------------------------------------------------------------------- 1 | import { observable, computed } from 'mobx'; 2 | import { hostname } from '../constant'; 3 | 4 | class Files { 5 | @observable breadcrumb = ['/'] 6 | @observable data = []; 7 | @observable total = 0; 8 | @observable category = 0; 9 | @observable dir = '/'; 10 | @observable selected = new Map(); 11 | 12 | async createFolder(name) { 13 | return await fetch(`${hostname}/folder`,{ 14 | method: 'POST', 15 | headers: { 16 | 'Accept': 'application/json', 17 | 'Content-Type': 'application/json' 18 | }, 19 | body: JSON.stringify({name, dir: this.dir}) 20 | }); 21 | } 22 | /** 23 | * type 24 | * {4:文档,1:视频,7:种子,2:音乐,6:其他,3:图片} 25 | */ 26 | // fetch 默认的模式是不带cookie等数据到服务器上去的 27 | async fetchFiles(params) { 28 | const url = `${hostname}/files${params && params.length ? '?' + params : ''}`; 29 | const files = await fetch(url, { 30 | cache: 'default' 31 | }); 32 | const { data, total } = await files.json(); 33 | this.data = data; 34 | this.total = total; 35 | this.selected.clear(); 36 | } 37 | 38 | async upload(file, params) { 39 | await fetch(`${hostname}/files/upload${ params ? '?' + params : ''}`, { 40 | qs: params, 41 | method: 'POST', 42 | headers: { 43 | 'Accept': 'application/json', 44 | 'Content-Type': 'application/json' 45 | }, 46 | body: JSON.stringify(file) 47 | }); 48 | this.selected.clear(); 49 | } 50 | 51 | rename(id, newName) { 52 | return fetch(`${hostname}/files/rename/${id}?name=${newName}`); 53 | } 54 | 55 | async moving(src, target) { 56 | await fetch(`${hostname}/files/moving`, { 57 | method: 'POST', 58 | headers: { 59 | 'Accept': 'application/json', 60 | 'Content-Type': 'application/json' 61 | }, 62 | body: JSON.stringify({src, target}) 63 | }); 64 | } 65 | 66 | multiRemove(ids) { 67 | return fetch(`${hostname}/files/delete`, { 68 | method: 'POST', 69 | headers: { 70 | 'Accept': 'application/json', 71 | 'Content-Type': 'application/json' 72 | }, 73 | body: JSON.stringify(ids) 74 | }); 75 | } 76 | 77 | remove(id) { 78 | return fetch(`${hostname}/files/${id}`, { 79 | method: 'DELETE', 80 | headers: { 81 | 'Accept': 'application/json', 82 | 'Content-Type': 'application/json' 83 | } 84 | }); 85 | } 86 | 87 | setCategory(type) { 88 | this.category = type; 89 | } 90 | 91 | setDir(dir) { 92 | this.dir = dir; 93 | } 94 | 95 | setBreadcrumb(data) { 96 | this.breadcrumb = data; 97 | } 98 | 99 | addBreadcrumb(step) { 100 | this.breadcrumb.push(step); 101 | } 102 | 103 | @computed 104 | get selectedSize() { 105 | return this.selected.size; 106 | } 107 | 108 | } 109 | 110 | export default new Files(); -------------------------------------------------------------------------------- /src/styles/header.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | .header { 4 | display: flex; 5 | color: $color; 6 | height: 75px; 7 | border-bottom: $border-width solid $border-color; 8 | background-color: $background-header; 9 | align-items: center; 10 | -webkit-user-select: none; 11 | -webkit-app-region: drag; 12 | 13 | .logo { 14 | width: 165px; 15 | font-size: 18px; 16 | font-weight: bold; 17 | padding: 10px; 18 | 19 | span { 20 | display: inline-block; 21 | width: 38px; 22 | height: 30px; 23 | background-image: url('./images/main_logo.png'); 24 | background-size: cover; 25 | vertical-align: middle; 26 | } 27 | } 28 | 29 | .nav { 30 | -webkit-app-region: no-drag; 31 | 32 | > ul { 33 | list-style: none; 34 | 35 | > li { 36 | float: left; 37 | 38 | > a { 39 | color: $color; 40 | display: block; 41 | padding: 12px 0; 42 | margin: 0 20px; 43 | border-bottom: 2px solid transparent; 44 | text-align: center; 45 | 46 | &.active, 47 | &:hover { 48 | color: $color-hover; 49 | border-bottom: $border-active-width solid $color-hover; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | .profile { 57 | flex-grow: 1; 58 | text-align: right; 59 | 60 | .avatar, 61 | .username, 62 | .operator { 63 | display: inline-block; 64 | vertical-align: middle; 65 | font-size: 12px; 66 | -webkit-app-region: no-drag; 67 | } 68 | 69 | .username { 70 | padding: 0 5px; 71 | color: #333333; 72 | 73 | span { 74 | display: inline-block; 75 | padding: 0 5px; 76 | } 77 | 78 | .sign { 79 | width: 18px; 80 | height: 18px; 81 | padding-right: 15px; 82 | } 83 | 84 | > a { 85 | display: inline-block; 86 | padding: 5px 10px; 87 | border-radius: 15px; 88 | } 89 | } 90 | 91 | .avatar { 92 | width: 35px; 93 | height: 35px; 94 | background-image: url('./images/head.png'); 95 | background-size: cover; 96 | border-radius: 50%; 97 | } 98 | 99 | .operator { 100 | padding-right: 10px; 101 | 102 | > ul { 103 | list-style: none; 104 | 105 | .separate { 106 | border-left: $border-width solid #d4d4d4; 107 | } 108 | 109 | > li { 110 | display: inline-block; 111 | 112 | > a { 113 | font-size: 14px; 114 | display: block; 115 | padding: 2px 7px; 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/styles/icon.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | .m-icons.empty { 3 | &::after { 4 | content: ' '; 5 | display: block; 6 | width: 181px; 7 | height: 127px; 8 | background-image: url('./images/PhoneEmpty.png'); 9 | position: absolute; 10 | top: 50%; 11 | left: 50%; 12 | transform: translate(-50%, -50%) 13 | } 14 | } 15 | .m-icons { 16 | position: relative; 17 | padding: 5px; 18 | height: calc(100vh - 189px); 19 | overflow: auto; // &::-webkit-scrollbar { 20 | // width: 5px; 21 | // height: 100%; 22 | // opacity: 0.6; 23 | // } 24 | &::-webkit-scrollbar-thumb { 25 | border-radius: 10px; 26 | } 27 | &::-webkit-scrollbar-track { 28 | opacity: 0.7; 29 | } 30 | .m-icon { 31 | padding: 4px; 32 | float: left; 33 | overflow: hidden; 34 | input[type="checkbox"] { 35 | display: none; 36 | } 37 | input:checked+a { 38 | background: rgba(234, 249, 252, 0.466); 39 | box-shadow: 0 0 1px 1px $color-hover; 40 | border-radius: 2px; 41 | } 42 | a { 43 | text-align: center; 44 | display: block; 45 | width: 125px; 46 | height: 120px; 47 | text-decoration: none; 48 | font-size: 12px; 49 | color: $color; // border: 1px solid #ddd; 50 | cursor: default; 51 | overflow: hidden; 52 | &.active { 53 | background: rgba(213, 234, 238, 0.466); 54 | box-shadow: 0 0 1px 1px $color-hover; 55 | border-radius: 2px; 56 | } 57 | &:not(.active):hover { 58 | background: rgba(234, 249, 252, 0.466); 59 | box-shadow: 0 0 1px 1px $color-hover; 60 | border-radius: 2px; 61 | } 62 | .ico { 63 | width: 80px; 64 | height: 80px; 65 | margin: 18px auto 0; 66 | background-size: cover; 67 | } 68 | .ico-zip { 69 | background-image: url('./images/fileType/RarType.png') 70 | } 71 | .ico-word { 72 | background-image: url('./images/fileType/DocType.png') 73 | } 74 | .ico-web { 75 | background-image: url('./images/fileType/OtherType.png') 76 | } 77 | .ico-video { 78 | background-image: url('./images/fileType/VideoType.png') 79 | } 80 | .ico-txt { 81 | background-image: url('./images/fileType/TxtType.png') 82 | } 83 | .ico-image { 84 | background-image: url('./images/fileType/ImgType.png') 85 | } 86 | .ico-pdf { 87 | background-image: url('./images/fileType/PdfType.png') 88 | } 89 | .ico-other { 90 | background-image: url('./images/fileType/OtherType.png') 91 | } 92 | .ico-links { 93 | background-image: url('./images/fileType/OtherType.png') 94 | } 95 | .ico-folder { 96 | background-image: url('./images/fileType/FolderType.png') 97 | } 98 | .ico-excel { 99 | background-image: url('./images/fileType/XlsType.png') 100 | } 101 | .ico-card { 102 | background-image: url('./images/fileType/Apps.png') 103 | } 104 | .ico-bt { 105 | background-image: url('./images/fileType/TorrentType.png') 106 | } 107 | .name { 108 | padding: 5px; 109 | width: 100%; 110 | overflow: hidden; 111 | white-space: nowrap; 112 | text-overflow: ellipsis; 113 | input.edit { 114 | border: 1px solid #e6e3e3; 115 | padding: 2px; 116 | width: 100%; 117 | color: $color; 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/views/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer, inject } from 'mobx-react'; 3 | import { Icon, Toolbar, Taskbar, Footer, Menu} from '../components'; 4 | import FileModel from '../stores/item'; 5 | import { formatSize } from '../utils'; 6 | import cls from 'classnames'; 7 | import PropTypes from 'prop-types'; 8 | 9 | 10 | window.ondragover = (e) => e.preventDefault(); 11 | window.ondrop = (e) => e.preventDefault(); 12 | 13 | @inject('user', 'files', 'window') 14 | @observer 15 | export default class Home extends Component { 16 | 17 | 18 | componentDidMount() { 19 | window.ondragenter = (e) => { 20 | e.preventDefault(); 21 | e.stopPropagation(); 22 | if (this.props.files.category === 0) { 23 | this.props.window.showLanding(); 24 | } 25 | }; 26 | 27 | window.ondragleave = (e) => { 28 | e.preventDefault(); 29 | e.stopPropagation(); 30 | if (this.props.files.category === 0) { 31 | this.props.window.showLanding(); 32 | } 33 | }; 34 | 35 | const params = new URLSearchParams(); 36 | params.append('dir', '/'); 37 | this.props.files.fetchFiles(params.toString()); 38 | 39 | } 40 | 41 | handleDrop = async (e) => { 42 | e.preventDefault(); 43 | e.stopPropagation(); 44 | if (this.props.files.category != 0) return false; 45 | 46 | this.props.window.showLanding(); 47 | const params = new URLSearchParams(); 48 | // params.append('category', this.props.files.category); 49 | params.append('dir', this.props.files.dir); 50 | const likeArray = e.dataTransfer.files[0]; 51 | const file = { 52 | name: likeArray.name, 53 | size: likeArray.size, 54 | type: likeArray.type, 55 | lastModified: likeArray.lastModified 56 | }; 57 | await this.props.files.upload(file, params.toString()); 58 | this.props.files.fetchFiles(params.toString()); 59 | } 60 | 61 | render() { 62 | const { files, user } = this.props; 63 | const used = {width: Math.round((user.userInfo.used / user.userInfo.totalSize) * 100) + '%'}; 64 | const items = files.data.map((item, index) => ); 65 | return ( 66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 | {formatSize(user.userInfo.used)} / {formatSize(user.userInfo.totalSize)} 75 | 扩容至5T 76 | 77 |
78 |
79 |
80 |
81 | 82 | 83 |
85 |
86 | {items} 87 |
88 |
89 |
90 |
91 |
92 | 93 | ); 94 | } 95 | } 96 | 97 | Home.propTypes = { 98 | files: PropTypes.object, 99 | window: PropTypes.object, 100 | user: PropTypes.object, 101 | }; -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, BrowserWindow, ipcMain, Tray, Menu, shell } = require('electron'); 4 | const path = require('path'); 5 | const { format } = require('url'); 6 | const server = require('./backend'); 7 | const isProd = process.env.NODE_ENV && process.env.NODE_ENV.trim() === 'production'; 8 | 9 | // global reference to mainWindow (necessary to prevent window from being garbage collected) 10 | let mainWindow; 11 | let tray = null; 12 | 13 | const externalUrl = { 14 | setting: 'https://github.com/zedwang/electron-bdcloud/', 15 | issue: 'https://github.com/zedwang/electron-bdcloud/issues', 16 | help: 'https://github.com/zedwang/electron-bdcloud/', 17 | about: 'https://github.com/zedwang/electron-bdcloud/', 18 | }; 19 | 20 | server.listen(10527, 'localhost', () => { 21 | console.log('API Server listening on port http://localhost:10527'); 22 | }); 23 | 24 | function createMainWindow() { 25 | 26 | const window = new BrowserWindow({ 27 | webPreferences: { 28 | webSecurity: false 29 | }, 30 | width: 986, 31 | height: 600, 32 | minWidth: 986, 33 | minHeight: 600, 34 | frame: false, 35 | icon: './package/resource/logo@2x.png' 36 | }); 37 | 38 | if (!isProd) { 39 | window.webContents.openDevTools(); 40 | window.loadURL('http://localhost:8597'); 41 | } else { 42 | window.webContents.openDevTools(); 43 | window.loadURL(format({ 44 | pathname: path.join(__dirname, 'dist', 'index.html'), 45 | protocol: 'file', 46 | slashes: true 47 | })); 48 | } 49 | 50 | window.on('closed', () => { 51 | mainWindow = null; 52 | tray.destroy(); 53 | }); 54 | 55 | window.on('show', () => { 56 | tray.setHighlightMode('always'); 57 | }); 58 | 59 | window.on('hide', () => { 60 | tray.setHighlightMode('never'); 61 | }); 62 | 63 | window.on('reize', () => { 64 | const [width, height] = window.getSize(); 65 | if (width < 986) { 66 | window.setSize(986, height); 67 | } 68 | if (height < 600) { 69 | window.setSize(width, height); 70 | } 71 | }); 72 | window.webContents.on('devtools-opened', () => { 73 | window.focus(); 74 | setImmediate(() => { 75 | window.focus(); 76 | }); 77 | }); 78 | 79 | return window; 80 | } 81 | 82 | 83 | function menuClick(menuItem) { 84 | shell.openExternal(externalUrl[menuItem.id]); 85 | } 86 | 87 | // quit application when all windows are closed 88 | app.on('window-all-closed', () => { 89 | // on macOS it is common for applications to stay open until the user explicitly quits 90 | if (process.platform !== 'darwin') { 91 | app.quit(); 92 | } 93 | }); 94 | 95 | app.on('activate', () => { 96 | // on macOS it is common to re-create a window even after all windows have been closed 97 | if (mainWindow === null) { 98 | mainWindow = createMainWindow(); 99 | } 100 | }); 101 | 102 | // create main BrowserWindow when electron is ready 103 | app.on('ready', () => { 104 | mainWindow = createMainWindow(); 105 | }); 106 | 107 | ipcMain.on('hidden-window', () => { 108 | if (!tray) { 109 | tray = new Tray('./resource/logo.png'); 110 | } 111 | tray.setToolTip('electron-bd'); 112 | mainWindow.hide(); 113 | 114 | tray.on('right-click', () => { 115 | const contextMenu = Menu.buildFromTemplate([ 116 | {id: 'setting', label: '设置', click: menuClick}, 117 | {id: 'issue', label: '意见反馈', click: menuClick}, 118 | {id: 'help', label: '帮助', click: menuClick}, 119 | {id: 'about', label: '关于', click: menuClick}, 120 | {id: 'setting', label: '退出', role: 'quit'} 121 | ]); 122 | tray.setContextMenu(contextMenu); 123 | }); 124 | tray.on('click', () => { 125 | mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); 126 | }); 127 | }); 128 | 129 | /** 130 | * 网络检测 131 | * 结合na 132 | */ -------------------------------------------------------------------------------- /backend/controller/file.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { signType, isNotEmpty } = require('../utils'); 3 | const fileModel = require('../nedbPromise')('files.db'); 4 | fileModel.ensureIndex({fieldName: 'dir'}); 5 | 6 | module.exports = { 7 | upload: async (req, res) => { 8 | const { dir = '/' } = req.query; 9 | const file = req.body; 10 | const docs = await fileModel.insert( 11 | { 12 | category: signType(file.type), 13 | dir: dir, 14 | ext: path.extname(file.name).substr(1), 15 | name: file.name, 16 | size: file.size, 17 | lastModified: file.lastModified, 18 | type: file.type 19 | } 20 | ); 21 | res.json({ 22 | code: 0, 23 | data: docs 24 | }); 25 | }, 26 | search: async (req, res) => { 27 | const param = {}; 28 | if (isNotEmpty(req.query.dir)) param.dir = req.query.dir; 29 | if (isNotEmpty(req.query.category)) { 30 | if (Number(req.query.category)) { 31 | param.category = req.query.category; 32 | } else { 33 | param.dir = '/'; 34 | } 35 | } 36 | if (isNotEmpty(req.query.q)) { 37 | param.name = {$regex: new RegExp(req.query.q)}; 38 | } 39 | const count = await fileModel.count(param); 40 | const docs = await fileModel.find(param); 41 | // sort causes fetch padding in prod env,why? 42 | // docs = await docs.sort({lastModified: -1, type: -1}); 43 | // res.charset = 'utf-8'; 44 | res.json({ 45 | code: 0, 46 | total: count, 47 | data: docs 48 | }); 49 | }, 50 | createFolder: async (req, res) => { 51 | const { dir = '/', name } = req.body; 52 | if (isNotEmpty(name)) { 53 | const docs = await fileModel.insert( 54 | { 55 | dir: dir, 56 | name: name, 57 | lastModified: new Date().getTime(), 58 | type: 'folder' 59 | } 60 | ); 61 | 62 | return res.json({ 63 | code: 0, 64 | data: docs 65 | }); 66 | } 67 | 68 | return res.json({ 69 | code: 1, 70 | errMsg: '文件名无效' 71 | }); 72 | }, 73 | rename: async (req, res) => { 74 | const id = req.params.id; 75 | const newName = req.query.name; 76 | if (isNotEmpty(id) && isNotEmpty(newName)) { 77 | const replaced = await fileModel.update({_id : id}, {$set: {name: newName, lastModified: new Date().getTime()}}); 78 | if (replaced) { 79 | return res.json({ 80 | code: 0, 81 | data: '成功修改文件/文件夹名!' 82 | }); 83 | } 84 | return res.json({ 85 | code: 1, 86 | data: replaced 87 | }); 88 | 89 | } 90 | return res.json({ 91 | code: 1, 92 | data: '参数无效,{id}或{name}不能为空!' 93 | }); 94 | }, 95 | move: async (req, res) => { 96 | 97 | // const { dir = '/', name } = req.body; 98 | console.log(req.body); 99 | // if (isNotEmpty(name)) { 100 | // const docs = await fileModel.insert( 101 | // { 102 | // dir: dir, 103 | // name: name, 104 | // lastModified: new Date().getTime(), 105 | // type: 'folder' 106 | // } 107 | // ); 108 | 109 | return res.json({ 110 | code: 0, 111 | data: [] 112 | }); 113 | }, 114 | 115 | delete: async (req, res) => { 116 | try { 117 | if (isNotEmpty(req.params.id)) { 118 | // 单个删除 119 | await fileModel.remove({_id: req.params.id}); 120 | } 121 | if (isNotEmpty(req.body)) { 122 | // 多个删除 123 | const deletetion = req.body; 124 | for (let i = 0; i < deletetion.length; i++) { 125 | await fileModel.remove({_id: deletetion[i]}); 126 | } 127 | } 128 | res.json({ 129 | code: 0, 130 | data: 'success' 131 | }); 132 | } catch (error) { 133 | res.json({ 134 | code: 1, 135 | data: error 136 | }); 137 | } 138 | } 139 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobx-react-cloud", 3 | "version": "1.0.0", 4 | "description": "仿百度云盘客户端", 5 | "main": "./main.js", 6 | "scripts": { 7 | "start": "npm-run-all --parallel serve dev", 8 | "serve": "node ./server.js", 9 | "dev": "electron . --debug", 10 | "compile": "set NODE_ENV=production && rm -rf package/dist && webpack", 11 | "compile:main": "set NODE_ENV=production && rm -rf package/main.js && webpack --config ./build/webpack.electron.js", 12 | "test": "echo \"Error: no test specified\"", 13 | "lint": "eslint --fix ./src", 14 | "release": "cd package && rm -rf release && electron-builder", 15 | "package:win": "cd package && rm -rf electron-* && electron-packager ./ --overwrite --platform=win32", 16 | "package": "npm-run-all compile compile:main package:win", 17 | "build": "npm-run-all compile compile:main release" 18 | }, 19 | "author": { 20 | "name": "woox.wzd", 21 | "email": "woox.wzd@gmail.com" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/zedwang/iCloud.git" 26 | }, 27 | "build": { 28 | "productName": "iCloud", 29 | "appId": "com.zed.app", 30 | "win": { 31 | "target": "nsis" 32 | }, 33 | "nsis": { 34 | "oneClick": false, 35 | "allowToChangeInstallationDirectory": true, 36 | "deleteAppDataOnUninstall": true 37 | }, 38 | "directories": { 39 | "output": "../release" 40 | } 41 | }, 42 | "license": "MIT", 43 | "pre-commit": [ 44 | "lint", 45 | "test" 46 | ], 47 | "devDependencies": { 48 | "@babel/core": "^7.0.0-beta.39", 49 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.39", 50 | "@babel/plugin-proposal-decorators": "^7.0.0-beta.39", 51 | "@babel/plugin-proposal-export-default-from": "^7.0.0-beta.39", 52 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.40", 53 | "@babel/polyfill": "^7.0.0-beta.39", 54 | "@babel/preset-env": "^7.0.0-beta.39", 55 | "@babel/preset-es2015": "^7.0.0-beta.39", 56 | "@babel/preset-react": "^7.0.0-beta.39", 57 | "@babel/register": "^7.0.0-beta.39", 58 | "autoprefixer": "^7.2.6", 59 | "autoprefixer-loader": "^3.2.0", 60 | "babel-eslint": "^8.2.2", 61 | "babel-loader": "^8.0.0-beta.0", 62 | "clean-webpack-plugin": "^0.1.17", 63 | "copy-webpack-plugin": "^4.5.1", 64 | "cross-env": "^5.1.3", 65 | "css-loader": "^0.28.7", 66 | "debug": "^3.1.0", 67 | "electron": "^1.8.2", 68 | "electron-builder": "^19.56.0", 69 | "electron-compile": "^6.4.2", 70 | "electron-json-storage": "^4.0.2", 71 | "electron-packager": "^11.1.0", 72 | "electron-reload": "^1.2.2", 73 | "electron-updater": "^2.20.1", 74 | "electron-webpack": "^1.12.1", 75 | "electron-window-state": "^4.1.1", 76 | "eslint": "^4.19.1", 77 | "eslint-plugin-react": "^7.7.0", 78 | "express-http-proxy": "^1.1.0", 79 | "file-loader": "^1.1.6", 80 | "html-webpack-plugin": "^2.30.1", 81 | "http-proxy-middleware": "^0.18.0", 82 | "iconv-lite": "^0.4.19", 83 | "jshint": "^2.9.5", 84 | "node-sass": "^4.7.2", 85 | "nodemon": "^1.17.2", 86 | "npm-run-all": "^4.1.2", 87 | "pm2": "^2.10.2", 88 | "pre-commit": "^1.2.2", 89 | "sass-loader": "^6.0.6", 90 | "style-loader": "^0.19.1", 91 | "uglifyjs-webpack-plugin": "^1.1.5", 92 | "webpack": "^3.10.0", 93 | "webpack-dev-middleware": "2.0.6", 94 | "webpack-dev-server": "2.11.1", 95 | "webpack-hot-middleware": "^2.21.2" 96 | }, 97 | "dependencies": { 98 | "@fortawesome/fontawesome": "^1.1.5", 99 | "@fortawesome/fontawesome-free-regular": "^5.0.10", 100 | "@fortawesome/fontawesome-free-solid": "^5.0.10", 101 | "@fortawesome/react-fontawesome": "^0.0.18", 102 | "classnames": "^2.2.5", 103 | "express": "^4.16.2", 104 | "history": "^4.7.2", 105 | "immutability-helper": "^2.6.4", 106 | "mobx": "^3.5.1", 107 | "mobx-react": "^4.4.1", 108 | "nedb": "^1.8.0", 109 | "nedb-promise": "^2.0.1", 110 | "normalize.css": "^8.0.0", 111 | "prop-types": "^15.6.0", 112 | "react": "^16.2.0", 113 | "react-addons-css-transition-group": "^15.6.2", 114 | "react-contextmenu": "^2.9.2", 115 | "react-dom": "^16.2.0", 116 | "react-router-dom": "^4.2.2" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/styles/layout.scss: -------------------------------------------------------------------------------- 1 | @import 'var'; 2 | 3 | * { 4 | box-sizing: border-box; 5 | :focus { 6 | outline: none; 7 | } 8 | } 9 | html { 10 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif, 'consolas', 'Microsoft yahei'; 11 | font-size: 14px; 12 | } 13 | 14 | body { 15 | background: $background; 16 | color: $color; 17 | overflow: hidden; 18 | } 19 | 20 | 21 | ul { 22 | margin: 0; 23 | padding: 0; 24 | } 25 | 26 | button, 27 | a { 28 | text-decoration-line: none; 29 | color: $color; 30 | } 31 | 32 | .bg-danger { 33 | background-color: #e45858; 34 | color: #fff; 35 | 36 | &:hover { 37 | background-color: #d65353; 38 | } 39 | } 40 | 41 | .toolbar { 42 | padding: 5px 10px; 43 | border-bottom: 1px solid $border-color-light; 44 | 45 | .btn { 46 | margin-left: 5px; 47 | } 48 | } 49 | 50 | .ani-enter { 51 | animation: bounceIn 500ms; 52 | } 53 | .ani-leave { 54 | animation: bounceOutRight 500ms; 55 | } 56 | 57 | .container { 58 | display: flex; 59 | 60 | .aside { 61 | width: 160px; 62 | border-right: $border-width solid $border-color; 63 | flex-shrink: 0; 64 | 65 | .menu-bar { 66 | flex-shrink: 0; 67 | width: 120px; 68 | border-right: 1px solid $border-color; 69 | 70 | ul { 71 | list-style: none; 72 | } 73 | } 74 | 75 | .aside-foot { 76 | position: absolute; 77 | bottom: 0; 78 | left: 0; 79 | height: 60px; 80 | width: 100%; 81 | padding: 15px 10px; 82 | 83 | .process { 84 | height: 10px; 85 | width: 100%; 86 | background: #e6e6e6; 87 | 88 | .used { 89 | width: 10%; 90 | height: 10px; 91 | background: #39d665; 92 | } 93 | } 94 | 95 | .desc { 96 | margin-top: 10px; 97 | > span { 98 | float: left; 99 | } 100 | 101 | > a { 102 | float: right; 103 | color: $color-hover; 104 | } 105 | } 106 | } 107 | } 108 | 109 | .content { 110 | flex-grow: 1; 111 | position: relative; 112 | 113 | 114 | 115 | .dragable::before { 116 | box-sizing: border-box; 117 | display: flex; 118 | justify-content: center; 119 | align-items: center; 120 | content: 'Drag and drop a file!'; 121 | font-size: 24px; 122 | position: absolute; 123 | top: 82px; 124 | left: 0; 125 | width: 100%; 126 | height: calc(100vh - 189px); 127 | background: rgba(238, 240, 246, 0.733);; 128 | border: 2px dashed $border-color; 129 | text-shadow: 0 13.36px 8.896px #c4b59d,0 -2px 1px #fff; 130 | } 131 | } 132 | 133 | .aside, 134 | .content { 135 | position: relative; 136 | height: calc(100vh - 75px); 137 | } 138 | } 139 | 140 | .checkbox.inline, 141 | .radio.inline { 142 | display: inline-block; 143 | } 144 | @keyframes bounceOutRight { 145 | 20% { 146 | opacity: 1; 147 | -webkit-transform: translate3d(-20px, 0, 0); 148 | transform: translate3d(-20px, 0, 0); 149 | } 150 | to { 151 | opacity: 0; 152 | -webkit-transform: translate3d(2000px, 0, 0); 153 | transform: translate3d(2000px, 0, 0); 154 | } 155 | } 156 | 157 | @keyframes bounceIn { 158 | from, 159 | 20%, 160 | 40%, 161 | 60%, 162 | 80%, 163 | to { 164 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 165 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 166 | } 167 | 0% { 168 | opacity: 0; 169 | -webkit-transform: scale3d(0.3, 0.3, 0.3); 170 | transform: scale3d(0.3, 0.3, 0.3); 171 | } 172 | 20% { 173 | -webkit-transform: scale3d(1.1, 1.1, 1.1); 174 | transform: scale3d(1.1, 1.1, 1.1); 175 | } 176 | 40% { 177 | -webkit-transform: scale3d(0.9, 0.9, 0.9); 178 | transform: scale3d(0.9, 0.9, 0.9); 179 | } 180 | 60% { 181 | opacity: 1; 182 | -webkit-transform: scale3d(1.03, 1.03, 1.03); 183 | transform: scale3d(1.03, 1.03, 1.03); 184 | } 185 | 80% { 186 | -webkit-transform: scale3d(0.97, 0.97, 0.97); 187 | transform: scale3d(0.97, 0.97, 0.97); 188 | } 189 | to { 190 | opacity: 1; 191 | -webkit-transform: scale3d(1, 1, 1); 192 | transform: scale3d(1, 1, 1); 193 | } 194 | } 195 | 196 | .checkbox, 197 | .radio { 198 | position: relative; 199 | display: block; 200 | margin-top: 10px; 201 | margin-bottom: 10px; 202 | margin-right: 10px; 203 | 204 | label { 205 | min-height: 20px; 206 | padding-left: 20px; 207 | margin-bottom: 0; 208 | font-weight: 400; 209 | cursor: pointer; 210 | } 211 | 212 | input[type=checkbox], 213 | input[type=radio] { 214 | position: absolute; 215 | margin-top: 4px\9; 216 | margin-left: -20px; 217 | } 218 | } 219 | 220 | .text-right { 221 | text-align: right; 222 | } -------------------------------------------------------------------------------- /src/components/icon.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { observable } from 'mobx'; 3 | import { observer, inject } from 'mobx-react'; 4 | import PropTypes from 'prop-types'; 5 | // import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 6 | import { ContextMenuTrigger, ContextMenu, MenuItem } from 'react-contextmenu'; 7 | import cls from 'classnames'; 8 | import { iconType } from '../utils'; 9 | import '../styles/icon.scss'; 10 | 11 | const ESCAPE_KEY = 27; 12 | const ENTER_KEY = 13; 13 | 14 | @inject('files', 'history') 15 | @observer 16 | export default class MediumIcon extends Component { 17 | @observable rename = false; 18 | @observable editText = '新建文件夹'; 19 | @observable selected = this.props.item.selected || false; 20 | @observable enter = true; 21 | @observable leave = false; 22 | 23 | componentDidMount() { 24 | this.rename = this.props.item.id ? false : true; 25 | } 26 | 27 | componentWillUnmount() { 28 | this.enter = false; 29 | this.leave = true; 30 | } 31 | 32 | handleDoubleClick = (e) => { 33 | e.preventDefault(); 34 | e.stopPropagation(); 35 | this.props.files.data = []; 36 | const path = this.props.item.name; 37 | const absPath = this.props.files.dir + path + '/'; 38 | const params = new URLSearchParams(); 39 | const currentHis = this.props.history.getCurrent().breadcrumb; 40 | this.props.files.setBreadcrumb(currentHis); 41 | this.props.files.addBreadcrumb(path); 42 | this.props.files.setDir(absPath); 43 | this.props.history.add(this.props.files); 44 | 45 | params.append('dir', absPath); 46 | this.props.files.fetchFiles(params.toString()); 47 | 48 | 49 | } 50 | 51 | handleChoose = (e) => { 52 | e.preventDefault(); 53 | e.stopPropagation(); 54 | this.selected = !this.selected; 55 | this.props.item.setSelected(this.selected); 56 | if (this.selected) { 57 | this.props.files.selected.set(this.props.item.id); 58 | } else { 59 | this.props.files.selected.delete(this.props.item.id); 60 | } 61 | } 62 | 63 | handleDelete = async (e, data) => { 64 | await this.props.files.remove(data.id); 65 | const params = new URLSearchParams(); 66 | params.append('dir', this.props.files.dir); 67 | if (this.props.files.category) { 68 | params.append('category', this.props.files.category); 69 | } 70 | this.props.files.fetchFiles(params.toString()); 71 | this.props.files.selected.clear(); 72 | } 73 | 74 | handleRename = (e) => { 75 | e.preventDefault(); 76 | e.stopPropagation(); 77 | this.rename = true; 78 | this.editText = this.props.item.name; 79 | } 80 | 81 | handleChange = (e) => { 82 | this.editText = e.target.value; 83 | } 84 | 85 | handleKeyDown = (e) => { 86 | if (e.which === ESCAPE_KEY) { 87 | this.editText = this.props.item.name; 88 | this.rename = false; 89 | } else if (e.which === ENTER_KEY) { 90 | this.handleSubmit(e); 91 | } 92 | } 93 | 94 | handleSubmit = async () => { 95 | const val = this.editText.trim(); 96 | if (val) { 97 | this.props.item.setName(val); 98 | this.editText = val; 99 | if (this.props.item.id) { 100 | await this.props.files.rename(this.props.item.id, val); 101 | } else { 102 | await this.props.files.createFolder(val); 103 | } 104 | const params = new URLSearchParams(); 105 | params.append('dir', this.props.files.dir); 106 | this.props.files.fetchFiles(params.toString()); 107 | } 108 | this.rename = false; 109 | } 110 | 111 | render() { 112 | const cn = `ico ico-${iconType(this.props.item.type)}`; 113 | return ( 114 | 115 | 116 | 132 | 133 | 134 | 打开 135 | 下载 136 | 分享 137 | 复制 138 | 剪切 139 | 移动到 140 | 推送设备 141 | 删除 142 | 重命名 143 | 属性 144 | 145 | 146 | ); 147 | } 148 | } 149 | MediumIcon.propTypes = { 150 | files: PropTypes.object, 151 | item: PropTypes.object, 152 | history: PropTypes.object, 153 | }; -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { observable } from 'mobx'; 4 | import { observer, inject } from 'mobx-react'; 5 | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; 6 | import Modal from './modal'; 7 | import Button from './button'; 8 | // import Dropdown from './dropdown'; 9 | import PropTypes from 'prop-types'; 10 | 11 | import '../styles/header.scss'; 12 | 13 | 14 | @inject('window', 'user') 15 | @observer 16 | export default class Header extends Component { 17 | @observable showCloseDialog = false; 18 | @observable choice = 'min'; 19 | @observable remenber = false; 20 | 21 | componentDidMount() { 22 | this.props.user.loadUser(); 23 | } 24 | 25 | handleMax = () => { 26 | this.props.window.max(); 27 | } 28 | 29 | handleMini = () => { 30 | this.props.window.mini(); 31 | } 32 | 33 | handleExit = () => { 34 | this.showCloseDialog = true; 35 | } 36 | 37 | handleRestore = () => { 38 | this.props.window.restore(); 39 | } 40 | 41 | handleOnInputChange = (event) => { 42 | const target = event.target; 43 | if (target.type === 'checkbox') { 44 | this.remenber = target.checked; 45 | } else { 46 | this.choice = target.value; 47 | } 48 | } 49 | 50 | onOk = () => { 51 | if (this.remenber) { 52 | // 进入用户信息 53 | } 54 | 55 | if (this.choice === 'exit') { 56 | this.props.window.exit(); 57 | } else { 58 | // 进托盘 59 | this.props.window.hiddenWindow(); 60 | } 61 | this.showCloseDialog = false; 62 | } 63 | 64 | onCancel = () => { 65 | this.showCloseDialog = false; 66 | } 67 | 68 | render() { 69 | const { window, user } = this.props; 70 | const state = window.isMax; 71 | const btns = (() => { 72 | if (state) { 73 | return (
  • ); 74 | } else { 75 | return (
  • ); 76 | } 77 | })(); 78 | 79 | return ( 80 | <>
    81 |
    82 | 百度网盘 83 |
    84 |
    85 |
      86 |
    • 我的网盘
    • 87 |
    • 分享
    • 88 |
    • 隐藏空间
    • 89 |
    • 功能宝箱
    • 90 |
    91 |
    92 |
    93 |
    94 |
    95 | {/* */} 96 | {user.userInfo.niceName} 97 | {/* */} 98 | 99 | 会员中心 100 |
    101 |
    102 |
      103 |
    • 104 |
    • 105 |
    • 106 | {btns} 107 |
    • 108 |
    109 |
    110 |
    111 |
    {this.showCloseDialog ? ( 112 | 113 |
    114 |
    115 |

    关闭窗口提示

    116 | 117 |
    118 |
    119 |

    你点击了关闭按钮,是希望:

    120 |
    121 | 122 |
    123 |
    124 | 125 |
    126 |
    127 | 128 |
    129 |
    130 |
    131 |
    134 |
    135 |
    ) : null 136 | } 137 | 138 | ); 139 | 140 | } 141 | } 142 | 143 | Header.propTypes = { 144 | window: PropTypes.object, 145 | user: PropTypes.object 146 | }; 147 | --------------------------------------------------------------------------------