├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package.json ├── proxy.conf.js ├── src ├── app │ ├── admin │ │ ├── admin-routing.module.ts │ │ ├── admin-sidebar │ │ │ ├── admin-sidebar.component.css │ │ │ ├── admin-sidebar.component.html │ │ │ ├── admin-sidebar.component.spec.ts │ │ │ └── admin-sidebar.component.ts │ │ ├── admin.component.css │ │ ├── admin.component.html │ │ ├── admin.component.spec.ts │ │ ├── admin.component.ts │ │ ├── admin.module.ts │ │ ├── article-editor │ │ │ ├── article-editor.module.ts │ │ │ ├── editor.component.css │ │ │ ├── editor.component.html │ │ │ ├── editor.component.spec.ts │ │ │ ├── editor.component.ts │ │ │ └── md │ │ │ │ ├── md.component.css │ │ │ │ ├── md.component.html │ │ │ │ └── md.component.ts │ │ ├── article-management │ │ │ ├── article-filter-panel │ │ │ │ ├── article-filter-panel.component.css │ │ │ │ ├── article-filter-panel.component.html │ │ │ │ ├── article-filter-panel.component.spec.ts │ │ │ │ └── article-filter-panel.component.ts │ │ │ ├── article-list │ │ │ │ ├── article-list.component.css │ │ │ │ ├── article-list.component.html │ │ │ │ ├── article-list.component.spec.ts │ │ │ │ └── article-list.component.ts │ │ │ ├── article-management-routing.module.ts │ │ │ ├── article-management.component.css │ │ │ ├── article-management.component.html │ │ │ ├── article-management.component.spec.ts │ │ │ ├── article-management.component.ts │ │ │ └── article-management.module.ts │ │ └── dashboard │ │ │ ├── dashboard-routing.module.ts │ │ │ ├── dashboard.component.css │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.spec.ts │ │ │ ├── dashboard.component.ts │ │ │ ├── dashboard.module.ts │ │ │ ├── dashboard.options.ts │ │ │ ├── dashboard.service.spec.ts │ │ │ └── dashboard.service.ts │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── header │ │ ├── header.component.css │ │ ├── header.component.html │ │ ├── header.component.spec.ts │ │ ├── header.component.ts │ │ └── header.module.ts │ ├── login │ │ ├── auth.guard.spec.ts │ │ ├── auth.guard.ts │ │ ├── login-routing.module.ts │ │ ├── login.component.css │ │ ├── login.component.html │ │ ├── login.component.spec.ts │ │ ├── login.component.ts │ │ ├── login.module.ts │ │ ├── login.service.spec.ts │ │ └── login.service.ts │ └── view │ │ ├── article-detail │ │ ├── article-detail-routing.module.ts │ │ ├── article-detail.component.css │ │ ├── article-detail.component.html │ │ ├── article-detail.component.spec.ts │ │ ├── article-detail.component.ts │ │ ├── article-detail.module.ts │ │ └── article-outline │ │ │ ├── article-outline.component.css │ │ │ ├── article-outline.component.html │ │ │ └── article-outline.component.ts │ │ ├── article-list │ │ ├── article-item │ │ │ ├── article-item.component.css │ │ │ ├── article-item.component.html │ │ │ ├── article-item.component.spec.ts │ │ │ └── article-item.component.ts │ │ ├── article-list-routing.module.ts │ │ ├── article-list.component.css │ │ ├── article-list.component.html │ │ ├── article-list.component.spec.ts │ │ ├── article-list.component.ts │ │ ├── article-list.module.ts │ │ └── sidebar │ │ │ ├── sidebar.component.css │ │ │ ├── sidebar.component.html │ │ │ ├── sidebar.component.spec.ts │ │ │ └── sidebar.component.ts │ │ ├── view-routing.module.ts │ │ ├── view.component.css │ │ ├── view.component.html │ │ ├── view.component.spec.ts │ │ ├── view.component.ts │ │ └── view.module.ts ├── assets │ ├── .gitkeep │ └── img │ │ ├── dashboard │ │ ├── article.png │ │ ├── avator.png │ │ ├── collect.png │ │ ├── like.png │ │ └── view.png │ │ ├── editor │ │ ├── icon_bold.png │ │ ├── icon_italic.png │ │ ├── icon_orderedlist.png │ │ ├── icon_redo.png │ │ ├── icon_table.png │ │ ├── icon_underline.png │ │ ├── icon_undo.png │ │ └── icon_unorderedlist.png │ │ └── login │ │ ├── image_illustration.png │ │ ├── login-text.png │ │ ├── login-title.png │ │ ├── login_backgroud.png │ │ └── login_image.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | # package-lock 49 | package-lock.json 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 DevUI. 4 | Copyright (c) 2020 Huawei Technologies Co., Ltd. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## DevUI tutorial web 3 | 4 | ### 技术 5 | * Angular8.x 6 | * ng-devui组件 7 | * echarts、markd、codemirror 8 | 9 | ### 启动开发 10 | 11 | ```shell 12 | npm start #默认端口4200 13 | ``` 14 | #### 构建 15 | 16 | ```shell 17 | npm run build 18 | ``` 19 | 20 | ### lint 21 | 22 | ```shell 23 | npm run lint 24 | ``` 25 | 26 | ### 代理配置 27 | 本地开发通过proxy配置http请求到 3000端口的server端 28 | 29 | ### postinstall 30 | [fix generating es5 bundles error ,count not find plugin "proposal-numeric-separator"](http://github.com/angular/angular-cli/issues/17262) -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "devui-tutorial-web": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/devui-tutorial-web", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "aot": false, 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "node_modules/@devui-design/icons/icomoon/devui-icon.css", 28 | "node_modules/ng-devui/devui.min.css", 29 | "src/styles.css" 30 | ], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "fileReplacements": [ 36 | { 37 | "replace": "src/environments/environment.ts", 38 | "with": "src/environments/environment.prod.ts" 39 | } 40 | ], 41 | "optimization": true, 42 | "outputHashing": "all", 43 | "sourceMap": false, 44 | "extractCss": true, 45 | "namedChunks": false, 46 | "aot": true, 47 | "extractLicenses": true, 48 | "vendorChunk": false, 49 | "buildOptimizer": true, 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "2mb", 54 | "maximumError": "5mb" 55 | }, 56 | { 57 | "type": "anyComponentStyle", 58 | "maximumWarning": "6kb", 59 | "maximumError": "10kb" 60 | } 61 | ] 62 | } 63 | } 64 | }, 65 | "serve": { 66 | "builder": "@angular-devkit/build-angular:dev-server", 67 | "options": { 68 | "browserTarget": "devui-tutorial-web:build", 69 | "proxyConfig": "proxy.conf.js" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "devui-tutorial-web:build:production" 74 | } 75 | } 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n", 79 | "options": { 80 | "browserTarget": "devui-tutorial-web:build" 81 | } 82 | }, 83 | "test": { 84 | "builder": "@angular-devkit/build-angular:karma", 85 | "options": { 86 | "main": "src/test.ts", 87 | "polyfills": "src/polyfills.ts", 88 | "tsConfig": "tsconfig.spec.json", 89 | "karmaConfig": "karma.conf.js", 90 | "assets": [ 91 | "src/favicon.ico", 92 | "src/assets" 93 | ], 94 | "styles": [ 95 | "node_modules/@devui-design/icons/icomoon/devui-icon.css", 96 | "node_modules/ng-devui/devui.min.css", 97 | "src/styles.css" 98 | ], 99 | "scripts": [] 100 | } 101 | }, 102 | "lint": { 103 | "builder": "@angular-devkit/build-angular:tslint", 104 | "options": { 105 | "tsConfig": [ 106 | "tsconfig.app.json", 107 | "tsconfig.spec.json", 108 | "e2e/tsconfig.json" 109 | ], 110 | "exclude": [ 111 | "**/node_modules/**" 112 | ] 113 | } 114 | }, 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "e2e/protractor.conf.js", 119 | "devServerTarget": "devui-tutorial-web:serve" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "devServerTarget": "devui-tutorial-web:serve:production" 124 | } 125 | } 126 | } 127 | } 128 | }}, 129 | "defaultProject": "devui-tutorial-web" 130 | } 131 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('devui-tutorial-web app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 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-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/devui-tutorial-web'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devui-tutorial-web", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build --prod", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "postinstall": "npm install @babel/compat-data@7.8.0" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~8.2.14", 16 | "@angular/common": "~8.2.14", 17 | "@angular/compiler": "~8.2.14", 18 | "@angular/core": "~8.2.14", 19 | "@angular/forms": "~8.2.14", 20 | "@angular/platform-browser": "~8.2.14", 21 | "@angular/platform-browser-dynamic": "~8.2.14", 22 | "@angular/router": "~8.2.14", 23 | "@babel/compat-data": "^7.8.0", 24 | "@devui-design/icons": "^1.0.0", 25 | "codemirror": "^5.52.0", 26 | "echarts": "^4.6.0", 27 | "marked": "^0.8.0", 28 | "ng-devui": "^8.0.0-beta.1", 29 | "rxjs": "~6.4.0", 30 | "tslib": "^1.10.0", 31 | "zone.js": "~0.9.1" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "~0.803.25", 35 | "@angular/cli": "~8.3.25", 36 | "@angular/compiler-cli": "~8.2.14", 37 | "@angular/language-service": "~8.2.14", 38 | "@types/node": "~8.9.4", 39 | "@types/jasmine": "~3.3.8", 40 | "@types/jasminewd2": "~2.0.3", 41 | "codelyzer": "^5.0.0", 42 | "jasmine-core": "~3.4.0", 43 | "jasmine-spec-reporter": "~4.2.1", 44 | "karma": "~4.1.0", 45 | "karma-chrome-launcher": "~2.2.0", 46 | "karma-coverage-istanbul-reporter": "~2.0.1", 47 | "karma-jasmine": "~2.0.1", 48 | "karma-jasmine-html-reporter": "^1.4.0", 49 | "protractor": "~5.4.0", 50 | "ts-node": "~7.0.0", 51 | "tslint": "~5.15.0", 52 | "typescript": "~3.5.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /proxy.conf.js: -------------------------------------------------------------------------------- 1 | const PROXY_CONFIG = {}; 2 | 3 | PROXY_CONFIG['/api'] = { 4 | target: 'http://localhost:3000', 5 | pathRewrite: { 6 | '^/api': '' 7 | }, 8 | secure: false, 9 | changeOrigin: true 10 | }; 11 | 12 | module.exports = PROXY_CONFIG; 13 | -------------------------------------------------------------------------------- /src/app/admin/admin-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AdminComponent } from './admin.component'; 4 | import { AuthGuard } from '../login/auth.guard'; 5 | 6 | const routes: Routes = [{ 7 | path: '', 8 | component: AdminComponent, 9 | canActivate: [AuthGuard], 10 | children: [ 11 | { 12 | path: '', 13 | redirectTo: 'dashboard', 14 | pathMatch: 'full', 15 | }, 16 | { 17 | path: 'dashboard', 18 | loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) 19 | }, 20 | { 21 | path: 'articles', 22 | loadChildren: () => import('./article-management/article-management.module').then(m => m.ArticleManagementModule), 23 | data: { 24 | layout: {occupy: 'content'}, 25 | } 26 | }, 27 | { 28 | path: 'articles/new', 29 | loadChildren: () => import('./article-editor/article-editor.module').then(m => m.ArticleEditorModule), 30 | data: { 31 | layout: {occupy: 'main'}, 32 | } 33 | }, 34 | { 35 | path: 'articles/edit/:id', 36 | loadChildren: () => import('./article-editor/article-editor.module').then(m => m.ArticleEditorModule), 37 | data: { 38 | layout: {occupy: 'main'}, 39 | } 40 | }, 41 | ], 42 | }, 43 | { 44 | path: '**', 45 | redirectTo: '' 46 | }]; 47 | 48 | @NgModule({ 49 | imports: [RouterModule.forChild(routes)], 50 | exports: [RouterModule] 51 | }) 52 | export class AdminRoutingModule { } 53 | -------------------------------------------------------------------------------- /src/app/admin/admin-sidebar/admin-sidebar.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | height: 100%; 4 | background-color: #fff; 5 | } 6 | nav { 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | padding: 8px; 11 | } 12 | nav a { 13 | flex: 0 0 40px; 14 | line-height: 32px; 15 | font-size: 14px; 16 | border-radius: 2px; 17 | color: #404040; 18 | padding: 8px; 19 | } 20 | nav a:not(:first-of-type) { 21 | margin-top: 16px; 22 | } 23 | nav a:hover { 24 | background-color: rgba(94, 124, 224, 0.08); 25 | } 26 | nav a.active { 27 | color: #5E7CE0; 28 | background-color: rgba(94, 124, 224, 0.153); 29 | } 30 | 31 | nav a::before { 32 | display: inline-block; 33 | content: ''; 34 | width: 16px; 35 | height: 16px; 36 | margin-right: 8px; 37 | } 38 | -------------------------------------------------------------------------------- /src/app/admin/admin-sidebar/admin-sidebar.component.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminSidebarComponent } from './admin-sidebar.component'; 4 | 5 | describe('AdminSidebarComponent', () => { 6 | let component: AdminSidebarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AdminSidebarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AdminSidebarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/admin/admin-sidebar/admin-sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-admin-sidebar', 5 | templateUrl: './admin-sidebar.component.html', 6 | styleUrls: ['./admin-sidebar.component.css'] 7 | }) 8 | export class AdminSidebarComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/admin/admin.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | header { 6 | /* background-color: burlywood; */ 7 | flex: 0 0 60px; 8 | } 9 | main { 10 | flex: 0 0 calc(100vh - 60px); 11 | display: flex; 12 | align-items: stretch; 13 | } 14 | aside { 15 | /* background-color: pink; */ 16 | flex: 1 1 150px; 17 | } 18 | aside.hide { 19 | display: none; 20 | } 21 | .content { 22 | /* background-color: seagreen; */ 23 | flex: 11 1 500px; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/admin/admin.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 10 |
11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/app/admin/admin.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminComponent } from './admin.component'; 4 | 5 | describe('AdminComponent', () => { 6 | let component: AdminComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AdminComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AdminComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/admin/admin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { ActivatedRoute, Router, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { filter } from 'rxjs/operators'; 5 | @Component({ 6 | selector: 'app-admin', 7 | templateUrl: './admin.component.html', 8 | styleUrls: ['./admin.component.css'] 9 | }) 10 | export class AdminComponent implements OnInit, OnDestroy { 11 | layout; 12 | subscription: Subscription; 13 | constructor(private route: ActivatedRoute, private router: Router) { } 14 | 15 | ngOnInit() { 16 | this.layout = this.extraLayoutData(this.route.snapshot); 17 | this.subscription = this.router.events.pipe( 18 | filter(event => event instanceof NavigationEnd) 19 | ).subscribe(event => { 20 | this.layout = this.extraLayoutData(this.route.snapshot); 21 | }); 22 | } 23 | ngOnDestroy() { 24 | if (this.subscription) { 25 | this.subscription.unsubscribe(); 26 | } 27 | } 28 | extraLayoutData(routeSnap: ActivatedRouteSnapshot) { 29 | let layout; 30 | if (routeSnap.firstChild && routeSnap.firstChild.data && routeSnap.firstChild.data.layout) { 31 | layout = routeSnap.firstChild.data.layout; 32 | } else if (routeSnap.data && routeSnap.data.layout) { 33 | layout = routeSnap.data.layout; 34 | } 35 | return layout; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AdminRoutingModule } from './admin-routing.module'; 5 | import { AdminComponent } from './admin.component'; 6 | import { AdminSidebarComponent } from './admin-sidebar/admin-sidebar.component'; 7 | import { HeaderModule } from '../header/header.module'; 8 | 9 | 10 | @NgModule({ 11 | declarations: [AdminComponent, AdminSidebarComponent ], 12 | imports: [ 13 | CommonModule, 14 | AdminRoutingModule, 15 | HeaderModule 16 | ] 17 | }) 18 | export class AdminModule { } 19 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/article-editor.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { EditorComponent } from './editor.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { DevUIModule } from 'ng-devui'; 6 | import { MarkdownComponent } from './md/md.component'; 7 | import { FormsModule } from '@angular/forms'; 8 | 9 | 10 | 11 | @NgModule({ 12 | declarations: [ 13 | MarkdownComponent, 14 | EditorComponent 15 | ], 16 | imports: [ 17 | CommonModule, 18 | FormsModule, 19 | DevUIModule, 20 | RouterModule.forChild([ 21 | { 22 | path: '', 23 | component: EditorComponent 24 | } 25 | ]) 26 | ] 27 | }) 28 | export class ArticleEditorModule { } 29 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/editor.component.css: -------------------------------------------------------------------------------- 1 | .devui-article-editor{ 2 | background-color: #f8f8f8; 3 | } 4 | .devui-breadcrumbs { 5 | height: 36px; 6 | line-height: 36px; 7 | padding-left: 10px; 8 | width: 100%; 9 | background-color: #fff; 10 | margin-bottom: 10px; 11 | } 12 | .devui-title-input{ 13 | width: 100%; 14 | margin-top: 40px; 15 | line-height: 40px; 16 | font-size: 24px; 17 | font-weight: 500; 18 | color: #293040; 19 | border-width: 0 0 1px 0; 20 | outline: none; 21 | padding: 0 10px; 22 | } 23 | .devui-md-editor{ 24 | margin: 0 10px; 25 | background: #fff; 26 | box-shadow: 1px 0 2px 0 rgba(41,48,64,.1); 27 | } 28 | .devui-operators{ 29 | margin: 10px; 30 | } 31 | .devui-operators d-button{ 32 | margin-right: 20px; 33 | } 34 | .toolbar-container{ 35 | position: fixed; 36 | top: 110px; 37 | width: calc(100% - 20px); 38 | border: none !important; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/editor.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 8 |
9 |
10 | 取消 11 | 保存草稿 12 | 发布 13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EditorComponent } from './editor.component'; 4 | 5 | describe('EditorComponent', () => { 6 | let component: EditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ EditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(EditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { Location } from '@angular/common'; 5 | @Component({ 6 | selector: 'app-md-editor', 7 | templateUrl: './editor.component.html', 8 | styleUrls: ['./editor.component.css'], 9 | encapsulation: ViewEncapsulation.None 10 | }) 11 | export class EditorComponent implements OnInit { 12 | initContent: string; 13 | currentContent: string; 14 | title = ''; 15 | currentId = ''; 16 | createTime = ''; 17 | status = ''; 18 | showEditor = false; 19 | 20 | constructor(private http: HttpClient, private route: ActivatedRoute, private router: Router, private location: Location) { 21 | 22 | } 23 | ngOnInit() { 24 | this.route.params.subscribe(params => { 25 | this.currentId = params.id; 26 | if (this.currentId) { 27 | this.http.get('/api/articles/' + this.currentId).subscribe(res => { 28 | const article = res['article']; 29 | this.initContent = article['draftContent'] || article['content']; 30 | this.title = article['title']; 31 | this.createTime = article['createTime']; 32 | this.status = article['status']; 33 | this.showEditor = true; 34 | }); 35 | } else { 36 | this.showEditor = true; 37 | } 38 | }); 39 | } 40 | 41 | contentChange(data) { 42 | this.currentContent = data; 43 | } 44 | 45 | postDoc(draft = false) { 46 | if (this.currentId) { 47 | const article = { 48 | id: this.currentId, 49 | title: this.title, 50 | tags: ['DevUI', '前端'], 51 | content: draft ? this.initContent : this.currentContent, 52 | draftContent: draft ? this.currentContent : undefined, 53 | createTime: this.createTime, 54 | status: this.newStatus(draft) 55 | }; 56 | this.http.put('/api/articles', article).subscribe( res => { 57 | this.router.navigate(draft ? ['/admin/articles/draft'] : ['/admin/articles']); 58 | }); 59 | } else { 60 | const article = { 61 | title: this.title, 62 | tags: ['DevUI', '前端'], 63 | content: this.currentContent, 64 | status: this.newStatus(draft) 65 | }; 66 | this.http.post('/api/articles', article).subscribe( res => { 67 | this.router.navigate(draft ? ['/admin/articles/draft'] : ['/admin/articles']); 68 | }); 69 | } 70 | } 71 | 72 | newStatus(draft) { 73 | if (this.status === 'published') { 74 | return draft ? 'published-draft' : 'published'; 75 | } else { 76 | return draft ? 'draft' : 'published'; 77 | } 78 | } 79 | 80 | cancelPost() { 81 | // chrome默认状态下history有两条记录 82 | if (window.history.length > 2) { 83 | this.location.back(); 84 | } else { 85 | this.router.navigate(['/admin/articles']); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/md/md.component.css: -------------------------------------------------------------------------------- 1 | /*导入codemirror样式*/ 2 | @import "codemirror/lib/codemirror.css"; 3 | 4 | /*工具条样式*/ 5 | .devui-md-container .toolbar-container { 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | height: 40px; 10 | border-bottom: 1px solid #cacfd8; 11 | background-color: #fff; 12 | } 13 | .toolbar-container .splitter-line { 14 | height: 22px; 15 | width: 1px; 16 | background: #cacfd8; 17 | margin-right: 10px; 18 | } 19 | 20 | .toolbar-container .toolbar-item { 21 | width: 24px; 22 | height: 24px; 23 | margin-right: 10px; 24 | cursor: pointer; 25 | } 26 | .toolbar-container .toolbar-item:hover { 27 | background-color: #f3f7fe; 28 | } 29 | .toolbar-container .toolbar-item .icon { 30 | display: block; 31 | width: 16px; 32 | height: 24px; 33 | background-repeat: no-repeat; 34 | background-position: center center; 35 | background-size: contain; 36 | } 37 | .toolbar-item.bold .icon, .toolbar-item.italic .icon, .toolbar-item.underline .icon{ 38 | height: 16px; 39 | margin-top: 4px; 40 | } 41 | 42 | .toolbar-container .toolbar-item.undo .icon{background-image: url(../../../../assets/img/editor/icon_undo.png);} 43 | .toolbar-container .toolbar-item.redo .icon{background-image: url(../../../../assets/img/editor/icon_redo.png);} 44 | .toolbar-container .toolbar-item.bold .icon{background-image: url(../../../../assets/img/editor/icon_bold.png);} 45 | .toolbar-container .toolbar-item.italic .icon{background-image: url(../../../../assets/img/editor/icon_italic.png);} 46 | .toolbar-container .toolbar-item.underline .icon{background-image: url(../../../../assets/img/editor/icon_underline.png);} 47 | .toolbar-container .toolbar-item.ol .icon{background-image: url(../../../../assets/img/editor/icon_orderedlist.png);} 48 | .toolbar-container .toolbar-item.ul .icon{background-image: url(../../../../assets/img/editor/icon_unorderedlist.png);} 49 | .toolbar-container .toolbar-item.table .icon{background-image: url(../../../../assets/img/editor/icon_table.png);} 50 | .toolbar-item.text-icon { 51 | font-size: 16px; 52 | text-align: center; 53 | line-height: 24px; 54 | color: #000; 55 | font-weight: 600; 56 | } 57 | 58 | /*编辑器主容器*/ 59 | .devui-md-container .editor-container { 60 | display: flex; 61 | height: calc(100vh - 260px); 62 | } 63 | .editor-container .md-editor { 64 | width: 50%; 65 | flex-shrink: 0; 66 | } 67 | .editor-container .view-container { 68 | width: 50%; 69 | flex-shrink: 0; 70 | padding: 20px; 71 | border-left: 1px solid #cacfd8; 72 | overflow-y: auto; 73 | background-color: #fff; 74 | } 75 | 76 | .CodeMirror-gutter.CodeMirror-linenumbers{ 77 | background-color: #fff; 78 | } 79 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/md/md.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 |
8 | 9 |
10 |
11 |
12 | H1 13 |
14 |
15 | H2 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /src/app/admin/article-editor/md/md.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | Output, 7 | ViewChild, 8 | ViewEncapsulation 9 | } from '@angular/core'; 10 | import * as CodeMirror from 'codemirror'; 11 | import 'codemirror/mode/markdown/markdown.js'; 12 | import * as marked from 'marked'; 13 | import { fromEvent } from 'rxjs'; 14 | 15 | @Component({ 16 | selector: 'app-md', 17 | templateUrl: './md.component.html', 18 | styleUrls: ['./md.component.css'], 19 | encapsulation: ViewEncapsulation.None 20 | }) 21 | export class MarkdownComponent implements AfterViewInit { 22 | @Input() content: string; 23 | 24 | @Output() contentChange = new EventEmitter(); 25 | 26 | @ViewChild('mdEditor', { static: true }) mdEditor; 27 | @ViewChild('previewContainer', { static: true }) previewContainer; 28 | 29 | // codemirror对象 30 | public cm; 31 | // 标记当前滚动容器 32 | cScroll = false; 33 | pScroll = false; 34 | 35 | /*ToolBar*/ 36 | public toolbarHandler = { 37 | /*撤销*/ 38 | undo: () => { 39 | this.cm.undo(); 40 | }, 41 | 42 | /*重做*/ 43 | redo: () => { 44 | this.cm.redo(); 45 | }, 46 | 47 | /*粗体*/ 48 | bold: () => { 49 | this.insertFontStyle('**'); 50 | }, 51 | 52 | /*斜体*/ 53 | italic: () => { 54 | this.insertFontStyle('*'); 55 | }, 56 | 57 | /*一级标题*/ 58 | h1: () => { 59 | this.insertParagraph('#'); 60 | }, 61 | 62 | /*二级标题*/ 63 | h2: () => { 64 | this.insertParagraph('##'); 65 | }, 66 | 67 | /*无序列表*/ 68 | ul: () => { 69 | const selection = this.cm.getSelection(); 70 | this.cm.focus(); 71 | if (selection === '') { 72 | this.cm.replaceSelection('- ' + selection); 73 | } else { 74 | const selectionText = selection.split('\n'); 75 | 76 | for (let i = 0, len = selectionText.length; i < len; i++) { 77 | selectionText[i] = 78 | selectionText[i] === '' ? '' : '- ' + selectionText[i]; 79 | } 80 | 81 | this.cm.replaceSelection(selectionText.join('\n')); 82 | } 83 | }, 84 | 85 | /*有序列表*/ 86 | ol: () => { 87 | // const cursor = this.cm.getCursor(); 88 | const selection = this.cm.getSelection(); 89 | this.cm.focus(); 90 | if (selection === '') { 91 | this.cm.replaceSelection('1. ' + selection); 92 | } else { 93 | const selectionText = selection.split('\n'); 94 | 95 | for (let i = 0, len = selectionText.length; i < len; i++) { 96 | selectionText[i] = 97 | selectionText[i] === '' ? '' : i + 1 + '. ' + selectionText[i]; 98 | } 99 | 100 | this.cm.replaceSelection(selectionText.join('\n')); 101 | } 102 | }, 103 | // 下划线 104 | underline: () => { 105 | const cursor = this.cm.getCursor(); 106 | const selection = this.cm.getSelection(); 107 | this.cm.focus(); 108 | this.cm.replaceSelection('' + selection + ''); 109 | if (selection === '') { 110 | this.cm.setCursor(cursor.line, cursor.ch + 5); 111 | } 112 | }, 113 | /* 表格 */ 114 | table: () => { 115 | const table = '| | | |' + '\n' + '|--|--|--|' + '\n' + '| | | |'; 116 | this.cm.replaceSelection(table); 117 | } 118 | }; 119 | 120 | insertParagraph(type) { 121 | const cursor = this.cm.getCursor(); 122 | const selection = this.cm.getSelection(); 123 | this.cm.focus(); 124 | if (cursor.ch !== 0) { 125 | this.cm.setCursor(cursor.line, 0); 126 | this.cm.replaceSelection(type + ' ' + selection); 127 | this.cm.setCursor(cursor.line, cursor.ch + type === '#' ? 2 : 3); 128 | } else { 129 | this.cm.replaceSelection(type + ' ' + selection); 130 | } 131 | } 132 | 133 | insertFontStyle(type) { 134 | const cursor = this.cm.getCursor(); 135 | const selection = this.cm.getSelection(); 136 | 137 | this.cm.replaceSelection(type + selection + type); 138 | this.cm.focus(); 139 | if (selection === '') { 140 | this.cm.setCursor(cursor.line, cursor.ch + type === '*' ? 1 : 2); 141 | } 142 | } 143 | 144 | constructor() {} 145 | 146 | ngAfterViewInit() { 147 | const myTextarea = this.mdEditor.nativeElement; 148 | this.cm = CodeMirror.fromTextArea(myTextarea, { 149 | mode: 'markdown', 150 | lineNumbers: true, 151 | lineWrapping: true 152 | }); 153 | this.cm.setSize('auto', '100%'); 154 | this.cm.focus(); 155 | 156 | /*绑定变化事件*/ 157 | this.cm.on('change', () => { 158 | /*内容上传*/ 159 | this.content = this.cm.getValue(); 160 | this.contentChange.emit(this.content); 161 | let timer = setTimeout(() => { 162 | clearTimeout(timer); 163 | this.saveToHTML(); 164 | timer = null; 165 | }, 300); 166 | }); 167 | this.cm.on('scroll', instance => { 168 | if (this.cScroll) { 169 | this.cScroll = false; 170 | return; 171 | } 172 | this.pScroll = true; 173 | this.editorScroll(instance); 174 | }); 175 | 176 | // 立刻执行一次emit content 177 | setTimeout(() => { 178 | this.contentChange.emit(this.cm.getValue()); 179 | this.saveToHTML(); 180 | const preview = this.previewContainer.nativeElement; 181 | // preview初始化绑定scroll事件 182 | fromEvent(preview, 'scroll').subscribe(e => { 183 | if (this.pScroll) { 184 | this.pScroll = false; 185 | return; 186 | } 187 | this.previewScroll(preview); 188 | }); 189 | }, 100); 190 | } 191 | 192 | /*获取编辑器内容并显示*/ 193 | saveToHTML() { 194 | const cmValue = this.cm.getValue(); 195 | const htmlContent = marked(cmValue, { 196 | breaks: true 197 | }); 198 | this.previewContainer.nativeElement.innerHTML = htmlContent; 199 | } 200 | 201 | // 处理editor滚动条 202 | editorScroll(instance) { 203 | const scrollInfo = this.cm.getScrollInfo(); 204 | const height = scrollInfo.height - scrollInfo.clientHeight; 205 | const ratio = parseFloat(scrollInfo.top) / height; 206 | const preview = this.previewContainer.nativeElement; 207 | const move = (preview.scrollHeight - preview.clientHeight) * ratio; 208 | preview.scrollTop = move; 209 | } 210 | 211 | // 处理preview滚动条 212 | previewScroll(preview) { 213 | this.cScroll = true; 214 | const height = preview.scrollHeight - preview.clientHeight; 215 | const ratio = parseFloat(preview.scrollTop) / height; 216 | const move = 217 | (this.cm.getScrollInfo().height - this.cm.getScrollInfo().clientHeight) * 218 | ratio; 219 | this.cm.scrollTo(0, move); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-filter-panel/article-filter-panel.component.css: -------------------------------------------------------------------------------- 1 | .form-item { 2 | display: flex; 3 | line-height: 32px; 4 | } 5 | .form-label { 6 | flex: 0 1 80px; 7 | } 8 | .form-control { 9 | flex: 0 1 calc( 100% - 120px); 10 | } 11 | .form-item:not(:first-child) { 12 | margin-top: 8px; 13 | } 14 | .datepicker { 15 | display: inline-block; 16 | } 17 | 18 | d-button, 19 | .datepicker:not(:first-child) { 20 | margin-left: 8px; 21 | } 22 | d-select { 23 | width: 240px; 24 | } 25 | .reset-date{ 26 | vertical-align: top; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-filter-panel/article-filter-panel.component.html: -------------------------------------------------------------------------------- 1 |
2 |
标签
3 |
4 | 13 | 14 |
15 |
16 |
17 |
日期
18 |
19 |
20 |
21 | 32 |
36 | 37 |
38 |
39 |
40 |
41 |
42 | 53 |
57 | 58 |
59 |
60 |
61 |
62 |
63 | 68 | 过滤 69 | 70 | 75 | 重置 76 | 77 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-filter-panel/article-filter-panel.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleFilterPanelComponent } from './article-filter-panel.component'; 4 | 5 | describe('ArticleFilterPanelComponent', () => { 6 | let component: ArticleFilterPanelComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleFilterPanelComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleFilterPanelComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-filter-panel/article-filter-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-article-filter-panel', 5 | templateUrl: './article-filter-panel.component.html', 6 | styleUrls: ['./article-filter-panel.component.css'] 7 | }) 8 | export class ArticleFilterPanelComponent implements OnInit { 9 | tagList = ['DevUI', '前端']; 10 | filterOption = { 11 | tags: [], 12 | date: [null, null] 13 | }; 14 | startDate = null; 15 | endDate = null; 16 | @Output() condition = new EventEmitter(); 17 | constructor() { } 18 | 19 | ngOnInit() { 20 | } 21 | updateCondition() { 22 | this.condition.emit(this.filterOption); 23 | } 24 | handleStartDate(event: Date) { 25 | this.filterOption.date[0] = +event; 26 | } 27 | handleEndDate(event: Date) { 28 | this.filterOption.date[1] = +(event.setDate(event.getDate() + 1)); 29 | } 30 | reset() { 31 | this.startDate = null; 32 | this.endDate = null; 33 | this.filterOption = { 34 | tags: [], 35 | date: [null, null] 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-list/article-list.component.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 16px 0; 5 | } 6 | .search-area > *, 7 | .operation-area > * { 8 | display: inline-block; 9 | } 10 | .search-area > *:not(:first-child), 11 | .operation-area > *:not(:first-child) { 12 | margin-left: 16px; 13 | } 14 | 15 | .operation-area ::ng-deep .devui-pagination .devui-pagination-list { 16 | margin: 0!important; 17 | } 18 | 19 | .single-pagination { 20 | text-align: right; 21 | } 22 | .filter-penal { 23 | background-color: #F5F5F5; 24 | padding: 16px; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-list/article-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 高级过滤 5 |
6 |
7 | 写文章 8 |
9 | 10 | 更多操作 11 | 12 | 13 | 16 |
17 |
(总条数:{{total}})
18 | 27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 | 35 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | {{cellItem| date:'yyyy-MM-dd HH:mm:ss'}} 55 | 56 | 57 | 58 | 59 | 60 | 61 | {{cellItem| date:'yyyy-MM-dd HH:mm:ss' }} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 | 77 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-list/article-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleListComponent } from './article-list.component'; 4 | 5 | describe('ArticleListComponent', () => { 6 | let component: ArticleListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-list/article-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; 2 | import { Router, ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { DialogService, DataTableComponent } from 'ng-devui'; 6 | 7 | @Component({ 8 | selector: 'app-article-list', 9 | templateUrl: './article-list.component.html', 10 | styleUrls: ['./article-list.component.css'] 11 | }) 12 | export class ArticleListComponent implements OnInit , OnDestroy { 13 | @ViewChild(DataTableComponent, {static: false}) dataTable: DataTableComponent; 14 | showFilterPanel = false; 15 | subscription: Subscription; 16 | activeListType: string; 17 | pageIndex = 1; 18 | pageSize = 10; 19 | total = 0; 20 | articleList = []; 21 | filterOption; 22 | keyword = ''; 23 | checkRows; 24 | constructor(private route: ActivatedRoute, private router: Router, private http: HttpClient, private dialogService: DialogService) { } 25 | 26 | 27 | 28 | ngOnInit() { 29 | this.activeListType = this.getActiveListType(this.route.snapshot); 30 | this.getData(); 31 | 32 | } 33 | getData() { 34 | const condition = Object.assign({keyword: this.keyword}, this.showFilterPanel ? this.filterOption : {}); 35 | this.http.post(`/api/articles/query/${this.activeListType}`, condition , {params: { 36 | page: this.pageIndex + '', 37 | size: this.pageSize + '' 38 | }} ).subscribe(res => { 39 | this.articleList = res['articles']; 40 | this.total = res['total']; 41 | }); 42 | } 43 | ngOnDestroy() { 44 | if (this.subscription) { 45 | this.subscription.unsubscribe(); 46 | } 47 | } 48 | toggleFilterPenal() { 49 | this.showFilterPanel = !this.showFilterPanel; 50 | } 51 | navigateTo(url) { 52 | this.router.navigateByUrl(url); 53 | } 54 | getActiveListType(routeSnap: ActivatedRouteSnapshot) { 55 | let activeListType = 'published'; 56 | if (routeSnap.url) { 57 | activeListType = routeSnap.url && routeSnap.url[0].path; 58 | } 59 | return activeListType; 60 | } 61 | pageIndexChange(event) { 62 | this.pageIndex = event; 63 | this.getData(); 64 | } 65 | pageSizeChange(event) { 66 | this.pageIndex = 1; 67 | this.pageSize = event; 68 | this.getData(); 69 | } 70 | 71 | view(id) { 72 | this.navigateTo('/articles/' + id); 73 | } 74 | edit(id) { 75 | this.navigateTo('/admin/articles/edit/' + id); 76 | } 77 | publish(id) { 78 | this.moveToPublished([id]); 79 | } 80 | 81 | 82 | 83 | moveToTrash = (ids, callback?) => { 84 | this.http.put(`/api/articles/trash`, {ids}).subscribe( 85 | res => { 86 | if (callback) {callback(); } 87 | } 88 | ); 89 | } 90 | moveToPublished = (ids, callback?) => { 91 | this.http.put(`/api/articles/published`, {ids}).subscribe(res => { 92 | if (callback) {callback(); } 93 | }); 94 | } 95 | deleteArticleMany = (ids, callback?) => { 96 | this.http.post(`/api/articles/batchDelete`, {ids}).subscribe( 97 | res => { 98 | if (callback) {callback(); } 99 | } 100 | ); 101 | } 102 | 103 | deleteArticle = (ids, callback?) => { 104 | const id = ids[0]; 105 | this.http.delete(`/api/articles/${id}`).subscribe( 106 | res => { 107 | if (callback) {callback(); } 108 | } 109 | ); 110 | } 111 | onSearch() { 112 | this.getData(); 113 | } 114 | onFilter(event) { 115 | this.filterOption = event; 116 | this.getData(); 117 | } 118 | 119 | delete(ids, batch= false) { 120 | let tips; 121 | let deleteArticle; 122 | switch (this.activeListType) { 123 | case 'trash': 124 | tips = '确认是否永久删除文章'; 125 | deleteArticle = this.deleteArticle; 126 | if (batch) { 127 | deleteArticle = this.deleteArticleMany; 128 | } 129 | break; 130 | case 'published': 131 | case 'draft': 132 | tips = '确认是否删除文章'; 133 | deleteArticle = this.moveToTrash; 134 | break; 135 | default: 136 | } 137 | 138 | const results = this.dialogService.open({ 139 | id: 'dialog-service', 140 | width: '400px', 141 | maxHeight: '600px', 142 | showAnimate: false, 143 | title: '', 144 | html: true, 145 | content: `
${tips}
`, 146 | backdropCloseable: true, 147 | dialogtype: 'warning', 148 | buttons: [ 149 | { 150 | cssClass: 'stress', 151 | text: '删除', 152 | handler: ($event: Event) => { 153 | results.modalInstance.hide(); 154 | deleteArticle(ids, () => { 155 | this.getData(); 156 | }); 157 | }, 158 | }, 159 | { 160 | id: 'btn-cancel', 161 | cssClass: 'common', 162 | text: '取消', 163 | handler: ($event: Event) => { 164 | results.modalInstance.hide(); 165 | } 166 | }], 167 | }); 168 | } 169 | batchDelete() { 170 | this.checkRows = this.dataTable.getCheckedRows(); 171 | if (!this.checkRows || !this.checkRows.length) { return; } 172 | const ids = this.checkRows.map(row => row.id); 173 | this.delete(ids, true); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-management-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ArticleManagementComponent } from './article-management.component'; 4 | import { ArticleListComponent } from './article-list/article-list.component'; 5 | 6 | 7 | const routes: Routes = [{ 8 | path: '', 9 | component: ArticleManagementComponent, 10 | children: [ 11 | { 12 | path: '', 13 | redirectTo: 'published', 14 | }, 15 | { 16 | path: 'published', 17 | component: ArticleListComponent, 18 | }, 19 | { 20 | path: 'draft', 21 | component: ArticleListComponent, 22 | }, 23 | { 24 | path: 'trash', 25 | component: ArticleListComponent, 26 | } 27 | ] 28 | }]; 29 | 30 | @NgModule({ 31 | imports: [RouterModule.forChild(routes)], 32 | exports: [RouterModule] 33 | }) 34 | export class ArticleManagementRoutingModule { } 35 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-management.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | min-height: calc(100% - 32px); 4 | margin: 16px 16px 0 16px; 5 | padding: 16px; 6 | background-color: #fff; 7 | overflow-x: auto; 8 | max-width: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-management.component.html: -------------------------------------------------------------------------------- 1 |

文章列表页

2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-management.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleManagementComponent } from './article-management.component'; 4 | 5 | describe('ArticleManagementComponent', () => { 6 | let component: ArticleManagementComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleManagementComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleManagementComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-management.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, ActivatedRoute, Router, NavigationEnd } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { filter } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-article-management', 8 | templateUrl: './article-management.component.html', 9 | styleUrls: ['./article-management.component.css'] 10 | }) 11 | export class ArticleManagementComponent implements OnInit, OnDestroy { 12 | activeTab; 13 | subscription: Subscription; 14 | constructor(private route: ActivatedRoute, private router: Router) { 15 | 16 | } 17 | 18 | ngOnInit() { 19 | this.activeTab = this.getActiveTab(this.route.snapshot); 20 | this.subscription = this.router.events.pipe( 21 | filter(event => event instanceof NavigationEnd) 22 | ).subscribe(event => { 23 | this.activeTab = this.getActiveTab(this.route.snapshot); 24 | }); 25 | } 26 | ngOnDestroy() { 27 | if (this.subscription) { 28 | this.subscription.unsubscribe(); 29 | } 30 | } 31 | getActiveTab(routeSnap: ActivatedRouteSnapshot) { 32 | let activeTab; 33 | if (routeSnap.firstChild && routeSnap.firstChild.url) { 34 | activeTab = routeSnap.firstChild.url && routeSnap.firstChild.url[0].path; 35 | } 36 | return activeTab; 37 | } 38 | activeTabChange(event) { 39 | this.router.navigate(['../', event], {relativeTo: this.route.firstChild} ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/admin/article-management/article-management.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ArticleManagementRoutingModule } from './article-management-routing.module'; 5 | import { ArticleManagementComponent } from './article-management.component'; 6 | import { DevUIModule } from 'ng-devui'; 7 | import { ArticleListComponent } from './article-list/article-list.component'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { ArticleFilterPanelComponent } from './article-filter-panel/article-filter-panel.component'; 10 | 11 | 12 | @NgModule({ 13 | declarations: [ArticleManagementComponent, ArticleListComponent, ArticleFilterPanelComponent], 14 | imports: [ 15 | CommonModule, 16 | ArticleManagementRoutingModule, 17 | FormsModule, 18 | DevUIModule, 19 | ], 20 | entryComponents: [ArticleListComponent] 21 | }) 22 | export class ArticleManagementModule { } 23 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | 6 | const routes: Routes = [{ 7 | path: '', 8 | component: DashboardComponent 9 | }]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class DashboardRoutingModule { } 16 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | .dashboard-container { 7 | height: 100%; 8 | margin: 16px; 9 | } 10 | 11 | .dashboard-card-container { 12 | height: 144px; 13 | margin-bottom: 16px; 14 | display: flex; 15 | flex-direction: row; 16 | } 17 | 18 | .author-list-container .nav-tabs { 19 | height: 52px; 20 | padding-left: 16px; 21 | } 22 | 23 | .author-list-container .nav-tabs li { 24 | height: 100%; 25 | padding-top: 15px; 26 | padding-left: 16px; 27 | } 28 | 29 | .tab-content { 30 | margin-left: 16px; 31 | } 32 | 33 | .tab-content li { 34 | height: 34px; 35 | margin-bottom: 20px; 36 | display: flex; 37 | align-items: center; 38 | justify-content: flex-start; 39 | } 40 | 41 | .author-name{ 42 | margin-left: 16px; 43 | } 44 | .dashboard-article-content{ 45 | margin-left: 100px; 46 | color: #797979; 47 | } 48 | 49 | .page-card-count, 50 | .read-card-count, 51 | .like-card-count, 52 | .collcet-card-count { 53 | flex-grow: 1; 54 | margin-right: 16px; 55 | background: #fff; 56 | width: 296px; 57 | box-shadow: 0px 2px 2px 0 rgba(41, 48, 64, 0.2); 58 | } 59 | 60 | .title { 61 | margin: 20px; 62 | font-size: 14px; 63 | } 64 | 65 | .dashboard-trend-container { 66 | height: 336px; 67 | margin-bottom: 16px; 68 | box-shadow: 0px 2px 2px 0 rgba(41, 48, 64, 0.2); 69 | background: #fff; 70 | margin-right: 16px; 71 | } 72 | 73 | .data-container { 74 | padding-left: 20px; 75 | display: flex; 76 | flex-direction: row; 77 | height: 40px; 78 | border-bottom: 1px solid #E5E5E5; 79 | justify-content: space-between; 80 | } 81 | 82 | .dashboard-trend-charts { 83 | height: 284px; 84 | width: 100%; 85 | padding-left: 20px; 86 | padding-top: 20px; 87 | padding-bottom: 20px; 88 | } 89 | 90 | .trend-time-field { 91 | display: flex; 92 | margin-right: 118px; 93 | } 94 | 95 | .time-picker-divider { 96 | margin-left: 9px; 97 | margin-top: 5px; 98 | } 99 | 100 | /* .time-date-picker { 101 | width:150px; margin-left: 10px; height: 30px; 102 | } */ 103 | 104 | .data-trend-container { 105 | height: 45px; 106 | display: flex; 107 | flex-direction: row; 108 | align-items: center; 109 | padding-left: 20px; 110 | } 111 | 112 | .data-trend-container>.label { 113 | color: #ACACAC; 114 | margin-right: 10px; 115 | } 116 | 117 | .data-trend-container>.data { 118 | color: #000; 119 | margin-right: 10px; 120 | } 121 | 122 | .data-trend-triangle-up { 123 | width: 0; 124 | height: 0; 125 | border: 4px solid transparent; 126 | border-bottom: 5px solid #d16b76; 127 | margin-top: -5px; 128 | margin-left: -5px; 129 | margin-right: 20px; 130 | } 131 | 132 | 133 | .data-trend-triangle-down { 134 | width: 0; 135 | height: 0; 136 | border: 4px solid transparent; 137 | border-top: 5px solid #7FBF45; 138 | margin-bottom: -6px; 139 | margin-left: -5px; 140 | margin-right: 20px; 141 | } 142 | 143 | .card-icon { 144 | margin-right: 33px; 145 | width: 13px; 146 | height: 16px; 147 | margin-top: 5px; 148 | } 149 | 150 | .card-view-icon { 151 | margin-right: 33px; 152 | width: 16px; 153 | height: 12px; 154 | margin-top: 5px; 155 | } 156 | 157 | .card-like-icon { 158 | margin-right: 33px; 159 | width: 16px; 160 | height: 16px; 161 | margin-top: 5px; 162 | } 163 | 164 | .big-data { 165 | font-size: 36px; 166 | font-weight: 600; 167 | } 168 | 169 | .dashboard-trend-title { 170 | height: 52px; 171 | border-bottom: 1px solid #E5E5E5; 172 | display: flex; 173 | flex-direction: row; 174 | align-items: center; 175 | padding-left: 20px; 176 | justify-content: space-between; 177 | } 178 | 179 | .dashboard-pie-container { 180 | height: 356px; 181 | margin-bottom: 16px; 182 | margin-right: 16px; 183 | display: flex; 184 | } 185 | 186 | .fans-age-container { 187 | box-shadow: 0px 2px 2px 0 rgba(41, 48, 64, 0.2); 188 | background: #fff; 189 | flex-grow: 1; 190 | margin-right: 16px; 191 | width: 608px; 192 | 193 | } 194 | 195 | .author-list-container { 196 | flex-grow: 1; 197 | box-shadow: 0px 2px 2px 0 rgba(41, 48, 64, 0.2); 198 | background: #fff; 199 | width: 608px; 200 | } 201 | 202 | 203 | 204 | .fans-age-charts { 205 | height: 302px; 206 | padding-right: 50px; 207 | /* margin-left: -20px; */ 208 | } -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

已发布文章

5 |
6 |

{{cards[0]?.count}}

7 | 8 |
9 |
10 | 周同比 11 | {{cards[0]?.week}} 12 | 13 | 日同比 14 | {{cards[0]?.day}} 15 | 16 |
17 |
18 |
19 |

总浏览量

20 |
21 |

{{cards[1]?.count}}

22 | 23 |
24 |
25 | 周同比 26 | {{cards[1]?.week}} 27 | 28 | 日同比 29 | {{cards[1]?.day}} 30 | 31 |
32 |
33 | 48 |
49 |

总收藏量

50 |
51 |

{{cards[3]?.count}}

52 | 53 |
54 |
55 | 周同比 56 | {{cards[3]?.week}} 57 | 58 | 日同比 59 | {{cards[3]?.day}} 60 | 61 |
62 |
63 |
64 |
65 |
66 | 粉丝人数趋势图 67 |
68 | 70 | 71 |
72 |
77 |
81 |
82 |
83 | 84 |
85 |
86 | 87 |
88 |
89 |
90 |
91 |
92 | 粉丝年龄段分析 93 |
94 |
95 |
96 |
97 | 98 | 99 | 100 |
    101 |
  • 102 | 103 |

    {{author.name}}

    104 |

    {{author.title}}

    105 | 106 |
  • 107 |
108 |
109 |
110 | 111 | 112 |
    113 |
  • 114 |

    {{column.name}}

    115 |

    {{column.title}}

    116 | 117 |
  • 118 |
119 |
120 |
121 |
122 |
123 | 124 |
-------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, AfterViewInit, ViewEncapsulation} from '@angular/core'; 2 | import { DashboardService } from './dashboard.service'; 3 | import * as echarts from 'echarts'; 4 | import { trendOption, data , fansOption } from './dashboard.options'; 5 | 6 | @Component({ 7 | selector: 'app-dashboard', 8 | templateUrl: './dashboard.component.html', 9 | encapsulation: ViewEncapsulation.None, 10 | styleUrls: ['./dashboard.component.css'] 11 | }) 12 | export class DashboardComponent implements OnInit, AfterViewInit { 13 | 14 | constructor(private dashboard: DashboardService) { } 15 | cards: Array = []; 16 | authors: any; 17 | columns: any; 18 | selectedStartDate = null; 19 | selectedEndDate = null; 20 | datePicker1: any; 21 | datePicker2: any; 22 | dateFormated = 'YYYY-MM-DD'; 23 | dateFormatOptions = [{ 24 | name: '今天', 25 | value: 'day' 26 | }, 27 | { 28 | name: '本周', 29 | value: 'week' 30 | }, 31 | { 32 | name: '本年', 33 | value: 'year' 34 | }]; 35 | dateFormat: object = { 36 | name: '今天', 37 | value: 'day' 38 | }; 39 | ngOnInit() { 40 | this.dashboard.getDashboard().subscribe( (res: any) => { 41 | this.cards = res.cards; 42 | this.authors = res.authors; 43 | this.columns = res.columns; 44 | }); 45 | } 46 | 47 | mergeOptions(optionData) { 48 | trendOption.xAxis['data'] = optionData.x; 49 | trendOption.series[0]['data'] = optionData.y; 50 | return trendOption; 51 | } 52 | 53 | selectChange() { 54 | if (this.dateFormat && data[this.dateFormat['value']]) { 55 | const mergedOptions = this.mergeOptions(data[this.dateFormat['value']]); 56 | this.initTrendChart(mergedOptions); 57 | } 58 | } 59 | 60 | ngAfterViewInit() { 61 | this.initTrendChart(trendOption); 62 | this.initFansAgeChart(); 63 | } 64 | 65 | initTrendChart(options) { 66 | const trendChartArea = document.querySelector('#trend-charts'); 67 | const trendChart = echarts.init(trendChartArea); 68 | trendChart.setOption(options); 69 | } 70 | 71 | initFansAgeChart() { 72 | const fansChartArea = document.querySelector('#fans-age'); 73 | const fansChart = echarts.init(fansChartArea); 74 | fansChart.setOption(fansOption); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { DashboardRoutingModule } from './dashboard-routing.module'; 5 | import { DashboardComponent } from './dashboard.component'; 6 | import { DashboardService } from './dashboard.service'; 7 | import { DevUIModule } from 'ng-devui'; 8 | 9 | @NgModule({ 10 | declarations: [DashboardComponent], 11 | providers: [ 12 | DashboardService 13 | ], 14 | imports: [ 15 | CommonModule, 16 | DashboardRoutingModule, 17 | FormsModule, 18 | DevUIModule 19 | ] 20 | }) 21 | export class DashboardModule { } 22 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.options.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts'; 2 | export const trendOption = { 3 | title: { 4 | text: '浏览人数', 5 | textStyle: { 6 | fontSize: '18px' 7 | } 8 | }, 9 | tooltip: { 10 | trigger: 'axis' 11 | }, 12 | grid: { 13 | left: '3%', 14 | right: '4%', 15 | bottom: '3%', 16 | containLabel: true 17 | }, 18 | xAxis: [ 19 | { 20 | type: 'category', 21 | boundaryGap: false, 22 | data: ['2020-02-01', '2020-02-06', '2020-02-11', '2020-02-16', '2020-02-21', '2020-02-26', '2020-03-01'] 23 | } 24 | ], 25 | yAxis: [ 26 | { 27 | type: 'value', 28 | splitLine: {show: false} 29 | } 30 | ], 31 | series: [ 32 | { 33 | name: '粉丝数目', 34 | type: 'line', 35 | stack: '总量', 36 | smooth: true, 37 | areaStyle: { 38 | normal: { 39 | color: new echarts.graphic.LinearGradient( 40 | 0, 0, 0, 1, 41 | [ 42 | { offset: 0, color: '#7C95E7' }, 43 | { offset: 0.5, color: '#7C95E7' }, 44 | { offset: 1, color: '#fff' } 45 | ] 46 | ) 47 | } 48 | }, 49 | itemStyle: { 50 | normal: { 51 | color: '#919FCC', 52 | lineStyle: { 53 | color: '#919FCC' 54 | } 55 | } 56 | }, 57 | 58 | data: [0, 472, 101, 920, 569, 200, 748] 59 | } 60 | ] 61 | }; 62 | 63 | export const data = { 64 | week: { 65 | x: ['2020-02-01', '2020-02-06', '2020-02-11', '2020-02-16', '2020-02-21', '2020-02-26', '2020-03-01'], 66 | y: [0, 472, 101, 920, 569, 200, 748] 67 | }, 68 | year: { 69 | x: ['2014', '2015', '2016', '2017', '2018', '2019', '2020'], 70 | y: [5000, 12000, 8900, 23456, 34567, 32000, 21000] 71 | }, 72 | day: { 73 | x: ['2020-02-01', '2020-02-02', '2020-02-03', '2020-02-04', '2020-02-05', '2020-02-06', '2020-02-07'], 74 | y: [100, 120, 230, 251, 234, 121, 530] 75 | } 76 | }; 77 | 78 | export const fansOption = { 79 | tooltip: { 80 | trigger: 'item', 81 | formatter: '{a}
{b}: {c} ({d}%)' 82 | }, 83 | legend: { 84 | orient: 'vertical', 85 | left: 'right', 86 | top: 'middle', 87 | data: ['60后', '70后', '80后', '90后'] 88 | }, 89 | color: [ '#F8B98E', '#ACB5EE', '#B9E986', '#F5AFB7'], 90 | series: [ 91 | { 92 | name: '粉丝占比', 93 | type: 'pie', 94 | radius: ['50%', '70%'], 95 | avoidLabelOverlap: false, 96 | label: { 97 | position: '', 98 | formatter: '{c}人 占比{d}%,', 99 | color: '#4C4C4C', 100 | fontSize: '14' 101 | }, 102 | labelLine: { 103 | normal: { 104 | show: true 105 | } 106 | }, 107 | data: [ 108 | {value: 369, name: '60后'}, 109 | {value: 609, name: '70后'}, 110 | {value: 807, name: '80后'}, 111 | {value: 1218, name: '90后'} 112 | ] 113 | } 114 | ] 115 | }; 116 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardService } from './dashboard.service'; 4 | 5 | describe('DashboardService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: DashboardService = TestBed.get(DashboardService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/admin/dashboard/dashboard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | 4 | 5 | 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class DashboardService { 11 | headers: HttpHeaders; 12 | constructor(private http: HttpClient) { 13 | const accessToken = localStorage.getItem('accessToken'); 14 | this.headers = new HttpHeaders({}); 15 | this.headers = this.headers.set('Authorization', accessToken ? accessToken : 'empty'); 16 | } 17 | 18 | getCards() { 19 | return this.http.get('/api/dashboard/cards', { headers: this.headers }); 20 | } 21 | 22 | getAuthors() { 23 | return this.http.get('/api/dashboard/authors', { headers: this.headers }); 24 | } 25 | 26 | getDashboard() { 27 | return this.http.get('/api/dashboard', { headers: this.headers }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { AppComponent } from './app.component'; 4 | 5 | 6 | const routes: Routes = [ { 7 | path: '', 8 | component: AppComponent, 9 | children: [ 10 | { 11 | path: '', 12 | loadChildren: () => import('./view/view.module').then(m => m.ViewModule) 13 | }, 14 | { 15 | path: 'admin', 16 | loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) 17 | }, 18 | { 19 | path: 'login', 20 | loadChildren: () => import('./login/login.module').then(m => m.LoginModule) 21 | } 22 | ] 23 | 24 | }, 25 | { 26 | path: '**', 27 | redirectTo: '' 28 | } 29 | ]; 30 | 31 | @NgModule({ 32 | imports: [RouterModule.forRoot(routes)], 33 | exports: [RouterModule] 34 | }) 35 | export class AppRoutingModule { } 36 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .main-content { 2 | flex-grow: 1; 3 | } -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'devui-tutorial-web'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('devui-tutorial-web'); 23 | }); 24 | 25 | it('should render title', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('.content span').textContent).toContain('devui-tutorial-web app is running!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'devui-tutorial-web'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { DevUIModule } from 'ng-devui'; 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { BrowserAnimationsModule} from '@angular/platform-browser/animations'; 7 | import { HttpClientModule } from '@angular/common/http'; 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | BrowserModule, 14 | DevUIModule, 15 | BrowserAnimationsModule, 16 | HttpClientModule, 17 | AppRoutingModule, 18 | BrowserAnimationsModule 19 | ], 20 | providers: [], 21 | bootstrap: [AppComponent] 22 | }) 23 | export class AppModule { } 24 | -------------------------------------------------------------------------------- /src/app/header/header.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | position: fixed; 4 | z-index: 1000; 5 | display: flex; 6 | justify-content: space-between; 7 | background-color: #333954; 8 | color: #ffffff; 9 | height: 60px; 10 | line-height: 28px; 11 | padding: 16px; 12 | } 13 | .logo { 14 | font-size: 20px; 15 | } 16 | .user-center { 17 | display: flex; 18 | } 19 | .quick-toolbar { 20 | padding: 0 8px; 21 | cursor: pointer; 22 | } 23 | .logo a { 24 | color: #ffffff; 25 | } 26 | .name { 27 | cursor: pointer; 28 | } 29 | .logout { 30 | margin-left: 10px; 31 | margin-right: 20px; 32 | cursor: pointer; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
写文章
4 |
5 | 6 | {{ userInfo.userName }} 7 | 退出 8 |
9 |
10 | -------------------------------------------------------------------------------- /src/app/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import {HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HeaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { LoginService } from '../login/login.service'; 3 | import { Router } from '@angular/router'; 4 | import { catchError } from 'rxjs/operators'; 5 | import { throwError } from 'rxjs'; 6 | @Component({ 7 | selector: 'app-header', 8 | templateUrl: './header.component.html', 9 | styleUrls: ['./header.component.css'] 10 | }) 11 | export class HeaderComponent implements OnInit { 12 | userInfo; 13 | constructor(private login: LoginService, private route: Router) { } 14 | 15 | ngOnInit() { 16 | this.login.isLogin().pipe( 17 | catchError(err => { 18 | console.log('Handling error locally and rethrowing it...', err); 19 | return throwError(err); 20 | }) 21 | ).subscribe((res) => { 22 | this.userInfo = res; 23 | }); 24 | } 25 | 26 | logout() { 27 | localStorage.removeItem('accessToken'); 28 | this.route.navigate(['/login']); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/app/header/header.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HeaderComponent } from './header.component'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | 7 | @NgModule({ 8 | declarations: [HeaderComponent], 9 | imports: [ 10 | CommonModule, 11 | RouterModule, 12 | ], 13 | exports: [HeaderComponent] 14 | }) 15 | export class HeaderModule { } 16 | -------------------------------------------------------------------------------- /src/app/login/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { AuthGuard } from './auth.guard'; 4 | 5 | describe('AuthGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AuthGuard] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([AuthGuard], (guard: AuthGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/login/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, Router } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { LoginService } from './login.service'; 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AuthGuard implements CanActivate { 9 | constructor(public auth: LoginService, public router: Router) { } 10 | canActivate(): boolean { 11 | if (!this.auth.isAuthenticated()) { 12 | this.router.navigate(['login']); 13 | return false; 14 | } 15 | return true; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/app/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import {LoginComponent} from './login.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: LoginComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class LoginRoutingModule { } 17 | -------------------------------------------------------------------------------- /src/app/login/login.component.css: -------------------------------------------------------------------------------- 1 | .login-content { 2 | background: url('../../assets/img/login/login_backgroud.png') no-repeat; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | } 7 | 8 | :host { 9 | height: 100vh; 10 | width: 100%; 11 | } 12 | 13 | .login-feild { 14 | width: 1184px; 15 | height: 681px; 16 | background: #fff; 17 | margin-top: 80px; 18 | display: flex; 19 | flex-direction: row; 20 | } 21 | 22 | .login-form, 23 | .login-img { 24 | flex-grow: 1; 25 | } 26 | 27 | .login-form { 28 | padding: 48px; 29 | margin-right: 12px; 30 | } 31 | 32 | .login-without-password { 33 | text-align: center; 34 | margin-top: 40px; 35 | font-size: 15px; 36 | } 37 | .login-without-password > a{ 38 | color:#5170ff; 39 | } 40 | 41 | .login-text { 42 | font-size: 18px; 43 | margin-bottom: 0; 44 | } 45 | 46 | .login-input-feild { 47 | display: flex; 48 | flex-direction: column; 49 | height: 400px; 50 | justify-content: space-evenly; 51 | padding: 0 5px; 52 | margin-top: -10px; 53 | } 54 | 55 | .login-btn { 56 | height: 50px; 57 | background: #5E7CE0; 58 | font-size: 18px; 59 | color: #fff; 60 | border-radius: 5px; 61 | border: 1px solid #5E7CE0; 62 | } 63 | 64 | .forget-password-field { 65 | font-size: 15px; 66 | text-align: right; 67 | padding-right: 10px; 68 | margin-top: -10px; 69 | } 70 | 71 | 72 | .forget-password-text { 73 | margin-right: 40px; 74 | } 75 | 76 | ::placeholder { 77 | color: #B0B0B0; 78 | } 79 | 80 | .login-input { 81 | height: 50px; 82 | border-radius: 5px; 83 | border: 1px solid #ccc; 84 | font-size: 18px; 85 | padding-left: 20px; 86 | } 87 | 88 | .login-img { 89 | width: 592px; 90 | background: url('../../assets/img/login/image_illustration.png') no-repeat; 91 | } -------------------------------------------------------------------------------- /src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { LoginService } from './login.service'; 4 | @Component({ 5 | selector: 'app-login', 6 | templateUrl: './login.component.html', 7 | styleUrls: ['./login.component.css'] 8 | }) 9 | export class LoginComponent implements OnInit { 10 | userName: string; 11 | password: string; 12 | msgs = []; 13 | constructor(private loginService: LoginService, private route: Router) { } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | doLogin() { 19 | if ( !this.userName || ! this.password) { 20 | this.msgs = [{ severity: 'error', summary: '提示', detail: '请填写完整的用户名密码!' }]; 21 | return; 22 | } 23 | this.loginService.login({ 24 | userName: this.userName, 25 | password: this.password 26 | }).subscribe((res) => { 27 | if (res) { 28 | this.route.navigate(['/admin/dashboard']); 29 | } else { 30 | this.msgs = [{ severity: 'error', summary: '提示', detail: '错误的用户名密码!' }]; 31 | } 32 | }); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/app/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { LoginRoutingModule } from './login-routing.module'; 5 | import { LoginComponent } from './login.component'; 6 | import { HeaderModule } from '../header/header.module'; 7 | import { HttpClientModule } from '@angular/common/http'; 8 | import { LoginService } from './login.service'; 9 | import { DevUIModule } from 'ng-devui'; 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | LoginRoutingModule, 14 | HeaderModule, 15 | HttpClientModule, 16 | FormsModule, 17 | DevUIModule 18 | ], 19 | declarations: [ 20 | LoginComponent 21 | ], 22 | providers: [ 23 | LoginService 24 | ] 25 | }) 26 | export class LoginModule { } 27 | -------------------------------------------------------------------------------- /src/app/login/login.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginService } from './login.service'; 4 | 5 | describe('LoginService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: LoginService = TestBed.get(LoginService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/login/login.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | import { tap } from 'rxjs/operators'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class LoginService { 10 | 11 | private userInfoSubject = new BehaviorSubject({}); 12 | userInfo = this.userInfoSubject.asObservable(); 13 | canActive = false; 14 | constructor(private http: HttpClient) { } 15 | 16 | login(userInfo) { 17 | return this.http.post('/api/login', userInfo).pipe( 18 | tap((data) => { 19 | if (data) { 20 | localStorage.setItem('accessToken', `Bear ${data['token']}`); 21 | this.userInfoSubject.next(data); 22 | } 23 | }) 24 | ); 25 | } 26 | 27 | isLogin() { 28 | const accessToken = localStorage.getItem('accessToken'); 29 | let headers = new HttpHeaders({}); 30 | headers = headers.set('Authorization', accessToken ? accessToken : 'empty'); 31 | return this.http.get('/api/isLogin', { headers }); 32 | } 33 | 34 | public isAuthenticated(): boolean { 35 | const token = localStorage.getItem('accessToken'); 36 | console.log(token); 37 | // Check whether the token is expired and return 38 | // true or false 39 | return !!token; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-detail-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ArticleDetailComponent } from './article-detail.component'; 4 | 5 | 6 | const routes: Routes = [{ 7 | path: '', 8 | component: ArticleDetailComponent, 9 | }]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class ArticleDetailRoutingModule { } 16 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-detail.component.css: -------------------------------------------------------------------------------- 1 | .devui-article-container { 2 | display: flex; 3 | width: 100vw; 4 | height: calc(100vh - 60px); 5 | overflow-y: auto; 6 | justify-content: center; 7 | padding: 24px 0; 8 | background-color: #fff; 9 | } 10 | 11 | .devui-article-container .article-main { 12 | flex-basis: 690px; 13 | } 14 | 15 | .devui-article-container .outline-side { 16 | flex-basis: 240px; 17 | padding: 20px; 18 | } 19 | 20 | .article-main .article-meta { 21 | padding: 5px 0; 22 | border-bottom: 1px solid #ececec; 23 | } 24 | 25 | .article-main .article-content { 26 | padding: 20px 0; 27 | border-bottom: 1px solid #ececec; 28 | } 29 | 30 | .article-main .article-bottom { 31 | margin: 20px 5px; 32 | display: flex; 33 | justify-content: space-between; 34 | font-size: 12px; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

{{title}}

5 | 8 |
9 |
10 |
11 | 上一篇 12 | 下一篇 13 |
14 |
15 |
16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleDetailComponent } from './article-detail.component'; 4 | 5 | describe('ArticleDetailComponent', () => { 6 | let component: ArticleDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import * as marked from 'marked'; 5 | @Component({ 6 | selector: 'app-article-detail', 7 | templateUrl: './article-detail.component.html', 8 | styleUrls: ['./article-detail.component.css'] 9 | }) 10 | export class ArticleDetailComponent implements OnInit { 11 | articleDom: HTMLElement; 12 | scrollContainer: HTMLElement; 13 | title = ''; 14 | htmlContent = ''; 15 | tags = []; 16 | prePageId; 17 | nextPageId; 18 | constructor(private route: ActivatedRoute, private http: HttpClient, private router: Router) { } 19 | 20 | ngOnInit() { 21 | this.route.params.subscribe(params => { 22 | const id = params.id; 23 | this.articleDom = null; 24 | this.http.get('/api/articles/' + id).subscribe(res => { 25 | const article = res['article']; 26 | this.title = article['title']; 27 | this.tags = article['tags']; 28 | const content = article['content']; 29 | this.prePageId = res['prePageId']; 30 | this.nextPageId = res['nextPageId']; 31 | this.htmlContent = marked(content, { 32 | breaks: true 33 | }); 34 | setTimeout(() => { 35 | this.articleDom = document.querySelector('.devui-article-container .article-content'); 36 | }); 37 | }); 38 | }); 39 | this.scrollContainer = document.querySelector('.devui-article-container'); 40 | } 41 | 42 | goPage(pageId) { 43 | this.router.navigate(['/articles', pageId]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-detail.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { DevUIModule } from 'ng-devui'; 4 | import { ArticleDetailRoutingModule } from './article-detail-routing.module'; 5 | import { ArticleDetailComponent } from './article-detail.component'; 6 | import { ArticleOutlineComponent } from './article-outline/article-outline.component'; 7 | 8 | 9 | 10 | @NgModule({ 11 | declarations: [ 12 | ArticleDetailComponent, 13 | ArticleOutlineComponent 14 | ], 15 | imports: [ 16 | CommonModule, 17 | DevUIModule, 18 | ArticleDetailRoutingModule 19 | ] 20 | }) 21 | export class ArticleDetailModule { } 22 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-outline/article-outline.component.css: -------------------------------------------------------------------------------- 1 | .devui-outline-container{ 2 | position: fixed; 3 | } 4 | .item-container{ 5 | cursor: pointer; 6 | } 7 | .item-content{ 8 | padding-top: 10px; 9 | max-width: 100%; 10 | overflow: hidden; 11 | text-overflow: ellipsis; 12 | white-space: nowrap; 13 | } 14 | .item-content:hover, .item-content.active{ 15 | color: #5170ff; 16 | } 17 | .item-content > span { 18 | padding-left: 10px; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-outline/article-outline.component.html: -------------------------------------------------------------------------------- 1 |
2 |
文章目录
3 |
4 | 5 |
6 | {{item.text}} 7 | {{item.text}} 8 |
9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/view/article-detail/article-outline/article-outline.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ElementRef, OnDestroy } from '@angular/core'; 2 | import { fromEvent, Subscription } from 'rxjs'; 3 | import { debounceTime, throttleTime } from 'rxjs/operators'; 4 | 5 | 6 | @Component({ 7 | selector: 'app-outline', 8 | templateUrl: './article-outline.component.html', 9 | styleUrls: ['./article-outline.component.css'] 10 | }) 11 | export class ArticleOutlineComponent implements OnDestroy { 12 | @Input() width = '200px'; 13 | @Input() scrollContainer: HTMLElement; 14 | private initArticleDom: HTMLElement; 15 | @Input() set articleDom(articleDom) { 16 | this.initArticleDom = articleDom; 17 | this.initOutline(); 18 | } 19 | get articleDom() { 20 | return this.initArticleDom; 21 | } 22 | activeIndex = 0; 23 | outlineData = []; 24 | manualScroll = false; 25 | scrollSub: Subscription; 26 | constructor(private el: ElementRef) { } 27 | 28 | initOutline() { 29 | this.outlineData = []; 30 | if (this.articleDom) { 31 | const articleOutLines = Array.from(this.articleDom.querySelectorAll('h1,h2')); 32 | articleOutLines.forEach(item => 33 | this.outlineData.push({ dom: item, text: item.textContent, flag: item.tagName.toLocaleLowerCase()}) 34 | ); 35 | 36 | this.scrollSub = fromEvent(this.scrollContainer, 'scroll').pipe(throttleTime(100)).subscribe(() => { 37 | if (!this.manualScroll) { 38 | const target = this.scrollContainer.scrollTop; 39 | const topData = this.outlineData.map((item, index) => { 40 | return {top: item.dom.getBoundingClientRect().top, order: index}; 41 | }); 42 | topData.sort((a, b) => Math.abs(a.top - target) - Math.abs(b.top - target)); 43 | this.activeIndex = topData[0].order; 44 | const doms = this.el.nativeElement.querySelectorAll('.item-content'); 45 | if (doms[this.activeIndex]) { 46 | doms[this.activeIndex].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); 47 | } 48 | } 49 | }); 50 | } 51 | } 52 | 53 | scrollToView(index) { 54 | this.manualScroll = true; 55 | this.activeIndex = index; 56 | this.outlineData[index].dom.scrollIntoView({ block: 'start', behavior: 'smooth', inline: 'nearest' }); 57 | // scrollIntoView 暂未提供callback https://github.com/w3c/csswg-drafts/issues/3744,简单处理一下,建议自己实现一个scroll to position方法 58 | setTimeout(() => { 59 | this.manualScroll = false; 60 | }, 2000); 61 | } 62 | 63 | ngOnDestroy(): void { 64 | if (this.scrollSub) { 65 | this.scrollSub.unsubscribe(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-item/article-item.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | padding: 16px; 4 | } 5 | .article-info > div { 6 | display: inline-block; 7 | color: #6f6f6f; 8 | } 9 | .article-info > div:not(:first-child) { 10 | margin-left: 8px; 11 | } 12 | .article-preview { 13 | padding: 8px 0; 14 | } 15 | a, a:visited { 16 | color: #404040; 17 | } 18 | a:hover, a:active { 19 | color: #5170ff; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-item/article-item.component.html: -------------------------------------------------------------------------------- 1 |

{{data?.title}}

2 | 5 |
6 | {{data?.content}} 7 |
8 |
9 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-item/article-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleItemComponent } from './article-item.component'; 4 | 5 | describe('ArticleItemComponent', () => { 6 | let component: ArticleItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleItemComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleItemComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-item/article-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import * as marked from 'marked'; 3 | @Component({ 4 | selector: 'app-article-item', 5 | templateUrl: './article-item.component.html', 6 | styleUrls: ['./article-item.component.css'] 7 | }) 8 | export class ArticleItemComponent implements OnInit { 9 | article; 10 | @Input() set data(data) { 11 | this.article = data; 12 | const htmlContent = marked(this.article['content'], { 13 | breaks: true 14 | }); 15 | this.article['content'] = htmlContent.replace(/<.*?>/g, '').substring(0, 100) + '...'; 16 | } 17 | get data() { 18 | return this.article; 19 | } 20 | constructor() { } 21 | 22 | ngOnInit() { 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-list-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ArticleListComponent } from './article-list.component'; 4 | 5 | 6 | const routes: Routes = [{ 7 | path: '', 8 | component: ArticleListComponent, 9 | }]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forChild(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class ArticleListRoutingModule { } 16 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-list.component.css: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | } 4 | aside { 5 | flex: 1 1 250px; 6 | } 7 | .content { 8 | flex: 11 1 500px; 9 | } 10 | .article-list { 11 | display: block; 12 | margin: 8px 8px 16px 8px; 13 | background-color: #fff; 14 | box-shadow: 0 0 1px 1px rgba(64, 64, 64, 0.1); 15 | } 16 | li:not(:first-of-type) { 17 | border-top: 1px solid rgba(64, 64, 64, 0.1); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | 5 |
  • 6 |
7 | 15 | 16 |
17 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleListComponent } from './article-list.component'; 4 | 5 | describe('ArticleListComponent', () => { 6 | let component: ArticleListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-list.component.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Component, OnInit } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-article-list', 6 | templateUrl: './article-list.component.html', 7 | styleUrls: ['./article-list.component.css'] 8 | }) 9 | export class ArticleListComponent implements OnInit { 10 | articleList = []; 11 | total = 0; 12 | pageSize = 10; 13 | pageIndex = 1; 14 | constructor(private http: HttpClient) { } 15 | 16 | ngOnInit() { 17 | this.fetchArticleList(); 18 | } 19 | 20 | pageIndexChange(event) { 21 | this.pageIndex = event; 22 | this.fetchArticleList(); 23 | } 24 | 25 | fetchArticleList() { 26 | this.http.get('/api/articles?page=' + this.pageIndex + '&size=' + this.pageSize).subscribe(res => { 27 | this.articleList = res['articles']; 28 | this.total = res['total']; 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/view/article-list/article-list.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { ArticleListRoutingModule } from './article-list-routing.module'; 6 | import { ArticleListComponent } from './article-list.component'; 7 | import { ArticleItemComponent } from './article-item/article-item.component'; 8 | import { PaginationModule, TagsModule } from 'ng-devui'; 9 | import { SidebarComponent } from './sidebar/sidebar.component'; 10 | 11 | 12 | 13 | @NgModule({ 14 | declarations: [ArticleListComponent, ArticleItemComponent, SidebarComponent], 15 | imports: [ 16 | CommonModule, 17 | ArticleListRoutingModule, 18 | RouterModule, 19 | PaginationModule, 20 | TagsModule 21 | ] 22 | }) 23 | export class ArticleListModule { } 24 | -------------------------------------------------------------------------------- /src/app/view/article-list/sidebar/sidebar.component.css: -------------------------------------------------------------------------------- 1 | .sidebar-box { 2 | display: block; 3 | margin: 8px 8px 16px; 4 | padding: 16px 8px; 5 | background-color: #fff; 6 | box-shadow: 0 0 1px 1px rgba(64, 64, 64, 0.1); 7 | } 8 | .sidebar-box-title { 9 | display: block; 10 | border-bottom: 1px solid rgba(64, 64, 64, 0.1); 11 | font-size: 16px; 12 | line-height: 26px; 13 | margin: -8px 0 8px 0; 14 | color: #6f6f6f; 15 | } 16 | .site-info ul { 17 | display: flex; 18 | justify-content: space-around; 19 | } 20 | .site-info ul li { 21 | flex: 1 1 auto; 22 | text-align: center; 23 | } 24 | .site-info ul li:not(:first-child) { 25 | border-left: 1px solid rgba(64, 64, 64, 0.1); 26 | } 27 | 28 | .article-rank-list li { 29 | counter-increment: rank; 30 | line-height: 26px; 31 | } 32 | .article-rank-list li a::before { 33 | content: counter(rank); 34 | display: inline-block; 35 | width: 2em; 36 | text-align: right; 37 | } 38 | a, a:visited { 39 | color: #404040; 40 | } 41 | a:hover, a:active { 42 | color: #5170ff; 43 | } 44 | -------------------------------------------------------------------------------- /src/app/view/article-list/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 | 13 |
14 | 15 |
    16 |
  1. 17 | 18 | {{article.title}} 19 | 20 |
  2. 21 |
22 |
23 | -------------------------------------------------------------------------------- /src/app/view/article-list/sidebar/sidebar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidebarComponent } from './sidebar.component'; 4 | 5 | describe('SidebarComponent', () => { 6 | let component: SidebarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SidebarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SidebarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/view/article-list/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-sidebar', 5 | templateUrl: './sidebar.component.html', 6 | styleUrls: ['./sidebar.component.css'] 7 | }) 8 | export class SidebarComponent implements OnInit { 9 | @Input() articleCount = 0; 10 | @Input() articleListView = []; 11 | constructor() { } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/view/view-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ViewComponent } from './view.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: ViewComponent, 9 | children: [ 10 | { 11 | path: '', 12 | pathMatch: 'full', 13 | redirectTo: 'articles' 14 | }, 15 | { 16 | path: 'articles', 17 | loadChildren: () => 18 | import('./article-list/article-list.module').then( 19 | m => m.ArticleListModule 20 | ) 21 | }, 22 | { 23 | path: 'articles/:id', 24 | loadChildren: () => 25 | import('./article-detail/article-detail.module').then( 26 | m => m.ArticleDetailModule 27 | ) 28 | } 29 | ] 30 | } 31 | ]; 32 | @NgModule({ 33 | imports: [RouterModule.forChild(routes)], 34 | exports: [RouterModule] 35 | }) 36 | export class ViewRoutingModule {} 37 | -------------------------------------------------------------------------------- /src/app/view/view.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | header { 6 | flex: 0 0 60px; 7 | } 8 | main { 9 | flex: 0 0 calc(100vh - 60px); 10 | display: flex; 11 | align-items: stretch; 12 | } 13 | main.fixed-width { 14 | max-width: 960px; 15 | align-self: center; 16 | justify-content: center; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/app/view/view.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /src/app/view/view.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ViewComponent } from './view.component'; 4 | 5 | describe('ViewComponent', () => { 6 | let component: ViewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ViewComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ViewComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/view/view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-view', 5 | templateUrl: './view.component.html', 6 | styleUrls: ['./view.component.css'] 7 | }) 8 | export class ViewComponent { 9 | 10 | constructor() { } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/view/view.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ViewRoutingModule } from './view-routing.module'; 5 | import { ViewComponent } from './view.component'; 6 | import { HeaderModule } from '../header/header.module'; 7 | 8 | 9 | @NgModule({ 10 | declarations: [ViewComponent], 11 | imports: [ 12 | CommonModule, 13 | ViewRoutingModule, 14 | HeaderModule 15 | ] 16 | }) 17 | export class ViewModule { } 18 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/img/dashboard/article.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/dashboard/article.png -------------------------------------------------------------------------------- /src/assets/img/dashboard/avator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/dashboard/avator.png -------------------------------------------------------------------------------- /src/assets/img/dashboard/collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/dashboard/collect.png -------------------------------------------------------------------------------- /src/assets/img/dashboard/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/dashboard/like.png -------------------------------------------------------------------------------- /src/assets/img/dashboard/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/dashboard/view.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_bold.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_italic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_italic.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_orderedlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_orderedlist.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_redo.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_table.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_underline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_underline.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_undo.png -------------------------------------------------------------------------------- /src/assets/img/editor/icon_unorderedlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/editor/icon_unorderedlist.png -------------------------------------------------------------------------------- /src/assets/img/login/image_illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/login/image_illustration.png -------------------------------------------------------------------------------- /src/assets/img/login/login-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/login/login-text.png -------------------------------------------------------------------------------- /src/assets/img/login/login-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/login/login-title.png -------------------------------------------------------------------------------- /src/assets/img/login/login_backgroud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/login/login_backgroud.png -------------------------------------------------------------------------------- /src/assets/img/login/login_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/assets/img/login/login_image.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevCloudFE/devui-tutorial-web/146ae7731972dcb65ca305e99dc98ed0f051be92/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DevUI Tutorial Web 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | color: #404040; 4 | line-height: 20px; 5 | background-color: #f5f5f5; 6 | } 7 | 8 | app-root { 9 | display: flex; 10 | flex-direction: column; 11 | height: 100%; 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "no-string-literal": false, 84 | "template-banana-in-box": true, 85 | "template-no-negated-async": true, 86 | "use-lifecycle-interface": true, 87 | "use-pipe-transform-interface": true 88 | }, 89 | "rulesDirectory": [ 90 | "codelyzer" 91 | ] 92 | } 93 | --------------------------------------------------------------------------------