├── .angular-cli.json
├── .angulardoc.json
├── .editorconfig
├── .gitignore
├── .vscode
└── launch.json
├── README.md
├── e2e
├── app.e2e-spec.ts
├── app.po.ts
└── tsconfig.e2e.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── proxy.config.json
├── server
├── .babelrc
├── package-lock.json
├── package.json
├── src
│ ├── app.js
│ ├── config
│ │ ├── development.js
│ │ ├── index.js
│ │ └── production.js
│ ├── models
│ │ ├── db.js
│ │ ├── index.js
│ │ └── schema.js
│ ├── routes
│ │ └── index.js
│ └── users
│ │ └── index.js
└── yarn.lock
├── src
├── app
│ ├── animations
│ │ └── global-router-animation.ts
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── component
│ │ ├── button
│ │ │ ├── button.component.html
│ │ │ ├── button.component.scss
│ │ │ ├── button.component.spec.ts
│ │ │ └── button.component.ts
│ │ └── dropdown
│ │ │ ├── dropdown.component.html
│ │ │ ├── dropdown.component.scss
│ │ │ ├── dropdown.component.spec.ts
│ │ │ └── dropdown.component.ts
│ ├── directives
│ │ └── markdown-editor
│ │ │ ├── markdown-editor.directive.spec.ts
│ │ │ ├── markdown-editor.directive.ts
│ │ │ └── simplemde.min.scss
│ ├── interceptor
│ │ └── global-response-interceptor.ts
│ ├── page
│ │ ├── add-link-note
│ │ │ ├── add-link-note.component.html
│ │ │ ├── add-link-note.component.scss
│ │ │ ├── add-link-note.component.spec.ts
│ │ │ └── add-link-note.component.ts
│ │ ├── add-note
│ │ │ ├── add-note.component.html
│ │ │ ├── add-note.component.scss
│ │ │ ├── add-note.component.spec.ts
│ │ │ └── add-note.component.ts
│ │ ├── classification
│ │ │ ├── classification.component.html
│ │ │ ├── classification.component.scss
│ │ │ ├── classification.component.spec.ts
│ │ │ └── classification.component.ts
│ │ ├── edit-note
│ │ │ ├── edit-note.component.html
│ │ │ ├── edit-note.component.scss
│ │ │ ├── edit-note.component.spec.ts
│ │ │ └── edit-note.component.ts
│ │ ├── index
│ │ │ ├── index.component.html
│ │ │ ├── index.component.scss
│ │ │ ├── index.component.spec.ts
│ │ │ └── index.component.ts
│ │ ├── search
│ │ │ ├── search.component.html
│ │ │ ├── search.component.scss
│ │ │ ├── search.component.spec.ts
│ │ │ └── search.component.ts
│ │ ├── tag
│ │ │ ├── tag.component.html
│ │ │ ├── tag.component.scss
│ │ │ ├── tag.component.spec.ts
│ │ │ └── tag.component.ts
│ │ └── view-note
│ │ │ ├── view-note.component.html
│ │ │ ├── view-note.component.scss
│ │ │ ├── view-note.component.spec.ts
│ │ │ └── view-note.component.ts
│ └── services
│ │ ├── loading-bar
│ │ ├── loading-bar.service.spec.ts
│ │ └── loading-bar.service.ts
│ │ ├── msg
│ │ ├── msg.service.spec.ts
│ │ └── msg.service.ts
│ │ ├── note
│ │ ├── note.service.spec.ts
│ │ └── note.service.ts
│ │ └── tag
│ │ ├── tag.service.spec.ts
│ │ └── tag.service.ts
├── assets
│ ├── .gitkeep
│ ├── fonts
│ │ ├── ionicons.eot
│ │ ├── ionicons.svg
│ │ ├── ionicons.ttf
│ │ └── ionicons.woff
│ ├── index.svg
│ ├── ionicons.min.css
│ └── logo.svg
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── typings.d.ts
├── tsconfig.json
└── tslint.json
/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "micro-note"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "app",
21 | "styles": [
22 | "styles.scss"
23 | ],
24 | "scripts": [],
25 | "environmentSource": "environments/environment.ts",
26 | "environments": {
27 | "dev": "environments/environment.ts",
28 | "prod": "environments/environment.prod.ts"
29 | }
30 | }
31 | ],
32 | "e2e": {
33 | "protractor": {
34 | "config": "./protractor.conf.js"
35 | }
36 | },
37 | "lint": [
38 | {
39 | "project": "src/tsconfig.app.json",
40 | "exclude": "**/node_modules/**"
41 | },
42 | {
43 | "project": "src/tsconfig.spec.json",
44 | "exclude": "**/node_modules/**"
45 | },
46 | {
47 | "project": "e2e/tsconfig.e2e.json",
48 | "exclude": "**/node_modules/**"
49 | }
50 | ],
51 | "test": {
52 | "karma": {
53 | "config": "./karma.conf.js"
54 | }
55 | },
56 | "defaults": {
57 | "styleExt": "scss",
58 | "component": {
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/.angulardoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "repoId": "0750bde0-9099-4e20-a314-ec2df01664e5",
3 | "lastSync": 1505296448645
4 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Editor directories and files
9 | .idea
10 | *.suo
11 | *.ntvs*
12 | *.njsproj
13 | *.sln
14 | .vscode/angulardoc.json
15 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${workspaceRoot}/server/src/app.js"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 笔记文章管理应用 [说明](https://lweiwei.com/view/59b5f6947ee00a2998057748)
2 |
3 | 基于`Angular4`+`koa2`实现的一个自动生成笔记文章的应用管理程序,在线预览[地址](http://lweiwei.com:8000)
4 | ### 开发
5 | (本地要安装mongodb,并且启动mongodb服务)
6 |
7 | ``` bash
8 | # git clone https://github.com/linguowei/micro-note.git
9 | # npm install 或者 yarn install 注意不要用cnpm!!
10 | # npm run start
11 | # cd /server // server文件夹是该应用的后端代码
12 | # npm install 或者 yarn install 注意不要用cnpm!!
13 | # npm run dev
14 | # 浏览器访问 localhost:4200
15 | ```
16 | ### 部署
17 | ``` bash
18 | # git clone https://github.com/linguowei/micro-note.git
19 | # npm install 或者 yarn install 注意不要用cnpm!!
20 | # npm run build
21 | # 1. 把build出来的文件夹dist和server文件夹拷贝到服务器放在一个目录里面
22 | # 2. 服务器安装node、mongodb、pm2
23 | # 3. 启动mongodb服务,进入server文件夹执行npm install, 然后执行npm run pro
24 | ```
25 | ### License
26 | [GPL 禁止商用](https://www.oschina.net/question/54100_9455)
27 |
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { MicroNotePage } from './app.po';
2 |
3 | describe('micro-note App', () => {
4 | let page: MicroNotePage;
5 |
6 | beforeEach(() => {
7 | page = new MicroNotePage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to app!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class MicroNotePage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 | angularCli: {
23 | environment: 'dev'
24 | },
25 | reporters: ['progress', 'kjhtml'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['Chrome'],
31 | singleRun: false
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "micro-note",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng serve --proxy-config proxy.config.json --port 4300",
8 | "build": "ng build --prod",
9 | "test": "ng test",
10 | "lint": "ng lint",
11 | "e2e": "ng e2e"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "^4.0.0",
16 | "@angular/common": "^4.0.0",
17 | "@angular/compiler": "^4.0.0",
18 | "@angular/core": "^4.0.0",
19 | "@angular/forms": "^4.0.0",
20 | "@angular/http": "^4.0.0",
21 | "@angular/platform-browser": "^4.0.0",
22 | "@angular/platform-browser-dynamic": "^4.0.0",
23 | "@angular/router": "^4.0.0",
24 | "core-js": "^2.4.1",
25 | "highlight.js": "^9.12.0",
26 | "marked": "^0.3.6",
27 | "rxjs": "^5.4.1",
28 | "simplemde": "^1.11.2",
29 | "zone.js": "^0.8.14"
30 | },
31 | "devDependencies": {
32 | "@angular/cli": "1.2.7",
33 | "@angular/compiler-cli": "^4.0.0",
34 | "@angular/language-service": "^4.0.0",
35 | "@types/jasmine": "~2.5.53",
36 | "@types/jasminewd2": "~2.0.2",
37 | "@types/node": "~6.0.60",
38 | "codelyzer": "~3.0.1",
39 | "jasmine-core": "~2.6.2",
40 | "jasmine-spec-reporter": "~4.1.0",
41 | "karma": "~1.7.0",
42 | "karma-chrome-launcher": "~2.1.1",
43 | "karma-cli": "~1.0.1",
44 | "karma-coverage-istanbul-reporter": "^1.2.1",
45 | "karma-jasmine": "~1.1.0",
46 | "karma-jasmine-html-reporter": "^0.2.2",
47 | "protractor": "~5.1.2",
48 | "ts-node": "~3.0.4",
49 | "tslint": "~5.3.2",
50 | "typescript": "~2.3.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/proxy.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api":{
3 | "target":"http://localhost:3002"
4 | }
5 | }
--------------------------------------------------------------------------------
/server/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-0"
5 | ],
6 | "plugins": []
7 | }
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "micro-note-server",
3 | "version": "1.0.0",
4 | "description": "this is micro-note-server",
5 | "main": "app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "NODE_ENV=development nodemon --exec babel-node ./src/app.js",
9 | "pro": "NODE_ENV=production pm2 start ./src/app.js"
10 | },
11 | "author": "1368033036@qq.com",
12 | "license": "ISC",
13 | "dependencies": {
14 | "bluebird": "^3.5.0",
15 | "koa": "^2.3.0",
16 | "koa-bodyparser": "^4.2.0",
17 | "koa-cors": "0.0.16",
18 | "koa-generic-session-mongo": "^0.4.0",
19 | "koa-router": "^7.2.1",
20 | "koa-session-minimal": "^3.0.4",
21 | "koa-static-cache": "^5.1.1",
22 | "mongoose": "^4.11.7",
23 | "node-readability": "^3.0.0",
24 | "phantom": "^4.0.5"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.25.0",
28 | "babel-preset-es2015": "^6.24.1",
29 | "babel-preset-stage-0": "^6.24.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/app.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa')
2 | const bodyParser = require('koa-bodyparser')
3 | const router = require('./routes/index')
4 | const cors = require('koa-cors')
5 | const path = require('path')
6 | const staticCache = require('koa-static-cache')
7 | const config = require('./config/index')
8 | const session = require('koa-session-minimal')
9 | const MongoStore = require('koa-generic-session-mongo')
10 |
11 | const resolve = file => path.resolve(__dirname, file)
12 | const app = new Koa()
13 |
14 | app.use(staticCache({
15 | dir: resolve('../../dist'),
16 | maxAge: 365 * 24 * 60 * 60,
17 | gzip: true
18 | }))
19 |
20 | let cookie = {
21 | maxAge: 5 * 60 * 1000, // cookie有效时长
22 | expires: '', // cookie失效时间
23 | path: '', // 写cookie所在的路径
24 | domain: '', // 写cookie所在的域名
25 | httpOnly: true, // 是否只用于http请求中获取
26 | overwrite: '', // 是否允许重写
27 | secure: '',
28 | sameSite: '',
29 | signed: '',
30 | }
31 | app.use(session({
32 | key: 'SESSION_ID',
33 | store: new MongoStore({}),
34 | cookie: cookie
35 | }))
36 |
37 | app.use(cors())
38 | app.use(bodyParser())
39 | app.use(router.routes())
40 |
41 | app.listen(config.port || 3001, ()=> {
42 | console.log('监听端口' + config.port || 3001)
43 | console.log("环境变量是" + process.env.NODE_ENV);
44 | })
--------------------------------------------------------------------------------
/server/src/config/development.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 开发环境的配置内容
3 | */
4 |
5 | module.exports = {
6 | env: 'development', //环境名称
7 | port: 3002, //服务端口号
8 | mongodb_url: 'mongodb://localhost:27017/microDev', //数据库地址
9 | }
--------------------------------------------------------------------------------
/server/src/config/index.js:
--------------------------------------------------------------------------------
1 | const development_env = require('./development');
2 | const production_env = require('./production');
3 |
4 | //根据不同的NODE_ENV,输出不同的配置对象,默认输出development的配置对象
5 | module.exports = {
6 | development: development_env,
7 | production: production_env
8 | }[process.env.NODE_ENV || 'development']
9 |
--------------------------------------------------------------------------------
/server/src/config/production.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 生产环境的配置内容
3 | */
4 |
5 | module.exports = {
6 | env: 'production', //环境名称
7 | port: 3001, //服务端口号
8 | mongodb_url: 'mongodb://localhost:27017/microPro', //数据库地址
9 | }
--------------------------------------------------------------------------------
/server/src/models/db.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | mongoose.Promise = require('bluebird')
3 | const config = require('../config')
4 |
5 | mongoose.connect(config.mongodb_url,{ useMongoClient: true })
6 | let db = mongoose.connection
7 |
8 | db.on('error', console.error.bind(console, '连接错误:'));
9 | db.once('open', function() {
10 | console.log(config.mongodb_url, '连接成功');
11 | })
12 |
13 |
14 | module.exports = mongoose
--------------------------------------------------------------------------------
/server/src/models/index.js:
--------------------------------------------------------------------------------
1 | const models = require('./schema')
2 |
3 | module.exports = models
--------------------------------------------------------------------------------
/server/src/models/schema.js:
--------------------------------------------------------------------------------
1 | const db = require('./db')
2 |
3 | const noteSchema = new db.Schema({
4 | user_name: String,
5 | title: String,
6 | content: String,
7 | tag: Array,
8 | date: Date,
9 | sourceLink: String
10 | })
11 |
12 | const tagSchema = new db.Schema({
13 | user_name: String,
14 | name: String
15 | })
16 |
17 | const Models = {
18 | NoteList: db.model('NoteList', noteSchema),
19 | TagList: db.model('TagList', tagSchema)
20 | }
21 |
22 | module.exports = Models
--------------------------------------------------------------------------------
/server/src/routes/index.js:
--------------------------------------------------------------------------------
1 | const Router = require('koa-router')
2 | const read = require('node-readability')
3 | const fs = require('fs')
4 | const path = require('path')
5 | const Models = require('../models')
6 | const phantom = require('phantom')
7 | const users = require('../users').items
8 |
9 | const resolve = file => path.resolve(__dirname, file)
10 | const router = new Router()
11 |
12 | // 定义成功的返回模板
13 | let successState = {
14 | msg: 'success',
15 | code: 200,
16 | data: ''
17 | }
18 | // 定义错误的返回模板
19 | let errorState = {
20 | msg: 'error',
21 | code: '',
22 | data: ''
23 | }
24 |
25 | const findUser = function(name, password){
26 | return users.find(function(item){
27 | return item.name === name && item.password === password;
28 | });
29 | };
30 |
31 | router.post('/api/login', async (ctx, next) => {
32 | let user = findUser(ctx.request.body.name, ctx.request.body.pwd);
33 | if(user){
34 | let session = ctx.session
35 | session.isLogin = true
36 | session.userName = ctx.request.body.name
37 | successState.msg = '登录成功!'
38 | ctx.body = successState
39 | }else{
40 | errorState.msg = '账号或密码错误!'
41 | errorState.code = 400
42 | ctx.body = errorState
43 | }
44 | })
45 |
46 | router.get('/api/logout', async (ctx, next) => {
47 | ctx.session = null;
48 | let success = Object.assign({}, successState, {
49 | code: 200,
50 | msg: '退出登录成功!'
51 | })
52 | ctx.response.body = success
53 | })
54 |
55 |
56 | /**
57 | * 笔记 C、R、U、D
58 | */
59 |
60 | router.get('/api/allNote', async (ctx, next) => {
61 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
62 | await Models.NoteList.find({user_name: ctx.session.userName}, (err, docs) => {
63 | if(err){
64 | ctx.throw(500)
65 | return
66 | }
67 | let success = Object.assign({}, successState, {
68 | data: docs.reverse()
69 | })
70 | ctx.response.body = success
71 | ctx.sessionHandler.regenerateId()
72 | })
73 | } else {
74 | let error = Object.assign({}, errorState, {
75 | code: 401,
76 | msg: 'Unauthorized'
77 | })
78 | ctx.body = error
79 | }
80 | })
81 |
82 | // 生成笔记
83 | router.post('/api/generateNote', async (ctx, next) => {
84 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
85 | const instance = await phantom.create()
86 | const page = await instance.createPage()
87 | const status = await page.open(ctx.request.body.link)
88 | const content = await page.property('content')
89 | await instance.exit()
90 | await new Promise((resolve, reject) => {
91 | read(content, (err, article, meta) => {
92 | if (err) {
93 | reject(err)
94 | } else {
95 | resolve(article)
96 | }
97 | })
98 | }).then((success) => {
99 | ctx.body = {
100 | title: success.title,
101 | content: success.content
102 | }
103 | })
104 | }else{
105 | let error = Object.assign({}, successState, {
106 | code: 401,
107 | msg: 'Unauthorized'
108 | })
109 | ctx.body = error
110 | }
111 | })
112 |
113 | // 新增笔记
114 | router.post('/api/addNote', async (ctx, next) => {
115 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
116 | ctx.request.body['user_name'] = ctx.session.userName
117 | await new Models.NoteList(ctx.request.body).save((err, docs) => {
118 | if(err){
119 | ctx.throw(500)
120 | return
121 | }
122 | successState.data = docs
123 | ctx.response.body = successState
124 | })
125 | } else {
126 | let error = Object.assign({}, errorState, {
127 | code: 401,
128 | msg: 'Unauthorized'
129 | })
130 | ctx.body = error
131 | }
132 | })
133 |
134 | // 删除笔记
135 | router.post('/api/deleteNote', async (ctx, next ) => {
136 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
137 | await Models.NoteList.remove({
138 | _id: ctx.request.body.id,
139 | user_name: ctx.session.userName
140 | }, (err, docs) => {
141 | if(err){
142 | ctx.throw(500)
143 | return
144 | }
145 | successState.data = docs
146 | ctx.response.body = successState
147 | })
148 | } else {
149 | let error = Object.assign({}, errorState, {
150 | code: 401,
151 | msg: 'Unauthorized'
152 | })
153 | ctx.body = error
154 | }
155 | })
156 |
157 | // 修改笔记
158 | router.post('/api/modify', async (ctx, next) => {
159 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
160 | await new Promise((resolve, reject) => {
161 | Models.NoteList.update({
162 | _id: ctx.request.body._id,
163 | user_name: ctx.session.userName
164 | }, ctx.request.body, (err, docs) => {
165 | if(err){
166 | reject(err)
167 | }else{
168 | resolve(docs)
169 | }
170 | })
171 | }).then((success) => {
172 | successState.data = ctx.request.body
173 | ctx.response.body = successState
174 | })
175 | } else {
176 | let error = Object.assign({}, errorState, {
177 | code: 401,
178 | msg: 'Unauthorized'
179 | })
180 | ctx.body = error
181 | }
182 | })
183 |
184 |
185 | /**
186 | * 标签 C、R、U、D
187 | */
188 |
189 | router.post('/api/addTag', async (ctx, next) => {
190 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
191 | ctx.request.body['user_name'] = ctx.session.userName
192 | await new Models.TagList(ctx.request.body).save((err, docs) => {
193 | if(err){
194 | ctx.throw(500)
195 | return
196 | }
197 | successState.data = docs
198 | ctx.response.body = successState
199 | })
200 | } else {
201 | let error = Object.assign({}, errorState, {
202 | code: 401,
203 | msg: 'Unauthorized'
204 | })
205 | ctx.body = error
206 | }
207 | })
208 |
209 | router.post('/api/deleteTag', async (ctx, next) => {
210 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
211 | await Models.TagList.remove({
212 | _id: ctx.request.body.id,
213 | user_name: ctx.session.userName
214 | }, (err, docs) => {
215 | if(err){
216 | ctx.throw(500)
217 | return
218 | }
219 | successState.data = docs
220 | ctx.response.body = successState
221 | })
222 | } else {
223 | let error = Object.assign({}, errorState, {
224 | code: 401,
225 | msg: 'Unauthorized'
226 | })
227 | ctx.body = error
228 | }
229 | })
230 |
231 | router.get('/api/TagList', async (ctx, next) => {
232 | if ( ctx.session && ctx.session.isLogin && ctx.session.userName ) {
233 | await Models.TagList.find({user_name: ctx.session.userName}, (err, docs) => {
234 | if(err){
235 | ctx.throw(500)
236 | return
237 | }
238 | let success = Object.assign({}, successState, {
239 | data: docs.reverse()
240 | })
241 | ctx.response.body = success
242 | })
243 | } else {
244 | let error = Object.assign({}, errorState, {
245 | code: 401,
246 | msg: 'Unauthorized'
247 | })
248 | ctx.body = error
249 | }
250 | })
251 |
252 | router.get('*', (ctx, next) => {
253 | const html = fs.readFileSync(resolve('../../../dist/' + 'index.html'), 'utf-8')
254 | ctx.body = html
255 | })
256 |
257 | module.exports = router
--------------------------------------------------------------------------------
/server/src/users/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | items: [
3 | {name: 'test', password: '123456'},
4 | {name: 'test2', password: '123456'},
5 | ]
6 | }
--------------------------------------------------------------------------------
/src/app/animations/global-router-animation.ts:
--------------------------------------------------------------------------------
1 | import { AnimationEntryMetadata, state } from '@angular/core';
2 | import { trigger, transition, animate, style, query, group } from '@angular/animations';
3 |
4 | export const routeAnimation: AnimationEntryMetadata =
5 | trigger('routeAnimation', [
6 | transition(':enter', [
7 | style({
8 | position: 'absolute'
9 | }),
10 | animate('0.5s ease-in-out')
11 | ]),
12 | transition('* => *', [
13 | query(':leave', style({ transform: 'translateX(0)', position: 'absolute'}), { optional: true }),
14 | query(':enter', style({ transform: 'translateX(100%)', position: 'absolute'}), { optional: true }),
15 |
16 | group([
17 | query(':leave', animate('.5s ease-in-out', style({transform: 'translateX(-100%)'})), { optional: true }),
18 | query(':enter', animate('.5s ease-in-out', style({transform: 'translateX(0)'})), { optional: true })
19 | ])
20 | ])
21 | ]);
22 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { EditNoteComponent } from './page/edit-note/edit-note.component';
2 | import { ViewNoteComponent } from './page/view-note/view-note.component';
3 | import { ClassificationComponent } from './page/classification/classification.component';
4 | import { TagComponent } from './page/tag/tag.component';
5 | import { SearchComponent } from './page/search/search.component';
6 | import { AddLinkNoteComponent } from './page/add-link-note/add-link-note.component';
7 | import { IndexComponent } from './page/index/index.component';
8 | import { AppComponent } from './app.component';
9 | import { AddNoteComponent } from './page/add-note/add-note.component';
10 | import { NgModule } from '@angular/core';
11 | import { Routes, RouterModule } from '@angular/router';
12 |
13 | const routes: Routes = [
14 | {
15 | path: 'index',
16 | component: IndexComponent
17 | },
18 | {
19 | path: 'addNote',
20 | component: AddNoteComponent,
21 | children: []
22 | },
23 | {
24 | path: 'addLinkNote',
25 | component: AddLinkNoteComponent,
26 | children: []
27 | },
28 | {
29 | path: 'tag',
30 | component: TagComponent,
31 | children: []
32 | },
33 | {
34 | path: 'classification',
35 | component: ClassificationComponent,
36 | children: []
37 | },
38 | {
39 | path: 'search',
40 | component: SearchComponent,
41 | children: []
42 | },
43 | {
44 | path: 'viewNote',
45 | component: ViewNoteComponent,
46 | children: []
47 | },
48 | {
49 | path: 'editNote',
50 | component: EditNoteComponent,
51 | children: []
52 | },
53 | {
54 | path: '**',
55 | redirectTo: 'index',
56 | pathMatch: 'full'
57 | }
58 | ];
59 |
60 | @NgModule({
61 | imports: [RouterModule.forRoot(routes)],
62 | exports: [RouterModule]
63 | })
64 | export class AppRoutingModule { }
65 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | .icon{
2 | font-size: 22px;
3 | }
4 | .left-menu{
5 | width: 60px;
6 | height: 100vh;
7 | box-shadow: 0 1px 6px #ccc;
8 | background-color: #fff;
9 | float: left;
10 | ul{
11 | li{
12 | text-align: center;
13 | padding: 15px 0;
14 | cursor: pointer;
15 | font-size: 12px;
16 | a{
17 | color: #919191;
18 | &:hover{
19 | color: #404040;
20 | }
21 | };
22 | .router-active{
23 | color: #404040;
24 | }
25 | }
26 | .personal-center{
27 | width: 60px;
28 | position: absolute;
29 | bottom: 0;
30 | left: 0;
31 | a{
32 | font-size: 26px;
33 | }
34 | }
35 | .personal-center-content{
36 | width: 130px;
37 | height: 100px;
38 | position: absolute;
39 | bottom: 10px;
40 | left: 65px;
41 | background-color: #fff;
42 | box-shadow: 0 1px 6px #ccc;
43 | border-radius: 3px;
44 | z-index: 30;
45 | padding: 5px;
46 | .title{
47 | height: 50px;
48 | border-bottom: 1px #ececec solid;
49 | i{
50 | font-size: 20px;
51 | color: #71777c;
52 | }
53 | }
54 | .logout{
55 | text-align: left;
56 | padding: 5px;
57 | font-size: 14px;
58 | color: #4a4a4a;
59 | margin-top: 10px;
60 | &:hover{
61 | background-color: #3db8c1;
62 | color: #fff;
63 | }
64 | }
65 | }
66 | };
67 | .logo{
68 | border-bottom: 1px solid #ececec;
69 | }
70 | }
71 | .content-wrap{
72 | overflow: hidden;
73 | height: 100vh;
74 | background-color: #f7f7f7;
75 | }
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 |
4 | import { AppComponent } from './app.component';
5 |
6 | describe('AppComponent', () => {
7 | beforeEach(async(() => {
8 | TestBed.configureTestingModule({
9 | imports: [
10 | RouterTestingModule
11 | ],
12 | declarations: [
13 | AppComponent
14 | ],
15 | }).compileComponents();
16 | }));
17 |
18 | it('should create the app', async(() => {
19 | const fixture = TestBed.createComponent(AppComponent);
20 | const app = fixture.debugElement.componentInstance;
21 | expect(app).toBeTruthy();
22 | }));
23 |
24 | it(`should have as title 'app'`, async(() => {
25 | const fixture = TestBed.createComponent(AppComponent);
26 | const app = fixture.debugElement.componentInstance;
27 | expect(app.title).toEqual('app');
28 | }));
29 |
30 | it('should render title in a h1 tag', async(() => {
31 | const fixture = TestBed.createComponent(AppComponent);
32 | fixture.detectChanges();
33 | const compiled = fixture.debugElement.nativeElement;
34 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
35 | }));
36 | });
37 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Router } from '@angular/router';
2 | import { MsgService } from './services/msg/msg.service';
3 | import { HttpClient } from '@angular/common/http';
4 | import { Component, DoCheck } from '@angular/core';
5 | import { routeAnimation } from './animations/global-router-animation';
6 |
7 | @Component({
8 | selector: 'app-root',
9 | templateUrl: './app.component.html',
10 | styleUrls: ['./app.component.scss'],
11 | host: {
12 | '(document:click)': 'documentClick($event)',
13 | }
14 | })
15 | export class AppComponent implements DoCheck {
16 | isShowAccount = false;
17 | userName = '';
18 |
19 | constructor(
20 | private http: HttpClient,
21 | private msg: MsgService,
22 | private router: Router
23 | ) {
24 | }
25 |
26 | ngDoCheck() {
27 | this.userName = localStorage.getItem('userName');
28 | }
29 |
30 | personalCenter(e) {
31 | this.isShowAccount = !this.isShowAccount;
32 | e.stopPropagation();
33 | }
34 |
35 | documentClick(e) {
36 | this.isShowAccount = false;
37 | e.stopPropagation();
38 | }
39 | personalCenterContent(e) {
40 | e.stopPropagation();
41 | }
42 |
43 | logout() {
44 | this.http.get('/api/logout').subscribe((res) => {
45 | if (res['code'] === 200) {
46 | this.msg.info(res['msg']);
47 | this.isShowAccount = false;
48 | setTimeout(() => {
49 | this.router.navigate(['/index']);
50 | }, 2000);
51 | }
52 | });
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { GlobalResponseInterceptor } from './interceptor/global-response-interceptor';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
5 | import { FormsModule } from '@angular/forms';
6 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
7 |
8 | // customize module
9 | import { AppRoutingModule } from './app-routing.module';
10 |
11 | // component
12 | import { AppComponent } from './app.component';
13 | import { AddNoteComponent } from './page/add-note/add-note.component';
14 | import { AddLinkNoteComponent, CalculationContentHeightDirective } from './page/add-link-note/add-link-note.component';
15 | import { SearchComponent } from './page/search/search.component';
16 | import { IndexComponent } from './page/index/index.component';
17 | import { DropdownComponent } from './component/dropdown/dropdown.component';
18 | import { ButtonComponent } from './component/button/button.component';
19 | import { ClassificationComponent, ClassificationTabsContentHeightDirective, FilterNoteContentPipe } from './page/classification/classification.component';
20 | import { ViewNoteComponent } from './page/view-note/view-note.component';
21 | import { EditNoteComponent } from './page/edit-note/edit-note.component';
22 | import { TagComponent } from './page/tag/tag.component';
23 |
24 | // directive
25 | import { MarkdownEditorDirective } from './directives/markdown-editor/markdown-editor.directive';
26 |
27 | // service
28 | import { LoadingBarService } from './services/loading-bar/loading-bar.service';
29 | import { MsgService } from './services/msg/msg.service';
30 | import { NoteService } from './services/note/note.service';
31 | import { TagService } from './services/tag/tag.service';
32 |
33 | @NgModule({
34 | declarations: [
35 | AppComponent,
36 | AddNoteComponent,
37 | AddLinkNoteComponent,
38 | SearchComponent,
39 | IndexComponent,
40 | MarkdownEditorDirective,
41 | CalculationContentHeightDirective,
42 | ClassificationTabsContentHeightDirective,
43 | DropdownComponent,
44 | ButtonComponent,
45 | TagComponent,
46 | ClassificationComponent,
47 | ViewNoteComponent,
48 | EditNoteComponent,
49 | FilterNoteContentPipe,
50 | ],
51 | imports: [
52 | BrowserModule,
53 | HttpClientModule, // http模块
54 | FormsModule, // 表单模块
55 | AppRoutingModule, // 路由配置模块
56 | BrowserAnimationsModule, // 动画模块
57 | ],
58 | providers: [
59 | {
60 | provide: HTTP_INTERCEPTORS,
61 | useClass: GlobalResponseInterceptor,
62 | multi: true,
63 | },
64 | LoadingBarService,
65 | TagService,
66 | NoteService,
67 | MsgService,
68 | ],
69 | bootstrap: [AppComponent], // 根组件
70 | schemas: [CUSTOM_ELEMENTS_SCHEMA]
71 | })
72 | export class AppModule { }
73 |
--------------------------------------------------------------------------------
/src/app/component/button/button.component.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
--------------------------------------------------------------------------------
/src/app/component/button/button.component.scss:
--------------------------------------------------------------------------------
1 | .my-btn{
2 | color: #495060;
3 | outline: 0;
4 | display: inline-block;
5 | margin-bottom: 0;
6 | font-weight: 400;
7 | text-align: center;
8 | cursor: pointer;
9 | border: 1px solid dddee1;
10 | white-space: nowrap;
11 | padding: 6px 15px;
12 | font-size: 12px;
13 | border-radius: 4px;
14 | background-color: transparent;
15 | border: 1px solid #ccc;
16 | vertical-align:middle
17 | }
--------------------------------------------------------------------------------
/src/app/component/button/button.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ButtonComponent } from './button.component';
4 |
5 | describe('ButtonComponent', () => {
6 | let component: ButtonComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ ButtonComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ButtonComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/component/button/button.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-button',
5 | templateUrl: './button.component.html',
6 | styleUrls: ['./button.component.scss']
7 | })
8 | export class ButtonComponent implements OnInit {
9 | @Input() Icon: string;
10 | constructor() {
11 | }
12 |
13 | ngOnInit() {
14 | }
15 |
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/app/component/dropdown/dropdown.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/component/dropdown/dropdown.component.scss:
--------------------------------------------------------------------------------
1 | ::-webkit-scrollbar{
2 | width: 0px;
3 | }
4 | .my-dropdown{
5 | position: relative;
6 | width: 100%;
7 | min-width: 80px;
8 | }
9 | .my-dropdown-rel{
10 | text-align: center;
11 | cursor: pointer;
12 | }
13 | .my-dropdown-menu{
14 | position: absolute;
15 | top: 25px;
16 | z-index: 99;
17 | width: 100%;
18 | background-color: #fff;
19 | border-radius: 4px;
20 | box-shadow: 0 1px 6px rgba(0,0,0,.2);
21 | height: 0px;
22 | overflow: hidden;
23 | }
24 | .my-dropdown-menu ul{
25 | padding: 4px 0;
26 | max-height: 192px;
27 | overflow: auto;
28 | }
29 | .my-dropdown-menu ul li{
30 | text-align: center;
31 | padding: 7px 16px;
32 | color: #495060;
33 | cursor: pointer;
34 | }
35 | .my-dropdown-menu ul li:hover{
36 | background: #f3f3f3;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/src/app/component/dropdown/dropdown.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { DropdownComponent } from './dropdown.component';
4 |
5 | describe('DropdownComponent', () => {
6 | let component: DropdownComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ DropdownComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(DropdownComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/component/dropdown/dropdown.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
2 | import { trigger, state, style, animate, transition } from '@angular/animations';
3 |
4 | @Component({
5 | selector: 'app-dropdown',
6 | templateUrl: './dropdown.component.html',
7 | styleUrls: ['./dropdown.component.scss'],
8 | animations: [
9 | trigger('dropdownMenuState', [
10 | state('inactive', style({
11 | height: '0'
12 | })),
13 | state('active', style({
14 | height: '200px'
15 | })),
16 | transition('inactive => active', animate('300ms ease-in')),
17 | transition('active => inactive', animate('300ms ease-out'))
18 | ])
19 | ],
20 | host: {
21 | '(document:click)': 'documentClick($event)',
22 | }
23 | })
24 | export class DropdownComponent implements OnInit {
25 | dropdownMenu = {
26 | state: 'inactive'
27 | };
28 | @Input() Label: string;
29 | @Input() DropdownMenu: Array;
30 | @Output() SelectItem = new EventEmitter();
31 | constructor() { }
32 |
33 | ngOnInit() {
34 | }
35 |
36 | documentClick(e) {
37 | this.dropdownMenu.state = 'inactive';
38 | e.stopPropagation();
39 | }
40 |
41 | toggleDropdownMenu(e) {
42 | this.dropdownMenu.state === 'active' ? this.dropdownMenu.state = 'inactive' : this.dropdownMenu.state = 'active';
43 | e.stopPropagation();
44 | }
45 | selectItem(data) {
46 | this.dropdownMenu.state = 'inactive';
47 | this.SelectItem.emit(data);
48 | }
49 | }
50 |
51 | export interface dropdownItem {
52 | name: object;
53 | id: number;
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/directives/markdown-editor/markdown-editor.directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { MarkdownEditorDirective } from './markdown-editor.directive';
2 |
3 | describe('MarkdownEditorDirective', () => {
4 | it('should create an instance', () => {
5 | const directive = new MarkdownEditorDirective();
6 | expect(directive).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/directives/markdown-editor/markdown-editor.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, ElementRef, Input, Output, EventEmitter } from '@angular/core';
2 | import SimpleMDE from 'simplemde';
3 | import marked from 'marked';
4 | import highlight from 'highlight.js';
5 |
6 | @Directive({
7 | selector: '[myMarkdownEditor]'
8 | })
9 | export class MarkdownEditorDirective {
10 | @Input() inputMarkdownEditorValue: String = '';
11 | // 指令往外部输出编辑器的值
12 | @Output() MarkdownEditorValue = new EventEmitter();
13 |
14 | constructor(private el: ElementRef) {
15 | setTimeout(() => {
16 | const simpleMDE = new SimpleMDE({
17 | element: el.nativeElement.querySelector('textarea'),
18 | autofocus: false,
19 | autosave: true,
20 | previewRender: function(plainText) {
21 | return marked(plainText, {
22 | renderer: new marked.Renderer(),
23 | gfm: true,
24 | pedantic: false,
25 | sanitize: false,
26 | tables: true,
27 | breaks: true,
28 | smartLists: true,
29 | smartypants: true,
30 | highlight: function (code) {
31 | return highlight.highlightAuto(code).value;
32 | }
33 | });
34 | },
35 | toolbar: ['bold', 'italic', 'strikethrough', 'heading', 'code', 'quote', 'unordered-list', 'ordered-list', 'clean-block', 'link', 'image', 'table', 'horizontal-rule', 'preview', {
36 | name: 'side-by-side',
37 | action: function(editor){
38 | const cm = editor.codemirror;
39 | const wrapper = cm.getWrapperElement();
40 | const preview = wrapper.nextSibling;
41 | const toolbarButton = editor.toolbarElements['side-by-side'];
42 | let useSideBySideListener = false;
43 | if (/editor-preview-active-side/.test(preview.className)) {
44 | preview.className = preview.className.replace(
45 | /\s*editor-preview-active-side\s*/g, ''
46 | );
47 | toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, '');
48 | wrapper.className = wrapper.className.replace(/\s*CodeMirror-sided\s*/g, ' ');
49 | } else {
50 | // When the preview button is clicked for the first time,
51 | // give some time for the transition from editor.css to fire and the view to slide from right to left,
52 | // instead of just appearing.
53 | setTimeout(function() {
54 | if (!cm.getOption('fullScreen')) {
55 | simpleMDE.toggleFullScreen(editor);
56 | }
57 |
58 | const fullscreenDom = document.querySelector('.CodeMirror-fullscreen');
59 | if (!fullscreenDom) {
60 | return;
61 | }
62 | fullscreenDom.style.height = window.innerHeight - 70 + 'px';
63 | fullscreenDom.style.maxHeight = window.innerHeight - 70 + 'px';
64 | preview.className += ' editor-preview-active-side';
65 | }, 1);
66 | toolbarButton.className += ' active';
67 | wrapper.className += ' CodeMirror-sided';
68 | useSideBySideListener = true;
69 | }
70 |
71 | // Hide normal preview if active
72 | const previewNormal = wrapper.lastChild;
73 | if (/editor-preview-active/.test(previewNormal.className)) {
74 | previewNormal.className = previewNormal.className.replace(
75 | /\s*editor-preview-active\s*/g, ''
76 | );
77 | const toolbar = editor.toolbarElements.preview;
78 | const toolbar_div = wrapper.previousSibling;
79 | toolbar.className = toolbar.className.replace(/\s*active\s*/g, '');
80 | toolbar_div.className = toolbar_div.className.replace(/\s*disabled-for-preview*/g, '');
81 | }
82 |
83 | const sideBySideRenderingFunction = function() {
84 | preview.innerHTML = editor.options.previewRender(editor.value(), preview);
85 | };
86 |
87 | if (!cm.sideBySideRenderingFunction) {
88 | cm.sideBySideRenderingFunction = sideBySideRenderingFunction;
89 | }
90 |
91 | if (useSideBySideListener) {
92 | preview.innerHTML = editor.options.previewRender(editor.value(), preview);
93 | cm.on('update', cm.sideBySideRenderingFunction);
94 | } else {
95 | cm.off('update', cm.sideBySideRenderingFunction);
96 | }
97 |
98 | // Refresh to fix selection being off (#309)
99 | cm.refresh();
100 | },
101 | className: 'fa fa-columns no-disable no-mobile',
102 | title: 'sideBySide'
103 | }, {
104 | name: 'fullscreen',
105 | action: function(editor){
106 | let saved_overflow = '';
107 | // Set fullscreen
108 | const cm = editor.codemirror;
109 | cm.setOption('fullScreen', !cm.getOption('fullScreen'));
110 |
111 | // Prevent scrolling on body during fullscreen active
112 | if (cm.getOption('fullScreen')) {
113 | saved_overflow = document.body.style.overflow;
114 | document.body.style.overflow = 'hidden';
115 | } else {
116 | document.body.style.overflow = saved_overflow;
117 | }
118 |
119 | // Update toolbar class
120 | const wrap = cm.getWrapperElement();
121 |
122 | if (!/fullscreen/.test(wrap.previousSibling.className)) {
123 | wrap.previousSibling.className += ' fullscreen';
124 | } else {
125 | wrap.previousSibling.className = wrap.previousSibling.className.replace(/\s*fullscreen\b/, '');
126 | }
127 |
128 | // Update toolbar button
129 | const toolbarButton = editor.toolbarElements.fullscreen;
130 |
131 | if (!/active/.test(toolbarButton.className)) {
132 | toolbarButton.className += 'active';
133 | } else {
134 | toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, '');
135 | }
136 |
137 | // Hide side by side if needed
138 | const sidebyside = cm.getWrapperElement().nextSibling;
139 | if (/editor-preview-active-side/.test(sidebyside.className)) {
140 | simpleMDE.toggleSideBySide(editor);
141 | }
142 |
143 | const fullscreenDom = document.querySelector('.CodeMirror-fullscreen');
144 | if (!fullscreenDom) {
145 | return;
146 | }
147 | fullscreenDom.style.height = window.innerHeight - 70 + 'px';
148 | fullscreenDom.style.maxHeight = window.innerHeight - 70 + 'px';
149 | // let toolbar = document.querySelector('.editor-toolbar')
150 | // let input = document.createElement('input')
151 | // input.value = '这是标题'
152 | // toolbar.appendChild(input)
153 | },
154 | className: 'fa fa-arrows-alt no-disable no-mobile',
155 | title: 'Fullscreen',
156 | }],
157 | styleSelectedText: false
158 | });
159 | simpleMDE.codemirror.on('change', () => {
160 | this.MarkdownEditorValue.emit(simpleMDE.value());
161 | });
162 | simpleMDE.value(this.inputMarkdownEditorValue);
163 | // simpleMDE.togglePreview()
164 | el.nativeElement.querySelector('.CodeMirror').style.height = window.innerHeight - 220 + 'px';
165 | el.nativeElement.querySelector('.CodeMirror').style.maxHeight = window.innerHeight - 220 + 'px';
166 | el.nativeElement.querySelector('.CodeMirror').style.overflow = 'auto';
167 | });
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/app/directives/markdown-editor/simplemde.min.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * simplemde v1.11.2
3 | * Copyright Next Step Webs, Inc.
4 | * @link https://github.com/NextStepWebs/simplemde-markdown-editor
5 | * @license MIT
6 | */
7 | .CodeMirror {
8 | color: #000;
9 | }
10 |
11 | .CodeMirror-lines {
12 | padding: 4px 0;
13 | }
14 |
15 | .CodeMirror pre {
16 | padding: 0 4px;
17 | }
18 |
19 | .CodeMirror-gutter-filler, .CodeMirror-scrollbar-filler {
20 | background-color: #fff;
21 | }
22 |
23 | .CodeMirror-gutters {
24 | border-right: 1px solid #ddd;
25 | background-color: #f7f7f7;
26 | white-space: nowrap;
27 | }
28 |
29 | .CodeMirror-linenumber {
30 | padding: 0 3px 0 5px;
31 | min-width: 20px;
32 | text-align: right;
33 | color: #999;
34 | white-space: nowrap;
35 | }
36 |
37 | .CodeMirror-guttermarker {
38 | color: #000;
39 | }
40 |
41 | .CodeMirror-guttermarker-subtle {
42 | color: #999;
43 | }
44 |
45 | .CodeMirror-cursor {
46 | border-left: 1px solid #000;
47 | border-right: none;
48 | width: 0;
49 | }
50 |
51 | .CodeMirror div.CodeMirror-secondarycursor {
52 | border-left: 1px solid silver;
53 | }
54 |
55 | .cm-fat-cursor .CodeMirror-cursor {
56 | width: auto;
57 | border: 0 !important;
58 | background: #7e7;
59 | }
60 |
61 | .cm-fat-cursor div.CodeMirror-cursors {
62 | z-index: 1;
63 | }
64 |
65 | .cm-animate-fat-cursor {
66 | width: auto;
67 | border: 0;
68 | -webkit-animation: blink 1.06s steps(1) infinite;
69 | -moz-animation: blink 1.06s steps(1) infinite;
70 | animation: blink 1.06s steps(1) infinite;
71 | background-color: #7e7;
72 | }
73 |
74 | @-moz-keyframes blink {
75 | 50% {
76 | background-color: transparent;
77 | }
78 | }
79 |
80 | @-webkit-keyframes blink {
81 | 50% {
82 | background-color: transparent;
83 | }
84 | }
85 |
86 | @keyframes blink {
87 | 50% {
88 | background-color: transparent;
89 | }
90 | }
91 |
92 | .cm-tab {
93 | display: inline-block;
94 | text-decoration: inherit;
95 | }
96 |
97 | .CodeMirror-ruler {
98 | border-left: 1px solid #ccc;
99 | position: absolute;
100 | }
101 |
102 | .cm-s-default .cm-header {
103 | color: #00f;
104 | }
105 |
106 | .cm-s-default .cm-quote {
107 | color: #090;
108 | }
109 |
110 | .cm-negative {
111 | color: #d44;
112 | }
113 |
114 | .cm-positive {
115 | color: #292;
116 | }
117 |
118 | .cm-header, .cm-strong {
119 | font-weight: 700;
120 | }
121 |
122 | .cm-em {
123 | font-style: italic;
124 | }
125 |
126 | .cm-link {
127 | text-decoration: underline;
128 | }
129 |
130 | .cm-strikethrough {
131 | text-decoration: line-through;
132 | }
133 |
134 | .cm-s-default .cm-keyword {
135 | color: #708;
136 | }
137 |
138 | .cm-s-default .cm-atom {
139 | color: #219;
140 | }
141 |
142 | .cm-s-default .cm-number {
143 | color: #164;
144 | }
145 |
146 | .cm-s-default .cm-def {
147 | color: #00f;
148 | }
149 |
150 | .cm-s-default .cm-variable-2 {
151 | color: #05a;
152 | }
153 |
154 | .cm-s-default .cm-variable-3 {
155 | color: #085;
156 | }
157 |
158 | .cm-s-default .cm-comment {
159 | color: #a50;
160 | }
161 |
162 | .cm-s-default .cm-string {
163 | color: #a11;
164 | }
165 |
166 | .cm-s-default .cm-string-2 {
167 | color: #f50;
168 | }
169 |
170 | .cm-s-default .cm-meta, .cm-s-default .cm-qualifier {
171 | color: #555;
172 | }
173 |
174 | .cm-s-default .cm-builtin {
175 | color: #30a;
176 | }
177 |
178 | .cm-s-default .cm-bracket {
179 | color: #997;
180 | }
181 |
182 | .cm-s-default .cm-tag {
183 | color: #170;
184 | }
185 |
186 | .cm-s-default .cm-attribute {
187 | color: #00c;
188 | }
189 |
190 | .cm-s-default .cm-hr {
191 | color: #999;
192 | }
193 |
194 | .cm-s-default .cm-link {
195 | color: #00c;
196 | }
197 |
198 | .cm-invalidchar, .cm-s-default .cm-error {
199 | color: red;
200 | }
201 |
202 | .CodeMirror-composing {
203 | border-bottom: 2px solid;
204 | }
205 |
206 | div.CodeMirror span.CodeMirror-matchingbracket {
207 | color: #0f0;
208 | }
209 |
210 | div.CodeMirror span.CodeMirror-nonmatchingbracket {
211 | color: #f22;
212 | }
213 |
214 | .CodeMirror-matchingtag {
215 | background: rgba(255, 150, 0, 0.3);
216 | }
217 |
218 | .CodeMirror-activeline-background {
219 | background: #e8f2ff;
220 | }
221 |
222 | .CodeMirror {
223 | position: relative;
224 | overflow: hidden;
225 | background: #fff;
226 | }
227 |
228 | .CodeMirror-scroll {
229 | overflow: scroll !important;
230 | margin-bottom: -30px;
231 | margin-right: -30px;
232 | padding-bottom: 30px;
233 | height: 100%;
234 | outline: 0;
235 | position: relative;
236 | }
237 |
238 | .CodeMirror-sizer {
239 | position: relative;
240 | border-right: 30px solid transparent;
241 | }
242 |
243 | .CodeMirror-gutter-filler, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-vscrollbar {
244 | position: absolute;
245 | z-index: 6;
246 | display: none;
247 | }
248 |
249 | .CodeMirror-vscrollbar {
250 | right: 0;
251 | top: 0;
252 | overflow-x: hidden;
253 | overflow-y: scroll;
254 | }
255 |
256 | .CodeMirror-hscrollbar {
257 | bottom: 0;
258 | left: 0;
259 | overflow-y: hidden;
260 | overflow-x: scroll;
261 | }
262 |
263 | .CodeMirror-scrollbar-filler {
264 | right: 0;
265 | bottom: 0;
266 | }
267 |
268 | .CodeMirror-gutter-filler {
269 | left: 0;
270 | bottom: 0;
271 | }
272 |
273 | .CodeMirror-gutters {
274 | position: absolute;
275 | left: 0;
276 | top: 0;
277 | min-height: 100%;
278 | z-index: 3;
279 | }
280 |
281 | .CodeMirror-gutter {
282 | white-space: normal;
283 | height: 100%;
284 | display: inline-block;
285 | vertical-align: top;
286 | margin-bottom: -30px;
287 | }
288 |
289 | .CodeMirror-gutter-wrapper {
290 | position: absolute;
291 | z-index: 4;
292 | background: 0 0 !important;
293 | border: none !important;
294 | -webkit-user-select: none;
295 | -moz-user-select: none;
296 | user-select: none;
297 | }
298 |
299 | .CodeMirror-gutter-background {
300 | position: absolute;
301 | top: 0;
302 | bottom: 0;
303 | z-index: 4;
304 | }
305 |
306 | .CodeMirror-gutter-elt {
307 | position: absolute;
308 | cursor: default;
309 | z-index: 4;
310 | }
311 |
312 | .CodeMirror-lines {
313 | cursor: text;
314 | min-height: 1px;
315 | }
316 |
317 | .CodeMirror pre {
318 | -moz-border-radius: 0;
319 | -webkit-border-radius: 0;
320 | border-radius: 0;
321 | border-width: 0;
322 | background: 0 0;
323 | font-family: inherit;
324 | font-size: inherit;
325 | margin: 0;
326 | white-space: pre;
327 | word-wrap: normal;
328 | line-height: inherit;
329 | color: inherit;
330 | z-index: 2;
331 | position: relative;
332 | overflow: visible;
333 | -webkit-tap-highlight-color: transparent;
334 | -webkit-font-variant-ligatures: none;
335 | font-variant-ligatures: none;
336 | }
337 |
338 | .CodeMirror-wrap pre {
339 | word-wrap: break-word;
340 | white-space: pre-wrap;
341 | word-break: normal;
342 |
343 | /* margin: 15px 0; */
344 | }
345 |
346 | .CodeMirror-linebackground {
347 | position: absolute;
348 | left: 0;
349 | right: 0;
350 | top: 0;
351 | bottom: 0;
352 | z-index: 0;
353 | }
354 |
355 | .CodeMirror-linewidget {
356 | position: relative;
357 | z-index: 2;
358 | overflow: auto;
359 | }
360 |
361 | .CodeMirror-code {
362 | outline: 0;
363 | }
364 |
365 | .CodeMirror-gutter, .CodeMirror-gutters, .CodeMirror-linenumber, .CodeMirror-scroll, .CodeMirror-sizer {
366 | -moz-box-sizing: content-box;
367 | box-sizing: content-box;
368 | }
369 |
370 | .CodeMirror-measure {
371 | position: absolute;
372 | width: 100%;
373 | height: 0;
374 | overflow: hidden;
375 | visibility: hidden;
376 | }
377 |
378 | .CodeMirror-cursor {
379 | position: absolute;
380 | }
381 |
382 | .CodeMirror-measure pre {
383 | position: static;
384 | }
385 |
386 | div.CodeMirror-cursors {
387 | visibility: hidden;
388 | position: relative;
389 | z-index: 3;
390 | }
391 |
392 | .CodeMirror-focused div.CodeMirror-cursors, div.CodeMirror-dragcursors {
393 | visibility: visible;
394 | }
395 |
396 | .CodeMirror-selected {
397 | background: #d9d9d9;
398 | }
399 |
400 | .CodeMirror-focused .CodeMirror-selected, .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection {
401 | background: #d7d4f0;
402 | }
403 |
404 | .CodeMirror-crosshair {
405 | cursor: crosshair;
406 | }
407 |
408 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection {
409 | background: #d7d4f0;
410 | }
411 |
412 | .cm-searching {
413 | background: #ffa;
414 | background: rgba(255, 255, 0, 0.4);
415 | }
416 |
417 | .cm-force-border {
418 | padding-right: .1px;
419 | }
420 |
421 | @media print {
422 | .CodeMirror div.CodeMirror-cursors {
423 | visibility: hidden;
424 | }
425 | }
426 |
427 | .cm-tab-wrap-hack:after {
428 | content: '';
429 | }
430 |
431 | span.CodeMirror-selectedtext {
432 | background: 0 0;
433 | }
434 |
435 | .CodeMirror {
436 | height: auto;
437 | min-height: 300px;
438 | border-top: 1px solid #fbfbfb;
439 | border-bottom-left-radius: 4px;
440 | border-bottom-right-radius: 4px;
441 | padding: 10px;
442 | font: inherit;
443 | z-index: 1;
444 | background-color: #f7f7f7;
445 | &::-webkit-scrollbar{
446 | width: 0;
447 | height: 0;
448 | }
449 | }
450 |
451 | .CodeMirror-scroll {
452 | min-height: 300px;
453 | }
454 |
455 | .CodeMirror-fullscreen {
456 | background: #fff;
457 | position: fixed !important;
458 | top: 50px;
459 | left: 0;
460 | right: 0;
461 | bottom: 0;
462 | height: auto;
463 | z-index: 9;
464 | border-top: 1px solid #ccc;
465 | }
466 |
467 | .CodeMirror-fullscreen ::-webkit-scrollbar {
468 | width: 0;
469 | height: 0;
470 | }
471 |
472 | .CodeMirror-sided {
473 | width: 49.5vw !important;
474 |
475 | /* overflow: hidden; */
476 | }
477 |
478 | .CodeMirror-sided ::-webkit-scrollbar {
479 | width: 0;
480 | height: 0;
481 | }
482 |
483 | .editor-toolbar {
484 | position: relative;
485 | opacity: .6;
486 | -webkit-user-select: none;
487 | -moz-user-select: none;
488 | -ms-user-select: none;
489 | -o-user-select: none;
490 | user-select: none;
491 | padding: 0 10px;
492 |
493 | /* border-top: 1px solid #bbb; */
494 | /* border-left: 1px solid #bbb; */
495 | /* border-right: 1px solid #bbb; */
496 | /* border-top-left-radius: 4px; */
497 | /* border-top-right-radius: 4px; */
498 | }
499 |
500 | .editor-toolbar:after, .editor-toolbar:before {
501 | display: block;
502 | content: ' ';
503 | height: 1px;
504 | }
505 |
506 | .editor-toolbar:before {
507 | margin-bottom: 8px;
508 | }
509 |
510 | .editor-toolbar:after {
511 | margin-top: 8px;
512 | }
513 |
514 | .editor-toolbar:hover, .editor-wrapper input.title:focus, .editor-wrapper input.title:hover {
515 | opacity: .8;
516 | }
517 |
518 | .editor-toolbar.fullscreen {
519 | width: 100%;
520 | height: 50px;
521 | overflow-x: auto;
522 | overflow-y: hidden;
523 | white-space: nowrap;
524 | padding-top: 10px;
525 | padding-bottom: 10px;
526 | box-sizing: border-box;
527 | background: #fff;
528 | border: 0;
529 | position: fixed;
530 | top: 0;
531 | left: 0;
532 | opacity: 1;
533 | z-index: 9;
534 | }
535 |
536 | .editor-toolbar.fullscreen::before {
537 | width: 20px;
538 | height: 50px;
539 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%);
540 | background: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 1)), color-stop(100%, rgba(255, 255, 255, 0)));
541 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%);
542 | background: -o-linear-gradient(left, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%);
543 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%);
544 | background: linear-gradient(to right, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%);
545 | position: fixed;
546 | top: 0;
547 | left: 0;
548 | margin: 0;
549 | padding: 0;
550 | }
551 |
552 | .editor-toolbar.fullscreen::after {
553 | width: 20px;
554 | height: 50px;
555 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
556 | background: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 0)), color-stop(100%, rgba(255, 255, 255, 1)));
557 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
558 | background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
559 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
560 | background: linear-gradient(to right, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%);
561 | position: fixed;
562 | top: 0;
563 | right: 0;
564 | margin: 0;
565 | padding: 0;
566 | }
567 |
568 | .editor-toolbar a {
569 | display: inline-block;
570 | text-align: center;
571 | text-decoration: none !important;
572 | color: #2c3e50 !important;
573 | width: 30px;
574 | height: 30px;
575 | margin: 0;
576 | border: 1px solid transparent;
577 | border-radius: 3px;
578 | cursor: pointer;
579 | }
580 |
581 | .editor-toolbar a.active, .editor-toolbar a:hover {
582 | background: #fcfcfc;
583 | border-color: #95a5a6;
584 | }
585 |
586 | .editor-toolbar a:before {
587 | line-height: 30px;
588 | }
589 |
590 | .editor-toolbar i.separator {
591 | display: inline-block;
592 | width: 0;
593 | border-left: 1px solid #d9d9d9;
594 | border-right: 1px solid #fff;
595 | color: transparent;
596 | text-indent: -10px;
597 | margin: 0 6px;
598 | }
599 |
600 | .editor-toolbar a.fa-header-x:after {
601 | font-family: Arial,"Helvetica Neue",Helvetica,sans-serif;
602 | font-size: 65%;
603 | vertical-align: text-bottom;
604 | position: relative;
605 | top: 2px;
606 | }
607 |
608 | .editor-toolbar a.fa-header-1:after {
609 | content: "1";
610 | }
611 |
612 | .editor-toolbar a.fa-header-2:after {
613 | content: "2";
614 | }
615 |
616 | .editor-toolbar a.fa-header-3:after {
617 | content: "3";
618 | }
619 |
620 | .editor-toolbar a.fa-header-bigger:after {
621 | content: "▲";
622 | }
623 |
624 | .editor-toolbar a.fa-header-smaller:after {
625 | content: "▼";
626 | }
627 |
628 | .editor-toolbar.disabled-for-preview a:not(.no-disable) {
629 | pointer-events: none;
630 | background: #fff;
631 | border-color: transparent;
632 | text-shadow: inherit;
633 | }
634 |
635 | @media only screen and (max-width: 700px) {
636 | .editor-toolbar a.no-mobile {
637 | display: none;
638 | }
639 | }
640 |
641 | .editor-statusbar {
642 | display: none;
643 | padding: 8px 10px;
644 | font-size: 12px;
645 | color: #959694;
646 | text-align: right;
647 | }
648 |
649 | .editor-statusbar span {
650 | display: inline-block;
651 | min-width: 4em;
652 | margin-left: 1em;
653 | }
654 |
655 | .editor-preview, .editor-preview-side {
656 | padding: 10px;
657 | background: #fafafa;
658 | overflow: auto;
659 | display: none;
660 | box-sizing: border-box;
661 | }
662 |
663 | .editor-preview, .editor-preview-side{
664 | &::-webkit-scrollbar{
665 | width: 0;
666 | }
667 | img {
668 | max-width: 100%;
669 | }
670 |
671 | th, td {
672 | border: 1px solid #E6E6E6;
673 | padding: 5px 8px;
674 | }
675 |
676 | table {
677 | border-collapse: collapse;
678 | }
679 |
680 | blockquote {
681 | margin: 1em 0;
682 | border-left: 4px solid #3db8c1;
683 | padding: 10px;
684 | color: #666;
685 | background-color: #eeeeee;
686 | p {
687 | line-height: 2;
688 | display: block;
689 | margin-bottom: 0px;
690 |
691 | a {
692 | color: #3db8c1;
693 | text-decoration: none;
694 | }
695 | }
696 | ul {
697 | list-style-type: none;
698 | padding: 0;
699 | margin: 0;
700 | }
701 | }
702 |
703 | h1 {
704 | margin: 1.3rem 0;
705 | line-height: 1.2;
706 | font-size: 2em;
707 | }
708 |
709 | h2 {
710 | border-bottom: 1px solid #eee;
711 | padding-bottom: 10px;
712 | }
713 |
714 | h3 {
715 | margin: 20px 0;
716 | }
717 |
718 | p {
719 | line-height: 2;
720 | display: block;
721 | margin-bottom: 20px;
722 |
723 | a {
724 | color: #3db8c1;
725 | text-decoration: none;
726 | }
727 |
728 | video{
729 | width: 100%;
730 | }
731 | }
732 |
733 | ul {
734 | padding-left: 2.7rem;
735 | display: block;
736 | list-style-type: disc;
737 |
738 | li {
739 | margin-bottom: .6rem;
740 | list-style: inherit;
741 | }
742 | }
743 |
744 | code {
745 | font-family: Menlo,Monaco,Consolas,Courier New,monospace;
746 | font-size: 1rem;
747 | padding: 2px 5px;
748 | word-break: break-word;
749 | color: #49a9ee;
750 | background-color: #f8f8f8;
751 | border-radius: 2px;
752 | overflow-x: scroll;
753 | font-size: 12px;
754 | margin: 0 4px;
755 | }
756 |
757 | pre {
758 | white-space: pre;
759 | margin: 1em 0px;
760 | padding: 5px;
761 | width: 98%;
762 | max-width: 100%;
763 | overflow: scroll;
764 | border-radius: 3px;
765 | background-color: #eeeeee;
766 |
767 | code {
768 | color: #333 !important;
769 | background-color: #eeeeee;
770 |
771 | .hljs-function {
772 | color: #333;
773 | }
774 |
775 | .hljs-keyword {
776 | color: #333;
777 | font-weight: 700;
778 | }
779 |
780 | .hljs-title {
781 | color: #900;
782 | font-weight: 700;
783 | }
784 |
785 | .hljs-params {
786 | color: #333;
787 | }
788 |
789 | .hljs-comment {
790 | color: #998;
791 | }
792 |
793 | .hljs-string {
794 | color: #d14;
795 | }
796 |
797 | .hljs-number {
798 | color: teal;
799 | }
800 |
801 | .hljs-literal {
802 | color: teal;
803 | }
804 | }
805 | }
806 | }
807 |
808 | .editor-statusbar .lines:before {
809 | content: 'lines: ';
810 | }
811 |
812 | .editor-statusbar .words:before {
813 | content: 'words: ';
814 | }
815 |
816 | .editor-statusbar .characters:before {
817 | content: 'characters: ';
818 | }
819 |
820 | .editor-preview {
821 | position: absolute;
822 | width: 100%;
823 | height: 100%;
824 | top: 0;
825 | left: 0;
826 | z-index: 7;
827 | }
828 |
829 | .editor-preview-side {
830 | position: fixed;
831 | bottom: 0;
832 | width: 50vw;
833 | top: 50px;
834 | right: 0;
835 | z-index: 9;
836 | border: 1px solid #ddd;
837 | }
838 |
839 | .editor-preview-active, .editor-preview-active-side {
840 | display: block;
841 | }
842 |
843 | .editor-preview-side > p, .editor-preview > p {
844 | line-height: 2;
845 | display: block;
846 | margin-bottom: 0px;
847 |
848 | a {
849 | color: #3db8c1;
850 | text-decoration: none;
851 | }
852 | }
853 |
854 | .editor-preview pre, .editor-preview-side pre {
855 | background: #eee;
856 | margin-bottom: 10px;
857 | }
858 |
859 | .editor-preview table td, .editor-preview table th, .editor-preview-side table td, .editor-preview-side table th {
860 | border: 1px solid #ddd;
861 | padding: 5px;
862 | }
863 |
864 | .CodeMirror .CodeMirror-code .cm-tag {
865 | color: #63a35c;
866 | }
867 |
868 | .CodeMirror .CodeMirror-code .cm-attribute {
869 | color: #795da3;
870 | }
871 |
872 | .CodeMirror .CodeMirror-code .cm-string {
873 | color: #183691;
874 | }
875 |
876 | .CodeMirror .CodeMirror-selected {
877 | background: #d9d9d9;
878 | }
879 |
880 | .CodeMirror .CodeMirror-code .cm-header-1 {
881 | font-size: 200%;
882 | line-height: 200%;
883 | }
884 |
885 | .CodeMirror .CodeMirror-code .cm-header-2 {
886 | font-size: 160%;
887 | line-height: 160%;
888 | }
889 |
890 | .CodeMirror .CodeMirror-code .cm-header-3 {
891 | font-size: 125%;
892 | line-height: 125%;
893 | }
894 |
895 | .CodeMirror .CodeMirror-code .cm-header-4 {
896 | font-size: 110%;
897 | line-height: 110%;
898 | }
899 |
900 | .CodeMirror .CodeMirror-code .cm-comment {
901 | /* background: rgba(0, 0, 0, 0.05); */
902 | border-radius: 2px;
903 | color: #3db8c1;
904 | }
905 |
906 | .CodeMirror .CodeMirror-code .cm-link {
907 | color: #7f8c8d;
908 | }
909 |
910 | .CodeMirror .CodeMirror-code .cm-url {
911 | color: #aab2b3;
912 | }
913 |
914 | .CodeMirror .CodeMirror-code .cm-strikethrough {
915 | text-decoration: line-through;
916 | }
917 |
918 | .CodeMirror .CodeMirror-placeholder {
919 | opacity: .5;
920 | }
921 |
922 | .CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) {
923 | /* background: rgba(255, 0, 0, 0.15); */
924 | }
925 |
--------------------------------------------------------------------------------
/src/app/interceptor/global-response-interceptor.ts:
--------------------------------------------------------------------------------
1 | import { MsgService } from './../services/msg/msg.service';
2 | import { Router, Routes } from '@angular/router';
3 | import { Injectable } from '@angular/core';
4 | import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
5 | import { Observable } from 'rxjs/Rx';
6 | import { HttpResponse } from '@angular/common/http';
7 |
8 | @Injectable()
9 | export class GlobalResponseInterceptor implements HttpInterceptor {
10 |
11 | constructor(
12 | private router: Router,
13 | private msg: MsgService
14 | ) {
15 | }
16 |
17 | intercept(req: HttpRequest < any > , next: HttpHandler): Observable < HttpEvent < any >> {
18 | return next.handle(req).map(event => {
19 | if (event instanceof HttpResponse) {
20 | if (event.body.code === 401) {
21 | this.msg.info('登录已过期!');
22 | setTimeout(() => {
23 | this.router.navigate(['/index']);
24 | }, 2000);
25 | }
26 | }
27 | return event;
28 | });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/page/add-link-note/add-link-note.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 注:动态生成笔记会比较耗时,请耐心等候!
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
31 |
32 |
--------------------------------------------------------------------------------
/src/app/page/add-link-note/add-link-note.component.scss:
--------------------------------------------------------------------------------
1 | @import '../add-note/add-note.component.scss';
2 | .add-link-note{
3 | height: 100vh;
4 | padding: 1vh 8vh 30vh 8vh;
5 | .input-wrap{
6 | height:52px;
7 | line-height:52px;
8 | margin-top: 19vh;
9 | .search-input{
10 | border-bottom: 1px solid #ccc;
11 | background-color: transparent;
12 | margin-bottom: 3vh;
13 | border: none;
14 | color: #ccc;
15 | height: 52px;
16 | width: 100%;
17 | overflow-x: scroll;
18 | font-size: 40px;
19 | outline: none;
20 | }
21 | };
22 | .note-name{
23 | border-bottom: 1px solid #ececec;
24 | padding-bottom: 3vh;
25 | font-size: 13px;
26 | margin-top: 3rem;
27 | }
28 | }
29 |
30 | .add-note-toolbar{
31 | padding: 10px 0;
32 | border-bottom: 1px solid #ececec;
33 | }
34 |
35 | .preview{
36 | font-size: 14px !important;
37 | padding: 10px 30px;
38 | background-color: #fbfbfb;
39 | &::-webkit-scrollbar{
40 | width: 0;
41 | }
42 | pre{
43 | background-color: #eee;
44 | padding: 5px;
45 | max-width: 100%;
46 | overflow: auto;
47 | }
48 | img{
49 | max-width: 100%;
50 | }
51 | th, td{
52 | border: 1px solid #E6E6E6;
53 | padding: 5px 8px;
54 | }
55 | table{
56 | border-collapse: collapse;
57 | }
58 | blockquote{
59 | margin: 1em 0;
60 | border-left: 4px solid #ddd;
61 | padding: 0 1.3rem;
62 | color: #666;
63 | }
64 | h1{
65 | margin: 1.3rem 0;
66 | line-height: 1.2;
67 | font-size: 2em;
68 | }
69 | h2{
70 | border-bottom: 1px solid #eee;
71 | padding-bottom: 10px;
72 | }
73 | h3{
74 | margin: 20px 0;
75 | }
76 | p{
77 | line-height: 2;
78 | display: block;
79 | margin-bottom: 20px;
80 | a{
81 | color: #3dbd7d;
82 | text-decoration: none;
83 | }
84 | video{
85 | width: 100%;
86 | }
87 | }
88 | ul{
89 | padding-left: 2.7rem;
90 | display: block;
91 | list-style-type: disc;
92 | li{
93 | margin-bottom: .6rem;
94 | list-style: inherit;
95 | }
96 | }
97 | code{
98 | font-family: Menlo,Monaco,Consolas,Courier New,monospace;
99 | font-size: 1rem;
100 | padding: 2px 5px;
101 | word-break: break-word;
102 | color: #49a9ee;
103 | background-color: #f8f8f8;
104 | border-radius: 2px;
105 | overflow-x: scroll;
106 | font-size: 12px;
107 | margin: 0 4px;
108 | }
109 | pre{
110 | white-space: pre;
111 | margin: 1em 0px;
112 | padding: 5px;
113 | width: 100%;
114 | max-width: 100%;
115 | overflow: scroll;
116 | border-radius: 3px;
117 | code{
118 | color: #333 !important;
119 | background-color: #eeeeee;
120 | .hljs-function{
121 | color: #333;
122 | }
123 | .hljs-keyword{
124 | color: #333;
125 | font-weight: 700;
126 | }
127 | .hljs-title{
128 | color: #900;
129 | font-weight: 700;
130 | }
131 | .hljs-params{
132 | color: #333;
133 | }
134 | .hljs-comment{
135 | color: #998;
136 | }
137 | .hljs-string{
138 | color: #d14;
139 | }
140 | .hljs-number{
141 | color: teal;
142 | }
143 | .hljs-literal{
144 | color: teal;
145 | }
146 | }
147 | }
148 | }
--------------------------------------------------------------------------------
/src/app/page/add-link-note/add-link-note.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { AddLinkNoteComponent } from './add-link-note.component';
4 |
5 | describe('AddLinkNoteComponent', () => {
6 | let component: AddLinkNoteComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ AddLinkNoteComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(AddLinkNoteComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/add-link-note/add-link-note.component.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Subscription } from 'rxjs/Subscription';
3 | import { Router } from '@angular/router';
4 | import { MsgService } from './../../services/msg/msg.service';
5 | import { NoteService } from './../../services/note/note.service';
6 | import { style } from '@angular/animations';
7 | import { TagService } from './../../services/tag/tag.service';
8 | import { LoadingBarService } from './../../services/loading-bar/loading-bar.service';
9 | import { Component, OnInit, ViewEncapsulation, Directive, ElementRef, OnDestroy } from '@angular/core';
10 | import 'rxjs/add/operator/map';
11 | import marked from 'marked';
12 | import highlight from 'highlight.js';
13 | import { DomSanitizer } from '@angular/platform-browser';
14 |
15 | @Directive({
16 | selector: '[previewHeight]'
17 | })
18 | export class CalculationContentHeightDirective {
19 | constructor(
20 | el: ElementRef
21 | ) {
22 | el.nativeElement.style.height = window.innerHeight - 170 + 'px';
23 | el.nativeElement.style.maxHeight = window.innerHeight - 170 + 'px';
24 | el.nativeElement.style.overflow = 'auto';
25 | }
26 | }
27 |
28 | @Component({
29 | selector: 'app-add-link-note',
30 | templateUrl: './add-link-note.component.html',
31 | styleUrls: ['./add-link-note.component.scss'],
32 | encapsulation: ViewEncapsulation.None
33 | })
34 | export class AddLinkNoteComponent implements OnInit, OnDestroy {
35 | dropdownMenuSub: Subscription;
36 | isShowMarkdownEditor = false;
37 | dropdownMenu = [];
38 | labelList = [];
39 | noteTitle = '';
40 | noteContent = '';
41 | sourceLink = '';
42 | constructor(
43 | private tagService: TagService,
44 | private loadingBar: LoadingBarService,
45 | private http: HttpClient,
46 | private sanitizer: DomSanitizer,
47 | private noteService: NoteService,
48 | private msg: MsgService,
49 | private router: Router
50 | ) { }
51 |
52 | ngOnInit() {
53 | this.dropdownMenuSub = this.tagService.tagList$.subscribe((data) => {
54 | this.dropdownMenu = data;
55 | });
56 | }
57 |
58 | ngOnDestroy() {
59 | this.dropdownMenuSub.unsubscribe();
60 | }
61 |
62 | onEnter(value) {
63 | this.sourceLink = value;
64 | this.loadingBar.$Loading.start();
65 | this.http.post('/api/generateNote', {
66 | link: value
67 | })
68 | .subscribe((data) => {
69 |
70 | marked(data['content'], function (err, content) {
71 | if (err) {
72 | throw err;
73 | }
74 | });
75 |
76 | this.loadingBar.$Loading.finish();
77 | this.isShowMarkdownEditor = true;
78 | this.noteTitle = data['title'];
79 | this.noteContent = data['content'];
80 | });
81 | }
82 |
83 | // 保存笔记
84 | save() {
85 | if (this.noteTitle === '' || this.noteContent === '' || this.labelList.length === 0) {
86 | this.msg.info('请输入完整的笔记信息!');
87 | } else {
88 | const sub = this.noteService._addNote({
89 | title: this.noteTitle,
90 | content: this.noteContent,
91 | tag: this.labelList,
92 | date: new Date(),
93 | sourceLink: this.sourceLink
94 | }).subscribe((res) => {
95 | if (res['code'] === 200) {
96 | this.msg.info('保存成功!');
97 | this.noteService._updateAllNote();
98 | localStorage.setItem('noteItemInfo', JSON.stringify(res['data']));
99 | this.router.navigate(['/viewNote']);
100 | }
101 | });
102 | }
103 | }
104 |
105 | selectItem(data) {
106 | this.labelList.push(data);
107 | }
108 |
109 | delectLabelItem(index) {
110 | this.labelList.splice(index, 1);
111 | }
112 |
113 | cancel() {
114 | this.isShowMarkdownEditor = false;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/app/page/add-note/add-note.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/page/add-note/add-note.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../directives/markdown-editor/simplemde.min.scss';
2 | .add-note-wrap {
3 | margin: 15px 10%;
4 | padding: 1rem;
5 | background-color: #fff;
6 | box-shadow: 0 1px 6px #ccc;
7 | border-radius: 3px;
8 | }
9 | .editor-toolbar{
10 | color: #ccc;
11 | }
12 | .add-note-title{
13 | height: 35px;
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | input{
18 | text-align: center;
19 | width: 100%;
20 | height: 30px;
21 | border: none;
22 | outline: none;
23 | font-size: 20px;
24 | color:#4e4e4e;
25 | }
26 | }
27 | ::-webkit-input-placeholder { /* WebKit browsers */
28 | color:#999;
29 | }
30 | .add-note-toolbar{
31 | width: 100%;
32 | display: flex;
33 | align-items: center;
34 | padding: 10px 0;
35 | border-bottom: 1px solid #ececec;
36 | }
37 | .add-note-toolbar-label{
38 | width: 80%;
39 | height: 32px;
40 | line-height: 32px;
41 | }
42 | .add-note-toolbar-dropdown{
43 | width: 150px;
44 | float: left;
45 | padding-top: 8px;
46 | }
47 | .tag-list-wrap{
48 | width: 70%;
49 | max-width: 70%;
50 | display: flex;
51 | flex-wrap: wrap;
52 | height: 32px;
53 | max-height: 32px;
54 | overflow: scroll;
55 | &::-webkit-scrollbar{
56 | width: 0;
57 | height: 0;
58 | }
59 | .tag-item{
60 | height: 25px;
61 | line-height: 25px;
62 | border: 1px solid #ccc;
63 | border-radius: 4px;
64 | margin: 3px 3px;
65 | display: flex;
66 | flex-wrap: wrap;
67 | cursor: pointer;
68 | div{
69 | line-height: 25px;
70 | padding: 0 14px;
71 | };
72 | i{
73 | line-height: 25px;
74 | padding: 0 6px;
75 | border-left: 1px solid #ccc;
76 | font-size: 12px;
77 | color: #ccc;
78 | };
79 | i:hover{
80 | color: #808b96;
81 | }
82 | &:hover{
83 | box-shadow: 1px 1px 4px rgba(0,0,0,.2);
84 | }
85 | }
86 | }
87 | .add-note-toolbar-btn{
88 | width: 20%;
89 | align-self: flex-end;
90 | app-button{
91 | float: right;
92 | margin-left: 5px;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/app/page/add-note/add-note.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { AddNoteComponent } from './add-note.component';
4 |
5 | describe('AddNoteComponent', () => {
6 | let component: AddNoteComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ AddNoteComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(AddNoteComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/add-note/add-note.component.ts:
--------------------------------------------------------------------------------
1 | import { Router } from '@angular/router';
2 | import { MsgService } from './../../services/msg/msg.service';
3 | import { Subscription } from 'rxjs/Subscription';
4 | import { NoteService } from '../../services/note/note.service';
5 | import { TagService } from './../../services/tag/tag.service';
6 | import { Component, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core';
7 | import { dropdownItem } from '../../component/dropdown/dropdown.component';
8 |
9 | @Component({
10 | selector: 'app-add-note',
11 | templateUrl: './add-note.component.html',
12 | styleUrls: ['./add-note.component.scss'],
13 | encapsulation: ViewEncapsulation.None
14 | })
15 | export class AddNoteComponent implements OnInit, OnDestroy {
16 | dropdownMenuSub: Subscription;
17 | dropdownMenu = [];
18 | title = '';
19 | content = '';
20 | tagList = [];
21 |
22 | constructor(
23 | private tagService: TagService,
24 | private noteService: NoteService,
25 | private msg: MsgService,
26 | private router: Router
27 | ) { }
28 |
29 | ngOnInit() {
30 | this.dropdownMenuSub = this.tagService.tagList$.subscribe((data) => {
31 | this.dropdownMenu = data;
32 | });
33 | }
34 |
35 | ngOnDestroy() {
36 | this.dropdownMenuSub.unsubscribe();
37 | }
38 |
39 | selectItem(data) {
40 | this.tagList.push(data);
41 | }
42 |
43 | delectLabelItem(index) {
44 | this.tagList.splice(index, 1);
45 | }
46 |
47 | markdownValueChange(data) {
48 | this.content = data;
49 | }
50 |
51 | // 保存笔记
52 | save() {
53 | if (this.title === '' || this.content === '' || this.tagList.length === 0) {
54 | this.msg.info('请输入完整的笔记信息!');
55 | } else {
56 | const sub = this.noteService._addNote({
57 | title: this.title,
58 | content: this.content,
59 | tag: this.tagList,
60 | date: new Date(),
61 | sourceLink: ''
62 | }).subscribe((res) => {
63 | if (res['code'] === 200) {
64 | this.msg.info('保存成功!');
65 | this.noteService._updateAllNote();
66 | localStorage.setItem('noteItemInfo', JSON.stringify(res['data']));
67 | this.router.navigate(['/viewNote']);
68 | }
69 | });
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/app/page/classification/classification.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | - 全部
4 | -
5 | {{item.name}}
6 |
7 |
8 |
9 |
10 |
11 | {{item.title}}
12 |
13 |
14 | {{item.date | date:"yyyy-MM-dd HH:mm:ss"}}
15 |
16 |
17 | {{item.content | filterNoteContent}}
18 |
19 |
20 |
21 | 该标签分类下暂无内容
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/page/classification/classification.component.scss:
--------------------------------------------------------------------------------
1 | .classification-wrap{
2 | margin: 0 3rem;
3 | padding: 1rem;
4 |
5 | .tabs{
6 | max-width: 100%;
7 | overflow: auto;
8 | border-bottom: 1px solid #d9d9d9;
9 | display: flex;
10 | &::-webkit-scrollbar{
11 | width: 0px;
12 | height: 0px;
13 | };
14 | li{
15 | font-size: 14px;
16 | line-height: 1.5;
17 | padding: 10px 15px;
18 | text-align: center;
19 | cursor: pointer;
20 | color: #919191;
21 | white-space: nowrap;
22 | &:hover{
23 | color: #404040;
24 | }
25 | };
26 | .tab-active{
27 | border-bottom: 2px solid #ccc;
28 | color: #404040;
29 | }
30 | };
31 |
32 | .tab-content{
33 | transition: opacity 1s;
34 | display: flex;
35 | flex-wrap: wrap;
36 | align-items: flex-start;
37 | align-content: flex-start;
38 | width: 100%;
39 | &::-webkit-scrollbar{
40 | width: 0px;
41 | }
42 | .note-item{
43 | width: 28%;
44 | height: 180px;
45 | background-color: #fff;
46 | box-shadow: 0 1px 6px #ccc;
47 | border-radius: 3px;
48 | margin: 1% 1%;
49 | padding: 1%;
50 | text-align: center;
51 | .title{
52 | font-size: 16px;
53 | color: #484848;
54 | cursor: pointer;
55 | height: 24px;
56 | max-height: 24px;
57 | overflow: hidden;
58 | white-space: nowrap;
59 | text-overflow: ellipsis;
60 | }
61 | .date{
62 | color: #aaa;
63 | font-size: 12px;
64 | padding: 10px 0;
65 | border-bottom: 1px dashed #ddd;
66 | }
67 | .content{
68 | text-align: left;
69 | padding: 10px 0;
70 | color: #666;
71 | height: 5rem;
72 | max-height: 5rem;
73 | line-height: 1.2rem;
74 | margin-bottom: 1rem;
75 | text-overflow: ellipsis;
76 | display: -webkit-box;
77 | -webkit-box-orient: vertical;
78 | -webkit-line-clamp: 5;
79 | overflow: hidden;
80 | }
81 | &:hover{
82 | cursor: pointer;
83 | transform: scale(1.02, 1.02)
84 | }
85 | }
86 | .tip{
87 | width: 100%;
88 | text-align: center;
89 | font-size: 30px;
90 | color: #ccc;
91 | margin-top: 20vh;
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/src/app/page/classification/classification.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ClassificationComponent } from './classification.component';
4 |
5 | describe('ClassificationComponent', () => {
6 | let component: ClassificationComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ ClassificationComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ClassificationComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/classification/classification.component.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@angular/animations';
2 | import { NoteService } from '../../services/note/note.service';
3 | import { Subscription } from 'rxjs/Subscription';
4 | import { TagService } from '../../services/tag/tag.service';
5 | import { Component, OnInit, Directive, ElementRef, Pipe, PipeTransform, OnDestroy } from '@angular/core';
6 | import { Router } from '@angular/router';
7 |
8 | @Directive({
9 | selector: '[classificationTabsContentHeight]'
10 | })
11 | export class ClassificationTabsContentHeightDirective {
12 | constructor(el: ElementRef) {
13 | el.nativeElement.style.height = window.innerHeight - 80 + 'px';
14 | el.nativeElement.style.maxHeight = window.innerHeight - 80 + 'px';
15 | el.nativeElement.style.overflow = 'auto';
16 | }
17 | }
18 |
19 | @Pipe({name: 'filterNoteContent'})
20 | export class FilterNoteContentPipe implements PipeTransform {
21 | transform(value: String): String {
22 | return value.replace(/[^\u4e00-\u9fa5]/gi, '');
23 | }
24 | }
25 |
26 | @Component({
27 | selector: 'app-classification',
28 | templateUrl: './classification.component.html',
29 | styleUrls: ['./classification.component.scss']
30 | })
31 | export class ClassificationComponent implements OnInit, OnDestroy {
32 | tagListSub: Subscription;
33 | tagList = [];
34 | content = '全部';
35 | tabIsActive = true;
36 | allNoteSub: Subscription;
37 | allNote = [];
38 | currentNoteList = [];
39 |
40 | constructor(
41 | private tagService: TagService,
42 | private noteService: NoteService,
43 | private router: Router
44 | ) {
45 | }
46 |
47 | ngOnInit() {
48 | this.tagListSub = this.tagService.tagList$.subscribe((data) => {
49 | this.tagList = data;
50 | });
51 |
52 | this._activeFalse();
53 |
54 | this.allNoteSub = this.noteService.allNote$.subscribe((data) => {
55 | this.allNote = data;
56 | this.currentNoteList = data;
57 | });
58 | }
59 |
60 | ngOnDestroy() {
61 | this.tagListSub.unsubscribe();
62 | this.allNoteSub.unsubscribe();
63 | }
64 |
65 | _activeFalse() {
66 | this.tagList.map((item) => {
67 | item['tabIsActive'] = false;
68 | });
69 | }
70 |
71 | all() {
72 | this._activeFalse();
73 | this.tabIsActive = true;
74 | this.content = '全部';
75 | this.currentNoteList = this.allNote;
76 | }
77 |
78 | tabClick(data) {
79 | this.tabIsActive = false;
80 | this._activeFalse();
81 | data.tabIsActive = true;
82 |
83 | const temporary = [];
84 | this.allNote.forEach((item, index) => {
85 | const jsonStringify = JSON.stringify(item.tag);
86 | if (jsonStringify.indexOf(data.name) !== -1) {
87 | temporary.push(item);
88 | }
89 | });
90 | this.currentNoteList = temporary;
91 | }
92 |
93 | viewNote(data) {
94 | this.router.navigate(['/viewNote']);
95 | localStorage.setItem('noteItemInfo', JSON.stringify(data));
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/app/page/edit-note/edit-note.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/page/edit-note/edit-note.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../directives/markdown-editor/simplemde.min.scss';
2 | .add-note-wrap {
3 | margin: 15px 10%;
4 | padding: 1rem;
5 | background-color: #fff;
6 | box-shadow: 0 1px 6px #ccc;
7 | border-radius: 3px;
8 | }
9 | .editor-toolbar{
10 | color: #ccc;
11 | }
12 | .add-note-title{
13 | height: 35px;
14 | display: flex;
15 | justify-content: center;
16 | align-items: center;
17 | input{
18 | text-align: center;
19 | width: 100%;
20 | height: 30px;
21 | border: none;
22 | outline: none;
23 | font-size: 20px;
24 | color:#4e4e4e;
25 | }
26 | }
27 | ::-webkit-input-placeholder { /* WebKit browsers */
28 | color:#999;
29 | }
30 | .add-note-toolbar{
31 | width: 100%;
32 | display: flex;
33 | align-items: center;
34 | padding: 10px 0;
35 | border-bottom: 1px solid #ececec;
36 | }
37 | .add-note-toolbar-label{
38 | width: 80%;
39 | height: 32px;
40 | line-height: 32px;
41 | }
42 | .add-note-toolbar-dropdown{
43 | width: 150px;
44 | float: left;
45 | padding-top: 8px;
46 | }
47 | .tag-list-wrap{
48 | width: 70%;
49 | max-width: 70%;
50 | display: flex;
51 | flex-wrap: wrap;
52 | height: 32px;
53 | max-height: 32px;
54 | overflow: scroll;
55 | &::-webkit-scrollbar{
56 | width: 0;
57 | height: 0;
58 | }
59 | .tag-item{
60 | height: 25px;
61 | line-height: 25px;
62 | border: 1px solid #ccc;
63 | border-radius: 4px;
64 | margin: 3px 3px;
65 | display: flex;
66 | flex-wrap: wrap;
67 | cursor: pointer;
68 | div{
69 | line-height: 25px;
70 | padding: 0 14px;
71 | };
72 | i{
73 | line-height: 25px;
74 | padding: 0 6px;
75 | border-left: 1px solid #ccc;
76 | font-size: 12px;
77 | color: #ccc;
78 | };
79 | i:hover{
80 | color: #808b96;
81 | }
82 | &:hover{
83 | box-shadow: 1px 1px 4px rgba(0,0,0,.2);
84 | }
85 | }
86 | }
87 | .add-note-toolbar-btn{
88 | width: 20%;
89 | align-self: flex-end;
90 | app-button{
91 | float: right;
92 | margin-left: 5px;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/app/page/edit-note/edit-note.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { EditNoteComponent } from './edit-note.component';
4 |
5 | describe('EditNoteComponent', () => {
6 | let component: EditNoteComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ EditNoteComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(EditNoteComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/edit-note/edit-note.component.ts:
--------------------------------------------------------------------------------
1 | import { Router } from '@angular/router';
2 | import { Subscription } from 'rxjs/Subscription';
3 | import { Component, OnInit, ViewEncapsulation, OnDestroy } from '@angular/core';
4 | import { TagService } from '../../services/tag/tag.service';
5 | import { NoteService } from '../../services/note/note.service';
6 | import { MsgService } from '../../services/msg/msg.service';
7 |
8 | @Component({
9 | selector: 'app-edit-note',
10 | templateUrl: './edit-note.component.html',
11 | styleUrls: ['./edit-note.component.scss'],
12 | encapsulation: ViewEncapsulation.None
13 | })
14 | export class EditNoteComponent implements OnInit, OnDestroy {
15 | dropdownMenuSub: Subscription;
16 | dropdownMenu = [];
17 | title = '';
18 | content = '';
19 | tagList = [];
20 | noteInfo = {
21 | content: String,
22 | date: String,
23 | sourceLink: String,
24 | tag: [],
25 | title: '',
26 | __v: Number,
27 | _id: ''
28 | };
29 |
30 | constructor(
31 | private tagService: TagService,
32 | private noteService: NoteService,
33 | private msg: MsgService,
34 | private router: Router
35 | ) { }
36 |
37 | ngOnInit() {
38 | this.dropdownMenuSub = this.tagService.tagList$.subscribe((data) => {
39 | this.dropdownMenu = data;
40 | });
41 |
42 | this.noteInfo = JSON.parse(localStorage.getItem('noteItemInfo'));
43 | this.tagList = this.noteInfo.tag;
44 | this.title = this.noteInfo.title;
45 | }
46 |
47 | ngOnDestroy() {
48 | this.dropdownMenuSub.unsubscribe();
49 | }
50 |
51 | selectItem(data) {
52 | this.tagList.push(data);
53 | }
54 |
55 | delectLabelItem(index) {
56 | this.tagList.splice(index, 1);
57 | }
58 |
59 | markdownValueChange(data) {
60 | this.content = data;
61 | }
62 |
63 | // 保存笔记
64 | save() {
65 | if (this.title === '' || this.content === '' || this.tagList.length === 0) {
66 | this.msg.info('请输入完整的笔记信息!');
67 | } else {
68 | const sub = this.noteService._modifyNote({
69 | title: this.title,
70 | content: this.content,
71 | tag: this.tagList,
72 | date: new Date(),
73 | sourceLink: '',
74 | _id: this.noteInfo._id
75 | }).subscribe((res) => {
76 | if (res['code'] === 200) {
77 | this.msg.info('修改成功!');
78 | this.noteService._updateAllNote();
79 | localStorage.setItem('noteItemInfo', JSON.stringify(res['data']));
80 | this.router.navigate(['/viewNote']);
81 | }
82 | });
83 | }
84 | }
85 |
86 | cancel() {
87 | this.router.navigate(['/viewNote']);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/page/index/index.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 | Micro-note
5 |
6 |
7 |
8 |
9 |
10 |
tips 账号:test 密码: 123456
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app/page/index/index.component.scss:
--------------------------------------------------------------------------------
1 | .login-wrap {
2 | width: 100vw;
3 | height: 100vh;
4 | background-color: #fff;
5 | position: fixed;
6 | display: block;
7 | top: 0;
8 | left: 0;
9 | z-index: 100;
10 | .login-title {
11 | position: absolute;
12 | top: 50%;
13 | width: 100%;
14 | margin-top: -230px;
15 | text-align: center;
16 | font-size: 40px;
17 | color: #3db8c1;
18 | img{
19 | width: 50px;
20 | height: 50px;
21 | }
22 | }
23 | .login-panel {
24 | position: absolute;
25 | left: 50%;
26 | top: 50%;
27 | width: 300px;
28 | height: 160px;
29 | margin: -150px 0 0 -190px;
30 | padding: 40px;
31 | border-radius: 5px;
32 | background: #f5f5f5;
33 | text-align: center;
34 | .login-name {
35 | margin-bottom: 1rem;
36 | -webkit-appearance: none;
37 | -moz-appearance: none;
38 | appearance: none;
39 | background-color: #fff;
40 | background-image: none;
41 | border-radius: 4px;
42 | border: 1px solid #bfcbd9;
43 | box-sizing: border-box;
44 | color: #1f2d3d;
45 | display: block;
46 | font-size: inherit;
47 | height: 36px;
48 | line-height: 1;
49 | outline: 0;
50 | padding: 3px 10px;
51 | transition: border-color .2s cubic-bezier(.645,.045,.355,1);
52 | width: 100%;
53 | }
54 | .login-pwd {
55 | margin-bottom: 1rem;
56 | -webkit-appearance: none;
57 | -moz-appearance: none;
58 | appearance: none;
59 | background-color: #fff;
60 | background-image: none;
61 | border-radius: 4px;
62 | border: 1px solid #bfcbd9;
63 | box-sizing: border-box;
64 | color: #1f2d3d;
65 | display: block;
66 | font-size: inherit;
67 | height: 36px;
68 | line-height: 1;
69 | outline: 0;
70 | padding: 3px 10px;
71 | transition: border-color .2s cubic-bezier(.645,.045,.355,1);
72 | width: 100%;
73 | }
74 | .login-submit {
75 | width: 300px;
76 | color: #fff;
77 | display: inline-block;
78 | line-height: 1;
79 | white-space: nowrap;
80 | cursor: pointer;
81 | background: #fff;
82 | border: none;
83 | -webkit-appearance: none;
84 | text-align: center;
85 | box-sizing: border-box;
86 | outline: 0;
87 | margin: 0;
88 | padding: 10px 15px;
89 | font-size: 14px;
90 | border-radius: 4px;
91 | background-color: #3db8c1;
92 | }
93 | .case-number {
94 | margin-top: 1rem;
95 | font-size: 14px;
96 | color: #5d5d5d;
97 | }
98 | }
99 | }
100 |
101 | .github{
102 | position: fixed;
103 | top: 10px;
104 | right: -40px;
105 | width: 120px;
106 | height: 30px;
107 | background-color: #3db8c1;
108 | transform: rotate(45deg);
109 | z-index: 101;
110 | a{
111 | color: #000;
112 | }
113 | i{
114 | font-size: 29px;
115 | position: fixed;
116 | top: -6px;
117 | right: 51px;
118 | z-index: 101;
119 | cursor: pointer;
120 | &:hover{
121 | transform: scale(1.05, 1.05)
122 | }
123 | }
124 | }
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/src/app/page/index/index.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { IndexComponent } from './index.component';
4 |
5 | describe('IndexComponent', () => {
6 | let component: IndexComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ IndexComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(IndexComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/index/index.component.ts:
--------------------------------------------------------------------------------
1 | import { Router } from '@angular/router';
2 | import { MsgService } from './../../services/msg/msg.service';
3 | import { HttpClient } from '@angular/common/http';
4 | import { Component, OnInit } from '@angular/core';
5 |
6 | @Component({
7 | selector: 'app-index',
8 | templateUrl: './index.component.html',
9 | styleUrls: ['./index.component.scss']
10 | })
11 | export class IndexComponent implements OnInit {
12 | name = '';
13 | pwd = '';
14 |
15 | constructor(
16 | private http: HttpClient,
17 | private msg: MsgService,
18 | private router: Router
19 | ) { }
20 |
21 | ngOnInit() {
22 |
23 | }
24 |
25 | login() {
26 | this.http.post('/api/login', {
27 | name: this.name,
28 | pwd: this.pwd
29 | }).subscribe((res) => {
30 | if (res['code'] !== 200) {
31 | this.msg.info( res['msg'] );
32 | } else {
33 | this.router.navigate(['/addLinkNote']);
34 | localStorage.setItem('userName', this.name);
35 | }
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/page/search/search.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{item.title}}
9 |
10 |
11 | {{item.date | date:"yyyy-MM-dd HH:mm:ss"}}
12 |
13 |
14 | {{item.content | filterNoteContent}}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/app/page/search/search.component.scss:
--------------------------------------------------------------------------------
1 | .search-wrap{
2 | height: 100vh;
3 | padding: 1vh 8vh 30vh 8vh;
4 | .input-wrap{
5 | height:52px;
6 | line-height:52px;
7 | margin-top: 19vh;
8 | padding-bottom: 3rem;
9 | border-bottom: 1px solid #ececec;
10 | .search-input{
11 | border-bottom: 1px solid #ccc;
12 | background-color: transparent;
13 | margin-bottom: 3vh;
14 | border: none;
15 | color: #ccc;
16 | height: 52px;
17 | width: 100%;
18 | overflow-x: scroll;
19 | font-size: 40px;
20 | outline: none;
21 | }
22 | }
23 |
24 | .tab-content{
25 | transition: opacity 1s;
26 | display: flex;
27 | flex-wrap: wrap;
28 | align-items: flex-start;
29 | align-content: flex-start;
30 | width: 100%;
31 | &::-webkit-scrollbar{
32 | width: 0px;
33 | }
34 | .note-item{
35 | width: 28%;
36 | height: 180px;
37 | background-color: #f7f7f7;
38 | box-shadow: 0 1px 6px #ccc;
39 | border-radius: 3px;
40 | margin: 1% 1%;
41 | padding: 1%;
42 | text-align: center;
43 | .title{
44 | font-size: 16px;
45 | color: #484848;
46 | cursor: pointer;
47 | height: 24px;
48 | max-height: 24px;
49 | overflow: hidden;
50 | white-space: nowrap;
51 | text-overflow: ellipsis;
52 | }
53 | .date{
54 | color: #aaa;
55 | font-size: 12px;
56 | padding: 10px 0;
57 | border-bottom: 1px dashed #ddd;
58 | }
59 | .content{
60 | text-align: left;
61 | padding: 10px 0;
62 | color: #666;
63 | height: 5rem;
64 | max-height: 5rem;
65 | line-height: 1.2rem;
66 | margin-bottom: 1rem;
67 | text-overflow: ellipsis;
68 | display: -webkit-box;
69 | -webkit-box-orient: vertical;
70 | -webkit-line-clamp: 5;
71 | overflow: hidden;
72 | }
73 | &:hover{
74 | transform: scale(1.02, 1.02);
75 | cursor: pointer;
76 | }
77 | }
78 | .tip{
79 | width: 100%;
80 | text-align: center;
81 | font-size: 30px;
82 | color: #ccc;
83 | margin-top: 20vh;
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/src/app/page/search/search.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { SearchComponent } from './search.component';
4 |
5 | describe('SearchComponent', () => {
6 | let component: SearchComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ SearchComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(SearchComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/search/search.component.ts:
--------------------------------------------------------------------------------
1 | import { Router } from '@angular/router';
2 | import { Subscription } from 'rxjs/Subscription';
3 | import { NoteService } from '../../services/note/note.service';
4 | import { Component, OnInit, OnDestroy } from '@angular/core';
5 |
6 | @Component({
7 | selector: 'app-search',
8 | templateUrl: './search.component.html',
9 | styleUrls: ['./search.component.scss']
10 | })
11 | export class SearchComponent implements OnInit, OnDestroy {
12 | allNoteSub: Subscription;
13 | allNote = [];
14 | resultNoteList = [];
15 | searchName = '';
16 |
17 | constructor(
18 | private noteService: NoteService,
19 | private router: Router
20 | ) { }
21 |
22 | ngOnInit() {
23 | this.allNoteSub = this.noteService.allNote$.subscribe((data) => {
24 | this.allNote = data;
25 | });
26 | }
27 |
28 | ngOnDestroy() {
29 | this.allNoteSub.unsubscribe();
30 | }
31 |
32 | onEnter(name) {
33 | const temporary = [];
34 | this.allNote.forEach((item, index) => {
35 | const jsonStringify = JSON.stringify(item.tag) + JSON.stringify(item.title);
36 | if (jsonStringify.indexOf(name) !== -1) {
37 | temporary.push(item);
38 | }
39 | });
40 | this.resultNoteList = temporary;
41 | this.searchName = '';
42 | }
43 |
44 | viewNote(data) {
45 | this.router.navigate(['/viewNote']);
46 | localStorage.setItem('noteItemInfo', JSON.stringify(data));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/page/tag/tag.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{item.name}}
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/app/page/tag/tag.component.scss:
--------------------------------------------------------------------------------
1 | .tag{
2 | height: 100vh;
3 | padding: 1vh 8vh 30vh 8vh;
4 | }
5 | .input-wrap{
6 | height:52px;
7 | line-height:52px;
8 | margin-top: 19vh;
9 | margin-bottom: 20px;
10 | padding-bottom: 30px;
11 | border-bottom: 1px solid #ccc;
12 | }
13 | .search-input{
14 | border-bottom: 1px solid #ccc;
15 | background-color: transparent;
16 | margin-bottom: 3vh;
17 | border: none;
18 | color: #ccc;
19 | height: 52px;
20 | width: 100%;
21 | overflow-x: scroll;
22 | font-size: 40px;
23 | outline: none;
24 | }
25 | .tag-list-wrap{
26 | display: flex;
27 | flex-wrap: wrap;
28 | overflow: auto;
29 | max-height: 61vh;
30 | .tag-item{
31 | height: 25px;
32 | line-height: 25px;
33 | border: 1px solid #ccc;
34 | border-radius: 4px;
35 | margin: 3px 3px;
36 | display: flex;
37 | flex-wrap: wrap;
38 | cursor: pointer;
39 | div{
40 | line-height: 25px;
41 | padding: 0 14px;
42 | };
43 | i{
44 | line-height: 25px;
45 | padding: 0 6px;
46 | border-left: 1px solid #ccc;
47 | font-size: 12px;
48 | color: #ccc;
49 | };
50 | i:hover{
51 | color: #808b96;
52 | }
53 | &:hover{
54 | box-shadow: 1px 1px 4px rgba(0,0,0,.2);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/page/tag/tag.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { TagComponent } from './tag.component';
4 |
5 | describe('TagComponent', () => {
6 | let component: TagComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ TagComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(TagComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/tag/tag.component.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { MsgService } from './../../services/msg/msg.service';
3 | import { TagService } from './../../services/tag/tag.service';
4 | import { Component, OnInit, OnDestroy } from '@angular/core';
5 | import { Subscription } from 'rxjs/Subscription';
6 |
7 | @Component({
8 | selector: 'app-tag',
9 | templateUrl: './tag.component.html',
10 | styleUrls: ['./tag.component.scss']
11 | })
12 | export class TagComponent implements OnInit , OnDestroy {
13 | tagListSub: Subscription;
14 | tagList = [];
15 | tagName = '';
16 | constructor(
17 | private tagService: TagService,
18 | private msg: MsgService,
19 | private http: HttpClient
20 | ) {
21 | }
22 |
23 | ngOnInit() {
24 | this.tagListSub = this.tagService.tagList$.subscribe((data) => {
25 | this.tagList = data;
26 | });
27 |
28 | }
29 |
30 | ngOnDestroy() {
31 | this.tagListSub.unsubscribe();
32 | }
33 |
34 | addTag(value) {
35 | if (value !== '') {
36 | this.tagService._addTag(value);
37 | this.tagName = '';
38 | } else {
39 | this.msg.info('标签名字不能为空!');
40 | }
41 | }
42 |
43 | delectTagItem(id) {
44 | this.tagService._deleteTag(id);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/page/view-note/view-note.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{noteInfo.title}}
4 |
5 |
22 |
23 |
24 |
25 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 删除笔记
41 |
42 |
43 | 确定删除《{{noteInfo.title}}》笔记吗?
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/app/page/view-note/view-note.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../../assets/ionicons.min.css'; /* 图标库 */
2 | .view-note-wrap{
3 | float: left;
4 | width: 70%;
5 | margin: 15px 25px 15px 65px;
6 | padding: 1rem;
7 | background-color: #fff;
8 | box-shadow: 0 1px 6px #ccc;
9 | border-radius: 3px;
10 | .view-note-title{
11 | text-align: center;
12 | height: 35px;
13 | font-size: 20px;
14 | width: 100%;
15 | max-width: 100%;
16 | overflow: hidden;
17 | text-overflow: ellipsis;
18 | white-space: nowrap;
19 | }
20 | .note-info{
21 | border-bottom: 1px #ccc dashed;
22 | padding: 10px;
23 | display: flex;
24 | position: relative;
25 | .edit{
26 | height: 25px;
27 | line-height: 30px;
28 | color: #999999;
29 | font-size: 16px;
30 | i{
31 | margin-right: 25px;
32 | cursor: pointer;
33 | }
34 | }
35 | .note-time{
36 | height: 25px;
37 | line-height: 30px;
38 | color: #999999;
39 | }
40 | .note-tag{
41 | display: flex;
42 | -ms-flex-wrap: wrap;
43 | flex-wrap: wrap;
44 | margin: 0;
45 | list-style: none;
46 | padding-left: 10px;
47 | li{
48 | min-width: 40px;
49 | text-align: center;
50 | height: 20px;
51 | line-height: 20px;
52 | border: 1px solid #ccc;
53 | border-radius: 4px;
54 | margin: 3px 3px;
55 | cursor: pointer;
56 | color: #919191;
57 | padding: 0 5px;
58 | }
59 | }
60 | .sourceLink{
61 | width: 170px;
62 | max-width: 170px;
63 | overflow: hidden;
64 | text-overflow: ellipsis;
65 | white-space: nowrap;
66 | line-height: 30px;
67 | height: 25px;
68 | position: absolute;
69 | right: 0;
70 | color: #3db8c1;
71 | cursor: pointer;
72 | }
73 | }
74 |
75 | .preview{
76 | font-size: 14px !important;
77 | padding: 10px 30px;
78 | background-color: #fbfbfb;
79 | &::-webkit-scrollbar{
80 | width: 0;
81 | }
82 | img{
83 | max-width: 100%;
84 | }
85 | th, td{
86 | border: 1px solid #E6E6E6;
87 | padding: 5px 8px;
88 | }
89 | table{
90 | border-collapse: collapse;
91 | }
92 | blockquote{
93 | margin: 1em 0;
94 | border-left: 4px solid #3db8c1;
95 | padding: 10px;
96 | color: #666;
97 | background-color: #eeeeee;
98 | p {
99 | line-height: 2;
100 | display: block;
101 | margin-bottom: 0px;
102 | a {
103 | color: #3db8c1;
104 | text-decoration: none;
105 | }
106 | }
107 | ul{
108 | list-style-type: none;
109 | padding: 0;
110 | margin: 0;
111 | }
112 | }
113 | h1{
114 | margin: 1.3rem 0;
115 | line-height: 1.2;
116 | font-size: 2em;
117 | }
118 | h2{
119 | border-bottom: 1px solid #eee;
120 | padding-bottom: 10px;
121 | }
122 | h3{
123 | margin: 20px 0;
124 | }
125 | p{
126 | line-height: 2;
127 | display: block;
128 | margin-bottom: 20px;
129 | a{
130 | color: #3db8c1;
131 | text-decoration: none;
132 | }
133 | video{
134 | width: 100%;
135 | }
136 | }
137 | ul{
138 | padding-left: 2.7rem;
139 | display: block;
140 | list-style-type: disc;
141 | li{
142 | margin-bottom: .6rem;
143 | list-style: inherit;
144 | }
145 | }
146 | code{
147 | font-family: Menlo,Monaco,Consolas,Courier New,monospace;
148 | font-size: 1rem;
149 | padding: 2px 5px;
150 | word-break: break-word;
151 | color: #49a9ee;
152 | background-color: #f8f8f8;
153 | border-radius: 2px;
154 | overflow-x: scroll;
155 | font-size: 12px;
156 | margin: 0 4px;
157 | }
158 | pre{
159 | white-space: pre;
160 | margin: 1em 0px;
161 | padding: 5px;
162 | width: 100%;
163 | max-width: 100%;
164 | overflow: scroll;
165 | border-radius: 3px;
166 | background-color: #eeeeee;
167 | code{
168 | color: #333 !important;
169 | background-color: #eeeeee;
170 | .hljs-function{
171 | color: #333;
172 | }
173 | .hljs-keyword{
174 | color: #333;
175 | font-weight: 700;
176 | }
177 | .hljs-title{
178 | color: #900;
179 | font-weight: 700;
180 | }
181 | .hljs-params{
182 | color: #333;
183 | }
184 | .hljs-comment{
185 | color: #998;
186 | }
187 | .hljs-string{
188 | color: #d14;
189 | }
190 | .hljs-number{
191 | color: teal;
192 | }
193 | .hljs-literal{
194 | color: teal;
195 | }
196 | }
197 | }
198 | }
199 | }
200 |
201 | // 目录
202 | .catalog-wrap{
203 | overflow: hidden;
204 | overflow: auto;
205 | height: 100vh;
206 | max-height: 100vh;
207 | // margin: 15px 15px 15px 0;
208 | // border-left: 1px solid #eee;
209 | box-shadow: 0 1px 1px #ccc;
210 | background-color: #fff;
211 | h3{
212 | padding: 5px 20px 5px 20px;
213 | }
214 | &::-webkit-scrollbar{
215 | width: 1px;
216 | }
217 | ul{
218 | list-style-type: none;
219 | .active{
220 | padding: 5px 20px 5px 20px;
221 | background-color: #f3f3f3;
222 | border-left: 3px solid #3db8c1;
223 | a{
224 | color: #3db8c1;
225 | }
226 | }
227 | li{
228 | padding: 5px 20px 5px 20px;
229 | a{
230 | text-decoration: none;
231 | color: #222222;
232 | cursor: pointer;
233 | }
234 | }
235 | .h3{
236 | padding-left: 40px;
237 | }
238 | .h4{
239 | padding-left: 80px;
240 | }
241 | }
242 | }
243 |
244 | // 删除提示
245 | .delect-wrap{
246 | position: fixed;
247 | top: 0;
248 | left: 0;
249 | width: 100vw;
250 | height: 100vh;
251 | opacity: .97;
252 | background-color: #fff;
253 | display: flex;
254 | align-items: center;
255 | .delect-content{
256 | width: 800px;
257 | height: 310px;
258 | margin: 0 auto;
259 | .icon{
260 | text-align: center;
261 | height: 42px;
262 | line-height: 42px;
263 | font-size: 36px;
264 | margin-bottom: 10px;
265 | color: #ababab;
266 | }
267 | .text{
268 | text-align: center;
269 | margin: 0 40%;
270 | height: 50px;
271 | line-height: 50px;
272 | border-bottom: 1px #d9d9d9 solid;
273 | }
274 | .tip{
275 | height: 120px;
276 | line-height: 120px;
277 | font-size: 20px;
278 | text-align: center;
279 | color: #383838;
280 | em{
281 | font-weight: bold;
282 | }
283 | }
284 | .btn{
285 | text-align: center;
286 | button{
287 | color: #606060;
288 | background-color: #ececec;
289 | border: 1px solid #d9d9d9;
290 | cursor: pointer;
291 | border-radius: 5px;
292 | margin: 0 4px;
293 | padding: 9px 71px;
294 | font-size: 13px;
295 | outline: none;
296 | }
297 | .delete{
298 | background-color: #3db8c1;
299 | color: #fff;
300 | border: none;
301 | }
302 | }
303 | }
304 | }
--------------------------------------------------------------------------------
/src/app/page/view-note/view-note.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ViewNoteComponent } from './view-note.component';
4 |
5 | describe('ViewNoteComponent', () => {
6 | let component: ViewNoteComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ ViewNoteComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ViewNoteComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should be created', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/page/view-note/view-note.component.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@angular/animations';
2 | import { Router } from '@angular/router';
3 | import { MsgService } from './../../services/msg/msg.service';
4 | import { NoteService } from './../../services/note/note.service';
5 | import { Component, OnInit, ViewEncapsulation, ElementRef, Input } from '@angular/core';
6 | import marked from 'marked';
7 | import highlight from 'highlight.js';
8 |
9 | @Component({
10 | selector: 'app-view-note',
11 | templateUrl: './view-note.component.html',
12 | styleUrls: ['./view-note.component.scss'],
13 | encapsulation: ViewEncapsulation.None
14 | })
15 | export class ViewNoteComponent implements OnInit {
16 | isShowEdit = false;
17 | noteInfo = {
18 | content: '',
19 | date: String,
20 | sourceLink: '',
21 | tag: Array,
22 | title: String,
23 | __v: Number,
24 | _id: String
25 | };
26 | isShowDelect = false;
27 | catalog = [];
28 |
29 | constructor(
30 | private noteService: NoteService,
31 | private msgService: MsgService,
32 | private router: Router
33 | ) { }
34 |
35 | ngOnInit() {
36 | this.noteInfo = JSON.parse(localStorage.getItem('noteItemInfo'));
37 | this.noteInfo.sourceLink === '' ? this.isShowEdit = true : this.isShowEdit = false;
38 | this.noteInfo.content = marked(this.noteInfo.content, {
39 | renderer: new marked.Renderer(),
40 | gfm: true,
41 | pedantic: false,
42 | sanitize: false,
43 | tables: true,
44 | breaks: true,
45 | smartLists: true,
46 | smartypants: true,
47 | highlight: function (code) {
48 | return highlight.highlightAuto(code).value;
49 | }
50 | });
51 |
52 | const contentDom = this.parseDom(this.noteInfo.content); // 把html字符串装换成DOM
53 | // 提取笔记内容生成目录信息
54 | Array.prototype.slice.call(contentDom.querySelectorAll('h1,h2,h3,h4,h5,h6')).forEach((item, index) => {
55 | item.id = item.localName + '-' + index;
56 | let active;
57 | index === 0 ? active = true : active = false;
58 | this.catalog.push({
59 | tagName: item.localName,
60 | text: item.innerText,
61 | href: '#' + item.localName + '-' + index,
62 | el: item,
63 | isActive: active
64 | });
65 | });
66 | const previewDom = document.querySelector('.preview');
67 | const catalogDom = document.querySelector('.catalog-wrap');
68 | previewDom.appendChild(contentDom);
69 | if (this.catalog.length === 0) {
70 | return;
71 | }
72 | previewDom.addEventListener('scroll', this.throttle(() => {
73 | this.catalog.forEach((item, index) => {
74 | if (index !== this.catalog.length - 1) {
75 | if ((previewDom.scrollTop + 125) > item.el.offsetTop && (previewDom.scrollTop + 125) < this.catalog[index + 1].el.offsetTop) {
76 | this.catalog.forEach(j => j.isActive = false);
77 | item.isActive = true;
78 | }
79 | }else {
80 | if ((previewDom.scrollTop + 125) > item.el.offsetTop) {
81 | this.catalog.forEach(j => j.isActive = false);
82 | item.isActive = true;
83 | }
84 | }
85 | });
86 | const activeDom = catalogDom.querySelector('.active');
87 | catalogDom.scrollTop = activeDom.offsetTop - window.innerHeight / 2;
88 | }, 200));
89 |
90 | }
91 |
92 | delect() {
93 | this.isShowDelect = true;
94 | }
95 |
96 | cancel() {
97 | this.isShowDelect = false;
98 | }
99 |
100 | confirmDelete() {
101 | const id = this.noteInfo._id;
102 | this.noteService._deleteNote(id).subscribe((res) => {
103 | if (res['data'].ok === 1) {
104 | this.isShowDelect = false;
105 | this.msgService.info('删除成功!');
106 | this.noteService._updateAllNote();
107 | this.router.navigate(['/classification']);
108 | }
109 | });
110 | }
111 |
112 | editNote() {
113 | this.router.navigate(['/editNote']);
114 | }
115 |
116 | catalogNavigation(data) {
117 | const previewDom = document.querySelector('.preview');
118 | const activeNode = previewDom.querySelector(`${data.href}`);
119 | previewDom.scrollTop = activeNode.offsetTop - 120;
120 | }
121 |
122 | private parseDom(arg) {
123 | const objl = document.createElement('div');
124 | objl.innerHTML = arg;
125 | return objl;
126 | }
127 |
128 | private throttle(fn, interval = 300) {
129 | let canRun = true;
130 | return function () {
131 | if (!canRun) {
132 | return;
133 | }
134 | canRun = false;
135 | setTimeout(function() {
136 | fn.apply(this, arguments);
137 | canRun = true;
138 | }, interval);
139 | };
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/app/services/loading-bar/loading-bar.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { LoadingBarService } from './loading-bar.service';
4 |
5 | describe('LoadingBarService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [LoadingBarService]
9 | });
10 | });
11 |
12 | it('should be created', inject([LoadingBarService], (service: LoadingBarService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/services/loading-bar/loading-bar.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable()
4 | export class LoadingBarService {
5 | constructor() { }
6 |
7 | public $Loading = {
8 | start: function(){
9 | const myLoadingBar = document.querySelector('.my-loading-bar');
10 | if (myLoadingBar !== null && myLoadingBar instanceof HTMLElement) {
11 | let LoadingBarDivWidth = 0;
12 | this.timer = setInterval(() => {
13 | LoadingBarDivWidth++;
14 | myLoadingBar.style.width = LoadingBarDivWidth + 'vw';
15 | if (LoadingBarDivWidth >= 100) {
16 | clearInterval(this.timer);
17 | }
18 | }, 25);
19 | } else {
20 | const LoadingBarDiv = document.createElement('div');
21 | LoadingBarDiv.className = 'my-loading-bar';
22 | const bodyEl = document.querySelector('body');
23 | bodyEl.appendChild(LoadingBarDiv);
24 | let LoadingBarDivWidth = 0;
25 | this.timer = setInterval(() => {
26 | LoadingBarDivWidth++;
27 | LoadingBarDiv.style.width = LoadingBarDivWidth + 'vw';
28 | if (LoadingBarDivWidth >= 100) {
29 | clearInterval(this.timer);
30 | }
31 | }, 25);
32 | }
33 | },
34 | finish: function(){
35 | const myLoadingBar = document.querySelector('.my-loading-bar');
36 | if (myLoadingBar !== null && myLoadingBar instanceof HTMLElement) {
37 | clearInterval(this.timer);
38 | myLoadingBar.style.width = 0 + 'vw';
39 | }
40 | }
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/services/msg/msg.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { MsgService } from './msg.service';
4 |
5 | describe('MsgService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [MsgService]
9 | });
10 | });
11 |
12 | it('should be created', inject([MsgService], (service: MsgService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/services/msg/msg.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable()
4 | export class MsgService {
5 |
6 | private createTpl(msg: String) {
7 | const tpl = `
8 |
9 |
10 | ${msg}
11 |
12 | `;
13 | return tpl;
14 | }
15 |
16 | constructor() { }
17 |
18 | info(msg: String) {
19 | const msgWrapEl = document.querySelector('.msg-wrap');
20 | if (!msgWrapEl) {
21 | const bodyEl = document.querySelector('body');
22 | bodyEl.appendChild(this.parseDom(this.createTpl(msg)));
23 | const msgWrapEl = document.querySelector('.msg-wrap');
24 | setTimeout(() => {
25 | bodyEl.removeChild(msgWrapEl);
26 | }, 2000);
27 | }
28 | }
29 |
30 | private parseDom(arg) {
31 | const objl = document.createElement('div');
32 | objl.className = 'msg-wrap';
33 | objl.innerHTML = arg;
34 | return objl;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/services/note/note.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { NoteService } from './note.service';
4 |
5 | describe('NoteService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [NoteService]
9 | });
10 | });
11 |
12 | it('should be created', inject([NoteService], (service: NoteService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/services/note/note.service.ts:
--------------------------------------------------------------------------------
1 | import { BehaviorSubject } from 'rxjs/BehaviorSubject';
2 | import { Injectable } from '@angular/core';
3 | import { HttpClient } from '@angular/common/http';
4 |
5 | @Injectable()
6 | export class NoteService {
7 | allNote = [];
8 | allNote$ = new BehaviorSubject>(this.allNote);
9 |
10 | constructor(
11 | private http: HttpClient
12 | ) {
13 | this._updateAllNote();
14 | }
15 |
16 | // 添加
17 | _addNote(param: editNote) {
18 | return this.http.post('/api/addNote', param);
19 | }
20 |
21 | // 修改
22 | _modifyNote(param: editNote) {
23 | return this.http.post('/api/modify', param);
24 | }
25 |
26 | // 删除
27 | _deleteNote(id) {
28 | return this.http.post('/api/deleteNote', {id: id});
29 | }
30 |
31 | _updateAllNote() {
32 | this.http.get('/api/allNote')
33 | .subscribe((data) => {
34 | this.allNote$.next(data['data']);
35 | });
36 | }
37 | }
38 |
39 | interface editNote {
40 | title: String;
41 | content: String;
42 | tag: Array