├── .editorconfig ├── .gitignore ├── .python-version ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── README.md ├── angular.json ├── browserslist ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── mock ├── data.json └── rest.http ├── package-lock.json ├── package.json ├── protractor.conf.js ├── server.ts ├── src ├── app │ ├── actions │ │ ├── auth.action.ts │ │ ├── index.ts │ │ ├── project.action.ts │ │ ├── quote.action.ts │ │ ├── router.action.ts │ │ ├── task-filter-vm.action.ts │ │ ├── task-filter.action.ts │ │ ├── task-history.action.ts │ │ ├── task-list.action.ts │ │ ├── task.action.ts │ │ ├── theme.action.ts │ │ └── user.action.ts │ ├── anim │ │ ├── card.anim.ts │ │ ├── index.ts │ │ ├── item.anim.ts │ │ ├── list.anim.ts │ │ └── router.anim.ts │ ├── app.module.ts │ ├── app.server.module.ts │ ├── core │ │ ├── app-routing.module.ts │ │ ├── components │ │ │ ├── footer.ts │ │ │ ├── header.spec.ts │ │ │ ├── header.ts │ │ │ └── sidebar.ts │ │ ├── containers │ │ │ ├── app.spec.ts │ │ │ ├── app.ts │ │ │ └── page-not-found.ts │ │ └── index.ts │ ├── directives │ │ ├── drag-drop.service.ts │ │ ├── drag-drop │ │ │ ├── drag.directive.ts │ │ │ └── drop.directive.ts │ │ └── index.ts │ ├── domain │ │ ├── auth.ts │ │ ├── err.ts │ │ ├── history.ts │ │ ├── index.ts │ │ ├── project.ts │ │ ├── quote.ts │ │ ├── task-filter.ts │ │ ├── task-list.ts │ │ ├── task.ts │ │ └── user.ts │ ├── effects │ │ ├── auth.effects.spec.ts │ │ ├── auth.effects.ts │ │ ├── index.ts │ │ ├── project.effects.ts │ │ ├── quote.effects.ts │ │ ├── task-filter-vm.effect.ts │ │ ├── task-filter.effect.ts │ │ ├── task-history.effects.ts │ │ ├── task-list.effects.ts │ │ ├── task.effects.ts │ │ └── user.effects.ts │ ├── login │ │ ├── containers │ │ │ ├── forgot.ts │ │ │ ├── login.spec.ts │ │ │ ├── login.ts │ │ │ └── register.ts │ │ ├── index.ts │ │ └── login-routing.module.ts │ ├── my-calendar │ │ ├── calendar-home │ │ │ └── index.ts │ │ ├── index.ts │ │ └── my-calendar-routing.module.ts │ ├── project │ │ ├── components │ │ │ ├── invite.ts │ │ │ ├── new-project.ts │ │ │ └── project-item.ts │ │ ├── containers │ │ │ └── project-list.ts │ │ ├── index.ts │ │ └── project-routing.module.ts │ ├── reducers │ │ ├── auth.reducer.spec.ts │ │ ├── auth.reducer.ts │ │ ├── index.ts │ │ ├── project.reducer.ts │ │ ├── quote.reducer.ts │ │ ├── role.reducer.ts │ │ ├── task-filter-vm.reducer.ts │ │ ├── task-filter.reducer.ts │ │ ├── task-history.reducer.ts │ │ ├── task-list.reducer.ts │ │ ├── task.reducer.ts │ │ ├── theme.reducer.ts │ │ └── user.reducer.ts │ ├── services │ │ ├── auth-guard.service.spec.ts │ │ ├── auth-guard.service.ts │ │ ├── auth.service.spec.ts │ │ ├── auth.service.ts │ │ ├── index.ts │ │ ├── my-cal.service.ts │ │ ├── project.service.ts │ │ ├── quote.service.ts │ │ ├── task-filter.service.ts │ │ ├── task-history.service.ts │ │ ├── task-list.service.ts │ │ ├── task.service.ts │ │ └── user.service.ts │ ├── shared │ │ ├── adapters │ │ │ ├── date-formats.ts │ │ │ └── datepicker-i18n.ts │ │ ├── components │ │ │ ├── age-input.ts │ │ │ ├── area-list.ts │ │ │ ├── chips-list.ts │ │ │ ├── confirm-dialog.ts │ │ │ ├── identity-input.ts │ │ │ └── image-list-select │ │ │ │ ├── image-list-select.component.html │ │ │ │ ├── image-list-select.component.scss │ │ │ │ ├── image-list-select.component.spec.ts │ │ │ │ └── index.ts │ │ └── index.ts │ ├── task │ │ ├── components │ │ │ ├── copy-task.ts │ │ │ ├── new-task-list.ts │ │ │ ├── new-task.ts │ │ │ ├── quick-task.ts │ │ │ ├── task-filter-nav │ │ │ │ ├── index.ts │ │ │ │ ├── task-filter-nav.component.html │ │ │ │ └── task-filter-nav.component.scss │ │ │ ├── task-history-item │ │ │ │ ├── index.ts │ │ │ │ ├── task-history-item.component.html │ │ │ │ └── task-history-item.component.scss │ │ │ ├── task-item │ │ │ │ ├── index.ts │ │ │ │ ├── task-item.component.html │ │ │ │ └── task-item.component.scss │ │ │ ├── task-list-header.ts │ │ │ └── task-list.ts │ │ ├── containers │ │ │ └── task-home │ │ │ │ ├── index.ts │ │ │ │ ├── task-home.component.html │ │ │ │ └── task-home.component.scss │ │ ├── index.ts │ │ └── task-routing.module.ts │ ├── utils │ │ ├── area.data.ts │ │ ├── area.util.ts │ │ ├── date.util.ts │ │ ├── history.util.ts │ │ ├── identity.data.ts │ │ ├── identity.util.ts │ │ ├── reduer.util.ts │ │ ├── router.util.ts │ │ ├── svg.util.ts │ │ ├── task-filter.util.ts │ │ ├── type.util.ts │ │ └── vm.util.ts │ └── vm │ │ ├── index.ts │ │ ├── project.vm.ts │ │ ├── task-filter.vm.ts │ │ ├── task-history.vm.ts │ │ ├── task-list.vm.ts │ │ └── task.vm.ts ├── assets │ ├── .gitkeep │ └── img │ │ ├── avatar │ │ ├── avatars.svg │ │ └── unassigned.svg │ │ ├── covers │ │ ├── 0.jpg │ │ ├── 0_tn.jpg │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 10_tn.jpg │ │ ├── 11.jpg │ │ ├── 11_tn.jpg │ │ ├── 12.jpg │ │ ├── 12_tn.jpg │ │ ├── 13.jpg │ │ ├── 13_tn.jpg │ │ ├── 14.jpg │ │ ├── 14_tn.jpg │ │ ├── 15.jpg │ │ ├── 15_tn.jpg │ │ ├── 16.jpg │ │ ├── 16_tn.jpg │ │ ├── 17.jpg │ │ ├── 17_tn.jpg │ │ ├── 18.jpg │ │ ├── 18_tn.jpg │ │ ├── 19.jpg │ │ ├── 19_tn.jpg │ │ ├── 1_tn.jpg │ │ ├── 2.jpg │ │ ├── 20.jpg │ │ ├── 20_tn.jpg │ │ ├── 21.jpg │ │ ├── 21_tn.jpg │ │ ├── 22.jpg │ │ ├── 22_tn.jpg │ │ ├── 23.jpg │ │ ├── 23_tn.jpg │ │ ├── 24.jpg │ │ ├── 24_tn.jpg │ │ ├── 25.jpg │ │ ├── 25_tn.jpg │ │ ├── 26.jpg │ │ ├── 26_tn.jpg │ │ ├── 27.jpg │ │ ├── 27_tn.jpg │ │ ├── 28.jpg │ │ ├── 28_tn.jpg │ │ ├── 29.jpg │ │ ├── 29_tn.jpg │ │ ├── 2_tn.jpg │ │ ├── 3.jpg │ │ ├── 30.jpg │ │ ├── 30_tn.jpg │ │ ├── 31.jpg │ │ ├── 31_tn.jpg │ │ ├── 32.jpg │ │ ├── 32_tn.jpg │ │ ├── 33.jpg │ │ ├── 33_tn.jpg │ │ ├── 34.jpg │ │ ├── 34_tn.jpg │ │ ├── 35.jpg │ │ ├── 35_tn.jpg │ │ ├── 36.jpg │ │ ├── 36_tn.jpg │ │ ├── 37.jpg │ │ ├── 37_tn.jpg │ │ ├── 38.jpg │ │ ├── 38_tn.jpg │ │ ├── 39.jpg │ │ ├── 39_tn.jpg │ │ ├── 3_tn.jpg │ │ ├── 4.jpg │ │ ├── 4_tn.jpg │ │ ├── 5.jpg │ │ ├── 5_tn.jpg │ │ ├── 6.jpg │ │ ├── 6_tn.jpg │ │ ├── 7.jpg │ │ ├── 7_tn.jpg │ │ ├── 8.jpg │ │ ├── 8_tn.jpg │ │ ├── 9.jpg │ │ └── 9_tn.jpg │ │ ├── days │ │ ├── day1.svg │ │ ├── day10.svg │ │ ├── day11.svg │ │ ├── day12.svg │ │ ├── day13.svg │ │ ├── day14.svg │ │ ├── day15.svg │ │ ├── day16.svg │ │ ├── day17.svg │ │ ├── day18.svg │ │ ├── day19.svg │ │ ├── day2.svg │ │ ├── day20.svg │ │ ├── day21.svg │ │ ├── day22.svg │ │ ├── day23.svg │ │ ├── day24.svg │ │ ├── day25.svg │ │ ├── day26.svg │ │ ├── day27.svg │ │ ├── day28.svg │ │ ├── day29.svg │ │ ├── day3.svg │ │ ├── day30.svg │ │ ├── day31.svg │ │ ├── day4.svg │ │ ├── day5.svg │ │ ├── day6.svg │ │ ├── day7.svg │ │ ├── day8.svg │ │ └── day9.svg │ │ ├── developer.png │ │ ├── icons │ │ ├── add.svg │ │ ├── burger-navigation.svg │ │ ├── delete.svg │ │ ├── hand-grab-o.svg │ │ └── move.svg │ │ ├── not-found.png │ │ ├── quote_fallback.jpg │ │ ├── quotes │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ │ └── sidebar │ │ ├── day.svg │ │ ├── month.svg │ │ ├── project.svg │ │ ├── projects.svg │ │ └── week.svg ├── environments │ ├── environment.hmr.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── hmr.ts ├── index.html ├── main.server.ts ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── theme.scss ├── tsconfig.app.json ├── tsconfig.server.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json ├── tslint.json ├── typedoc.json ├── webpack.server.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | quote_type = single 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | /out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | *.log 35 | *.log* 36 | /typings 37 | /.esm-cache 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | 47 | # generated docs 48 | /doc 49 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | anaconda3-5.0.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | addons: 3 | chrome: stable 4 | 5 | language: node_js 6 | node_js: 7 | - '9' 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | before_script: 14 | - npm install -g @angular/cli 15 | 16 | script: 17 | - npm run build:ssr 18 | # - ng test 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | 5 | { 6 | "type": "chrome", 7 | "request": "launch", 8 | "name": "Launch Chrome against localhost", 9 | "url": "http://localhost:8000", 10 | "webRoot": "${workspaceRoot}", 11 | "sourceMaps": true, 12 | "runtimeArgs": [ 13 | "--disable-session-crashed-bubble", 14 | "--disable-infobars", 15 | "--user-data-dir", 16 | "--remote-debugging-port=9222" 17 | ], 18 | "trace": true, 19 | "userDataDir": "${workspaceRoot}/out/chrome" 20 | }, 21 | { 22 | "type": "chrome", 23 | "request": "attach", 24 | "name": "Attach to Chrome", 25 | "port": 9222, 26 | "webRoot": "${workspaceRoot}", 27 | "sourceMaps": true, 28 | "trace": true 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // 将设置放入此文件中以覆盖默认值和用户设置。 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.svn": true, 6 | "**/.hg": true, 7 | "**/.DS_Store": true, 8 | "node_modules": true, 9 | "dist": true, 10 | "dist-server": true, 11 | "out": true 12 | }, 13 | "files.trimTrailingWhitespace": true, 14 | "editor.fontSize": 16, 15 | "editor.lineHeight": 24, 16 | "editor.fontLigatures": true, 17 | "editor.fontFamily": 18 | "'Fira Code', 'Operator Mono', Menlo, Monaco, 'Courier New', monospace", 19 | "editor.tabSize": 2, 20 | "editor.insertSpaces": true, 21 | "editor.snippetSuggestions": "top", 22 | "editor.trimAutoWhitespace": true, 23 | "files.insertFinalNewline": true, 24 | "terminal.integrated.fontSize": 18, 25 | "terminal.integrated.fontFamily": 26 | "'Fira Code', 'Operator Mono', Menlo, Monaco, 'Courier New', monospace" 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 多用户任务管理系统 2 | 3 | [![Build Status](https://travis-ci.org/wpcfan/taskmgr.svg?branch=master)](https://travis-ci.org/wpcfan/taskmgr) 4 | 5 | ## 采用的技术 6 | 7 | - 基于 `@ngrx/store` 和 `@ngrx/effects` 管理状态以及状态产生的影响。并且使用 `@ngrx/entity` 减少了 reducer 的重复代码。 8 | - 使用 `rxjs` 实现响应式编程 9 | - 使用 `lettable` 操作符,让 rx 的依赖更小 10 | - 使用 `json-server` 生成原型 REST API 11 | - 使用 `@angular/flex-layout` 作为布局类库 12 | - 使用 `@angular/material` 为界面组件库以及实现界面主题 13 | - 使用 `@angular/animations` 完成动画 14 | - 封装了若干自定义组件、表单组件、指令、管道等 15 | - 使用 `karma` 进行单元测试:组件、服务、 `effects` 和 `reducer` 等。 16 | 17 | ## 开发工具链 18 | 19 | - 使用 `yarn` 作为包管理工具 20 | - 使用 `@angular/cli` 作为脚手架 21 | - 在 `package.json` 中使用 `concurrently` 把 `json-server` 和 `ng serve` 一起启动了 22 | 23 | ## 安装 24 | 25 | 1. fork 这个项目 26 | 2. git clone 项目 27 | 3. `cd taskmgr` 28 | 4. `yarn install` 29 | 5. `npm start` 或者 `yarn start` 启动前端和 json-server ,在浏览器中访问 `8000` 端口 30 | 6. `npm run start:ssr` 启动服务端渲染版本 (Server Side Rendering) 31 | -------------------------------------------------------------------------------- /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/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { TaskmgrPage } from './app.po'; 2 | import { createWriteStream } from 'fs'; 3 | // abstract writing screen shot to a file 4 | function writeScreenShot(data, filename) { 5 | const stream = createWriteStream(filename); 6 | stream.write(Buffer.from(data, 'base64')); 7 | stream.end(); 8 | } 9 | 10 | describe('taskmgr App', () => { 11 | let page: TaskmgrPage; 12 | 13 | beforeEach(() => { 14 | page = new TaskmgrPage(); 15 | }); 16 | 17 | it('should display welcome message', () => { 18 | page.navigateTo(); 19 | page.fillInfo().then(result => writeScreenShot(result, 'sc001.jpg')); 20 | expect(page.getParagraphText()).toContain('企业协作平台'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class TaskmgrPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root mat-sidenav-container')).getText(); 10 | } 11 | 12 | fillInfo() { 13 | element(by.id('mat-input-0')).sendKeys('dev'); 14 | element(by.id('mat-input-1')).sendKeys('dev'); 15 | element(by.buttonText('登录')).click(); 16 | return browser.takeScreenshot(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-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 | files: [ 19 | 20 | { pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css' } 21 | ], 22 | coverageIstanbulReporter: { 23 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 24 | fixWebpackSourcePaths: true 25 | }, 26 | 27 | reporters: ['progress', 'kjhtml'], 28 | port: 9876, 29 | colors: true, 30 | logLevel: config.LOG_INFO, 31 | autoWatch: true, 32 | browsers: ['Chrome'], 33 | singleRun: false 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | // These are important and needed before anything else 2 | import 'zone.js/dist/zone-node'; 3 | import 'reflect-metadata'; 4 | 5 | import { renderModuleFactory } from '@angular/platform-server'; 6 | import { enableProdMode } from '@angular/core'; 7 | 8 | import * as express from 'express'; 9 | import { join } from 'path'; 10 | import { readFileSync } from 'fs'; 11 | 12 | // Import module map for lazy loading 13 | import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; 14 | 15 | // Faster server renders w/ Prod mode (dev mode never needed) 16 | enableProdMode(); 17 | 18 | // Express server 19 | const app = express(); 20 | 21 | const PORT = process.env.PORT || 4000; 22 | const DIST_FOLDER = join(process.cwd(), 'dist'); 23 | 24 | // Our index.html we'll use as our template 25 | const template = readFileSync( 26 | join(DIST_FOLDER, 'browser', 'index.html') 27 | ).toString(); 28 | 29 | const { 30 | AppServerModuleNgFactory, 31 | LAZY_MODULE_MAP 32 | } = require('./dist/server/main'); 33 | 34 | app.engine('html', (_, options, callback) => { 35 | renderModuleFactory(AppServerModuleNgFactory, { 36 | // Our index.html 37 | document: template, 38 | url: options.req.url, 39 | // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it) 40 | extraProviders: [provideModuleMap(LAZY_MODULE_MAP)] 41 | }).then(html => { 42 | callback(null, html); 43 | }); 44 | }); 45 | 46 | app.set('view engine', 'html'); 47 | app.set('views', join(DIST_FOLDER, 'browser')); 48 | 49 | // Server static files from /browser 50 | app.get('*.*', express.static(join(DIST_FOLDER, 'browser'))); 51 | 52 | // All regular routes use the Universal engine 53 | app.get('*', (req, res) => { 54 | res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req }); 55 | }); 56 | 57 | // Start up the Node server 58 | app.listen(PORT, () => { 59 | console.log(`Node server listening on http://localhost:${PORT}`); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/actions/auth.action.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | import {Auth, Err, User} from '../domain'; 3 | 4 | export const LOGIN = '[Auth] Login'; 5 | export const LOGIN_SUCCESS = '[Auth] Login Success'; 6 | export const LOGIN_FAIL = '[Auth] Login Fail'; 7 | export const REGISTER = '[Auth] Register'; 8 | export const REGISTER_SUCCESS = '[Auth] Register Success'; 9 | export const REGISTER_FAIL = '[Auth] Register Fail'; 10 | export const LOGOUT = '[Auth] Logout'; 11 | 12 | export class LoginAction implements Action { 13 | readonly type = LOGIN; 14 | 15 | constructor(public payload: { email: string; password: string }) { 16 | } 17 | } 18 | 19 | export class LoginSuccessAction implements Action { 20 | readonly type = LOGIN_SUCCESS; 21 | 22 | constructor(public payload: Auth) { 23 | } 24 | } 25 | 26 | export class LoginFailAction implements Action { 27 | readonly type = LOGIN_FAIL; 28 | 29 | constructor(public payload: Err) { 30 | } 31 | } 32 | 33 | export class RegisterAction implements Action { 34 | readonly type = REGISTER; 35 | 36 | constructor(public payload: User) { 37 | } 38 | } 39 | 40 | export class RegisterSuccessAction implements Action { 41 | readonly type = REGISTER_SUCCESS; 42 | 43 | constructor(public payload: Auth) { 44 | } 45 | } 46 | 47 | export class RegisterFailAction implements Action { 48 | readonly type = REGISTER_FAIL; 49 | 50 | constructor(public payload: Err) { 51 | } 52 | } 53 | 54 | export class LogoutAction implements Action { 55 | readonly type = LOGOUT; 56 | 57 | constructor() { 58 | } 59 | } 60 | 61 | export type Actions 62 | = LoginAction 63 | | LoginSuccessAction 64 | | LoginFailAction 65 | | RegisterAction 66 | | RegisterSuccessAction 67 | | RegisterFailAction 68 | | LogoutAction; 69 | -------------------------------------------------------------------------------- /src/app/actions/index.ts: -------------------------------------------------------------------------------- 1 | import * as authActions from './auth.action'; 2 | import * as projectActions from './project.action'; 3 | import * as quoteActions from './quote.action'; 4 | import * as taskListActions from './task-list.action'; 5 | import * as taskActions from './task.action'; 6 | import * as HistoryActions from './task-history.action'; 7 | import * as userActions from './user.action'; 8 | import * as themeActions from './theme.action'; 9 | import * as routerActions from './router.action'; 10 | -------------------------------------------------------------------------------- /src/app/actions/quote.action.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | import {Err, Quote} from '../domain'; 3 | 4 | export const QUOTE = '[Quote] Quote'; 5 | export const QUOTE_SUCCESS = '[Quote] Quote Success'; 6 | export const QUOTE_FAIL = '[Quote] Quote Fail'; 7 | 8 | export class QuoteAction implements Action { 9 | readonly type = QUOTE; 10 | } 11 | 12 | export class QuoteSuccessAction implements Action { 13 | readonly type = QUOTE_SUCCESS; 14 | 15 | constructor(public payload: Quote) { 16 | } 17 | } 18 | 19 | export class QuoteFailAction implements Action { 20 | readonly type = QUOTE_FAIL; 21 | 22 | constructor(public payload: string) { 23 | } 24 | } 25 | 26 | 27 | export type Actions 28 | = QuoteAction 29 | | QuoteSuccessAction 30 | | QuoteFailAction; 31 | -------------------------------------------------------------------------------- /src/app/actions/router.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { NavigationExtras } from '@angular/router'; 3 | 4 | export const GO = '[Router] Go'; 5 | export const BACK = '[Router] Back'; 6 | export const FORWARD = '[Router] Forward'; 7 | 8 | export class Go implements Action { 9 | readonly type = GO; 10 | 11 | constructor(public payload: { 12 | path: any[]; 13 | query?: object; 14 | extras?: NavigationExtras; 15 | }) {} 16 | } 17 | 18 | export class Back implements Action { 19 | readonly type = BACK; 20 | } 21 | 22 | export class Forward implements Action { 23 | readonly type = FORWARD; 24 | } 25 | 26 | export type Actions 27 | = Go 28 | | Back 29 | | Forward; 30 | -------------------------------------------------------------------------------- /src/app/actions/task-filter-vm.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { TaskFilterVM } from '../vm'; 3 | import { TaskFilter, User } from '../domain'; 4 | 5 | export const UPDATE = '[TaskFilterVM] Update'; 6 | 7 | export const LOAD_OWNERS = '[TaskFilterVM] Load Owners'; 8 | export const LOAD_OWNERS_SUCCESS = '[TaskFilterVM] Load Owners Success'; 9 | export const LOAD_OWNERS_FAIL = '[TaskFilterVM] Load Owners Fail'; 10 | 11 | export class UpdateTaskFilterVMAction implements Action { 12 | readonly type = UPDATE; 13 | 14 | constructor(public payload: TaskFilterVM) { 15 | } 16 | } 17 | 18 | export class LoadTaskFilterOwnersAction implements Action { 19 | readonly type = LOAD_OWNERS; 20 | 21 | constructor(public payload: TaskFilter) { 22 | } 23 | } 24 | 25 | export class LoadTaskFilterOwnersSuccessAction implements Action { 26 | readonly type = LOAD_OWNERS_SUCCESS; 27 | 28 | constructor(public payload: User[]) { 29 | } 30 | } 31 | 32 | export class LoadTaskFilterOwnersFailAction implements Action { 33 | readonly type = LOAD_OWNERS_FAIL; 34 | 35 | constructor(public payload: string) { 36 | } 37 | } 38 | 39 | export type Actions 40 | = UpdateTaskFilterVMAction 41 | | LoadTaskFilterOwnersAction 42 | | LoadTaskFilterOwnersSuccessAction 43 | | LoadTaskFilterOwnersFailAction 44 | ; 45 | -------------------------------------------------------------------------------- /src/app/actions/task-filter.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { TaskFilter, Project } from '../domain'; 3 | import { TaskFilterVM } from '../vm'; 4 | 5 | export const LOAD = '[TaskFilter] Load'; 6 | export const LOAD_SUCCESS = '[TaskFilter] Load Success'; 7 | export const LOAD_FAIL = '[TaskFilter] Load Fail'; 8 | 9 | export const ADD = '[TaskFilter] Add'; 10 | export const ADD_SUCCESS = '[TaskFilter] Add Success'; 11 | export const ADD_FAIL = '[TaskFilter] Add Fail'; 12 | 13 | export const UPDATE = '[TaskFilter] Update'; 14 | export const UPDATE_SUCCESS = '[TaskFilter] Update Succecss'; 15 | export const UPDATE_FAIL = '[TaskFilter] Update Fail'; 16 | 17 | export class LoadTaskFilterAction implements Action { 18 | readonly type = LOAD; 19 | 20 | constructor(public payload: string) { 21 | } 22 | } 23 | 24 | export class LoadTaskFilterSuccessAction implements Action { 25 | readonly type = LOAD_SUCCESS; 26 | 27 | constructor(public payload: TaskFilter) { 28 | } 29 | } 30 | 31 | export class LoadTaskFilterFailAction implements Action { 32 | readonly type = LOAD_FAIL; 33 | 34 | constructor(public payload: string) { 35 | } 36 | } 37 | 38 | export class AddTaskFilterAction implements Action { 39 | readonly type = ADD; 40 | 41 | constructor(public payload: Project) { 42 | } 43 | } 44 | 45 | export class AddTaskFilterSuccessAction implements Action { 46 | readonly type = ADD_SUCCESS; 47 | 48 | constructor(public payload: Project) { 49 | } 50 | } 51 | 52 | export class AddTaskFilterFailAction implements Action { 53 | readonly type = ADD_FAIL; 54 | 55 | constructor(public payload: string) { 56 | } 57 | } 58 | 59 | export class UpdateTaskFilterAction implements Action { 60 | readonly type = UPDATE; 61 | 62 | constructor(public payload: TaskFilterVM) { 63 | } 64 | } 65 | 66 | export class UpdateTaskFilterSuccessAction implements Action { 67 | readonly type = UPDATE_SUCCESS; 68 | 69 | constructor(public payload: TaskFilter) { 70 | } 71 | } 72 | 73 | export class UpdateTaskFilterFailAction implements Action { 74 | readonly type = UPDATE_FAIL; 75 | 76 | constructor(public payload: string) { 77 | } 78 | } 79 | 80 | export type Actions 81 | = LoadTaskFilterAction 82 | | LoadTaskFilterSuccessAction 83 | | LoadTaskFilterFailAction 84 | | AddTaskFilterAction 85 | | AddTaskFilterSuccessAction 86 | | AddTaskFilterFailAction 87 | | UpdateTaskFilterAction 88 | | UpdateTaskFilterSuccessAction 89 | | UpdateTaskFilterFailAction 90 | ; 91 | -------------------------------------------------------------------------------- /src/app/actions/task-history.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Task, TaskHistory, TaskOperations } from '../domain'; 3 | 4 | export const LOAD = '[TaskHistory] Load'; 5 | export const LOAD_SUCCESS = '[TaskHistory] Load Success'; 6 | export const LOAD_FAIL = '[TaskHistory] Load Fail'; 7 | 8 | export const ADD = '[TaskHistory] Add'; 9 | export const ADD_SUCCESS = '[TaskHistory] Add Success'; 10 | export const ADD_FAIL = '[TaskHistory] Add Fail'; 11 | 12 | export class LoadTaskHistoryAction implements Action { 13 | readonly type = LOAD; 14 | 15 | constructor(public payload: string) { 16 | } 17 | } 18 | 19 | export class LoadHistorySuccessAction implements Action { 20 | readonly type = LOAD_SUCCESS; 21 | 22 | constructor(public payload: TaskHistory[]) { 23 | } 24 | } 25 | 26 | export class LoadHistoryFailAction implements Action { 27 | readonly type = LOAD_FAIL; 28 | 29 | constructor(public payload: string) { 30 | } 31 | } 32 | 33 | export class AddTaskHistoryAction implements Action { 34 | readonly type = ADD; 35 | 36 | constructor(public payload: { taskId: string, operation: TaskOperations }) { 37 | } 38 | } 39 | 40 | export class AddTaskHistorySuccessAction implements Action { 41 | readonly type = ADD_SUCCESS; 42 | 43 | constructor(public payload: TaskHistory) { 44 | } 45 | } 46 | 47 | export class AddTaskHistoryFailAction implements Action { 48 | readonly type = ADD_FAIL; 49 | 50 | constructor(public payload: string) { 51 | } 52 | } 53 | 54 | export type Actions 55 | = LoadTaskHistoryAction 56 | | LoadHistorySuccessAction 57 | | LoadHistoryFailAction 58 | | AddTaskHistoryAction 59 | | AddTaskHistorySuccessAction 60 | | AddTaskHistoryFailAction 61 | ; 62 | -------------------------------------------------------------------------------- /src/app/actions/theme.action.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | 3 | export const SWITCH_THEME = '[THEME] Switch Theme'; 4 | 5 | export class SwitchThemeAction implements Action { 6 | readonly type = SWITCH_THEME; 7 | 8 | constructor(public payload: boolean) { 9 | } 10 | } 11 | 12 | export type Actions 13 | = SwitchThemeAction; 14 | -------------------------------------------------------------------------------- /src/app/anim/card.anim.ts: -------------------------------------------------------------------------------- 1 | import { trigger, state, transition, style, animate } from '@angular/animations'; 2 | 3 | export const cardAnim = trigger('card', [ 4 | state('out', style({transform: 'scale(1)', 'box-shadow': 'none'})), 5 | state('hover', style({transform: 'scale(1.1)', 'box-shadow': '3px 3px 5px 6px #ccc'})), 6 | transition('out => hover', animate('200ms ease-in')), 7 | transition('hover => out', animate('200ms ease-out')) 8 | ]); 9 | -------------------------------------------------------------------------------- /src/app/anim/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card.anim'; 2 | export * from './router.anim'; 3 | export * from './item.anim'; 4 | export * from './list.anim'; 5 | -------------------------------------------------------------------------------- /src/app/anim/item.anim.ts: -------------------------------------------------------------------------------- 1 | import { trigger, state, transition, style, animate } from '@angular/animations'; 2 | 3 | export const itemAnim = trigger('item', [ 4 | state('in', style({'border-left-width': '3px'})), 5 | state('out', style({'border-left-width': '8px'})), 6 | transition('out => in', animate('500ms ease-in')), 7 | transition('in => out', animate('500ms ease-out')) 8 | ]); 9 | 10 | -------------------------------------------------------------------------------- /src/app/anim/list.anim.ts: -------------------------------------------------------------------------------- 1 | import { trigger, stagger, transition, style, animate, query } from '@angular/animations'; 2 | 3 | export const listAnimation = trigger('listAnim', [ 4 | transition('* => *', [ 5 | query(':enter', style({opacity: 0}), { optional: true }), 6 | query(':enter', stagger(100, [ 7 | animate('1s', style({opacity: 1})) 8 | ]), { optional: true }), 9 | query(':leave', style({opacity: 1}), { optional: true }), 10 | query(':leave', stagger(100, [ 11 | animate('1s', style({opacity: 0})) 12 | ]), { optional: true }) 13 | ]) 14 | ]); 15 | -------------------------------------------------------------------------------- /src/app/anim/router.anim.ts: -------------------------------------------------------------------------------- 1 | import {animate, state, style, transition, trigger, group, AnimationTriggerMetadata} from '@angular/animations'; 2 | 3 | export const slideToRight = trigger('routeAnim', [ 4 | state('void', style({display: 'flex', overflow: 'auto'})), 5 | state('*', style({display: 'flex', overflow: 'auto'})), 6 | transition(':enter', [ 7 | style({transform: 'translateX(-100%)', opacity: 0}), 8 | group([ 9 | animate('.5s ease-in-out', style({transform: 'translateX(0)'})), 10 | animate('.3s ease-in', style({opacity: 1})), 11 | ]) 12 | ]), 13 | transition(':leave', [ 14 | style({transform: 'translateX(0)', opacity: 1}), 15 | group([ 16 | animate('.5s ease-in-out', style({transform: 'translateX(100%)'})), 17 | animate('.3s ease-in', style({opacity: 0})), 18 | ]) 19 | ]), 20 | ]); 21 | 22 | const slideToBottom = trigger('routeAnim', [ 23 | state('void', style({width: '100%', height: '80%'}) ), 24 | state('*', style({width: '100%', height: '80%'}) ), 25 | transition(':enter', [ 26 | style({transform: 'translateY(-100%)'}), 27 | animate('0.5s ease-in-out', style({transform: 'translateY(0%)'})) 28 | ]), 29 | transition(':leave', [ 30 | style({transform: 'translateY(0%)'}), 31 | animate('0.5s ease-in-out', style({transform: 'translateY(100%)'})) 32 | ]) 33 | ]); 34 | 35 | const slideToTop = trigger('routeAnim', [ 36 | state('void', style({position: 'fixed', width: '100%', height: '100%'}) ), 37 | state('*', style({position: 'fixed', width: '100%', height: '100%'}) ), 38 | transition(':enter', [ 39 | style({transform: 'translateY(100%)'}), 40 | animate('0.5s ease-in-out', style({transform: 'translateY(0%)'})) 41 | ]), 42 | transition(':leave', [ 43 | style({transform: 'translateY(0%)'}), 44 | animate('0.5s ease-in-out', style({transform: 'translateY(-100%)'})) 45 | ]) 46 | ]); 47 | 48 | export const defaultRouteAnim: AnimationTriggerMetadata = slideToRight; 49 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { TransferHttpCacheModule } from '@nguniversal/common'; 3 | import { NgModule } from '@angular/core'; 4 | import { CoreModule } from './core'; 5 | import { SharedModule } from './shared'; 6 | import { LoginModule } from './login'; 7 | import { AppComponent } from './core/containers/app'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule.withServerTransition({ appId: 'taskmgr' }), 12 | TransferHttpCacheModule, 13 | SharedModule, 14 | LoginModule, 15 | CoreModule 16 | ], 17 | bootstrap: [AppComponent] 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | ServerModule, 4 | ServerTransferStateModule 5 | } from '@angular/platform-server'; 6 | import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; 7 | import { AppModule } from './app.module'; 8 | import { AppComponent } from './core/containers/app'; 9 | 10 | /** 11 | * Used for server rendering 12 | */ 13 | @NgModule({ 14 | imports: [ 15 | AppModule, 16 | ServerModule, 17 | ServerTransferStateModule, 18 | ModuleMapLoaderModule // <-- *Important* to have lazy-loaded routes work 19 | ], 20 | bootstrap: [AppComponent] 21 | }) 22 | export class AppServerModule {} 23 | -------------------------------------------------------------------------------- /src/app/core/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {AuthGuardService} from '../services'; 4 | import {PageNotFoundComponent} from './containers/page-not-found'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | redirectTo: '/login', 10 | pathMatch: 'full' 11 | }, 12 | { 13 | path: 'projects', 14 | loadChildren: () => import('app/project').then(m => m.ProjectModule), 15 | pathMatch: 'full', 16 | canActivate: [AuthGuardService] 17 | }, 18 | { 19 | path: 'tasklists/:id', 20 | loadChildren: () => import('app/task').then(m => m.TaskModule), 21 | canActivate: [AuthGuardService] 22 | }, 23 | { 24 | path: 'mycal/:view', 25 | loadChildren: () => import('app/my-calendar').then(m => m.MyCalendarModule), 26 | canActivate: [AuthGuardService] 27 | }, 28 | { 29 | path: '**', component: PageNotFoundComponent 30 | } 31 | ]; 32 | 33 | @NgModule({ 34 | imports: [RouterModule.forRoot(routes, {useHash: true})], 35 | exports: [RouterModule] 36 | }) 37 | export class AppRoutingModule { 38 | } 39 | -------------------------------------------------------------------------------- /src/app/core/components/footer.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | template: ` 6 | 7 | 8 | © 2017 版权所有: 接灰的电子产品 9 | 10 | 11 | `, 12 | styles: [` 13 | `], 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class FooterComponent {} 17 | -------------------------------------------------------------------------------- /src/app/core/components/header.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { StoreModule } from '@ngrx/store'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { MatToolbarModule } from '@angular/material/toolbar'; 5 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 6 | import { reducers, metaReducers } from '../../reducers'; 7 | import { HeaderComponent } from './header'; 8 | 9 | describe('测试顶部组件:HeaderComponent', () => { 10 | let component: HeaderComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [HeaderComponent], 16 | imports: [ 17 | StoreModule.forRoot(reducers, { metaReducers: metaReducers }), 18 | MatIconModule, 19 | MatToolbarModule, 20 | MatSlideToggleModule 21 | ] 22 | }).compileComponents(); 23 | })); 24 | 25 | beforeEach(() => { 26 | fixture = TestBed.createComponent(HeaderComponent); 27 | component = fixture.componentInstance; 28 | fixture.detectChanges(); 29 | }); 30 | 31 | it('组件应该被创建', () => { 32 | expect(component).toBeTruthy(); 33 | }); 34 | 35 | it('组件模板的元素应该被正确创建', () => { 36 | const compiled = fixture.debugElement.nativeElement; 37 | expect(compiled.querySelector('span').innerText).toContain('企业协作平台'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/app/core/components/header.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, Output, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-header', 5 | template: ` 6 | 7 | 10 | 企业协作平台 11 | 12 | 黑夜模式 13 | 退出 14 | 15 | `, 16 | styles: [` 17 | `], 18 | changeDetection: ChangeDetectionStrategy.OnPush, 19 | }) 20 | export class HeaderComponent { 21 | 22 | @Input() auth = false; 23 | @Output() toggle = new EventEmitter(); 24 | @Output() toggleDarkTheme = new EventEmitter(); 25 | @Output() logout = new EventEmitter(); 26 | 27 | onClick() { 28 | this.toggle.emit(); 29 | } 30 | 31 | handleLogout() { 32 | this.logout.emit(); 33 | } 34 | 35 | onChange(checked: boolean) { 36 | this.toggleDarkTheme.emit(checked); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/core/components/sidebar.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, Output, Input} from '@angular/core'; 2 | import {getDate} from 'date-fns'; 3 | import {Project} from '../../domain'; 4 | 5 | @Component({ 6 | selector: 'app-sidebar', 7 | template: ` 8 |
9 | 10 |

项目

11 | 12 | 13 | 项目首页 14 | 查看您参与的全部项目 15 | 16 | 17 | 18 | 19 | {{ prj.name }} 20 | 21 | {{ prj.desc }} 22 | 23 | 24 |

日历

25 | 26 | 27 | 月视图 28 | 按月方式查看事件 29 | 30 | 31 | 32 | 星期视图 33 | 按星期方式查看事件 34 | 35 | 36 | 37 | 当日视图 38 | 按天方式查看事件 39 | 40 |
41 |
42 | `, 43 | styles: [` 44 | .day-num { 45 | font-size: 48px; 46 | width: 48px; 47 | height: 48px; 48 | } 49 | 50 | mat-icon { 51 | align-self: flex-start; 52 | } 53 | `], 54 | changeDetection: ChangeDetectionStrategy.OnPush 55 | }) 56 | export class SidebarComponent { 57 | 58 | @Input() projects: Project[]; 59 | @Input() auth = false; 60 | @Output() navClicked = new EventEmitter(); 61 | @Output() prjClicked = new EventEmitter(); 62 | 63 | today = 'day'; 64 | 65 | constructor() { 66 | this.today = `day${getDate(new Date())}`; 67 | } 68 | 69 | handleClicked(ev: Event) { 70 | ev.preventDefault(); 71 | this.navClicked.emit(); 72 | } 73 | 74 | onPrjClicked(ev: Event, prj: Project) { 75 | ev.preventDefault(); 76 | this.prjClicked.emit(prj); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/core/containers/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { MatSidenavModule } from '@angular/material/sidenav'; 3 | import { APP_BASE_HREF } from '@angular/common'; 4 | import { RouterModule } from '@angular/router'; 5 | import { CoreModule } from '../'; 6 | import { AppComponent } from './app'; 7 | 8 | describe('测试根模块:AppComponent', () => { 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [MatSidenavModule, RouterModule.forRoot([]), CoreModule], 12 | providers: [ 13 | { 14 | provide: APP_BASE_HREF, 15 | useValue: '/' 16 | } 17 | ] 18 | }).compileComponents(); 19 | })); 20 | 21 | it('应该创建应用', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | const app = fixture.debugElement.componentInstance; 24 | expect(app).toBeTruthy(); 25 | })); 26 | 27 | it('应该包含一个 .site 的元素', async(() => { 28 | const fixture = TestBed.createComponent(AppComponent); 29 | fixture.detectChanges(); 30 | const compiled = fixture.debugElement.nativeElement; 31 | expect(compiled.querySelector('.site')).toBeTruthy(); 32 | })); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/core/containers/app.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {OverlayContainer} from '@angular/cdk/overlay'; 3 | import {Observable} from 'rxjs'; 4 | import {Store, select} from '@ngrx/store'; 5 | import {Auth, Project} from '../../domain'; 6 | import * as fromRoot from '../../reducers'; 7 | import * as actions from '../../actions/auth.action'; 8 | import * as prjActions from '../../actions/project.action'; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | template: ` 13 | 14 | 15 | 20 | 21 |
22 |
23 | 28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 | `, 39 | styles: [` 40 | mat-sidenav-container.myapp-dark-theme { 41 | background: black; 42 | } 43 | 44 | mat-sidenav { 45 | width: 300px; 46 | } 47 | `] 48 | }) 49 | export class AppComponent { 50 | 51 | private _dark = false; 52 | auth$: Observable; 53 | projects$: Observable; 54 | 55 | constructor( 56 | private oc: OverlayContainer, 57 | private store$: Store) { 58 | this.auth$ = this.store$.pipe(select(fromRoot.getAuth)); 59 | this.projects$ = this.store$.pipe(select(fromRoot.getProjects)); 60 | } 61 | 62 | get dark() { 63 | return this._dark; 64 | } 65 | 66 | switchDarkTheme(dark: boolean) { 67 | this._dark = dark; 68 | if (dark) { 69 | this.oc.getContainerElement().classList.add('myapp-dark-theme'); 70 | } else { 71 | this.oc.getContainerElement().classList.remove('myapp-dark-theme'); 72 | } 73 | } 74 | 75 | onLogout() { 76 | this.store$.dispatch(new actions.LogoutAction()); 77 | } 78 | 79 | onPrjClicked(prj: Project) { 80 | this.store$.dispatch(new prjActions.SelectProjectAction(prj)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/core/containers/page-not-found.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | template: ` 6 | 7 | `, 8 | styles: [` 9 | `], 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | }) 12 | export class PageNotFoundComponent {} 13 | -------------------------------------------------------------------------------- /src/app/directives/drag-drop.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { Observable } from 'rxjs'; 4 | 5 | export interface DragData { 6 | tag: string; // 用于标识该拖拽对象,在具有多个可拖拽的层级中标识该层级,需要用户自己维护唯一性 7 | data: any; // 要传递的数据 8 | } 9 | 10 | @Injectable() 11 | export class DragDropService { 12 | private _dragData = new BehaviorSubject(null); 13 | 14 | setDragData(data: DragData) { 15 | this._dragData.next(data); 16 | } 17 | 18 | getDragData(): Observable { 19 | return this._dragData.asObservable(); 20 | } 21 | 22 | clearDragData() { 23 | this._dragData.next(null); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/directives/drag-drop/drag.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, Input, ElementRef, Renderer2, HostListener} from '@angular/core'; 2 | import {DragDropService} from '../drag-drop.service'; 3 | 4 | @Directive({ 5 | selector: '[appDraggable][dragTag][draggedClass][dragData]', 6 | }) 7 | export class DragDirective { 8 | 9 | private _isDraggable = false; 10 | @Input() dragTag: string; 11 | @Input() draggedClass: string; 12 | @Input() dragData: any; 13 | @Input('appDraggable') 14 | set isDraggable(draggable: boolean) { 15 | this._isDraggable = draggable; 16 | this.rd.setAttribute(this.el.nativeElement, 'draggable', `${draggable}`); 17 | } 18 | 19 | get isDraggable() { 20 | return this._isDraggable; 21 | } 22 | 23 | constructor( 24 | private el: ElementRef, 25 | private rd: Renderer2, 26 | private service: DragDropService) { 27 | } 28 | 29 | @HostListener('dragstart', ['$event']) 30 | onDragStart(ev: Event) { 31 | if (this.el.nativeElement === ev.target) { 32 | this.rd.addClass(this.el.nativeElement, this.draggedClass); 33 | this.service.setDragData({tag: this.dragTag, data: this.dragData}); 34 | } 35 | } 36 | 37 | @HostListener('dragend', ['$event']) 38 | onDragEnd(ev: Event) { 39 | if (this.el.nativeElement === ev.target) { 40 | this.rd.removeClass(this.el.nativeElement, this.draggedClass); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/directives/drag-drop/drop.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, Input, Output, EventEmitter, HostListener, ElementRef, Renderer2} from '@angular/core'; 2 | import {DragDropService, DragData} from '../drag-drop.service'; 3 | import { Observable } from 'rxjs'; 4 | import { take } from 'rxjs/operators'; 5 | 6 | @Directive({ 7 | selector: '[appDroppable][dropTags][dragEnterClass]', 8 | }) 9 | export class DropDirective { 10 | 11 | @Output() dropped: EventEmitter = new EventEmitter(); 12 | @Input() dropTags: string[] = []; 13 | @Input() dragEnterClass = ''; 14 | private drag$: Observable; 15 | 16 | constructor( 17 | private el: ElementRef, 18 | private rd: Renderer2, 19 | private service: DragDropService) { 20 | this.drag$ = this.service.getDragData().pipe(take(1)); 21 | } 22 | 23 | @HostListener('dragenter', ['$event']) 24 | onDragEnter(ev: Event) { 25 | ev.preventDefault(); 26 | ev.stopPropagation(); 27 | if (this.el.nativeElement === ev.target) { 28 | this.drag$.subscribe(dragData => { 29 | if (dragData && this.dropTags.indexOf(dragData.tag) > -1) { 30 | this.rd.addClass(this.el.nativeElement, this.dragEnterClass); 31 | this.rd.setProperty(this.el.nativeElement, 'dataTransfer.effectAllowed', 'all'); 32 | this.rd.setProperty(this.el.nativeElement, 'dataTransfer.dropEffect', 'move'); 33 | } 34 | }); 35 | } 36 | } 37 | 38 | @HostListener('dragover', ['$event']) 39 | onDragOver(ev: Event) { 40 | ev.preventDefault(); 41 | ev.stopPropagation(); 42 | if (this.el.nativeElement === ev.target) { 43 | this.drag$.subscribe(dragData => { 44 | if (dragData && this.dropTags.indexOf(dragData.tag) > -1) { 45 | this.rd.setProperty(ev, 'dataTransfer.effectAllowed', 'all'); 46 | this.rd.setProperty(ev, 'dataTransfer.dropEffect', 'move'); 47 | } else { 48 | this.rd.setProperty(ev, 'dataTransfer.effectAllowed', 'none'); 49 | this.rd.setProperty(ev, 'dataTransfer.dropEffect', 'none'); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | @HostListener('dragleave', ['$event']) 56 | onDragLeave(ev: Event) { 57 | ev.preventDefault(); 58 | ev.stopPropagation(); 59 | if (this.el.nativeElement === ev.target) { 60 | this.drag$.subscribe(dragData => { 61 | if (dragData && this.dropTags.indexOf(dragData.tag) > -1) { 62 | this.rd.removeClass(this.el.nativeElement, this.dragEnterClass); 63 | } 64 | }); 65 | } 66 | } 67 | 68 | @HostListener('drop', ['$event']) 69 | onDrop(ev: Event) { 70 | ev.preventDefault(); 71 | ev.stopPropagation(); 72 | if (this.el.nativeElement === ev.target) { 73 | this.drag$.subscribe(dragData => { 74 | if (dragData && this.dropTags.indexOf(dragData.tag) > -1) { 75 | this.rd.removeClass(this.el.nativeElement, this.dragEnterClass); 76 | this.dropped.emit(dragData); 77 | this.service.clearDragData(); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/app/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DragDirective } from './drag-drop/drag.directive'; 3 | import { DropDirective } from './drag-drop/drop.directive'; 4 | import { DragDropService, DragData } from './drag-drop.service'; 5 | 6 | @NgModule({ 7 | providers: [DragDropService], 8 | declarations: [DragDirective, DropDirective], 9 | exports: [DragDirective, DropDirective] 10 | }) 11 | export class DirectivesModule {} 12 | export { DragData }; 13 | -------------------------------------------------------------------------------- /src/app/domain/auth.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | import { Err } from './err'; 3 | 4 | export interface Auth { 5 | user?: User; 6 | userId?: string; 7 | err?: string; 8 | token?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/domain/err.ts: -------------------------------------------------------------------------------- 1 | export interface Err { 2 | timestamp?: Date; 3 | status?: number; 4 | error?: string; 5 | exception?: string; 6 | message?: string; 7 | path?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './err'; 3 | export * from './project'; 4 | export * from './quote'; 5 | export * from './task-list'; 6 | export * from './task'; 7 | export * from './user'; 8 | export * from './history'; 9 | export * from './task-filter'; 10 | -------------------------------------------------------------------------------- /src/app/domain/project.ts: -------------------------------------------------------------------------------- 1 | export interface Project { 2 | id: string | undefined; 3 | name: string; 4 | desc?: string; 5 | coverImg?: string; 6 | enabled?: boolean; 7 | taskFilterId?: string; 8 | taskLists?: string[]; // 存储 TaskList ID 9 | members?: string[]; // 存储成员 key 为 ID, value 为角色 10 | } 11 | -------------------------------------------------------------------------------- /src/app/domain/quote.ts: -------------------------------------------------------------------------------- 1 | export interface Quote { 2 | cn: string; 3 | en: string; 4 | pic: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/domain/task-filter.ts: -------------------------------------------------------------------------------- 1 | export interface TaskFilter { 2 | id: string | undefined; 3 | projectId: string; 4 | sort: string; 5 | hasOwner: boolean; 6 | hasDueDate: boolean; 7 | hasCreateDate: boolean; 8 | hasPriority: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/domain/task-list.ts: -------------------------------------------------------------------------------- 1 | export interface TaskList { 2 | id: string | undefined; 3 | name: string; 4 | projectId: string; 5 | order: number; 6 | taskIds?: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/domain/task.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | 3 | export interface Task { 4 | id: string | undefined; 5 | taskListId: string; 6 | desc: string; 7 | completed: boolean; 8 | ownerId: string; 9 | participantIds: string[]; 10 | dueDate?: Date; 11 | priority: number; 12 | // order: number; 13 | remark?: string; 14 | // tags?: string[]; 15 | reminder?: Date; 16 | createDate?: Date; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/domain/user.ts: -------------------------------------------------------------------------------- 1 | export enum IdentityType { 2 | IdCard = 0, 3 | Insurance, 4 | Passport, 5 | Military, 6 | Other 7 | } 8 | 9 | export interface Address { 10 | id?: number; 11 | province: string; 12 | city: string; 13 | district: string; 14 | street?: string; 15 | } 16 | 17 | export interface Identity { 18 | identityNo: string | null; 19 | identityType: IdentityType | null; 20 | } 21 | 22 | export interface User { 23 | id: string | undefined; 24 | email: string; 25 | name?: string; 26 | password?: string; 27 | avatar?: string; 28 | projectIds?: string[]; 29 | taskIds?: string[]; 30 | address?: Address; 31 | dateOfBirth?: string; 32 | identity?: Identity; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/effects/auth.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { provideMockActions } from '@ngrx/effects/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { hot, cold } from 'jasmine-marbles'; 4 | import { of } from 'rxjs'; 5 | import { Observable } from 'rxjs'; 6 | import { fakeAsync, TestBed } from '@angular/core/testing'; 7 | import { AuthEffects } from './auth.effects'; 8 | import { AuthService } from '../services/auth.service'; 9 | import * as actions from '../actions/auth.action'; 10 | 11 | describe('测试 AuthEffects', () => { 12 | let effects: AuthEffects; 13 | let actions$: Observable; 14 | 15 | beforeEach(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [RouterTestingModule.withRoutes([])], 18 | providers: [ 19 | AuthEffects, 20 | { 21 | provide: AuthService, 22 | useValue: jasmine.createSpyObj('authService', ['login', 'register']) 23 | }, 24 | provideMockActions(() => actions$) 25 | ] 26 | }); 27 | effects = TestBed.get(AuthEffects); 28 | }); 29 | 30 | function setup(methodName: string, params?: { returnedAuth: any }) { 31 | const authService = TestBed.get(AuthService); 32 | if (params) { 33 | if (methodName === 'login') { 34 | authService.login.and.returnValue(params.returnedAuth); 35 | } else { 36 | authService.register.and.returnValue(params.returnedAuth); 37 | } 38 | } 39 | 40 | return { 41 | authEffects: TestBed.get(AuthEffects) 42 | }; 43 | } 44 | 45 | describe('登录逻辑:login$', () => { 46 | it('登录成功发送 LoginSuccessAction', fakeAsync(() => { 47 | const auth = { 48 | token: '', 49 | user: { 50 | id: '123abc', 51 | name: 'wang', 52 | email: 'wang@163.com' 53 | } 54 | }; 55 | actions$ = hot('--a-', { 56 | a: new actions.LoginAction({ 57 | email: 'wang@dev.local', 58 | password: '123abc' 59 | }) 60 | }); 61 | const { authEffects } = setup('login', { returnedAuth: of(auth) }); 62 | 63 | const expectedResult = cold('--b', { 64 | b: new actions.LoginSuccessAction(auth) 65 | }); 66 | expect(effects.login$).toBeObservable(expectedResult); 67 | })); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/app/effects/auth.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { Action } from '@ngrx/store'; 4 | import { Router } from '@angular/router'; 5 | import { Observable } from 'rxjs'; 6 | import { of } from 'rxjs'; 7 | import { map, switchMap, catchError, tap } from 'rxjs/operators'; 8 | import { AuthService } from '../services'; 9 | import * as actions from '../actions/auth.action'; 10 | import * as routerActions from '../actions/router.action'; 11 | 12 | @Injectable() 13 | export class AuthEffects { 14 | /** 15 | * 16 | */ 17 | @Effect() 18 | login$: Observable = this.actions$.pipe( 19 | ofType(actions.LOGIN), 20 | map((action: actions.LoginAction) => action.payload), 21 | switchMap((val: { email: string; password: string }) => 22 | this.authService.login(val.email, val.password).pipe( 23 | map(auth => new actions.LoginSuccessAction(auth)), 24 | catchError(err => 25 | of( 26 | new actions.LoginFailAction({ 27 | status: 501, 28 | message: err.message, 29 | exception: err.stack, 30 | path: '/login', 31 | timestamp: new Date() 32 | }) 33 | ) 34 | ) 35 | ) 36 | ) 37 | ); 38 | 39 | /** 40 | * 41 | */ 42 | @Effect() 43 | register$: Observable = this.actions$.pipe( 44 | ofType(actions.REGISTER), 45 | map(action => action.payload), 46 | switchMap(val => 47 | this.authService.register(val).pipe( 48 | map(auth => new actions.RegisterSuccessAction(auth)), 49 | catchError(err => of(new actions.RegisterFailAction(err))) 50 | ) 51 | ) 52 | ); 53 | 54 | @Effect() 55 | navigateHome$: Observable = this.actions$.pipe( 56 | ofType(actions.LOGIN_SUCCESS), 57 | map(() => new routerActions.Go({ path: ['/projects'] })) 58 | ); 59 | 60 | @Effect() 61 | registerAndHome$: Observable = this.actions$.pipe( 62 | ofType(actions.REGISTER_SUCCESS), 63 | map(() => new routerActions.Go({ path: ['/projects'] })) 64 | ); 65 | 66 | @Effect() 67 | logout$: Observable = this.actions$.pipe( 68 | ofType(actions.LOGOUT), 69 | map(() => new routerActions.Go({ path: ['/'] })) 70 | ); 71 | 72 | @Effect({ dispatch: false }) 73 | navigate$ = this.actions$.pipe( 74 | ofType(routerActions.GO), 75 | map((action: routerActions.Go) => action.payload), 76 | tap(({ path, query: queryParams, extras }) => 77 | this.router.navigate(path, { queryParams, ...extras }) 78 | ) 79 | ); 80 | 81 | /** 82 | * 83 | * @param actions$ 84 | * @param authService 85 | */ 86 | constructor( 87 | private actions$: Actions, 88 | private router: Router, 89 | private authService: AuthService 90 | ) {} 91 | } 92 | -------------------------------------------------------------------------------- /src/app/effects/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { EffectsModule } from '@ngrx/effects'; 3 | import { AuthEffects } from './auth.effects'; 4 | import { QuoteEffects } from './quote.effects'; 5 | import { ProjectEffects } from './project.effects'; 6 | import { TaskListEffects } from './task-list.effects'; 7 | import { TaskEffects } from './task.effects'; 8 | import { TaskFilterEffects } from './task-filter.effect'; 9 | import { TaskFilterVMEffects } from './task-filter-vm.effect'; 10 | import { TaskHistoryEffects } from './task-history.effects'; 11 | import { UserEffects } from './user.effects'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | EffectsModule.forRoot([ 16 | AuthEffects, 17 | QuoteEffects, 18 | ProjectEffects, 19 | TaskListEffects, 20 | TaskEffects, 21 | TaskFilterEffects, 22 | TaskFilterVMEffects, 23 | TaskHistoryEffects, 24 | UserEffects 25 | ]) 26 | ], 27 | }) 28 | export class AppEffectsModule { } 29 | -------------------------------------------------------------------------------- /src/app/effects/quote.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { Action } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | import { of } from 'rxjs'; 6 | import { switchMap, map, catchError } from 'rxjs/operators'; 7 | import { QuoteService } from '../services'; 8 | import * as actions from '../actions/quote.action'; 9 | 10 | @Injectable() 11 | export class QuoteEffects { 12 | /** 13 | * 14 | */ 15 | @Effect() 16 | quote$: Observable = this.actions$.pipe( 17 | ofType(actions.QUOTE), 18 | switchMap(() => 19 | this.quoteService.getQuote().pipe( 20 | map(quote => new actions.QuoteSuccessAction(quote)), 21 | catchError(err => of(new actions.QuoteFailAction(JSON.stringify(err)))) 22 | ) 23 | ) 24 | ); 25 | 26 | /** 27 | * 28 | * @param actions$ 29 | * @param quoteService 30 | */ 31 | constructor(private actions$: Actions, private quoteService: QuoteService) {} 32 | } 33 | -------------------------------------------------------------------------------- /src/app/effects/task-filter-vm.effect.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { Action, Store, select } from '@ngrx/store'; 4 | import { Observable } from 'rxjs'; 5 | import { of } from 'rxjs'; 6 | import { TaskFilter, User } from '../domain'; 7 | import * as actions from '../actions/task-filter-vm.action'; 8 | import * as fromRoot from '../reducers'; 9 | import { map, switchMap, catchError } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class TaskFilterVMEffects { 13 | @Effect() 14 | loadTaskFilterOwners$: Observable = this.actions$.pipe( 15 | ofType(actions.LOAD_OWNERS), 16 | map(action => action.payload), 17 | switchMap((taskFilter: TaskFilter) => 18 | this.store$.pipe( 19 | select(fromRoot.getProjectMembers(taskFilter.projectId)), 20 | map( 21 | (users: User[]) => 22 | new actions.LoadTaskFilterOwnersSuccessAction(users) 23 | ), 24 | catchError(err => 25 | of(new actions.LoadTaskFilterOwnersFailAction(JSON.stringify(err))) 26 | ) 27 | ) 28 | ) 29 | ); 30 | 31 | constructor( 32 | private actions$: Actions, 33 | private store$: Store 34 | ) {} 35 | } 36 | -------------------------------------------------------------------------------- /src/app/login/containers/forgot.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {Store} from '@ngrx/store'; 4 | 5 | import * as fromRoot from '../../reducers'; 6 | 7 | @Component({ 8 | selector: 'app-forgot', 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | template: ` 11 |
12 | 13 | 14 | 忘记密码: 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

还没有账户? 注册

24 |

已有账户 登录

25 |
26 |
27 |
28 | `, 29 | styles: [` 30 | .text-right { 31 | margin: 10px; 32 | text-align: end; 33 | } 34 | `] 35 | }) 36 | export class ForgotComponent implements OnInit { 37 | form: FormGroup; 38 | 39 | constructor(private fb: FormBuilder, 40 | private store$: Store) { 41 | } 42 | 43 | ngOnInit() { 44 | this.form = this.fb.group({ 45 | email: ['', Validators.required] 46 | }); 47 | } 48 | 49 | onSubmit({value, valid}: FormGroup) { 50 | if (!valid) { 51 | return; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/login/containers/login.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | import {LoginComponent} from './login'; 3 | import {SharedModule} from '../../shared'; 4 | import {StoreModule} from '@ngrx/store'; 5 | import {reducers, metaReducers} from '../../reducers'; 6 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 7 | 8 | describe('测试登录组件:LoginComponent', () => { 9 | let component: LoginComponent; 10 | let fixture: ComponentFixture; 11 | 12 | // async beforeEach 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [LoginComponent], 16 | imports: [ 17 | SharedModule, 18 | StoreModule.forRoot(reducers, { metaReducers: metaReducers }), 19 | BrowserAnimationsModule 20 | ] 21 | }) 22 | .compileComponents(); // compile template and css 23 | })); 24 | 25 | beforeEach(() => { 26 | fixture = TestBed.createComponent(LoginComponent); 27 | component = fixture.componentInstance; 28 | fixture.detectChanges(); 29 | }); 30 | 31 | it('组件模板的元素应该被正确创建', () => { 32 | const compiled = fixture.debugElement.nativeElement; 33 | expect(compiled.querySelector('mat-card-header mat-card-title').innerText).toContain('登录'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/login/containers/login.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ChangeDetectionStrategy} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {Store, select} from '@ngrx/store'; 4 | import {Observable} from 'rxjs'; 5 | import {Quote} from '../../domain'; 6 | import * as fromRoot from '../../reducers'; 7 | import * as authActions from '../../actions/auth.action'; 8 | import * as actions from '../../actions/quote.action'; 9 | 10 | @Component({ 11 | selector: 'app-login', 12 | template: ` 13 |
14 | 15 | 16 | 登录: 17 | 18 | 19 | 20 | 21 | 用户名是必填项哦 22 | 23 | 24 | 25 | 密码不正确哦 26 | 27 | 28 | 29 | 30 |

还没有账户? 注册

31 |

忘记 密码?

32 |
33 |
34 | 35 | 36 | 佳句 37 | 38 | {{ (quote$ | async)?.cn }} 39 | 40 | 41 | 42 | 43 |

{{ (quote$ | async)?.en }}

44 |
45 |
46 |
47 | `, 48 | styles: [` 49 | .text-right { 50 | margin: 10px; 51 | text-align: end; 52 | } 53 | `], 54 | changeDetection: ChangeDetectionStrategy.OnPush 55 | }) 56 | export class LoginComponent implements OnInit { 57 | form: FormGroup; 58 | quote$: Observable; 59 | 60 | constructor(private fb: FormBuilder, 61 | private store$: Store) { 62 | this.quote$ = this.store$.pipe(select(fromRoot.getQuoteState)); 63 | } 64 | 65 | ngOnInit() { 66 | this.form = this.fb.group({ 67 | email: ['wpcfan@163.com', Validators.compose([Validators.required, Validators.email])], 68 | password: ['wp123456', Validators.required] 69 | }); 70 | this.store$.dispatch({type: actions.QUOTE}); 71 | } 72 | 73 | onSubmit({value, valid}: FormGroup, e: Event) { 74 | e.preventDefault(); 75 | if (!valid) { 76 | return; 77 | } 78 | this.store$.dispatch( 79 | new authActions.LoginAction(value)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/login/index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {SharedModule} from '../shared'; 3 | import {LoginRoutingModule} from './login-routing.module'; 4 | import {LoginComponent} from './containers/login'; 5 | import {RegisterComponent} from './containers/register'; 6 | import {ForgotComponent} from './containers/forgot'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | SharedModule, 11 | LoginRoutingModule 12 | ], 13 | declarations: [LoginComponent, RegisterComponent, ForgotComponent] 14 | }) 15 | export class LoginModule { 16 | } 17 | -------------------------------------------------------------------------------- /src/app/login/login-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {LoginComponent} from './containers/login'; 4 | import {RegisterComponent} from './containers/register'; 5 | import {ForgotComponent} from './containers/forgot'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: 'login', 10 | component: LoginComponent, 11 | }, 12 | { 13 | path: 'register', 14 | component: RegisterComponent, 15 | }, 16 | { 17 | path: 'forgot', 18 | component: ForgotComponent, 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class LoginRoutingModule { 27 | } 28 | -------------------------------------------------------------------------------- /src/app/my-calendar/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared'; 3 | import { CalendarModule, DateAdapter } from 'angular-calendar'; 4 | import { adapterFactory } from 'angular-calendar/date-adapters/date-fns'; 5 | import { CalendarRoutingModule } from './my-calendar-routing.module'; 6 | import { CalendarHomeComponent } from './calendar-home'; 7 | 8 | @NgModule({ 9 | declarations: [CalendarHomeComponent], 10 | imports: [ 11 | SharedModule, 12 | CalendarRoutingModule, 13 | CalendarModule.forRoot({ 14 | provide: DateAdapter, 15 | useFactory: adapterFactory 16 | }) 17 | ] 18 | }) 19 | export class MyCalendarModule {} 20 | -------------------------------------------------------------------------------- /src/app/my-calendar/my-calendar-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {CalendarHomeComponent} from './calendar-home'; 4 | 5 | const routes: Routes = [ 6 | {path: '', component: CalendarHomeComponent} 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forChild(routes)], 11 | exports: [RouterModule] 12 | }) 13 | export class CalendarRoutingModule { 14 | } 15 | -------------------------------------------------------------------------------- /src/app/project/components/invite.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { NgForm } from '@angular/forms'; 4 | import { User } from '../../domain'; 5 | 6 | @Component({ 7 | selector: 'app-invite', 8 | template: ` 9 |

{{ dialogTitle }}

10 |
11 | 12 | 13 |
14 | 22 | 23 |
24 |
25 | `, 26 | styles: [``] 27 | }) 28 | export class InviteComponent implements OnInit { 29 | members: User[] = []; 30 | dialogTitle: string; 31 | 32 | constructor( 33 | @Inject(MAT_DIALOG_DATA) private data: any, 34 | private dialogRef: MatDialogRef 35 | ) {} 36 | 37 | ngOnInit() { 38 | this.members = [...this.data.members]; 39 | this.dialogTitle = this.data.dialogTitle 40 | ? this.data.dialogTitle 41 | : '邀请成员'; 42 | } 43 | 44 | onSubmit(ev: Event, { value, valid }: NgForm) { 45 | ev.preventDefault(); 46 | if (!valid) { 47 | return; 48 | } 49 | this.dialogRef.close(this.members); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/project/components/project-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | HostBinding, 6 | HostListener, 7 | Input, 8 | Output 9 | } from '@angular/core'; 10 | import {cardAnim} from '../../anim'; 11 | import {Project} from '../../domain'; 12 | 13 | @Component({ 14 | selector: 'app-project-item', 15 | template: ` 16 | 17 | 18 | 19 | 20 | {{ item.name }} 21 | 22 | 23 | 24 | 25 | 26 |

{{ item.desc }}

27 |
28 | 29 | 33 | 37 | 41 | 42 |
43 | `, 44 | styles: [``], 45 | changeDetection: ChangeDetectionStrategy.OnPush, 46 | animations: [cardAnim], 47 | }) 48 | export class ProjectItemComponent { 49 | @Input() item: Project; 50 | @Output() itemSelected = new EventEmitter(); 51 | @Output() launchUpdateDialog = new EventEmitter(); 52 | @Output() launchInviteDailog = new EventEmitter(); 53 | @Output() launchDeleteDailog = new EventEmitter(); 54 | @HostBinding('@card') cardState = 'out'; 55 | 56 | @HostListener('mouseenter') 57 | onMouseEnter() { 58 | this.cardState = 'hover'; 59 | } 60 | 61 | @HostListener('mouseleave') 62 | onMouseLeave() { 63 | this.cardState = 'out'; 64 | } 65 | 66 | onClick(ev: Event) { 67 | ev.preventDefault(); 68 | this.itemSelected.emit(); 69 | } 70 | 71 | openUpdateDialog(ev: Event) { 72 | ev.preventDefault(); 73 | ev.stopPropagation(); 74 | this.launchUpdateDialog.emit(); 75 | } 76 | 77 | openInviteDialog(ev: Event) { 78 | ev.preventDefault(); 79 | ev.stopPropagation(); 80 | this.launchInviteDailog.emit(); 81 | } 82 | 83 | openDeleteDialog(ev: Event) { 84 | ev.preventDefault(); 85 | ev.stopPropagation(); 86 | this.launchDeleteDailog.emit(); 87 | } 88 | 89 | constructor() {} 90 | } 91 | -------------------------------------------------------------------------------- /src/app/project/index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {SharedModule} from '../shared'; 3 | import {ProjectRoutingModule} from './project-routing.module'; 4 | import {ProjectListComponent} from './containers/project-list'; 5 | import {NewProjectComponent} from './components/new-project'; 6 | import {InviteComponent} from './components/invite'; 7 | import {ProjectItemComponent} from './components/project-item'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | SharedModule, 12 | ProjectRoutingModule 13 | ], 14 | exports: [ProjectListComponent], 15 | entryComponents: [NewProjectComponent, InviteComponent], 16 | declarations: [ 17 | ProjectListComponent, 18 | NewProjectComponent, 19 | InviteComponent, 20 | ProjectItemComponent 21 | ] 22 | }) 23 | export class ProjectModule { 24 | } 25 | -------------------------------------------------------------------------------- /src/app/project/project-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {ProjectListComponent} from './containers/project-list'; 4 | 5 | const routes: Routes = [ 6 | {path: '', component: ProjectListComponent} 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forChild(routes)], 11 | exports: [RouterModule] 12 | }) 13 | export class ProjectRoutingModule { 14 | } 15 | -------------------------------------------------------------------------------- /src/app/reducers/auth.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fromAuth from './auth.reducer'; 2 | import {reducer} from './auth.reducer'; 3 | import * as actions from '../actions/auth.action'; 4 | import {async} from '@angular/core/testing'; 5 | 6 | describe('测试 AuthReducer', () => { 7 | describe('未定义Action', () => { 8 | it('应该返回一个默认状态', async(() => { 9 | const action = {} as any; 10 | const result = reducer(undefined, action); 11 | expect(result).toEqual(fromAuth.initialState); 12 | })); 13 | }); 14 | 15 | describe('登录成功', () => { 16 | it('应该返回一个 Err 为 undefined 而 userId 不为空的 Auth 对象', async(() => { 17 | const action = new actions.LoginSuccessAction({ 18 | token: '', 19 | user: { 20 | id: '1', 21 | email: '123@123.com', 22 | password: '123456' 23 | } 24 | }); 25 | const result = reducer(undefined, action); 26 | expect(result).toEqual({token: '', userId: '1'}); 27 | expect(result.err).toBeUndefined(); 28 | })); 29 | }); 30 | 31 | describe('登录失败', () => { 32 | it('应该返回一个 Err 不为 undefined 而 User 为 undefined Auth 对象', async(() => { 33 | const action = new actions.LoginFailAction({ 34 | status: 501, 35 | message: 'Server Error' 36 | }); 37 | const result = reducer(undefined, action); 38 | expect(result.err).toBeDefined(); 39 | expect(result.user).toBeUndefined(); 40 | })); 41 | }); 42 | 43 | describe('注册成功', () => { 44 | it('应该返回一个 Err 为 undefined 而 User 不为空的 Auth 对象', async(() => { 45 | const action = new actions.RegisterSuccessAction({ 46 | token: '', 47 | user: { 48 | id: '123abc', 49 | name: 'wang', 50 | email: 'wang@163.com' 51 | } 52 | }); 53 | const result = reducer(undefined, action); 54 | expect(result).toEqual({token: '', userId: '123abc'}); 55 | expect(result.err).toBeUndefined(); 56 | })); 57 | }); 58 | 59 | describe('注册失败', () => { 60 | it('应该返回一个 Err 不为 undefined 而 User 为 undefined Auth 对象', async(() => { 61 | const action = new actions.RegisterFailAction({ 62 | status: 501, 63 | message: 'Server Error' 64 | }); 65 | const result = reducer(undefined, action); 66 | expect(result.err).toBeDefined(); 67 | expect(result.user).toBeUndefined(); 68 | })); 69 | }); 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /src/app/reducers/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import {Auth} from '../domain'; 2 | import * as actions from '../actions/auth.action'; 3 | 4 | export const initialState: Auth = {}; 5 | 6 | export function reducer(state: Auth = initialState, action: actions.Actions): Auth { 7 | switch (action.type) { 8 | case actions.LOGIN_SUCCESS: 9 | case actions.REGISTER_SUCCESS: { 10 | const auth = action.payload; 11 | return { 12 | token: auth.token, 13 | userId: auth.user ? auth.user.id : undefined 14 | }; 15 | } 16 | case actions.LOGIN_FAIL: 17 | case actions.REGISTER_FAIL: { 18 | return {err: action.payload}; 19 | } 20 | default: { 21 | return state; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/reducers/project.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '../domain'; 2 | import { createSelector } from '@ngrx/store'; 3 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 4 | import * as actions from '../actions/project.action'; 5 | 6 | export interface State extends EntityState { 7 | selectedId: string | null; 8 | } 9 | 10 | export function sortByName(a: Project, b: Project): number { 11 | return a.name.localeCompare(b.name); 12 | } 13 | 14 | export const adapter: EntityAdapter = createEntityAdapter({ 15 | selectId: (project: Project) => project.id, 16 | sortComparer: sortByName, 17 | }); 18 | 19 | export const initialState: State = adapter.getInitialState({ 20 | // additional entity state properties 21 | selectedId: null 22 | }); 23 | 24 | export function reducer(state = initialState, action: actions.Actions): State { 25 | switch (action.type) { 26 | case actions.ADD_SUCCESS: 27 | return { ...adapter.addOne(action.payload, state), selectedId: null }; 28 | case actions.DELETE_SUCCESS: 29 | return { ...adapter.removeOne(action.payload.id, state), selectedId: null }; 30 | case actions.INVITE_SUCCESS: 31 | case actions.UPDATE_LISTS_SUCCESS: 32 | case actions.UPDATE_SUCCESS: 33 | case actions.INSERT_FILTER_SUCCESS: 34 | return { ...adapter.updateOne({ id: action.payload.id, changes: action.payload }, state), selectedId: null }; 35 | case actions.LOADS_SUCCESS: 36 | return { ...adapter.addMany(action.payload, state), selectedId: null }; 37 | case actions.SELECT: 38 | return { ...state, selectedId: action.payload.id }; 39 | default: 40 | return state; 41 | } 42 | } 43 | 44 | export const getSelectedId = (state: State) => state.selectedId; 45 | -------------------------------------------------------------------------------- /src/app/reducers/quote.reducer.ts: -------------------------------------------------------------------------------- 1 | import {Quote} from '../domain'; 2 | import * as actions from '../actions/quote.action'; 3 | 4 | export const initialState: Quote = { 5 | cn: '满足感在于不断的努力,而不是现有成就。全心努力定会胜利满满。', 6 | en: 'Satisfaction lies in the effort, not in the attainment. Full effort is full victory. ', 7 | pic: 'assets/img/quote_fallback.jpg', 8 | }; 9 | 10 | export function reducer(state: Quote = initialState, action: actions.Actions): Quote { 11 | switch (action.type) { 12 | case actions.QUOTE_SUCCESS: 13 | return {...action.payload}; 14 | case actions.QUOTE_FAIL: 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/reducers/role.reducer.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/app/reducers/role.reducer.ts -------------------------------------------------------------------------------- /src/app/reducers/task-filter-vm.reducer.ts: -------------------------------------------------------------------------------- 1 | import { TaskFilterVM } from '../vm'; 2 | import { 3 | getDefaultTaskFilterVM, 4 | getTaskFilterVM, 5 | getOwnerVMs 6 | } from '../utils/task-filter.util'; 7 | import * as actions from '../actions/task-filter-vm.action'; 8 | import * as taskFilterActions from '../actions/task-filter.action'; 9 | 10 | export const initialState: TaskFilterVM = getDefaultTaskFilterVM(); 11 | 12 | export function reducer(state = initialState, action: actions.Actions | taskFilterActions.Actions): TaskFilterVM { 13 | switch (action.type) { 14 | case taskFilterActions.LOAD_SUCCESS: 15 | return getTaskFilterVM(action.payload); 16 | case actions.LOAD_OWNERS_SUCCESS: 17 | return { ...state, ownerVMs: getOwnerVMs(action.payload) }; 18 | case actions.UPDATE: 19 | return { ...action.payload }; 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/reducers/task-filter.reducer.ts: -------------------------------------------------------------------------------- 1 | import { TaskFilter } from '../domain'; 2 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 3 | import { getDefaultTaskFilter } from '../utils/task-filter.util'; 4 | import * as actions from '../actions/task-filter.action'; 5 | import * as projectActions from '../actions/project.action'; 6 | 7 | export const initialState: TaskFilter = getDefaultTaskFilter(); 8 | 9 | export function reducer(state = initialState, action: actions.Actions | projectActions.Actions): TaskFilter { 10 | switch (action.type) { 11 | case actions.LOAD_SUCCESS: 12 | return { ...action.payload }; 13 | case actions.UPDATE_SUCCESS: 14 | return { ...action.payload }; 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/reducers/task-history.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Task, TaskHistory } from '../domain'; 2 | import { TaskVM } from '../vm'; 3 | import { createSelector } from '@ngrx/store'; 4 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 5 | import * as actions from '../actions/task-history.action'; 6 | import * as taskActions from '../actions/task.action'; 7 | 8 | export interface State extends EntityState { 9 | selectedTask: TaskVM | null; 10 | updatedTask: TaskVM | null; 11 | } 12 | 13 | export const adapter: EntityAdapter = createEntityAdapter({ 14 | }); 15 | 16 | export const initialState: State = adapter.getInitialState({ 17 | selectedTask: null, 18 | updatedTask: null, 19 | }); 20 | 21 | export function reducer(state = initialState, action: actions.Actions | taskActions.Actions): State { 22 | switch (action.type) { 23 | case taskActions.SELECT: 24 | return { ids: [], entities: {}, selectedTask: action.payload, updatedTask: null }; 25 | case taskActions.UPDATING: 26 | return { ...state, updatedTask: action.payload }; 27 | case actions.LOAD_SUCCESS: 28 | return { ...adapter.addAll(action.payload, state) }; 29 | case actions.ADD_SUCCESS: 30 | return { ...adapter.addOne(action.payload, state) }; 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | export const getSelectedTask = (state: State): TaskVM | null => state.selectedTask; 37 | export const getUpdatedTask = (state: State): TaskVM | null => state.updatedTask; 38 | -------------------------------------------------------------------------------- /src/app/reducers/task-list.reducer.ts: -------------------------------------------------------------------------------- 1 | import {TaskList, Project} from '../domain'; 2 | import {createSelector} from '@ngrx/store'; 3 | import {EntityState, EntityAdapter, createEntityAdapter} from '@ngrx/entity'; 4 | import * as _ from 'lodash'; 5 | import * as actions from '../actions/task-list.action'; 6 | import * as prjActions from '../actions/project.action'; 7 | 8 | export interface State extends EntityState { 9 | } 10 | 11 | export function sortByOrder(a: TaskList, b: TaskList): number { 12 | return a.order > b.order ? 1 : a.order === b.order ? 0 : -1; 13 | } 14 | 15 | export const adapter: EntityAdapter = createEntityAdapter({ 16 | selectId: (taskList: TaskList) => taskList.id, 17 | sortComparer: sortByOrder, 18 | }); 19 | 20 | export const initialState: State = adapter.getInitialState(); 21 | 22 | const delListByPrj = (state: State, action: prjActions.DeleteProjectSuccessAction) => { 23 | const project = action.payload; 24 | const taskListIds = project.taskLists; 25 | return adapter.removeMany(taskListIds, state); 26 | }; 27 | 28 | const swapOrder = (state: State, action: actions.SwapOrderSuccessAction) => { 29 | const taskLists = action.payload; 30 | if (taskLists === null) { 31 | return state; 32 | } 33 | return adapter.updateMany(taskLists.map((tl: TaskList) => ({id: tl.id, changes: tl})), state); 34 | }; 35 | 36 | export function reducer(state: State = initialState, action: actions.Actions | prjActions.Actions): State { 37 | switch (action.type) { 38 | case actions.ADD_SUCCESS: 39 | return {...adapter.addOne(action.payload, state)}; 40 | case actions.DELETE_SUCCESS: 41 | return {...adapter.removeOne(action.payload.id, state)}; 42 | case actions.UPDATE_SUCCESS: 43 | return {...adapter.updateOne({id: action.payload.id, changes: action.payload}, state)}; 44 | case actions.SWAP_ORDER_SUCCESS: 45 | return {...swapOrder(state, action)}; 46 | case actions.LOADS_SUCCESS: 47 | return {...adapter.addMany(action.payload, state)}; 48 | case prjActions.DELETE_SUCCESS: 49 | return {...delListByPrj(state, action)}; 50 | default: 51 | return state; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/reducers/theme.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions/theme.action'; 2 | 3 | export interface State { 4 | darkmode: boolean; 5 | } 6 | 7 | export const initialState: State = { 8 | darkmode: false 9 | }; 10 | 11 | export function reducer(state = initialState, action: actions.Actions): State { 12 | switch (action.type) { 13 | case actions.SWITCH_THEME: { 14 | return {...state, darkmode: action.payload}; 15 | } 16 | default: { 17 | return state; 18 | } 19 | } 20 | } 21 | 22 | export const getTheme = (state: State) => state.darkmode; 23 | -------------------------------------------------------------------------------- /src/app/reducers/user.reducer.ts: -------------------------------------------------------------------------------- 1 | import {createSelector} from '@ngrx/store'; 2 | import {EntityState, EntityAdapter, createEntityAdapter} from '@ngrx/entity'; 3 | import * as actions from '../actions/user.action'; 4 | import * as authActions from '../actions/auth.action'; 5 | import {User, Auth} from '../domain'; 6 | 7 | export interface State extends EntityState {} 8 | 9 | export function sortByOrder(a: User, b: User): number { 10 | return a.email.localeCompare(b.email); 11 | } 12 | 13 | export const adapter: EntityAdapter = createEntityAdapter({ 14 | selectId: (user: User) => user.id, 15 | sortComparer: sortByOrder, 16 | }); 17 | 18 | export const initialState: State = adapter.getInitialState(); 19 | 20 | const register = (state: State, action: authActions.LoginSuccessAction | authActions.RegisterSuccessAction): State => { 21 | const auth = action.payload; 22 | return (state.ids).indexOf(auth.userId) === -1 ? 23 | {...adapter.addOne(auth.user, state)} : state; 24 | }; 25 | 26 | export function reducer(state: State = initialState, action: actions.Actions | authActions.Actions): State { 27 | switch (action.type) { 28 | case authActions.LOGIN_SUCCESS: 29 | case authActions.REGISTER_SUCCESS: 30 | return register(state, action); 31 | case actions.ADD_USER_PROJECT_SUCCESS: 32 | return {...adapter.addOne(action.payload, state)}; 33 | case actions.REMOVE_USER_PROJECT_SUCCESS: 34 | return {...adapter.removeOne(action.payload.id, state)}; 35 | case actions.SEARCH_USERS_SUCCESS: 36 | case actions.LOAD_USERS_BY_PRJ_SUCCESS: 37 | case actions.BATCH_UPDATE_USER_PROJECT_SUCCESS: 38 | return {...adapter.addMany(action.payload, state)}; 39 | default: { 40 | return state; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/services/auth-guard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { async, inject, TestBed } from '@angular/core/testing'; 3 | import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { AuthGuardService } from './auth-guard.service'; 6 | import { Store, StoreModule, select } from '@ngrx/store'; 7 | import { getAuth, reducers, metaReducers, State } from '../reducers'; 8 | import * as authActions from '../actions/auth.action'; 9 | import { withLatestFrom } from 'rxjs/operators'; 10 | 11 | const mockSnapshot: any = jasmine 12 | .createSpyObj('RouterStateSnapshot', ['toString']); 13 | 14 | @Component({ 15 | template: `` 16 | }) 17 | class RoutingComponent { 18 | } 19 | 20 | @Component({ 21 | template: `` 22 | }) 23 | class DummyComponent { 24 | } 25 | 26 | 27 | describe('测试路由守卫服务:AuthGuardService', () => { 28 | beforeEach(async(() => { 29 | TestBed.configureTestingModule({ 30 | imports: [ 31 | RouterTestingModule.withRoutes([ 32 | { path: 'route1', component: DummyComponent }, 33 | { path: 'route2', component: DummyComponent }, 34 | ]), 35 | StoreModule.forRoot(reducers, { metaReducers: metaReducers }), 36 | ], 37 | declarations: [DummyComponent, RoutingComponent], 38 | providers: [ 39 | AuthGuardService, 40 | { provide: RouterStateSnapshot, useValue: mockSnapshot } 41 | ] 42 | }).compileComponents(); 43 | })); 44 | 45 | it('不应该允许绕过守卫', 46 | async(inject([AuthGuardService, Store], 47 | (service: AuthGuardService, store$: Store) => { 48 | const fixture = TestBed.createComponent(RoutingComponent); 49 | const guard$ = service.canActivate(new ActivatedRouteSnapshot(), mockSnapshot); 50 | const auth$ = store$.pipe(select(getAuth)); 51 | const merge$ = guard$.pipe( 52 | withLatestFrom( 53 | auth$, (g, a) => ({ result: g, auth: a.err === undefined && a.user !== undefined }))); 54 | merge$.subscribe(r => { 55 | expect(r.result).toBe(r.auth); 56 | }); 57 | store$.dispatch({ 58 | type: authActions.LOGIN_SUCCESS, 59 | payload: { 60 | token: 'xxxx', 61 | user: { id: 'xxxx', email: 'abc@dev.local', name: 'xxxx', password: 'sssss' } 62 | } 63 | }); 64 | }))); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /src/app/services/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { Store, select } from '@ngrx/store'; 5 | import { map, defaultIfEmpty } from 'rxjs/operators'; 6 | import * as routerActions from '../actions/router.action'; 7 | import * as fromRoot from '../reducers'; 8 | 9 | @Injectable() 10 | export class AuthGuardService implements CanActivate { 11 | /** 12 | * 构造函数用于注入服务的依赖以及进行必要的初始化 13 | * 14 | * @param router 路由注入,用于导航处理 15 | * @param store$ redux store注入,用于状态管理 16 | */ 17 | constructor(private store$: Store) { 18 | } 19 | 20 | /** 21 | * 用于判断是否可以激活该路由 22 | * 23 | * @param route 24 | */ 25 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 26 | return this.checkAuth(); 27 | } 28 | 29 | checkAuth(): Observable { 30 | return this.store$ 31 | .pipe( 32 | select(s => s.auth), 33 | map(auth => { 34 | const result = auth.token !== undefined && auth.token !== null; 35 | if (!result) { 36 | this.store$.dispatch(new routerActions.Go({ path: ['/login'] })); 37 | } 38 | return result; 39 | }), 40 | defaultIfEmpty(false) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, inject, TestBed } from '@angular/core/testing'; 2 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 3 | import { 4 | HttpClientTestingModule, 5 | HttpTestingController 6 | } from '@angular/common/http/testing'; 7 | import { AuthService } from './auth.service'; 8 | import { User } from '../domain'; 9 | 10 | describe('测试鉴权服务:AuthService', () => { 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [HttpClientTestingModule], 14 | providers: [AuthService] 15 | }); 16 | })); 17 | 18 | afterEach(inject( 19 | [HttpTestingController], 20 | (backend: HttpTestingController) => { 21 | backend.verify(); 22 | } 23 | )); 24 | 25 | it('注册后应该返回一个 Observable', async( 26 | inject( 27 | [AuthService, HttpTestingController], 28 | (service: AuthService, mockBackend: HttpTestingController) => { 29 | const mockUser: User = { 30 | id: undefined, 31 | name: 'someuser@dev.local', 32 | password: '123abc', 33 | email: 'someuser@dev.local' 34 | }; 35 | const mockResponse = { 36 | id: 'obj123abc', 37 | name: 'someuser@dev.local', 38 | email: 'someuser@dev.local', 39 | password: '123abc' 40 | }; 41 | service.register(mockUser).subscribe(auth => { 42 | expect(auth.token).toBeDefined(); 43 | expect(auth.userId).toEqual(mockUser.id); 44 | }); 45 | 46 | mockBackend 47 | .expectOne('users') 48 | .flush(JSON.stringify(mockResponse), { 49 | status: 200, 50 | statusText: 'Ok' 51 | }); 52 | } 53 | ) 54 | )); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpHeaders, HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { Auth, User } from '../domain'; 5 | import { map, switchMap } from 'rxjs/operators'; 6 | 7 | /** 8 | * 认证服务主要用于用户的注册和登录功能 9 | */ 10 | @Injectable() 11 | export class AuthService { 12 | private headers = new HttpHeaders({ 13 | 'Content-Type': 'application/json' 14 | }); 15 | 16 | private token = 17 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' + 18 | '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9' + 19 | '.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ'; 20 | 21 | /** 22 | * 构造函数用于注入服务的依赖以及进行必要的初始化 23 | * 24 | * @param http 注入Http 25 | * @param config 注入基础配置 26 | */ 27 | constructor( 28 | private http: HttpClient, 29 | @Inject('BASE_CONFIG') private config: { uri: string } 30 | ) {} 31 | 32 | /** 33 | * 使用用户提供的个人信息进行注册,成功则返回 User,否则抛出异常 34 | * 35 | * @param user 用户信息,id 属性会被忽略,因为服务器端会创建新的 id 36 | */ 37 | register(user: User): Observable { 38 | const params = new HttpParams().set('email', user.email); 39 | const uri = `${this.config.uri}/users`; 40 | return this.http.get(uri, { params }).pipe( 41 | switchMap(res => { 42 | if ((res).length > 0) { 43 | return throwError('username existed'); 44 | } 45 | return this.http 46 | .post(uri, JSON.stringify(user), { headers: this.headers }) 47 | .pipe(map(r => ({ token: this.token, user: r }))); 48 | }) 49 | ); 50 | } 51 | 52 | /** 53 | * 使用用户名和密码登录 54 | * 55 | * @param email 用户名 56 | * @param password 密码(明文),服务器会进行加密处理 57 | */ 58 | login(email: string, password: string): Observable { 59 | const uri = `${this.config.uri}/users`; 60 | const params = new HttpParams() 61 | .set('email', email) 62 | .set('password', password); 63 | return this.http.get(uri, { params }).pipe( 64 | map(res => { 65 | const users = res; 66 | if (users.length === 0) { 67 | throw new Error('Username or password incorrect'); 68 | } 69 | return { 70 | token: this.token, 71 | user: users[0] 72 | }; 73 | }) 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AuthService } from './auth.service'; 3 | import { ProjectService } from './project.service'; 4 | import { QuoteService } from './quote.service'; 5 | import { TaskListService } from './task-list.service'; 6 | import { TaskService } from './task.service'; 7 | import { AuthGuardService } from './auth-guard.service'; 8 | import { UserService } from './user.service'; 9 | import { MyCalService } from './my-cal.service'; 10 | import { TaskHistoryService } from './task-history.service'; 11 | import { TaskFilterService } from './task-filter.service'; 12 | 13 | export { 14 | AuthGuardService, 15 | AuthService, 16 | ProjectService, 17 | QuoteService, 18 | TaskListService, 19 | TaskService, 20 | TaskFilterService, 21 | TaskHistoryService, 22 | UserService, 23 | MyCalService, 24 | }; 25 | 26 | @NgModule() 27 | export class ServicesModule { 28 | static forRoot() { 29 | return { 30 | ngModule: ServicesModule, 31 | providers: [ 32 | AuthGuardService, 33 | AuthService, 34 | ProjectService, 35 | QuoteService, 36 | TaskListService, 37 | TaskService, 38 | TaskFilterService, 39 | TaskHistoryService, 40 | UserService, 41 | MyCalService, 42 | ] 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/services/my-cal.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Task } from '../domain'; 5 | import { CalendarEvent } from 'angular-calendar'; 6 | import { endOfDay, startOfDay } from 'date-fns'; 7 | import { map } from 'rxjs/operators'; 8 | 9 | const colors: any = { 10 | red: { 11 | primary: '#ad2121', 12 | secondary: '#FAE3E3' 13 | }, 14 | blue: { 15 | primary: '#1e90ff', 16 | secondary: '#D1E8FF' 17 | }, 18 | yellow: { 19 | primary: '#e3bc08', 20 | secondary: '#FDF1BA' 21 | } 22 | }; 23 | 24 | const getPriorityColor = (priority: number) => { 25 | switch (priority) { 26 | case 1: 27 | return colors.red; 28 | case 2: 29 | return colors.yellow; 30 | case 3: 31 | default: 32 | return colors.blue; 33 | } 34 | }; 35 | 36 | @Injectable() 37 | export class MyCalService { 38 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, private http: HttpClient) { 39 | } 40 | 41 | getUserTasks(userId: string): Observable { 42 | const uri = `${this.config.uri}/tasks`; 43 | const params = new HttpParams() 44 | .set('ownerId', userId); 45 | return this.http.get(uri, { params }) 46 | .pipe( 47 | map((tasks: Task[]) => tasks.map( 48 | (task: Task) => ({ 49 | start: startOfDay(task.createDate), 50 | end: task.dueDate ? endOfDay(task.dueDate) : endOfDay(task.createDate), 51 | title: task.desc, 52 | color: getPriorityColor(task.priority) 53 | }) 54 | )) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/services/quote.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Quote } from '../domain'; 5 | 6 | @Injectable() 7 | export class QuoteService { 8 | // private uri: string = 'https://api.hzy.pw/saying/v1/ciba'; 9 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, 10 | private http: HttpClient) { 11 | } 12 | 13 | getQuote(): Observable { 14 | const uri = `${this.config.uri}/quotes/${Math.floor(Math.random() * 10)}`; 15 | return this.http.get(uri); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/services/task-filter.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { TaskFilter } from '../domain'; 5 | 6 | @Injectable() 7 | export class TaskFilterService { 8 | private readonly domain: string = 'taskFilter'; 9 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 10 | 11 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, private http: HttpClient) { 12 | } 13 | 14 | addTaskFilter(filter: TaskFilter): Observable { 15 | const uri = `${this.config.uri}/${this.domain}`; 16 | return this.http.post(uri, JSON.stringify(filter), { headers: this.headers }); 17 | } 18 | 19 | getTaskFilter(id: string): Observable { 20 | const uri = `${this.config.uri}/${this.domain}/${id}`; 21 | return this.http.get(uri); 22 | } 23 | 24 | updateTaskFilter(filter: TaskFilter): Observable { 25 | const uri = `${this.config.uri}/${this.domain}/${filter.id}`; 26 | return this.http 27 | .patch(uri, JSON.stringify(filter), { headers: this.headers }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/services/task-history.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { TaskHistory } from '../domain'; 5 | 6 | @Injectable() 7 | export class TaskHistoryService { 8 | private readonly domain = 'taskHistory'; 9 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 10 | 11 | constructor(@Inject('BASE_CONFIG') private config: { uri: string }, private http: HttpClient) { 12 | } 13 | 14 | addTaskHistory(history: TaskHistory): Observable { 15 | const uri = `${this.config.uri}/${this.domain}`; 16 | return this.http.post(uri, JSON.stringify(history), { headers: this.headers }); 17 | } 18 | 19 | getTaskHistory(taskId: string): Observable { 20 | const uri = `${this.config.uri}/${this.domain}`; 21 | const params = new HttpParams() 22 | .set('taskId', taskId); 23 | 24 | return this.http.get(uri, { params }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/services/task-list.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpHeaders, HttpParams, HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { merge } from 'rxjs'; 5 | import { reduce, map, mapTo } from 'rxjs/operators'; 6 | import { concat } from 'rxjs'; 7 | import { Project, TaskList, Task } from '../domain'; 8 | 9 | @Injectable() 10 | export class TaskListService { 11 | private readonly domain = 'taskLists'; 12 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 13 | 14 | constructor( 15 | @Inject('BASE_CONFIG') private config: { uri: string }, 16 | private http: HttpClient 17 | ) {} 18 | 19 | add(taskList: TaskList): Observable { 20 | const uri = `${this.config.uri}/${this.domain}`; 21 | return this.http.post(uri, JSON.stringify(taskList), { 22 | headers: this.headers 23 | }); 24 | } 25 | 26 | update(taskList: TaskList): Observable { 27 | const uri = `${this.config.uri}/${this.domain}/${taskList.id}`; 28 | const toUpdate = { 29 | name: taskList.name 30 | }; 31 | return this.http.patch(uri, JSON.stringify(toUpdate), { 32 | headers: this.headers 33 | }); 34 | } 35 | 36 | del(taskList: TaskList): Observable { 37 | const uri = `${this.config.uri}/${this.domain}/${taskList.id}`; 38 | return this.http.delete(uri).pipe(mapTo(taskList)); 39 | } 40 | 41 | // GET /tasklist 42 | get(projectId: string): Observable { 43 | const uri = `${this.config.uri}/${this.domain}`; 44 | const params = new HttpParams().set('projectId', projectId); 45 | return this.http.get(uri, { params }); 46 | } 47 | 48 | swapOrder(src: TaskList, target: TaskList): Observable { 49 | const dragUri = `${this.config.uri}/${this.domain}/${src.id}`; 50 | const dropUri = `${this.config.uri}/${this.domain}/${target.id}`; 51 | const drag$ = this.http.patch( 52 | dragUri, 53 | JSON.stringify({ order: target.order }), 54 | { headers: this.headers } 55 | ); 56 | const drop$ = this.http.patch( 57 | dropUri, 58 | JSON.stringify({ order: src.order }), 59 | { headers: this.headers } 60 | ); 61 | return concat(drag$, drop$).pipe( 62 | reduce((r: TaskList[], x: TaskList) => [...r, x], []) 63 | ); 64 | } 65 | 66 | initializeTaskLists(prj: Project): Observable { 67 | const id = prj.id; 68 | return merge( 69 | this.add({ id: undefined, name: '待办', projectId: id, order: 1 }), 70 | this.add({ id: undefined, name: '进行中', projectId: id, order: 2 }), 71 | this.add({ id: undefined, name: '已完成', projectId: id, order: 3 }) 72 | ).pipe( 73 | reduce((r: TaskList[], x: TaskList) => [...r, x], []), 74 | map((tls: TaskList[]) => ({ 75 | ...prj, 76 | taskLists: tls.map(tl => tl.id) 77 | })) 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { HttpHeaders, HttpClient, HttpParams } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { User, Project } from '../domain'; 5 | import { from } from 'rxjs'; 6 | import { switchMap, filter, reduce } from 'rxjs/operators'; 7 | 8 | @Injectable() 9 | export class UserService { 10 | private readonly domain = 'users'; 11 | private headers = new HttpHeaders().set('Content-Type', 'application/json'); 12 | 13 | constructor( 14 | @Inject('BASE_CONFIG') private config: { uri: string }, 15 | private http: HttpClient 16 | ) {} 17 | 18 | searchUsers(filterStr: string): Observable { 19 | const uri = `${this.config.uri}/${this.domain}`; 20 | const params = new HttpParams().set('email_like', filterStr); 21 | return this.http.get(uri, { params }); 22 | } 23 | 24 | getUsersByProject(projectId: string): Observable { 25 | const uri = `${this.config.uri}/users`; 26 | const params = new HttpParams().set('projectId', projectId); 27 | return this.http.get(uri, { params }); 28 | } 29 | 30 | addProjectRef(user: User, projectId: string): Observable { 31 | const uri = `${this.config.uri}/${this.domain}/${user.id}`; 32 | const projectIds = user.projectIds ? user.projectIds : []; 33 | return this.http.patch( 34 | uri, 35 | JSON.stringify({ projectIds: [...projectIds, projectId] }), 36 | { headers: this.headers } 37 | ); 38 | } 39 | 40 | removeProjectRef(user: User, projectId: string): Observable { 41 | const uri = `${this.config.uri}/${this.domain}/${user.id}`; 42 | const projectIds = user.projectIds ? user.projectIds : []; 43 | const index = projectIds.indexOf(projectId); 44 | const toUpdate = [ 45 | ...projectIds.slice(0, index), 46 | ...projectIds.slice(index + 1) 47 | ]; 48 | return this.http.patch( 49 | uri, 50 | JSON.stringify({ projectIds: toUpdate }), 51 | { headers: this.headers } 52 | ); 53 | } 54 | 55 | batchUpdateProjectRef(project: Project): Observable { 56 | const projectId = project.id; 57 | const memberIds = project.members ? project.members : []; 58 | return from(memberIds).pipe( 59 | switchMap(id => { 60 | const uri = `${this.config.uri}/${this.domain}/${id}`; 61 | return this.http.get(uri); 62 | }), 63 | filter( 64 | (user: User) => 65 | user.projectIds ? user.projectIds.indexOf(projectId) < 0 : false 66 | ), 67 | switchMap((u: User) => this.addProjectRef(u, projectId)), 68 | reduce((users: User[], curr: User) => [...users, curr], []) 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/shared/adapters/date-formats.ts: -------------------------------------------------------------------------------- 1 | import { MatDateFormats } from '@angular/material/core'; 2 | 3 | export const MD_FNS_DATE_FORMATS: MatDateFormats = { 4 | parse: { 5 | dateInput: null 6 | }, 7 | display: { 8 | dateInput: { year: 'numeric', month: 'numeric', day: 'numeric' }, 9 | monthYearLabel: { year: 'numeric', month: 'short' }, 10 | dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' }, 11 | monthYearA11yLabel: { year: 'numeric', month: 'long' } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/shared/adapters/datepicker-i18n.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | /** Datepicker data that requires internationalization. */ 5 | @Injectable() 6 | export class DatepickerI18n { 7 | /** 8 | * Stream that emits whenever the labels here are changed. Use this to notify 9 | * components if the labels have changed after initialization. 10 | */ 11 | changes: Subject = new Subject(); 12 | 13 | /** A label for the calendar popup (used by screen readers). */ 14 | calendarLabel = '日历'; 15 | 16 | /** A label for the button used to open the calendar popup (used by screen readers). */ 17 | openCalendarLabel = '打开日历'; 18 | 19 | /** A label for the previous month button (used by screen readers). */ 20 | prevMonthLabel = '上月'; 21 | 22 | /** A label for the next month button (used by screen readers). */ 23 | nextMonthLabel = '下月'; 24 | 25 | /** A label for the previous year button (used by screen readers). */ 26 | prevYearLabel = '前一年'; 27 | 28 | /** A label for the next year button (used by screen readers). */ 29 | nextYearLabel = '下一年'; 30 | 31 | /** A label for the 'switch to month view' button (used by screen readers). */ 32 | switchToMonthViewLabel = '切换月视图'; 33 | 34 | /** A label for the 'switch to year view' button (used by screen readers). */ 35 | switchToYearViewLabel = '切换年视图'; 36 | } 37 | -------------------------------------------------------------------------------- /src/app/shared/components/confirm-dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | 4 | export interface ConfirmDialog { 5 | title: string; 6 | content: string; 7 | confirmAction: string; 8 | } 9 | 10 | @Component({ 11 | selector: 'app-confirm-dialog', 12 | template: ` 13 |

{{ dialog.title }}

14 |
{{ dialog.content }}
15 |
16 | 19 | 27 |
28 | `, 29 | styles: [``], 30 | changeDetection: ChangeDetectionStrategy.OnPush 31 | }) 32 | export class ConfirmDialogComponent { 33 | dialog: ConfirmDialog; 34 | 35 | constructor( 36 | @Inject(MAT_DIALOG_DATA) private data: any, 37 | private dialogRef: MatDialogRef 38 | ) { 39 | if (this.data.dialog !== undefined || this.data.dialog !== null) { 40 | this.dialog = this.data.dialog; 41 | } 42 | } 43 | 44 | handleAction(result: boolean) { 45 | this.dialogRef.close(result); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/image-list-select.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ title }} 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 |
17 | 18 | checked 19 | 20 |
21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/image-list-select.component.scss: -------------------------------------------------------------------------------- 1 | mat-icon.avatar { 2 | overflow: hidden; 3 | width: 64px; 4 | height: 64px; 5 | border-radius: 50%; 6 | margin: 12px; 7 | } 8 | 9 | .scroll-container { 10 | overflow-y: scroll; 11 | height: 200px; 12 | } 13 | 14 | .image-container { 15 | position: relative; 16 | display: inline-block; 17 | } 18 | 19 | .image-container img { 20 | display: block; 21 | } 22 | 23 | .image-container .after { 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | width: 100%; 28 | height: 100%; 29 | display: none; 30 | color: #FFF; 31 | } 32 | 33 | .image-container:hover .after { 34 | display: block; 35 | background: rgba(0, 0, 0, .6); 36 | } 37 | 38 | .image-container .after .zoom { 39 | color: #DDD; 40 | font-size: 48px; 41 | position: absolute; 42 | top: 50%; 43 | left: 50%; 44 | margin: -30px 0 0 -19px; 45 | height: 50px; 46 | width: 45px; 47 | cursor: pointer; 48 | } 49 | 50 | .image-container .after .zoom:hover { 51 | color: #FFF; 52 | } 53 | 54 | .cover { 55 | width: 150px; 56 | } 57 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/image-list-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { MatGridListModule } from '@angular/material/grid-list'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { ImageListSelectComponent } from './'; 5 | 6 | describe('ImageListSelectComponent', () => { 7 | let component: ImageListSelectComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ImageListSelectComponent], 13 | imports: [MatGridListModule, MatIconModule] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(ImageListSelectComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/shared/components/image-list-select/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | forwardRef, 6 | Input, 7 | Output 8 | } from '@angular/core'; 9 | import { 10 | ControlValueAccessor, 11 | FormControl, 12 | NG_VALIDATORS, 13 | NG_VALUE_ACCESSOR 14 | } from '@angular/forms'; 15 | 16 | @Component({ 17 | selector: 'app-image-list-select', 18 | templateUrl: './image-list-select.component.html', 19 | styleUrls: ['./image-list-select.component.scss'], 20 | providers: [ 21 | { 22 | provide: NG_VALUE_ACCESSOR, 23 | useExisting: forwardRef(() => ImageListSelectComponent), 24 | multi: true 25 | }, 26 | { 27 | provide: NG_VALIDATORS, 28 | useExisting: forwardRef(() => ImageListSelectComponent), 29 | multi: true 30 | } 31 | ], 32 | changeDetection: ChangeDetectionStrategy.OnPush 33 | }) 34 | export class ImageListSelectComponent implements ControlValueAccessor { 35 | selected: string; 36 | @Input() title = '选择封面:'; 37 | @Input() items: string[] = []; 38 | @Input() cols = 8; 39 | @Input() rowHeight = '64px'; 40 | @Input() itemWidth = '80px'; 41 | @Input() useSvgIcon = false; 42 | @Output() itemChange = new EventEmitter(); 43 | 44 | // 这里是做一个空函数体,真正使用的方法在 registerOnChange 中 45 | // 由框架注册,然后我们使用它把变化发回表单 46 | // 注意,和 EventEmitter 尽管很像,但发送回的对象不同 47 | private propagateChange = (_: any) => {}; 48 | 49 | // 写入控件值 50 | public writeValue(obj: any) { 51 | if (obj && obj !== '') { 52 | this.selected = obj; 53 | } 54 | } 55 | 56 | // 当表单控件值改变时,函数 fn 会被调用 57 | // 这也是我们把变化 emit 回表单的机制 58 | public registerOnChange(fn: any) { 59 | this.propagateChange = fn; 60 | } 61 | 62 | // 验证表单,验证结果正确返回 null 否则返回一个验证结果对象 63 | public validate(c: FormControl) { 64 | return this.selected 65 | ? null 66 | : { 67 | imageListSelect: { 68 | valid: false 69 | } 70 | }; 71 | } 72 | 73 | // 这里没有使用,用于注册 touched 状态 74 | public registerOnTouched() {} 75 | 76 | // 列表元素选择发生改变触发 77 | onChange(i: number) { 78 | this.selected = this.items[i]; 79 | // 更新表单 80 | this.propagateChange(this.items[i]); 81 | this.itemChange.emit(this.items[i]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/task/components/copy-task.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { Observable } from 'rxjs'; 4 | import { TaskList } from '../../domain'; 5 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'app-copy-task', 9 | template: ` 10 |
11 | {{ dialogTitle }} 12 |
13 | 14 | 19 | 20 | {{ list.name }} 21 | 22 | 23 | 24 |
25 | 33 | 34 |
35 |
36 |
37 | `, 38 | styles: [``] 39 | }) 40 | export class CopyTaskComponent implements OnInit { 41 | form: FormGroup; 42 | dialogTitle: string; 43 | lists$: Observable; 44 | 45 | constructor( 46 | private fb: FormBuilder, 47 | @Inject(MAT_DIALOG_DATA) private data: any, 48 | private dialogRef: MatDialogRef 49 | ) {} 50 | 51 | ngOnInit() { 52 | this.lists$ = this.data.lists; 53 | this.dialogTitle = '移动所有任务'; 54 | this.form = this.fb.group({ 55 | targetList: ['', Validators.required] 56 | }); 57 | } 58 | 59 | onSubmit({ value, valid }: FormGroup, ev: Event) { 60 | ev.preventDefault(); 61 | if (!valid) { 62 | return; 63 | } 64 | this.dialogRef.close({ 65 | srcListId: this.data.srcListId, 66 | targetListId: value.targetList 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/task/components/new-task-list.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | Inject, 5 | OnInit 6 | } from '@angular/core'; 7 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 8 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 9 | 10 | @Component({ 11 | selector: 'app-new-task-list', 12 | template: ` 13 |
14 |

{{ dialogTitle }}

15 |
16 | 17 | 18 | 19 |
20 |
21 | 29 | 30 |
31 |
32 | `, 33 | styles: [ 34 | ` 35 | :host { 36 | margin: 0; 37 | padding: 0; 38 | display: flex; 39 | flex-direction: column; 40 | flex-wrap: nowrap; 41 | } 42 | ` 43 | ], 44 | changeDetection: ChangeDetectionStrategy.OnPush 45 | }) 46 | export class NewTaskListComponent implements OnInit { 47 | form: FormGroup; 48 | dialogTitle: string; 49 | 50 | constructor( 51 | private fb: FormBuilder, 52 | @Inject(MAT_DIALOG_DATA) private data: any, 53 | private dialogRef: MatDialogRef 54 | ) {} 55 | 56 | ngOnInit() { 57 | if (!this.data.name) { 58 | this.form = this.fb.group({ 59 | name: [ 60 | '', 61 | Validators.compose([Validators.required, Validators.maxLength(10)]) 62 | ] 63 | }); 64 | this.dialogTitle = '创建列表:'; 65 | } else { 66 | this.form = this.fb.group({ 67 | name: [ 68 | this.data.name, 69 | Validators.compose([Validators.required, Validators.maxLength(10)]) 70 | ] 71 | }); 72 | this.dialogTitle = '修改列表:'; 73 | } 74 | } 75 | 76 | onSubmit(ev: Event) { 77 | ev.preventDefault(); 78 | if (!this.form.valid) { 79 | return; 80 | } 81 | this.dialogRef.close(this.form.value.name); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/task/components/quick-task.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter, HostListener, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-quick-task', 5 | template: ` 6 | 7 | 8 | 11 | 12 | `, 13 | styles: [``], 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class QuickTaskComponent { 17 | 18 | desc: string; 19 | @Output() quickTask = new EventEmitter(); 20 | 21 | constructor() { } 22 | 23 | @HostListener('keyup.enter') 24 | sendQuickTask() { 25 | if (!this.desc || this.desc.length === 0 || !this.desc.trim() || this.desc.length > 20) { 26 | return; 27 | } 28 | this.quickTask.emit(this.desc); 29 | this.desc = ''; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/task/components/task-history-item/index.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; 2 | import { TaskHistoryVM } from '../../../vm'; 3 | 4 | @Component({ 5 | selector: 'app-task-history-item', 6 | templateUrl: './task-history-item.component.html', 7 | styleUrls: ['./task-history-item.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class TaskHistoryItemComponent implements OnInit { 11 | 12 | @Input() item: TaskHistoryVM; 13 | 14 | constructor() { } 15 | 16 | ngOnInit() { 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/task/components/task-history-item/task-history-item.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ item.icon }} 4 | {{ item.title }} 5 | {{ item.dateDesc }} 6 |
7 |
8 | {{ item.content }} 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/task/components/task-history-item/task-history-item.component.scss: -------------------------------------------------------------------------------- 1 | .task-history-item { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | font-size: 12px; 6 | color: #A6A6A6; 7 | margin-bottom: 10px; 8 | } 9 | 10 | .task-history-item-title { 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | // width: 100%; //task-history-item default align-items is stretch 15 | } 16 | 17 | .task-history-item-title-icon { 18 | font-size: 18px; 19 | line-height: 24px; 20 | } 21 | 22 | .task-history-item-title-desc { 23 | margin-left: 10px; 24 | } 25 | 26 | .task-history-item-title-date { 27 | flex: 1 1 auto; 28 | text-align: end; 29 | } 30 | 31 | .task-history-item-desc { 32 | margin-left: 34px; 33 | border-left: 5px solid #A6A6A6; 34 | padding-left: 10px; 35 | color: #383838; 36 | } 37 | -------------------------------------------------------------------------------- /src/app/task/components/task-item/index.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, HostListener, Input, OnInit, Output} from '@angular/core'; 2 | import {Task} from '../../../domain'; 3 | import {itemAnim} from '../../../anim/item.anim'; 4 | import {TaskVM} from '../../../vm/task.vm'; 5 | 6 | @Component({ 7 | selector: 'app-task-item', 8 | templateUrl: './task-item.component.html', 9 | styleUrls: ['./task-item.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | animations: [itemAnim] 12 | }) 13 | export class TaskItemComponent implements OnInit { 14 | 15 | @Output() taskComplete = new EventEmitter(); 16 | @Output() taskClick = new EventEmitter(); 17 | @Input() item: TaskVM; 18 | avatar: string; 19 | widerPriority = 'in'; 20 | 21 | constructor() { 22 | } 23 | 24 | ngOnInit() { 25 | this.avatar = (this.item.owner) ? this.item.owner.avatar : 'unassigned'; 26 | } 27 | 28 | onCheckboxClick(ev: Event) { 29 | ev.stopPropagation(); 30 | } 31 | 32 | checkboxChanged() { 33 | this.taskComplete.emit(); 34 | } 35 | 36 | itemClicked(ev: Event) { 37 | ev.preventDefault(); 38 | this.taskClick.emit(); 39 | } 40 | 41 | @HostListener('mouseenter') 42 | handleMouseEnter() { 43 | this.widerPriority = 'out'; 44 | } 45 | 46 | @HostListener('mouseleave') 47 | handleMouseLeave() { 48 | this.widerPriority = 'in'; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/app/task/components/task-item/task-item.component.html: -------------------------------------------------------------------------------- 1 | 13 | 18 | 19 |
20 | {{ item.desc }} 21 |
22 |
23 | {{ item.dueDate | date:"yy-MM-dd" }} 24 | alarm 25 |
26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /src/app/task/components/task-item/task-item.component.scss: -------------------------------------------------------------------------------- 1 | mat-icon.avatar { 2 | overflow: hidden; 3 | width: 64px; 4 | height: 64px; 5 | border-radius: 50%; 6 | margin: 12px; 7 | order: 3; 8 | } 9 | 10 | .completed { 11 | opacity: 0.64; 12 | color: #d9d9d9; 13 | text-decoration: line-through; 14 | } 15 | 16 | .priority-normal { 17 | border-left: 3px solid #a6a6a6; 18 | } 19 | 20 | .priority-important { 21 | border-left: 3px solid #ffaf38; 22 | } 23 | 24 | .priority-emergency { 25 | border-left: 3px solid red; 26 | } 27 | 28 | .checkbox-section { 29 | border: 0 solid #a6a6a6; 30 | } 31 | 32 | .duedate { 33 | background-color: #ff4f3e; 34 | color: #fff; 35 | } 36 | 37 | .alarm { 38 | font-size: 18px; 39 | } 40 | 41 | .bottom-bar { 42 | margin-top: 3px; 43 | margin-bottom: 2px; 44 | font-size: 10px; 45 | width: 100%; 46 | order: 1; 47 | display: flex; 48 | flex: 0 0 18px; 49 | } 50 | 51 | .status { 52 | order: -1; 53 | } 54 | 55 | .content { 56 | order: 1; 57 | width: 100%; 58 | padding: 5px; 59 | } 60 | 61 | .container { 62 | width: 100%; 63 | border-radius: 3px; 64 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 65 | } 66 | 67 | .drag-start { 68 | opacity: 0.5; 69 | border: #ff525b dashed 2px; 70 | } 71 | 72 | :host { 73 | width: 100%; 74 | } 75 | -------------------------------------------------------------------------------- /src/app/task/components/task-list-header.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-task-list-header', 5 | template: ` 6 |
7 |
8 |

{{ header }}

9 |
10 |
11 | 15 |
16 |
17 | 20 |
21 |
22 | 23 | 27 | 28 | 32 | 33 | 37 | 38 | `, 39 | styles: [` 40 | .material-icon{ 41 | line-height: 1; 42 | } 43 | 44 | .fill{ 45 | text-align: center; 46 | } 47 | 48 | .header-container{ 49 | width: 100%; 50 | } 51 | `], 52 | changeDetection: ChangeDetectionStrategy.OnPush 53 | }) 54 | export class TaskListHeaderComponent { 55 | @Output() changeListName = new EventEmitter(); 56 | @Output() deleteList = new EventEmitter(); 57 | @Output() moveAllTasks = new EventEmitter(); 58 | @Output() newTask = new EventEmitter(); 59 | @Input() header = ''; 60 | 61 | constructor() { 62 | } 63 | 64 | onChangeListName(ev: Event) { 65 | ev.preventDefault(); 66 | this.changeListName.emit(); 67 | } 68 | 69 | onMoveAllTasks(ev: Event) { 70 | ev.preventDefault(); 71 | this.moveAllTasks.emit(); 72 | } 73 | 74 | onDeleteList(ev: Event) { 75 | ev.preventDefault(); 76 | this.deleteList.emit(); 77 | } 78 | 79 | addNewTask(ev: Event) { 80 | ev.preventDefault(); 81 | this.newTask.emit(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/task/components/task-list.ts: -------------------------------------------------------------------------------- 1 | import {ChangeDetectionStrategy, Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-task-list', 5 | template: ` 6 | 7 | 8 | 9 | `, 10 | styles: [``], 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class TaskListComponent {} 14 | -------------------------------------------------------------------------------- /src/app/task/containers/task-home/task-home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | visibility 4 | 视图 5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 |
14 | 17 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 | 31 | -------------------------------------------------------------------------------- /src/app/task/containers/task-home/task-home.component.scss: -------------------------------------------------------------------------------- 1 | .drag-start { 2 | opacity: 0.5; 3 | border: #ff525b dashed 2px; 4 | } 5 | 6 | .drag-enter { 7 | background-color: dimgray; 8 | } 9 | 10 | .task-lists-navigation { 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: flex-end; 14 | align-items: center; 15 | height: 50px; 16 | color: #808080; 17 | background: #F5F5F5; 18 | border-bottom: 1px solid #D9D9D9; 19 | } 20 | 21 | .task-lists-navigation-menu-container { 22 | display: flex; 23 | flex-direction: row; 24 | padding-left: 10px; 25 | padding-right: 10px; 26 | border-left: 1px solid #808080; 27 | cursor: pointer; 28 | } 29 | 30 | .task-lists-navigation-menu-container:hover { 31 | color: #3da8f5; 32 | } 33 | 34 | .task-lists-navigation-opened { 35 | color: #3da8f5; 36 | } 37 | 38 | .task-lists-navigation-menu-icon { 39 | font-size: 18px; 40 | line-height: 22px; 41 | } 42 | 43 | .task-lists-navigation-menu-text { 44 | font-size: 14px; 45 | } 46 | 47 | .task-lists-content-container { 48 | flex: 1; 49 | position: relative; 50 | } 51 | 52 | mat-sidenav-container { 53 | position: absolute; 54 | } 55 | 56 | .list-container { 57 | min-height: 100%; 58 | overflow-y: auto; 59 | overflow-x: hidden; 60 | } 61 | 62 | .task-lists { 63 | height: 100%; 64 | overflow-x: auto; 65 | } 66 | 67 | .fab-button { 68 | position: fixed; 69 | right: 32px; 70 | bottom: 96px; 71 | z-index: 998; 72 | } 73 | 74 | :host { 75 | display: flex; 76 | flex-direction: column; 77 | flex: 1 78 | } 79 | -------------------------------------------------------------------------------- /src/app/task/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '../shared'; 3 | import { TaskListComponent } from './components/task-list'; 4 | import { TaskItemComponent } from './components/task-item'; 5 | import { TaskRoutingModule } from './task-routing.module'; 6 | import { TaskHomeComponent } from './containers/task-home'; 7 | import { TaskListHeaderComponent } from './components/task-list-header'; 8 | import { NewTaskComponent } from './components/new-task'; 9 | import { NewTaskListComponent } from './components/new-task-list'; 10 | import { CopyTaskComponent } from './components/copy-task'; 11 | import { QuickTaskComponent } from './components/quick-task'; 12 | import { TaskHistoryItemComponent } from './components/task-history-item'; 13 | import { TaskFilterNavComponent } from './components/task-filter-nav'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | SharedModule, 18 | TaskRoutingModule 19 | ], 20 | declarations: [ 21 | TaskListComponent, 22 | TaskItemComponent, 23 | TaskHomeComponent, 24 | TaskListHeaderComponent, 25 | NewTaskComponent, 26 | NewTaskListComponent, 27 | CopyTaskComponent, 28 | QuickTaskComponent, 29 | TaskHistoryItemComponent, 30 | TaskFilterNavComponent, 31 | ], 32 | entryComponents: [ 33 | NewTaskComponent, 34 | NewTaskListComponent, 35 | CopyTaskComponent 36 | ] 37 | }) 38 | export class TaskModule { 39 | } 40 | -------------------------------------------------------------------------------- /src/app/task/task-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {TaskHomeComponent} from './containers/task-home'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: TaskHomeComponent, 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class TaskRoutingModule { 17 | } 18 | -------------------------------------------------------------------------------- /src/app/utils/area.util.ts: -------------------------------------------------------------------------------- 1 | import { city_data } from './area.data'; 2 | 3 | export const getProvinces = () => { 4 | const provinces: string[] = []; 5 | for (const province in city_data) { 6 | if (province) { 7 | provinces.push(province); 8 | } 9 | } 10 | return [...provinces]; 11 | }; 12 | 13 | export const getCitiesByProvince = (province: string) => { 14 | if (!province || !city_data[province]) { 15 | return []; 16 | } 17 | const cities = city_data[province]; 18 | const citiesByProvice: string[] = []; 19 | for (const city in cities) { 20 | if (city) { 21 | citiesByProvice.push(city); 22 | } 23 | } 24 | return [...citiesByProvice]; 25 | }; 26 | 27 | export const getAreasByCity = (province: string, city: string) => { 28 | if (!province || !city || !city_data[province][city]) { 29 | return []; 30 | } 31 | const areas = city_data[province][city]; 32 | return [...areas]; 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/utils/date.util.ts: -------------------------------------------------------------------------------- 1 | import { isValid, parseISO, format } from 'date-fns'; 2 | export const isValidDate = (dateStr: string) => { 3 | const date = parseISO(dateStr); 4 | return isValid(date); 5 | }; 6 | 7 | export const convertToDate = (date: Date) => { 8 | return format(date, 'yyyy-MM-dd'); 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/utils/identity.util.ts: -------------------------------------------------------------------------------- 1 | import { GB2260 } from './identity.data'; 2 | 3 | export function extractInfo(idNo: string) { 4 | const addrPart = idNo.substring(0, 6); // 前六位地址码 5 | const birthPart = idNo.substring(6, 14); // 八位生日 6 | const genderPart = parseInt(idNo.substring(14, 17), 10); // 性别 7 | 8 | return { 9 | addrCode: addrPart, 10 | dateOfBirth: birthPart, 11 | gender: genderPart % 2 !== 0 12 | }; 13 | } 14 | 15 | export function isValidAddr(code: string): boolean { 16 | return GB2260[code] !== undefined; 17 | } 18 | 19 | export const getAddrByCode = (code: string) => { 20 | const provinceStr = GB2260[code.substring(0, 2) + '0000']; 21 | const cityStr = GB2260[code.substring(0, 4) + '00']; 22 | const districtStr = GB2260[code]; 23 | const city = cityStr.replace(provinceStr, ''); 24 | const district = districtStr.replace(cityStr, ''); 25 | return { 26 | province: provinceStr, 27 | city: city, 28 | district: district 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/utils/reduer.util.ts: -------------------------------------------------------------------------------- 1 | export function covertArrToObj(arr: T[]) { 2 | return arr.reduce((entities, obj: T) => ({...entities, [obj.id]: obj}), {}); 3 | } 4 | 5 | export function buildObjFromArr(arr: string[], dict: {[id: string]: T}) { 6 | return arr.reduce((entities, id) => ({...entities, [id]: dict[id]}), {}); 7 | } 8 | 9 | export function loadCollection(state: {ids: string[]; entities: {[id: string]: T}}, collection: T[]) { 10 | const newItems = collection.filter(item => !state.entities[item.id]); 11 | const newIds = newItems.map(item => item.id); 12 | const newEntities = covertArrToObj(newItems); 13 | return { 14 | ids: [...state.ids, ...newIds], 15 | entities: {...state.entities, ...newEntities} 16 | }; 17 | } 18 | 19 | export function updateOne(state: {ids: string[]; entities: {[id: string]: T}}, updated: T) { 20 | const entities = {...state.entities, [updated.id]: updated}; 21 | return {...state, entities: entities}; 22 | } 23 | 24 | export function deleteOne(state: {ids: string[]; entities: {[id: string]: T}}, deleted: T) { 25 | const newIds = state.ids.filter(id => id !== deleted.id); 26 | const newEntities = buildObjFromArr(newIds, state.entities); 27 | return {ids: newIds, entities: newEntities}; 28 | } 29 | 30 | export function addOne(state: {ids: string[]; entities: {[id: string]: T}}, added: T) { 31 | const newIds = [...state.ids, added.id]; 32 | const newEntities = {...state.entities, [added.id]: added}; 33 | return {ids: newIds, entities: newEntities}; 34 | } 35 | -------------------------------------------------------------------------------- /src/app/utils/router.util.ts: -------------------------------------------------------------------------------- 1 | import { RouterStateSerializer } from '@ngrx/router-store'; 2 | import { RouterStateSnapshot, Params } from '@angular/router'; 3 | 4 | /** 5 | * The RouterStateSerializer takes the current RouterStateSnapshot 6 | * and returns any pertinent information needed. The snapshot contains 7 | * all information about the state of the router at the given point in time. 8 | * The entire snapshot is complex and not always needed. In this case, you only 9 | * need the URL and query parameters from the snapshot in the store. Other items could be 10 | * returned such as route parameters and static route data. 11 | */ 12 | 13 | export interface RouterStateUrl { 14 | url: string; 15 | queryParams: Params; 16 | } 17 | 18 | export class CustomRouterStateSerializer 19 | implements RouterStateSerializer { 20 | serialize(routerState: RouterStateSnapshot): RouterStateUrl { 21 | const { url } = routerState; 22 | const queryParams = routerState.root.queryParams; 23 | 24 | return { url, queryParams }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/utils/svg.util.ts: -------------------------------------------------------------------------------- 1 | import { MatIconRegistry } from '@angular/material/icon'; 2 | import { DomSanitizer } from '@angular/platform-browser'; 3 | import * as _ from 'lodash'; 4 | 5 | /** 6 | * a utility to load all needed svg resources to the app for mat-icon to use 7 | * 8 | * @param ir a MatIconRegistry instance to use external svg resources for mat-icon use 9 | * @param ds a DomSanitizer instance to bypass security and return a url 10 | */ 11 | export const loadSvgResources = (ir: MatIconRegistry, ds: DomSanitizer) => { 12 | const imgDir = 'assets/img'; 13 | const avatarDir = `${imgDir}/avatar`; 14 | const sidebarDir = `${imgDir}/sidebar`; 15 | const iconDir = `${imgDir}/icons`; 16 | const dayDir = `${imgDir}/days`; 17 | ir.addSvgIconSetInNamespace( 18 | 'avatars', 19 | ds.bypassSecurityTrustResourceUrl(`${avatarDir}/avatars.svg`) 20 | ) 21 | .addSvgIcon( 22 | 'unassigned', 23 | ds.bypassSecurityTrustResourceUrl(`${avatarDir}/unassigned.svg`) 24 | ) 25 | .addSvgIcon( 26 | 'project', 27 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/project.svg`) 28 | ) 29 | .addSvgIcon( 30 | 'projects', 31 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/projects.svg`) 32 | ) 33 | .addSvgIcon( 34 | 'month', 35 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/month.svg`) 36 | ) 37 | .addSvgIcon( 38 | 'week', 39 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/week.svg`) 40 | ) 41 | .addSvgIcon( 42 | 'day', 43 | ds.bypassSecurityTrustResourceUrl(`${sidebarDir}/day.svg`) 44 | ) 45 | .addSvgIcon( 46 | 'move', 47 | ds.bypassSecurityTrustResourceUrl(`${iconDir}/move.svg`) 48 | ); 49 | const days = _.range(1, 31); 50 | days.forEach(day => 51 | ir.addSvgIcon( 52 | `day${day}`, 53 | ds.bypassSecurityTrustResourceUrl(`${dayDir}/day${day}.svg`) 54 | ) 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/utils/type.util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function coerces a string into a string literal type. 3 | * Using tagged union types in TypeScript 2.0, this enables 4 | * powerful typechecking of our reducers. 5 | * 6 | * Since every action label passes through this function it 7 | * is a good place to ensure all of our action labels 8 | * are unique. 9 | */ 10 | const typeCache: { [label: string]: boolean } = {}; 11 | 12 | export function type(label: T | ''): T { 13 | if (typeCache[label]) { 14 | throw new Error(`Action type "${label}" is not unique"`); 15 | } 16 | typeCache[label] = true; 17 | return label; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/utils/vm.util.ts: -------------------------------------------------------------------------------- 1 | import {TaskVM, TaskListVM, ProjectVM} from '../vm'; 2 | import {Task, TaskList, Project, User} from '../domain'; 3 | 4 | export const covertToTask = (taskVM: TaskVM): Task => { 5 | return { 6 | id: taskVM.id, 7 | desc: taskVM.desc, 8 | completed: taskVM.completed, 9 | priority: taskVM.priority, 10 | taskListId: taskVM.taskListId, 11 | dueDate: taskVM.dueDate, 12 | createDate: taskVM.createDate, 13 | reminder: taskVM.reminder, 14 | remark: taskVM.remark, 15 | ownerId: taskVM.owner ? taskVM.owner.id : '', 16 | participantIds: taskVM.participants ? taskVM.participants.map(user => user.id) : [] 17 | }; 18 | }; 19 | 20 | export const converToTaskList = (taskListVM: TaskListVM): TaskList => { 21 | return { 22 | id: taskListVM.id, 23 | name: taskListVM.name, 24 | order: taskListVM.order, 25 | projectId: taskListVM.projectId, 26 | taskIds: taskListVM.tasks.map((task: TaskVM) => task.id) 27 | }; 28 | }; 29 | 30 | export const convertToProject = (projectVM: ProjectVM): Project => { 31 | return { 32 | id: projectVM.id, 33 | name: projectVM.name, 34 | coverImg: projectVM.coverImg, 35 | desc: projectVM.desc, 36 | enabled: projectVM.enabled, 37 | members: projectVM.members ? projectVM.members.map((user: User) => user.id) : [], 38 | taskLists: projectVM.taskLists ? projectVM.taskLists.map((tl: TaskListVM) => tl.id) : [] 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/app/vm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './project.vm'; 2 | export * from './task-list.vm'; 3 | export * from './task.vm'; 4 | export * from './task-history.vm'; 5 | export * from './task-filter.vm'; 6 | -------------------------------------------------------------------------------- /src/app/vm/project.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain/user'; 2 | import { TaskListVM } from './task-list.vm'; 3 | 4 | export interface ProjectVM { 5 | id: string | null; 6 | name: string; 7 | desc?: string; 8 | coverImg?: string; 9 | enabled?: boolean; 10 | taskLists?: TaskListVM[]; 11 | members?: User[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/vm/task-filter.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain'; 2 | 3 | export interface TaskFilterVM { 4 | id: string | undefined; 5 | projectId: string; 6 | desc?: string; 7 | sort: string; 8 | hasOwner: boolean; 9 | hasDueDate: boolean; 10 | hasCreateDate: boolean; 11 | hasPriority: boolean; 12 | customCreateDate: TaskFilterCustomDate; 13 | sortVMs: TaskFilterItemVM[]; 14 | ownerVMs: TaskFilterOwnerVM[]; 15 | dueDateVMs: TaskFilterItemVM[]; 16 | createDateVMs: TaskFilterItemVM[]; 17 | priorityVMs: TaskFilterPriorityVM[]; 18 | categoryVMs: TaskFilterItemVM[]; 19 | } 20 | 21 | export interface TaskFilterItemVM { 22 | label: string; 23 | value: string; 24 | checked: boolean; 25 | hasExtra?: boolean; 26 | } 27 | 28 | export interface TaskFilterOwnerVM { 29 | owner?: User; 30 | checked: boolean; 31 | } 32 | 33 | export interface TaskFilterPriorityVM { 34 | label: string; 35 | value: number; 36 | checked: boolean; 37 | } 38 | 39 | export interface TaskFilterCustomDate { 40 | startDate: Date; 41 | endDate: Date; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/vm/task-history.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain'; 2 | 3 | export interface TaskHistoryVM { 4 | id?: string; 5 | taskId: string; 6 | icon?: string; 7 | title: string; 8 | content?: string; 9 | date: Date; 10 | dateDesc: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/vm/task-list.vm.ts: -------------------------------------------------------------------------------- 1 | import { TaskVM } from './task.vm'; 2 | 3 | export interface TaskListVM { 4 | id?: string | null; 5 | name: string; 6 | projectId: string; 7 | order: number; 8 | tasks: TaskVM[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/vm/task.vm.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain/user'; 2 | export interface TaskVM { 3 | id?: string; 4 | taskListId: string; 5 | desc: string; 6 | completed: boolean; 7 | owner?: User; 8 | participants?: User[]; 9 | dueDate?: Date; 10 | priority: number; 11 | // order: number; 12 | remark?: string; 13 | // tags?: string[]; 14 | reminder?: Date; 15 | createDate?: Date; 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/img/avatar/unassigned.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/covers/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/0.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/0_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/0_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/1.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/10.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/10_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/10_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/11.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/11_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/11_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/12.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/12_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/12_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/13.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/13_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/13_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/14.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/14_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/14_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/15.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/15_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/15_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/16.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/16_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/16_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/17.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/17_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/17_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/18.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/18_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/18_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/19.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/19_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/19_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/1_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/1_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/2.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/20.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/20_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/20_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/21.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/21_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/21_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/22.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/22_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/22_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/23.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/23_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/23_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/24.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/24_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/24_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/25.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/25_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/25_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/26.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/26_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/26_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/27.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/27_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/27_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/28.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/28_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/28_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/29.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/29_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/29_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/2_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/2_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/3.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/30.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/30_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/30_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/31.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/31.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/31_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/31_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/32.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/32_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/32_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/33.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/33.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/33_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/33_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/34.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/34.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/34_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/34_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/35.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/35.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/35_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/35_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/36.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/36.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/36_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/36_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/37.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/37.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/37_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/37_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/38.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/38.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/38_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/38_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/39.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/39_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/39_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/3_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/3_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/4.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/4_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/4_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/5.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/5_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/5_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/6.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/6_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/6_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/7.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/7_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/7_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/8.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/8_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/8_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/9.jpg -------------------------------------------------------------------------------- /src/assets/img/covers/9_tn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/covers/9_tn.jpg -------------------------------------------------------------------------------- /src/assets/img/days/day1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day11.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day14.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day17.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/days/day7.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/developer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/developer.png -------------------------------------------------------------------------------- /src/assets/img/icons/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/icons/burger-navigation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/icons/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/icons/move.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/not-found.png -------------------------------------------------------------------------------- /src/assets/img/quote_fallback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quote_fallback.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/0.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/1.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/2.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/3.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/4.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/5.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/6.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/7.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/8.jpg -------------------------------------------------------------------------------- /src/assets/img/quotes/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/assets/img/quotes/9.jpg -------------------------------------------------------------------------------- /src/assets/img/sidebar/day.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/month.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/project.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/projects.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/sidebar/week.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/environments/environment.hmr.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | hmr: true 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | hmr: false 4 | }; 5 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | hmr: false 9 | }; 10 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcfan/taskmgr/8903dc9ea72c58c6adc9ace1485688cabacd5849/src/favicon.ico -------------------------------------------------------------------------------- /src/hmr.ts: -------------------------------------------------------------------------------- 1 | import { NgModuleRef, ApplicationRef } from '@angular/core'; 2 | import { createNewHosts } from '@angularclass/hmr'; 3 | 4 | export const hmrBootstrap = (module: any, bootstrap: () => Promise>) => { 5 | let ngModule: NgModuleRef; 6 | module.hot.accept(); 7 | bootstrap().then(mod => ngModule = mod); 8 | module.hot.dispose(() => { 9 | const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef); 10 | const elements = appRef.components.map(c => c.location.nativeElement); 11 | const makeVisible = createNewHosts(elements); 12 | ngModule.destroy(); 13 | makeVisible(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 企业协作平台 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | Loading... 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { environment } from './environments/environment'; 2 | import { enableProdMode } from '@angular/core'; 3 | 4 | if (environment.production) { 5 | enableProdMode(); 6 | } 7 | 8 | export {AppServerModule} from './app/app.server.module'; 9 | -------------------------------------------------------------------------------- /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 | import { hmrBootstrap } from './hmr'; 7 | /** 8 | * HammerJS needed to import here to make universal build works 9 | */ 10 | import 'hammerjs'; 11 | 12 | if (environment.production) { 13 | enableProdMode(); 14 | } 15 | 16 | document.addEventListener('DOMContentLoaded', () => { 17 | platformBrowserDynamic() 18 | .bootstrapModule(AppModule) 19 | .catch(err => console.log(err)); 20 | }); 21 | -------------------------------------------------------------------------------- /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 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 24 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 25 | /** Evergreen browsers require these. **/ 26 | // import 'core-js/es6/reflect'; 27 | 28 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | /*************************************************************************************************** 31 | * Zone JS is required by Angular itself. 32 | */ 33 | import 'zone.js/dist/zone'; // Included with Angular CLI. 34 | 35 | /*************************************************************************************************** 36 | * APPLICATION IMPORTS 37 | */ 38 | 39 | /** 40 | * Date, currency, decimal and percent pipes. 41 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 42 | */ 43 | // import 'intl'; // Run `npm install --save intl`. 44 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'theme.scss'; 2 | 3 | @mixin md-icon-size($size: 24px) { 4 | font-size: $size; 5 | height: $size; 6 | width: $size; 7 | line-height: $size; 8 | } 9 | 10 | html, 11 | body, 12 | app-root, 13 | mat-sidenav-container { 14 | margin: 0; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | .site { 20 | width: 100%; 21 | min-height: 100%; 22 | } 23 | 24 | .full-width { 25 | width: 100%; 26 | } 27 | 28 | .fill-remaining-space { 29 | // 使用 flexbox 填充剩余空间 30 | // @angular/material 中的很多控件使用了 flex 布局 31 | flex: 1 1 auto; 32 | } 33 | -------------------------------------------------------------------------------- /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/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "strict": false, 6 | "strictNullChecks": true, 7 | "skipLibCheck": true, 8 | "baseUrl": "", 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "baseUrl": "./", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ], 13 | "angularCompilerOptions": { 14 | "entryModule": "app/app.server.module#AppServerModule" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "", 6 | "types": ["jasmine", "node"] 7 | }, 8 | "files": ["test.ts", "polyfills.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | 4 | interface NodeModule { 5 | id: string; 6 | [key:string]: any; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "module": "esnext", 8 | "outDir": "./dist/out-tsc", 9 | "sourceMap": true, 10 | "declaration": false, 11 | "moduleResolution": "node", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es2015", 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["es2017", "dom"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "eofline": true, 13 | "forin": true, 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "import-spacing": true, 16 | "indent": [true, "spaces"], 17 | "interface-over-type-literal": true, 18 | "label-position": true, 19 | "max-line-length": [true, 140], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | { 24 | "order": [ 25 | "static-field", 26 | "instance-field", 27 | "static-method", 28 | "instance-method" 29 | ] 30 | } 31 | ], 32 | "no-arg": true, 33 | "no-bitwise": true, 34 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 35 | "no-construct": true, 36 | "no-debugger": true, 37 | "no-duplicate-super": true, 38 | "no-empty": false, 39 | "no-empty-interface": true, 40 | "no-eval": true, 41 | "no-inferrable-types": [true, "ignore-params"], 42 | "no-misused-new": true, 43 | "no-non-null-assertion": true, 44 | "no-shadowed-variable": true, 45 | "no-string-literal": false, 46 | "no-string-throw": true, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unnecessary-initializer": true, 50 | "no-unused-expression": true, 51 | "no-use-before-declare": true, 52 | "no-var-keyword": true, 53 | "object-literal-sort-keys": false, 54 | "one-line": [ 55 | true, 56 | "check-open-brace", 57 | "check-catch", 58 | "check-else", 59 | "check-whitespace" 60 | ], 61 | "prefer-const": true, 62 | "quotemark": [true, "single"], 63 | "radix": true, 64 | "semicolon": [true, "always"], 65 | "triple-equals": [true, "allow-null-check"], 66 | "typedef-whitespace": [ 67 | true, 68 | { 69 | "call-signature": "nospace", 70 | "index-signature": "nospace", 71 | "parameter": "nospace", 72 | "property-declaration": "nospace", 73 | "variable-declaration": "nospace" 74 | } 75 | ], 76 | "unified-signatures": true, 77 | "variable-name": false, 78 | "whitespace": [ 79 | true, 80 | "check-branch", 81 | "check-decl", 82 | "check-operator", 83 | "check-separator", 84 | "check-type" 85 | ], 86 | "no-output-on-prefix": true, 87 | "no-inputs-metadata-property": true, 88 | "no-outputs-metadata-property": true, 89 | "no-host-metadata-property": true, 90 | "no-input-rename": true, 91 | "no-output-rename": true, 92 | "use-lifecycle-interface": true, 93 | "use-pipe-transform-interface": true, 94 | "component-class-suffix": true, 95 | "directive-class-suffix": true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "modules", 3 | "out": "doc", 4 | "theme": "default", 5 | "ignoreCompilerErrors": "true", 6 | "experimentalDecorators": "true", 7 | "emitDecoratorMetadata": "true", 8 | "target": "ES5", 9 | "moduleResolution": "node", 10 | "preserveConstEnums": "true", 11 | "stripInternal": "true", 12 | "suppressExcessPropertyErrors": "true", 13 | "suppressImplicitAnyIndexErrors": "true", 14 | "module": "commonjs" 15 | } 16 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | mode: 'none', 6 | entry: { 7 | server: './server.ts' 8 | }, 9 | target: 'node', 10 | resolve: { extensions: ['.ts', '.js'] }, 11 | optimization: { 12 | minimize: false 13 | }, 14 | output: { 15 | // Puts the output at the root of the dist folder 16 | path: path.join(__dirname, 'dist'), 17 | filename: '[name].js' 18 | }, 19 | module: { 20 | rules: [ 21 | { test: /\.ts$/, loader: 'ts-loader' }, 22 | { 23 | // Mark files inside `@angular/core` as using SystemJS style dynamic imports. 24 | // Removing this will cause deprecation warnings to appear. 25 | test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/, 26 | parser: { system: true } 27 | } 28 | ] 29 | }, 30 | plugins: [ 31 | new webpack.ContextReplacementPlugin( 32 | // fixes WARNING Critical dependency: the request of a dependency is an expression 33 | /(.+)?angular(\\|\/)core(.+)?/, 34 | path.join(__dirname, 'src'), // location of your src 35 | {} // a map of your routes 36 | ), 37 | new webpack.ContextReplacementPlugin( 38 | // fixes WARNING Critical dependency: the request of a dependency is an expression 39 | /(.+)?express(\\|\/)(.+)?/, 40 | path.join(__dirname, 'src'), 41 | {} 42 | ) 43 | ] 44 | }; 45 | --------------------------------------------------------------------------------