├── master ├── CNAME ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── utils │ └── storage.js ├── constants │ └── index.js ├── components │ ├── Head.js │ ├── ListItem.js │ ├── List.js │ ├── Preview.js │ ├── Layer.js │ ├── Edit.js │ ├── App.js │ └── App.css ├── index.js ├── containers │ ├── ContainerList.js │ ├── ContainerLayer.js │ ├── ContainerPreview.js │ └── ContainerEdit.js ├── index.css ├── actions │ └── note.js ├── registerServiceWorker.js └── reducers │ └── index.js ├── README.md ├── package.json ├── LICENSE └── .gitignore /master: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | yunbi.txliang.com 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seymoe/yunbi-notes/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | export const setStorage = (key, value) => { 2 | if (!window.localStorage) return alert('很抱歉,您的客户端不支持本地存储。') 3 | localStorage.setItem(key, value) 4 | } 5 | 6 | export const getStorage = (key) => { 7 | return localStorage.getItem(key) 8 | } -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const ADD_NOTE = 'ADD_NOTE' 2 | export const DELETE_NOTE = 'DELETE_NOTE' 3 | export const SHOW_NOTE = 'SHOW_NOTE' 4 | //export const MODIFY_NOTE = 'MODIFY_NOTE' 5 | //export const SAVE_NOTE = 'SAVE_NOTE' 6 | export const SHOW_LAYER = 'SHOW_LAYER' 7 | export const SHOW_EDITER = 'SHOW_EDITER' -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Head.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Head = ({isFullScreen}) => { 4 | return ( 5 |
6 |

芸笔

7 | 11 |
12 | ) 13 | } 14 | 15 | export default Head -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 芸笔 2 | 3 | ### 基于create-react-app, React+Redux构建的记事本类应用 4 | ### 在线预览地址:[芸笔](http://yunbi.txliang.com/) 5 | 6 | 别人老是推荐你写个todolist,是不是觉得听的疲劳了?那来写个记事本吧~ 7 | 8 | `git clone https://github.com/ximolang/yunbi-notes.git` 9 | 10 | ### `npm start` 11 | 启动预览 12 | 13 | ### `npm run build` 14 | 构建生产环境文件 15 | 16 | ### 使用gh-pages进行预览 17 | 若不存在gh-pages分支,则可以直接执行命令 18 | ``` 19 | git subtree push --prefix=build origin gh-pages 20 | ``` 21 | 若存在,则可以在github上删掉此分支再执行上一条命令,其他的方式没踩过坑,这种方式暴力有效。 -------------------------------------------------------------------------------- /src/components/ListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ListItem = ({ id, title, content, time, clickPreview, changeStatusShow, showNoteDetail, cls}) => { 4 | return ( 5 |
  • { 6 | clickPreview(id) 7 | changeStatusShow(!showNoteDetail) 8 | }}> 9 |
    10 |

    {title}

    11 |

    {content}

    12 | {time} 13 |
    14 |
  • 15 | ) 16 | } 17 | 18 | export default ListItem -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createStore } from 'redux' 4 | import {Provider } from 'react-redux' 5 | import App from './components/App' 6 | 7 | import reducer from './reducers' 8 | 9 | import registerServiceWorker from './registerServiceWorker' 10 | import './index.css' 11 | 12 | let store = createStore(reducer) 13 | 14 | console.log(store.getState()) 15 | 16 | ReactDOM.render( 17 | 18 | 19 | 20 | , document.getElementById('root')) 21 | registerServiceWorker() 22 | -------------------------------------------------------------------------------- /src/containers/ContainerList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import List from '../components/List' 3 | import { showNote, showLayer } from '../actions/note' 4 | 5 | const mapStateToProps = (state, ownProps) => ({ 6 | listData: state.notes, 7 | isShowLayer: state.isShowLayer 8 | }) 9 | 10 | const mapDispatchToProps = (dispatch, ownProps) => ({ 11 | onClick: (id) => { 12 | dispatch(showNote(id)) 13 | }, 14 | addBtnClick: (isSL) => { 15 | dispatch(showLayer(isSL)) 16 | } 17 | }) 18 | 19 | const ContainerList = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(List) 23 | 24 | export default ContainerList -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yunbi", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "github-markdown-css": "^2.6.0", 7 | "highlight.js": "^9.12.0", 8 | "marked": "^0.3.6", 9 | "moment": "^2.18.1", 10 | "react": "^15.5.4", 11 | "react-dom": "^15.5.4", 12 | "react-redux": "^5.0.5", 13 | "redux": "^3.6.0" 14 | }, 15 | "devDependencies": { 16 | "react-scripts": "1.0.7" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/containers/ContainerLayer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Layer from '../components/Layer' 3 | import { addNote, showLayer } from '../actions/note' 4 | 5 | const mapStateToProps = (state, ownProps) => ({ 6 | isShowLayer: state.isShowLayer 7 | }) 8 | 9 | const mapDispatchToProps = (dispatch, ownProps) => ({ 10 | addNewClick: (title, content) => { 11 | dispatch(addNote(title, content)) 12 | dispatch(showLayer(false)) 13 | }, 14 | closeLayerWrap: () => dispatch(showLayer(false)) 15 | }) 16 | 17 | const ContainerLayer = connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(Layer) 21 | 22 | export default ContainerLayer -------------------------------------------------------------------------------- /src/containers/ContainerPreview.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Preview from '../components/Preview' 3 | import { deleteNote, showEditer } from '../actions/note' 4 | 5 | const mapStateToProps = (state, ownProps) => ({ 6 | currentNote: state.cnote, 7 | isShowEditer: state.isShowEditer 8 | }) 9 | 10 | const mapDispatchToProps = (dispatch, ownProps) => ({ 11 | editClick: (isSE) => { 12 | dispatch(showEditer(isSE)) 13 | }, 14 | deleteClick: (id) => { 15 | dispatch(deleteNote(id)) 16 | } 17 | }) 18 | 19 | const ContainerPreview = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Preview) 23 | 24 | export default ContainerPreview -------------------------------------------------------------------------------- /src/containers/ContainerEdit.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Edit from '../components/Edit' 3 | import { addNote, showEditer } from '../actions/note' 4 | 5 | const mapStateToProps = (state, ownProps) => ({ 6 | isShowEditer: state.isShowEditer, 7 | id: state.cnote.id, 8 | title: state.cnote.title, 9 | content: state.cnote.content 10 | }) 11 | 12 | const mapDispatchToProps = (dispatch, ownProps) => ({ 13 | addNewClick: (title, content, id) => { 14 | dispatch(addNote(title, content, id)) 15 | dispatch(showEditer(false)) 16 | }, 17 | closeLayerWrap: () => dispatch(showEditer(false)) 18 | }) 19 | 20 | const ContainerEditer = connect( 21 | mapStateToProps, 22 | mapDispatchToProps 23 | )(Edit) 24 | 25 | export default ContainerEditer -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ximolang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ListItem from './ListItem' 3 | 4 | const List = ({ listData, isShowLayer, showNoteDetail, changeStatusShow, onClick, addBtnClick }) => { 5 | let showDetailCls = showNoteDetail ? 'toLeft' : 'toRight' 6 | let hasNoteToggle 7 | if (listData.length === 0) { 8 | hasNoteToggle =

    你还没有动过笔呐~

    9 | } else { 10 | hasNoteToggle = 19 | } 20 | return ( 21 |
    22 |

    所有笔记

    23 | addBtnClick(!isShowLayer)}> 24 | {hasNoteToggle} 25 |
    26 | ) 27 | } 28 | 29 | export default List -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | } 5 | html{ 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; 7 | font-size: 14px; 8 | line-height: 1.5; 9 | -ms-text-size-adjust: 100%; 10 | -webkit-text-size-adjust: 100%; 11 | } 12 | a{ 13 | background-color: transparent; 14 | -webkit-text-decoration-skip: objects; 15 | } 16 | *:active{ 17 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 18 | } 19 | 20 | @font-face { 21 | font-family: 'iconfont'; /* project id 267234 */ 22 | src: url('//at.alicdn.com/t/font_bpfbfrt4hdea5rk9.eot'); 23 | src: url('//at.alicdn.com/t/font_bpfbfrt4hdea5rk9.eot?#iefix') format('embedded-opentype'), 24 | url('//at.alicdn.com/t/font_bpfbfrt4hdea5rk9.woff') format('woff'), 25 | url('//at.alicdn.com/t/font_bpfbfrt4hdea5rk9.ttf') format('truetype'), 26 | url('//at.alicdn.com/t/font_bpfbfrt4hdea5rk9.svg#iconfont') format('svg'); 27 | } 28 | 29 | .iconfont{ 30 | font-family:"iconfont" !important; 31 | font-size:16px;font-style:normal; 32 | -webkit-font-smoothing: antialiased; 33 | -webkit-text-stroke-width: 0.2px; 34 | -moz-osx-font-smoothing: grayscale; 35 | } -------------------------------------------------------------------------------- /src/components/Preview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | const marked = require('marked') 3 | 4 | export default class Preview extends Component { 5 | render() { 6 | let showDetailCls = this.props.showNoteDetail ? 'toLeft' : 'toRight' 7 | let isFullScreen = this.props.isFullScreen 8 | return ( 9 |
    10 |
    11 | this.props.changeStatusShow(!this.props.showNoteDetail)} className="iconfont"> 12 | { 13 | this.props.changeFullScreen(!isFullScreen) 14 | }}> 15 | { 16 | if (this.props.currentNote.id === undefined) return 17 | this.props.editClick(!this.props.isShowEditer) 18 | }}> 19 | { 20 | const _id = this.props.currentNote.id 21 | if (_id === undefined) return 22 | this.props.deleteClick(_id) 23 | this.props.changeStatusShow(!this.props.showNoteDetail) 24 | }}> 25 |
    26 |
    27 |
    28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /src/actions/note.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import { getStorage } from '../utils/storage' 3 | import { ADD_NOTE, DELETE_NOTE, SHOW_NOTE, SHOW_LAYER, SHOW_EDITER } from '../constants' 4 | 5 | let noteId 6 | let notesArr2 7 | let notesArr = JSON.parse(getStorage('notes')) 8 | 9 | if (notesArr instanceof Array && notesArr.length !== 0) { 10 | if (notesArr.length === 1) { 11 | notesArr2 = notesArr 12 | } else { 13 | notesArr2 = notesArr.sort(function (a,b){ 14 | return a.id < b.id 15 | }) 16 | } 17 | noteId = notesArr2[0]['id']+1 18 | } else { 19 | noteId = 0 20 | } 21 | 22 | // 增加一篇笔记 或者 编辑之后保存 23 | export const addNote = (title, content, id, time) => { 24 | if (id === undefined && time === undefined) { 25 | return { 26 | type: ADD_NOTE, 27 | id: noteId++, 28 | title, 29 | content, 30 | time: moment().format("YYYY-MM-DD HH:mm") 31 | } 32 | } else { 33 | return { 34 | type: ADD_NOTE, 35 | id, 36 | title, 37 | content, 38 | time: moment().format("YYYY-MM-DD HH:mm") 39 | } 40 | } 41 | } 42 | 43 | // 删除一篇笔记 44 | export const deleteNote = (id) => { 45 | return { 46 | type: DELETE_NOTE, 47 | id 48 | } 49 | } 50 | 51 | // 预览一篇笔记 52 | export const showNote = (id) => { 53 | return { 54 | type: SHOW_NOTE, 55 | id 56 | } 57 | } 58 | 59 | // 显示浮出层 60 | export const showLayer = (isShowLayer) => { 61 | return { 62 | type: SHOW_LAYER, 63 | isShowLayer 64 | } 65 | } 66 | 67 | // 显示编辑浮出层 68 | export const showEditer = (isShowEditer) => { 69 | return { 70 | type: SHOW_EDITER, 71 | isShowEditer 72 | } 73 | } -------------------------------------------------------------------------------- /src/components/Layer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | const marked = require('marked') 3 | 4 | class Layer extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | this.state = { 8 | title: '', 9 | content: '' 10 | } 11 | this.saveANote = this.saveANote.bind(this) 12 | this.changeTitle = this.changeTitle.bind(this) 13 | this.changeContent = this.changeContent.bind(this) 14 | } 15 | 16 | saveANote (tit, ctt) { 17 | if (!tit.trim() || !ctt.trim()) return 18 | this.props.addNewClick(tit, ctt) 19 | } 20 | 21 | changeTitle(e) { 22 | this.setState({ 23 | title: e.target.value 24 | }) 25 | } 26 | 27 | changeContent(e) { 28 | this.setState({ 29 | content: e.target.value 30 | }) 31 | } 32 | 33 | render() { 34 | let markedString = `# ${this.state.title}\n\n${this.state.content}` 35 | return ( 36 |
    37 |
    38 |
    39 |

    新增

    40 | this.saveANote(this.state.title, this.state.content)}> 41 | 42 |
    43 | 44 |
    45 |
    46 | 47 |
    48 |
    49 |
    50 |

    预览

    51 |
    52 |
    53 |
    54 |
    55 | ) 56 | } 57 | } 58 | 59 | export default Layer -------------------------------------------------------------------------------- /src/components/Edit.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | const marked = require('marked') 3 | 4 | class Edit extends React.Component { 5 | constructor(props) { 6 | super(props) 7 | const _t = this.props.title 8 | const _c = this.props.content 9 | this.state = { 10 | title: _t, 11 | content: _c 12 | } 13 | this.saveANote = this.saveANote.bind(this) 14 | this.changeTitle = this.changeTitle.bind(this) 15 | this.changeContent = this.changeContent.bind(this) 16 | } 17 | 18 | saveANote(tit, ctt, id) { 19 | if (!tit.trim() || !ctt.trim()) return 20 | this.props.addNewClick(tit, ctt, id) 21 | } 22 | 23 | changeTitle(e) { 24 | this.setState({ 25 | title: e.target.value 26 | }) 27 | } 28 | 29 | changeContent(e) { 30 | this.setState({ 31 | content: e.target.value 32 | }) 33 | } 34 | 35 | render() { 36 | let markedString = `# ${this.state.title}\n\n${this.state.content}` 37 | return ( 38 |
    39 |
    40 |
    41 |

    编辑

    42 | this.saveANote(this.state.title, this.state.content, this.props.id)}> 43 | 44 |
    45 | 46 |
    47 |
    48 | 49 |
    50 |
    51 |
    52 |

    预览

    53 |
    54 |
    55 |
    56 |
    57 | ) 58 | } 59 | } 60 | 61 | export default Edit -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import Head from './Head' 5 | import ContainerList from '../containers/ContainerList' 6 | import ContainerPreview from '../containers/ContainerPreview' 7 | import ContainerLayer from '../containers/ContainerLayer' 8 | import ContainerEdit from '../containers/ContainerEdit' 9 | import '../../node_modules/github-markdown-css/github-markdown.css' 10 | import './App.css'; 11 | 12 | 13 | class Root extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | showNoteDetail: false, 18 | isFullScreen: false 19 | } 20 | this.changeStatusShow = this.changeStatusShow.bind(this) 21 | this.changeFullScreen = this.changeFullScreen.bind(this) 22 | } 23 | 24 | changeStatusShow(status) { 25 | this.setState({ 26 | showNoteDetail: status 27 | }) 28 | } 29 | 30 | changeFullScreen(status) { 31 | this.setState({ 32 | isFullScreen: status 33 | }) 34 | } 35 | 36 | render() { 37 | const layer = this.props.isShowLayer ? : '' 38 | const editer = this.props.isShowEditer ? : '' 39 | return ( 40 |
    41 | 42 |
    43 | 44 | 48 |
    49 | {layer} 50 | {editer} 51 |
    52 | ) 53 | } 54 | } 55 | 56 | const mapStateToProps = (state, ownProps) => ({ 57 | isShowLayer: state.isShowLayer, 58 | isShowEditer: state.isShowEditer 59 | }) 60 | 61 | const App = connect( 62 | mapStateToProps 63 | )(Root) 64 | 65 | export default App 66 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 29 | 芸笔 30 | 31 | 32 | 35 |
    36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { ADD_NOTE, DELETE_NOTE, SHOW_NOTE, SHOW_LAYER, SHOW_EDITER } from '../constants' 2 | import {setStorage, getStorage} from '../utils/storage' 3 | 4 | // 获取初始数据 5 | const localnotes = JSON.parse(getStorage('notes')) 6 | const localcnote = getStorage('cnote') 7 | const initialState = { 8 | notes: localnotes || [], 9 | cnote: localcnote || {}, 10 | isShowLayer: false, 11 | idShowEditer: false 12 | } 13 | 14 | const note = (state = {}, action) => { 15 | switch (action.type) { 16 | case ADD_NOTE: 17 | return Object.assign({}, state, { 18 | id: action.id, 19 | title: action.title, 20 | content: action.content, 21 | time: action.time 22 | }) 23 | default: 24 | return state 25 | } 26 | } 27 | 28 | const notes = (state = [], action) => { 29 | switch (action.type) { 30 | case ADD_NOTE: 31 | let isNew = true 32 | const _arr = state.map((item) => { 33 | if (item.id === action.id) { 34 | isNew = false 35 | item.title = action.title 36 | item.content = action.content 37 | item.time = action.time 38 | } 39 | return item 40 | }) 41 | 42 | if (isNew) { 43 | return [...state, note({}, action)] 44 | } else { 45 | return _arr 46 | } 47 | default: 48 | return state 49 | } 50 | } 51 | 52 | const noteApp = (state = initialState, action) => { 53 | switch (action.type) { 54 | case ADD_NOTE: 55 | let _notes = notes(state.notes, action) 56 | // 本地存储 57 | setStorage('notes',JSON.stringify(_notes)) 58 | 59 | return Object.assign({}, state, { 60 | notes: _notes 61 | }) 62 | case SHOW_NOTE: 63 | // 选中时增加active样式 64 | const notesArr = state.notes.map((item) => { 65 | if (item.id === action.id) { 66 | item.isActive = true 67 | } else { 68 | item.isActive = false 69 | } 70 | return item 71 | }) 72 | let _cnote = state.notes.filter(item => item.id === action.id)[0] 73 | 74 | // 本地存储 75 | setStorage('notes',JSON.stringify(notesArr)) 76 | setStorage('cnote',_cnote) 77 | 78 | return Object.assign({}, state, { 79 | notes: notesArr, 80 | cnote: _cnote 81 | }) 82 | // 删除列表 83 | case DELETE_NOTE: 84 | let newnotes = state.notes.filter(item => item.id !== action.id) 85 | 86 | // 本地存储 87 | setStorage('notes',JSON.stringify(newnotes)) 88 | setStorage('cnote',{}) 89 | 90 | return Object.assign({}, state, { 91 | notes: newnotes, 92 | cnote: {} 93 | }) 94 | case SHOW_LAYER: 95 | return Object.assign({}, state, { 96 | isShowLayer: action.isShowLayer 97 | }) 98 | case SHOW_EDITER: 99 | return Object.assign({}, state, { 100 | isShowEditer: action.isShowEditer 101 | }) 102 | default: 103 | return state 104 | } 105 | } 106 | 107 | export default noteApp 108 | 109 | -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | background-color: #fff; 8 | } 9 | 10 | .head-wrap { 11 | width: 100%; 12 | height: 60px; 13 | line-height: 60px; 14 | background-color: #24292e; 15 | box-shadow: 0 1px 5px rgba(0,0,0,0.1); 16 | } 17 | 18 | .logo { 19 | float: left; 20 | margin-left: 15px; 21 | font-weight: normal; 22 | font-size: 16px; 23 | color: #fff; 24 | } 25 | 26 | .head-nav{ 27 | overflow: hidden; 28 | float: right; 29 | margin-right: 5px; 30 | list-style: none; 31 | } 32 | .head-nav li{ 33 | float: left; 34 | } 35 | .head-nav li a{ 36 | display: block; 37 | padding: 0 10px; 38 | height: inherit; 39 | color: #fff; 40 | text-decoration: none; 41 | cursor: pointer; 42 | } 43 | .head-nav li a:hover{ 44 | color: #ddd; 45 | } 46 | 47 | main { 48 | position: absolute; 49 | top: 60px; 50 | bottom: 0; 51 | left: 0; 52 | right: 0; 53 | overflow: hidden; 54 | -webkit-transition: top .5s; 55 | transition: top .5s; 56 | 57 | } 58 | main.notop{ 59 | top: 0; 60 | } 61 | main.hastop{ 62 | top: 60px; 63 | } 64 | 65 | .left-box { 66 | position: relative; 67 | float: left; 68 | width: 20%; 69 | height: 100%; 70 | overflow-y: auto; 71 | background-color: #fafafa; 72 | -webkit-transition: width .5s; 73 | transition:width .5s; 74 | } 75 | 76 | .left-box>h1 { 77 | padding: 10px 0; 78 | text-indent: 15px; 79 | font-size: 16px; 80 | font-weight: normal; 81 | height: 30px; 82 | line-height: 30px; 83 | background-color: #fff; 84 | box-shadow: 0 1px 5px rgba(0,0,0,.15); 85 | } 86 | 87 | .left-box>.add-new { 88 | position: fixed; 89 | display: block; 90 | width: 50px; 91 | height: 50px; 92 | line-height: 50px; 93 | left: 10%; 94 | margin-left: -25px; 95 | bottom: 25px; 96 | text-align: center; 97 | color: #fff; 98 | cursor: pointer; 99 | border-radius: 100%; 100 | background-color: rgba(36, 41, 46, 0.5); 101 | box-shadow: 0px 0px 20px 5px rgba(0,0,0,.1); 102 | z-index: 99; 103 | } 104 | .left-box>.add-new:hover{ 105 | background-color: #24292e; 106 | color: #fff; 107 | } 108 | 109 | .right-box { 110 | position: relative; 111 | float: left; 112 | width: 80%; 113 | height: 100%; 114 | overflow-y: auto; 115 | z-index: 100; 116 | background-color: #fff; 117 | -webkit-transition: width .5s; 118 | transition: width .5s; 119 | } 120 | 121 | .list-wrap { 122 | list-style: none; 123 | } 124 | 125 | .list-wrap li { 126 | padding: 5px 15px; 127 | border-bottom: 1px solid #f1f1f1; 128 | cursor: pointer; 129 | } 130 | .list-wrap li .listbox{ 131 | width: 100%; 132 | } 133 | .list-wrap .listbox p{ 134 | display: none; 135 | color: #47423f; 136 | } 137 | .list-wrap .listbox span{ 138 | display: block; 139 | line-height: 30px; 140 | } 141 | .list-wrap .listbox span i{ 142 | margin-right: 5px; 143 | display: inline-block; 144 | vertical-align: baseline; 145 | } 146 | .list-wrap .listtitle{ 147 | margin-bottom: 0; 148 | width: 100%; 149 | overflow: hidden; 150 | text-overflow: ellipsis; 151 | white-space: nowrap; 152 | } 153 | 154 | .list-wrap li:hover { 155 | background-color: #fff; 156 | color: #47423f; 157 | } 158 | 159 | .list-wrap li.active { 160 | background-color: #fff; 161 | color: #47423f; 162 | } 163 | 164 | p.tips{ 165 | margin: 30px 0; 166 | text-align: center; 167 | color: #47423f; 168 | } 169 | 170 | /* 右边的预览区域样式*/ 171 | .preview-top{ 172 | height: 50px; 173 | line-height: 50px; 174 | text-align: right; 175 | box-shadow: 0 1px 5px rgba(0,0,0,.15); 176 | } 177 | .preview-top>i{ 178 | position: absolute; 179 | left: 10px; 180 | top: 10px; 181 | height: 30px; 182 | width: 30px; 183 | line-height: 30px; 184 | text-align: center; 185 | border-radius: 100%; 186 | } 187 | .preview-top>i:active,.preview-top>i:hover{ 188 | background-color: #24292e; 189 | color: #fff; 190 | } 191 | 192 | a.modify-btn,a.delete-btn,.full-btn{ 193 | display: inline-block; 194 | padding: 0 7px; 195 | height: 30px; 196 | line-height: 30px; 197 | margin: 10px; 198 | border-radius: 15px; 199 | cursor: pointer; 200 | } 201 | a.delete-btn{ 202 | margin-right: 20px; 203 | } 204 | 205 | a.modify-btn:hover,a.modify-btn:active,a.full-btn:hover,a.full-btn:active{ 206 | background-color: #24292e; 207 | color: #fff; 208 | } 209 | 210 | a.delete-btn:hover,a.delete-btn:active{ 211 | background-color: red; 212 | color: #fff; 213 | } 214 | 215 | .preview-wrap{ 216 | padding: 10px; 217 | } 218 | 219 | .preview-wrap h1 { 220 | margin-bottom: 10px; 221 | color: #47423f; 222 | } 223 | .preview-wrap p { 224 | line-height: 1.5; 225 | color: #8e857e; 226 | } 227 | 228 | 229 | /* 悬浮层样式 */ 230 | 231 | .layer-wrap { 232 | position: absolute; 233 | top: 0; 234 | right: 0; 235 | bottom: 0; 236 | left: 0; 237 | padding: 0; 238 | } 239 | 240 | .layer-container { 241 | position: absolute; 242 | top: 20px; 243 | right: 20px; 244 | bottom: 20px; 245 | left: 20px; 246 | padding: 10px; 247 | box-shadow: 0 0 10px 5px rgba(0,0,0,0.1); 248 | background-color: rgb(255, 255, 255); 249 | border-radius: 5px; 250 | z-index: 100; 251 | } 252 | .layer-left,.layer-right{ 253 | padding: 5px; 254 | position: relative; 255 | width: 50%; 256 | height: 100%; 257 | box-sizing: border-box; 258 | } 259 | .layer-left{ 260 | float: left; 261 | border-right: 1px solid rgba(0,0,0,0.1); 262 | } 263 | 264 | .layer-right{ 265 | float: right; 266 | } 267 | .layer-preview{ 268 | position: absolute; 269 | top: 46px; 270 | left: 5px; 271 | right: 5px; 272 | bottom: 0; 273 | overflow-y: auto; 274 | } 275 | 276 | .layer-left>h3,.layer-right>h3{ 277 | height: 35px; 278 | line-height: 35px; 279 | border-bottom: 1px solid #f4efea; 280 | margin-bottom: 10px; 281 | } 282 | .icon-save,.icon-close{ 283 | position: absolute; 284 | top: 0; 285 | display: block; 286 | height: 35px; 287 | width: 35px; 288 | line-height: 35px; 289 | text-align: center; 290 | z-index: 101; 291 | cursor: pointer; 292 | border-radius: 100%; 293 | } 294 | .icon-save{ 295 | right: 50px; 296 | } 297 | .icon-close{ 298 | right: 10px; 299 | } 300 | .icon-save:hover,.icon-close:hover{ 301 | background-color: #24292e; 302 | color: #fff; 303 | } 304 | 305 | .input-box{ 306 | margin-bottom: 10px; 307 | height: 50px; 308 | border-bottom: 1px solid rgba(0,0,0,0.1); 309 | } 310 | .input-box input{ 311 | display: block; 312 | width: 100%; 313 | height: inherit; 314 | line-height: 50px; 315 | border: none; 316 | outline: none; 317 | font-size: 16px; 318 | } 319 | .textarea-box{ 320 | position: absolute; 321 | top: 107px; 322 | left: 5px; 323 | right: 5px; 324 | bottom: 0; 325 | } 326 | .textarea-box textarea{ 327 | width: 100%; 328 | height: 100%; 329 | border: none; 330 | resize: none; 331 | outline: none; 332 | font-size: 16px; 333 | line-height: 1.5; 334 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; 335 | } 336 | 337 | .layer-preview{ 338 | line-height: 1.6; 339 | } 340 | .markdown-body>*:first-child{ 341 | text-align: center; 342 | } 343 | 344 | /* 滑动动画 */ 345 | .left-box.toLeft{ 346 | width: 0; 347 | } 348 | .left-box.toRight{ 349 | width: 20%; 350 | } 351 | .right-box.toLeft{ 352 | width: 100%; 353 | } 354 | .right-box.toRight{ 355 | width: 80%; 356 | } 357 | 358 | @media screen and (max-width: 480px) { 359 | .head-wrap{ 360 | height: 50px; 361 | line-height: 50px; 362 | } 363 | .head-show{ 364 | height: 50px !important; 365 | } 366 | main{ 367 | top: 50px; 368 | } 369 | main.hastop{ 370 | top: 50px; 371 | } 372 | .left-box{ 373 | float: none; 374 | position: absolute; 375 | top: 0; 376 | bottom: 0; 377 | width: 100%; 378 | z-index: 2; 379 | -webkit-transition: left .5s; 380 | transition: left .5s; 381 | } 382 | .left-box h1{ 383 | display: none; 384 | } 385 | .left-box>.add-new{ 386 | left: 50%; 387 | background-color: #24292e; 388 | } 389 | 390 | .list-wrap li{ 391 | border-bottom: none; 392 | margin-bottom: 10px; 393 | box-shadow: 0 1px 5px rgba(0,0,0,.15); 394 | } 395 | 396 | .list-wrap .listbox p{ 397 | display: block; 398 | } 399 | 400 | .list-wrap .listtitle{ 401 | margin-bottom: 5px; 402 | width: 100%; 403 | font-size: 18px; 404 | } 405 | 406 | .right-box{ 407 | float: none; 408 | position: absolute; 409 | bottom: 0; 410 | width: 100%; 411 | left: 100%; 412 | top: -100%; 413 | -webkit-transition: left .5s; 414 | transition: left .5s; 415 | } 416 | 417 | /* 右边预览区域*/ 418 | .preview-top{ 419 | height: 40px; 420 | line-height: 40px; 421 | } 422 | .preview-top>i{ 423 | display: block; 424 | left: 5px; 425 | top: 5px; 426 | } 427 | a.modify-btn,a.delete-btn,.full-btn{ 428 | margin: 5px; 429 | } 430 | a.delete-btn{ 431 | margin-right: 5px; 432 | } 433 | 434 | .layer-left{ 435 | float: none; 436 | width: 100%; 437 | border-right: none; 438 | } 439 | .layer-right{ 440 | display: none; 441 | } 442 | .input-box{ 443 | height: 40px; 444 | } 445 | .input-box input{ 446 | line-height: 40px; 447 | } 448 | .textarea-box{ 449 | top: 97px; 450 | } 451 | 452 | /* 滑动动画 */ 453 | .left-box.toLeft{ 454 | left: -100%; 455 | width: 100% 456 | } 457 | .left-box.toRight{ 458 | left: 0; 459 | width: 100%; 460 | } 461 | .right-box.toLeft{ 462 | left: 0; 463 | top: 0; 464 | width: 100%; 465 | } 466 | .right-box.toRight{ 467 | left: 100%; 468 | width: 100%; 469 | } 470 | } --------------------------------------------------------------------------------