├── 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 |
31 |
32 |
33 |
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 | 
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,`click me`
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 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {/* */}
67 |
68 | {/* */}
69 |
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 |
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 |
132 |
133 |
134 |
135 | ) : null
136 | }>
137 |
138 | );
139 |
140 | }
141 | }
142 |
143 | Header.propTypes = {
144 | window: PropTypes.object,
145 | user: PropTypes.object
146 | };
147 |
--------------------------------------------------------------------------------