├── .browserslistrc ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .lintstagedrc.js ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README-zh_CN.md ├── README.md ├── _cli-tpl └── test │ └── __path__ │ └── __name@dasherize@if-flat__ │ ├── __name@dasherize__.component.html │ ├── __name@dasherize__.component.spec.ts │ └── __name@dasherize__.component.ts ├── _mock ├── README.md ├── _user.ts └── index.ts ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── image-files ├── etc │ └── nginx │ │ └── nginx.conf └── usr │ └── local │ └── bin │ ├── docker-entrypoint-dev.sh │ └── docker-entrypoint.sh ├── karma.conf.js ├── package-lock.json ├── package.json ├── proxy.conf.json ├── scripts └── color-less.js ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── README.md │ │ ├── core.module.ts │ │ ├── i18n │ │ │ ├── i18n.service.spec.ts │ │ │ └── i18n.service.ts │ │ ├── index.ts │ │ ├── module-import-guard.ts │ │ ├── net │ │ │ └── default.interceptor.ts │ │ └── startup │ │ │ └── startup.service.ts │ ├── global-config.module.ts │ ├── layout │ │ ├── default │ │ │ ├── default.component.html │ │ │ ├── default.component.ts │ │ │ ├── header │ │ │ │ ├── components │ │ │ │ │ ├── fullscreen.component.ts │ │ │ │ │ ├── i18n.component.ts │ │ │ │ │ ├── icon.component.ts │ │ │ │ │ ├── notify.component.ts │ │ │ │ │ ├── search.component.ts │ │ │ │ │ ├── storage.component.ts │ │ │ │ │ ├── task.component.ts │ │ │ │ │ └── user.component.ts │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.ts │ │ │ │ └── index.md │ │ │ ├── setting-drawer │ │ │ │ ├── setting-drawer-item.component.html │ │ │ │ ├── setting-drawer-item.component.ts │ │ │ │ ├── setting-drawer.component.html │ │ │ │ └── setting-drawer.component.ts │ │ │ └── sidebar │ │ │ │ ├── sidebar.component.html │ │ │ │ └── sidebar.component.ts │ │ ├── fullscreen │ │ │ ├── fullscreen.component.html │ │ │ └── fullscreen.component.ts │ │ ├── layout.module.ts │ │ └── passport │ │ │ ├── passport.component.html │ │ │ ├── passport.component.less │ │ │ └── passport.component.ts │ ├── routes │ │ ├── account │ │ │ ├── account-routing.module.ts │ │ │ ├── account.module.ts │ │ │ ├── form │ │ │ │ ├── form.component.html │ │ │ │ └── form.component.ts │ │ │ ├── index │ │ │ │ ├── index.component.html │ │ │ │ ├── index.component.less │ │ │ │ └── index.component.ts │ │ │ └── view │ │ │ │ ├── view.component.html │ │ │ │ └── view.component.ts │ │ ├── analysis │ │ │ ├── analysis-routing.module.ts │ │ │ ├── analysis.module.ts │ │ │ └── index │ │ │ │ ├── index.component.html │ │ │ │ ├── index.component.less │ │ │ │ └── index.component.ts │ │ ├── callback │ │ │ └── callback.component.ts │ │ ├── dashboard │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.less │ │ │ └── dashboard.component.ts │ │ ├── exception │ │ │ ├── 403.component.ts │ │ │ ├── 404.component.ts │ │ │ ├── 500.component.ts │ │ │ ├── exception-routing.module.ts │ │ │ ├── exception.module.ts │ │ │ └── trigger.component.ts │ │ ├── passport │ │ │ ├── lock │ │ │ │ ├── lock.component.html │ │ │ │ ├── lock.component.less │ │ │ │ └── lock.component.ts │ │ │ ├── login │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.less │ │ │ │ └── login.component.ts │ │ │ ├── register-result │ │ │ │ ├── register-result.component.html │ │ │ │ └── register-result.component.ts │ │ │ └── register │ │ │ │ ├── register.component.html │ │ │ │ ├── register.component.less │ │ │ │ └── register.component.ts │ │ ├── record │ │ │ ├── create-by-desc │ │ │ │ ├── create-by-desc.component.html │ │ │ │ └── create-by-desc.component.ts │ │ │ ├── form │ │ │ │ ├── form.component.html │ │ │ │ └── form.component.ts │ │ │ ├── import │ │ │ │ ├── import.component.html │ │ │ │ └── import.component.ts │ │ │ ├── index │ │ │ │ ├── index.component.html │ │ │ │ ├── index.component.less │ │ │ │ └── index.component.ts │ │ │ ├── record-routing.module.ts │ │ │ └── record.module.ts │ │ ├── recurrence │ │ │ ├── form │ │ │ │ ├── form.component.html │ │ │ │ └── form.component.ts │ │ │ ├── index │ │ │ │ ├── index.component.html │ │ │ │ └── index.component.ts │ │ │ ├── recurrence-routing.module.ts │ │ │ └── recurrence.module.ts │ │ ├── routes-routing.module.ts │ │ ├── routes.module.ts │ │ └── settings │ │ │ ├── categories │ │ │ ├── categories.component.html │ │ │ ├── categories.component.ts │ │ │ ├── edit │ │ │ │ ├── edit.component.html │ │ │ │ └── edit.component.ts │ │ │ └── view │ │ │ │ ├── view.component.html │ │ │ │ └── view.component.ts │ │ │ ├── personal │ │ │ ├── base │ │ │ │ ├── base.component.html │ │ │ │ └── base.component.ts │ │ │ ├── binding │ │ │ │ ├── binding.component.html │ │ │ │ └── binding.component.ts │ │ │ ├── personal.component.html │ │ │ ├── personal.component.less │ │ │ ├── personal.component.ts │ │ │ └── security │ │ │ │ ├── security.component.html │ │ │ │ └── security.component.ts │ │ │ ├── rules │ │ │ ├── form │ │ │ │ ├── form.component.html │ │ │ │ └── form.component.ts │ │ │ ├── rules.component.html │ │ │ └── rules.component.ts │ │ │ ├── settings-routing.module.ts │ │ │ ├── settings.module.ts │ │ │ └── tags │ │ │ ├── edit │ │ │ ├── edit.component.html │ │ │ └── edit.component.ts │ │ │ ├── tags.component.html │ │ │ ├── tags.component.ts │ │ │ └── view │ │ │ ├── view.component.html │ │ │ └── view.component.ts │ └── shared │ │ ├── index.ts │ │ ├── json-schema │ │ └── json-schema.module.ts │ │ ├── search │ │ ├── search.component.html │ │ ├── search.component.less │ │ └── search.component.ts │ │ ├── shared-delon.module.ts │ │ ├── shared-zorro.module.ts │ │ ├── shared.module.ts │ │ ├── st-widget │ │ └── st-widget.module.ts │ │ └── utils │ │ └── yuan.ts ├── assets │ ├── .gitkeep │ ├── alain-default.less │ ├── import-template.csv │ ├── logo-color.svg │ ├── logo-full.svg │ ├── logo.svg │ ├── tmp │ │ ├── app-data.json │ │ ├── i18n │ │ │ ├── el-GR.json │ │ │ ├── en-US.json │ │ │ ├── hr-HR.json │ │ │ ├── ko-KR.json │ │ │ ├── pl-PL.json │ │ │ ├── sl-SI.json │ │ │ ├── tr-TR.json │ │ │ ├── zh-CN.json │ │ │ └── zh-TW.json │ │ └── img │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── avatar.jpg │ │ │ ├── bg1.jpg │ │ │ ├── bg10.jpg │ │ │ ├── bg2.jpg │ │ │ ├── bg3.jpg │ │ │ ├── bg4.jpg │ │ │ ├── bg5.jpg │ │ │ ├── bg6.jpg │ │ │ ├── bg7.jpg │ │ │ ├── bg8.jpg │ │ │ ├── bg9.jpg │ │ │ └── half-float-bg-1.jpg │ └── zorro.svg ├── environments │ ├── environment.hmr.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── hmr.ts ├── index.html ├── main.ts ├── polyfills.ts ├── style-icons-auto.ts ├── style-icons.ts ├── styles.less ├── styles │ ├── index.less │ └── theme.less ├── test.ts └── typings.d.ts ├── tsconfig.app.json ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 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 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [forecho] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Deploy CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [14.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Setup kernel for react native, increase watchers 18 | run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 19 | 20 | - name: Cache node modules 21 | uses: actions/cache@v1 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-node- 27 | 28 | - name: Node ${{ matrix.node-version }} 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | 33 | - name: npm ci and npm run build 34 | run: | 35 | npm ci 36 | (echo y; echo y; echo y; echo y;) | sudo npm install -g @angular/cli 37 | ng build --prod 38 | 39 | - name: Deploy 40 | uses: peaceiris/actions-gh-pages@v3 41 | with: 42 | github_token: ${{ secrets.GITHUB_TOKEN }} 43 | publish_dir: ./dist 44 | user_name: 'github-actions[bot]' 45 | user_email: 'github-actions[bot]@users.noreply.github.com' 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | module.exports = { 4 | 'src/**/*.ts': () => [`npm run lint:ts`, 'git add'], 5 | 'src/**/*.html': ['./node_modules/.bin/prettier --write', 'git add'], 6 | 'src/**/*.less': ['npm run lint:style', 'git add'], 7 | }; 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.14.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # add files you wish to ignore here 2 | **/*.md 3 | **/*.svg 4 | **/test.ts 5 | 6 | .stylelintrc 7 | .prettierrc 8 | 9 | src/assets/* 10 | src/index.html 11 | node_modules/ 12 | .vscode/ 13 | coverage/ 14 | dist/ 15 | package.json 16 | tslint.json 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "trailingComma": "all", 6 | "proseWrap": "never" 7 | } 8 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-rational-order", 5 | "stylelint-config-prettier" 6 | ], 7 | "plugins": [ 8 | "stylelint-order", 9 | "stylelint-declaration-block-no-ignored-properties" 10 | ], 11 | "rules": { 12 | "no-descending-specificity": null, 13 | "plugin/declaration-block-no-ignored-properties": true, 14 | "selector-type-no-unknown": [ 15 | true, 16 | { 17 | "ignoreTypes": [ 18 | "/^g2-/", 19 | "/^nz-/", 20 | "/^app-/" 21 | ] 22 | } 23 | ], 24 | "selector-pseudo-element-no-unknown": [ 25 | true, 26 | { 27 | "ignorePseudoElements": [ 28 | "ng-deep" 29 | ] 30 | } 31 | ] 32 | }, 33 | "ignoreFiles": [ 34 | "src/assets/**/*" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "cipchk.ng-alain-extension-pack" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceRoot}", 13 | "sourceMaps": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.tslint": true, 6 | "source.fixAll.stylelint": true 7 | }, 8 | "[markdown]": { 9 | "editor.formatOnSave": false 10 | }, 11 | "[javascript]": { 12 | "editor.formatOnSave": false 13 | }, 14 | "[json]": { 15 | "editor.formatOnSave": false 16 | }, 17 | "[jsonc]": { 18 | "editor.formatOnSave": false 19 | }, 20 | "files.watcherExclude": { 21 | "**/.git/*/**": true, 22 | "**/node_modules/*/**": true, 23 | "**/dist/*/**": true, 24 | "**/coverage/*/**": true 25 | }, 26 | "files.associations": { 27 | "*.json": "jsonc", 28 | ".prettierrc": "jsonc", 29 | ".stylelintrc": "jsonc" 30 | }, 31 | // Angular schematics 插件: https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics 32 | "ngschematics.schematics": [ 33 | "ng-alain" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:stable-alpine AS production-stage 2 | 3 | WORKDIR /srv 4 | 5 | COPY image-files/ / 6 | 7 | COPY dist /srv 8 | 9 | EXPOSE 80 10 | 11 | ENTRYPOINT [ "docker-entrypoint.sh" ] 12 | 13 | CMD ["nginx", "-g", "daemon off;"] 14 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | ################################# 2 | # Development 3 | ################################# 4 | FROM node:13-alpine as build-stage 5 | 6 | WORKDIR /app 7 | 8 | COPY package*.json ./ 9 | 10 | # Install app dependencies 11 | RUN npm install -g @angular/cli 12 | RUN npm set progress=false 13 | RUN npm install 14 | 15 | # Copy all files to app folder 16 | COPY . . 17 | 18 | # Add configuration files 19 | COPY image-files/ / 20 | 21 | # Install nginx 22 | RUN apk --update add nginx 23 | 24 | # Expose port 80 25 | EXPOSE 80 26 | 27 | ENTRYPOINT ["docker-entrypoint-dev.sh"] 28 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

CashWarden

8 | 9 |
10 | 一款开源的资产管理系统 11 | 12 | [![CI](https://github.com/cashwarden/web/workflows/Deploy%20CI/badge.svg)](https://github.com/cashwarden/web/actions) 13 | [![GitHub Release Date](https://img.shields.io/github/release-date/cashwarden/web.svg?style=flat-square)](https://github.com/cashwarden/web/releases) 14 | [![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://prettier.io/) 15 | [![GitHub](https://img.shields.io/github/license/cashwarden/web)](https://github.com/cashwarden/web/blob/master/LICENSE) 16 | [![Telegram](https://img.shields.io/badge/telegram-cashwarden-green?logo=telegram&;style=flat)](https://t.me/cashwarden) 17 | 18 |
19 | 20 | [English](README.md) | 简体中文 21 | 22 | ## 特性 23 | 24 | + 记账以及快速记账 25 | + 使用 Telegram Bot 快速记账 26 | + 账户管理 27 | + 标签和分类 28 | + 定时记账 29 | 30 | ## 使用 31 | 32 | - [简介以及使用指南](https://blog.forecho.com/hello-cashwarden.html) 33 | 34 | ## 开发 35 | 36 | - [基于 NG-ALAIN](https://ng-alain.com/) 37 | - [基于 NG-ZORRO](https://ng.ant.design/) 38 | - [Cashwarden Core API](https://github.com/cashwarden/core-api) 39 | 40 | ## 应用截图 41 | 42 | ![仪表盘](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20200922LW1UYR.jpg) 43 | ![定时记账](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/202009224a6mYh.jpg) 44 | ![规则设置](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20200922dSaFoD.jpg) 45 | ![记录](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20200922P5FGaQ.jpg) 46 | ![账户](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/202009228aZsEz.jpg) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

CashWarden

8 | 9 |
10 | An open source asset management system 11 | 12 | [![CI](https://github.com/cashwarden/web/workflows/Deploy%20CI/badge.svg)](https://github.com/cashwarden/web/actions) 13 | [![GitHub Release Date](https://img.shields.io/github/release-date/cashwarden/web.svg?style=flat-square)](https://github.com/cashwarden/web/releases) 14 | [![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://prettier.io/) 15 | [![GitHub](https://img.shields.io/github/license/cashwarden/web)](https://github.com/cashwarden/web/blob/master/LICENSE) 16 | [![Telegram](https://img.shields.io/badge/telegram-cashwarden-green?logo=telegram&;style=flat)](https://t.me/cashwarden) 17 | 18 |
19 | 20 | English | [简体中文](README-zh_CN.md) 21 | 22 | ## Characteristics 23 | 24 | + Bookkeeping and QuickBooks 25 | + QuickBooks with Telegram Bot 26 | + Account management 27 | + Tags and classifications 28 | + Bookkeeping 29 | 30 | ## Use 31 | 32 | - [Introduction and user's guide](https://blog.forecho.com/hello-cashwarden.html) 33 | 34 | ## Development 35 | 36 | - [Based on NG-ALAIN](https://ng-alain.com/) 37 | - [Based on NG-ZORRO](https://ng.ant.design/) 38 | - [Cashwarden Core API](https://github.com/cashwarden/core-api) 39 | 40 | ## App screenshots 41 | 42 | ![Dashboard](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20200922LW1UYR.jpg) 43 | ![Timed bookkeeping](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/202009224a6mYh.jpg) 44 | ![Rule setting](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20200922dSaFoD.jpg) 45 | ![Record](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/20200922P5FGaQ.jpg) 46 | ![Account](https://blog-1251237404.cos.ap-guangzhou.myqcloud.com/202009228aZsEz.jpg) 47 | 48 | -------------------------------------------------------------------------------- /_cli-tpl/test/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /_cli-tpl/test/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { <%= componentName %> } from './<%= dasherize(name) %>.component'; 3 | 4 | describe('<%= componentName %>', () => { 5 | let component: <%= componentName %>; 6 | let fixture: ComponentFixture<<%= componentName %>>; 7 | 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ <%= componentName %> ] 11 | }) 12 | .compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(<%= componentName %>); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /_cli-tpl/test/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core'; 2 | import { _HttpClient } from '@delon/theme'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | 5 | @Component({ 6 | selector: '<%= selector %>', 7 | templateUrl: './<%= dasherize(name) %>.component.html',<% if(!inlineStyle) { %><% } else { %> 8 | styleUrls: ['./<%= dasherize(name) %>.component.<%= style %>']<% } %><% if(!!viewEncapsulation) { %>, 9 | encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>, 10 | changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %> 11 | }) 12 | export class <%= componentName %> implements OnInit { 13 | 14 | constructor(private http: _HttpClient, private msg: NzMessageService) { } 15 | 16 | ngOnInit() { } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /_mock/README.md: -------------------------------------------------------------------------------- 1 | [Document](https://ng-alain.com/mock) 2 | -------------------------------------------------------------------------------- /_mock/_user.ts: -------------------------------------------------------------------------------- 1 | import { MockRequest } from '@delon/mock'; 2 | 3 | const list: any[] = []; 4 | const total = 50; 5 | 6 | for (let i = 0; i < total; i += 1) { 7 | list.push({ 8 | id: i + 1, 9 | disabled: i % 6 === 0, 10 | href: 'https://ant.design', 11 | avatar: [ 12 | 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 13 | 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', 14 | ][i % 2], 15 | no: `TradeCode ${i}`, 16 | title: `一个任务名称 ${i}`, 17 | owner: '曲丽丽', 18 | description: '这是一段描述', 19 | callNo: Math.floor(Math.random() * 1000), 20 | status: Math.floor(Math.random() * 10) % 4, 21 | updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`), 22 | createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`), 23 | progress: Math.ceil(Math.random() * 100), 24 | }); 25 | } 26 | 27 | function genData(params: any) { 28 | let ret = [...list]; 29 | const pi = +params.pi; 30 | const ps = +params.ps; 31 | const start = (pi - 1) * ps; 32 | 33 | if (params.no) { 34 | ret = ret.filter(data => data.no.indexOf(params.no) > -1); 35 | } 36 | 37 | return { total: ret.length, list: ret.slice(start, ps * pi) }; 38 | } 39 | 40 | function saveData(id: number, value: any) { 41 | const item = list.find(w => w.id === id); 42 | if (!item) { return { msg: '无效用户信息' }; } 43 | Object.assign(item, value); 44 | return { msg: 'ok' }; 45 | } 46 | 47 | export const USERS = { 48 | '/user': (req: MockRequest) => genData(req.queryString), 49 | '/user/:id': (req: MockRequest) => list.find(w => w.id === +req.params.id), 50 | 'POST /user/:id': (req: MockRequest) => saveData(+req.params.id, req.body), 51 | '/user/current': { 52 | name: 'Cipchk', 53 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', 54 | userid: '00000001', 55 | email: 'cipchk@qq.com', 56 | signature: '海纳百川,有容乃大', 57 | title: '交互专家', 58 | group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED', 59 | tags: [ 60 | { 61 | key: '0', 62 | label: '很有想法的', 63 | }, 64 | { 65 | key: '1', 66 | label: '专注撩妹', 67 | }, 68 | { 69 | key: '2', 70 | label: '帅~', 71 | }, 72 | { 73 | key: '3', 74 | label: '通吃', 75 | }, 76 | { 77 | key: '4', 78 | label: '专职后端', 79 | }, 80 | { 81 | key: '5', 82 | label: '海纳百川', 83 | }, 84 | ], 85 | notifyCount: 12, 86 | country: 'China', 87 | geographic: { 88 | province: { 89 | label: '上海', 90 | key: '330000', 91 | }, 92 | city: { 93 | label: '市辖区', 94 | key: '330100', 95 | }, 96 | }, 97 | address: 'XX区XXX路 XX 号', 98 | phone: '你猜-你猜你猜猜猜', 99 | }, 100 | 'POST /user/avatar': 'ok', 101 | 'POST /login/account': (req: MockRequest) => { 102 | const data = req.body; 103 | if (!(data.userName === 'admin' || data.userName === 'user') || data.password !== 'ng-alain.com') { 104 | return { msg: `Invalid username or password(admin/ng-alain.com)` }; 105 | } 106 | return { 107 | msg: 'ok', 108 | user: { 109 | token: '123456789', 110 | name: data.userName, 111 | email: `${data.userName}@qq.com`, 112 | id: 10000, 113 | time: +new Date(), 114 | }, 115 | }; 116 | }, 117 | 'POST /register': { 118 | msg: 'ok', 119 | }, 120 | }; 121 | -------------------------------------------------------------------------------- /_mock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_user'; 2 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('cashwarden-web app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /image-files/etc/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 | '$status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | 19 | access_log /var/log/nginx/access.log main; 20 | 21 | sendfile on; 22 | keepalive_timeout 65; 23 | 24 | server { 25 | listen 80; 26 | sendfile on; 27 | default_type application/octet-stream; 28 | 29 | gzip on; 30 | gzip_http_version 1.1; 31 | gzip_disable "MSIE [1-6]\."; 32 | gzip_min_length 256; 33 | gzip_vary on; 34 | gzip_proxied expired no-cache no-store private auth; 35 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; 36 | gzip_comp_level 9; 37 | 38 | root /srv; 39 | 40 | location / { 41 | try_files $uri $uri/ /index.html =404; 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /image-files/usr/local/bin/docker-entrypoint-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | printf "Starting nginx...\n\n" 4 | nginx -g "daemon on;" 5 | 6 | printf "Building and watching app...\n\n" 7 | npm run build -- --watch --poll=1000 --output-path=/srv 8 | -------------------------------------------------------------------------------- /image-files/usr/local/bin/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo ' _____ .__ ' 4 | echo ' / _ \ ____ ____ __ __| | _____ _______ ' 5 | echo ' / /_\ \ / \ / ___\| | \ | \__ \\_ __ \' 6 | echo '/ | \ | \/ /_/ > | / |__/ __ \| | \/' 7 | echo '\____|__ /___| /\___ /|____/|____(____ /__| ' 8 | echo ' \/ \//_____/ \/ ' 9 | 10 | exec "$@" 11 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/cashwarden-web'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cashwarden-web", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "npm run color-less && ng s -o", 7 | "build": "npm run color-less && node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng build --prod", 8 | "test": "ng test", 9 | "lint": "npm run lint:ts && npm run lint:style", 10 | "e2e": "ng e2e", 11 | "analyze": "npm run color-less && node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng build --prod --stats-json", 12 | "test-coverage": "ng test --code-coverage --watch=false", 13 | "color-less": "node scripts/color-less.js", 14 | "icon": "ng g ng-alain:plugin icon", 15 | "lint:ts": "ng lint --fix", 16 | "lint:style": "stylelint \"src/**/*.less\" --syntax less --fix", 17 | "pretty-quick": "pretty-quick", 18 | "tslint-check": "tslint-config-prettier-check ./tslint.json", 19 | "hmr": "ng serve -c=hmr" 20 | }, 21 | "private": true, 22 | "dependencies": { 23 | "@angular/animations": "~10.0.6", 24 | "@angular/common": "~10.0.6", 25 | "@angular/compiler": "~10.0.6", 26 | "@angular/core": "~10.0.6", 27 | "@angular/forms": "~10.0.6", 28 | "@angular/platform-browser": "~10.0.6", 29 | "@angular/platform-browser-dynamic": "~10.0.6", 30 | "@angular/router": "~10.0.6", 31 | "@antv/g2plot": "^1.1.28", 32 | "@delon/abc": "^10.0.0-beta.2", 33 | "@delon/acl": "^10.0.0-beta.2", 34 | "@delon/auth": "^10.0.0-beta.2", 35 | "@delon/cache": "^9.5.5", 36 | "@delon/chart": "^10.0.0-beta.2", 37 | "@delon/form": "^10.0.0-beta.2", 38 | "@delon/mock": "^10.0.0-beta.2", 39 | "@delon/theme": "^10.0.0-beta.2", 40 | "@delon/util": "^10.0.0-beta.2", 41 | "@ngx-translate/core": "^13.0.0", 42 | "@ngx-translate/http-loader": "^6.0.0", 43 | "ajv": "^6.12.3", 44 | "file-saver": "^2.0.2", 45 | "ng-alain": "^10.0.0-beta.2", 46 | "ng-zorro-antd": "^10.0.0-beta.0", 47 | "ngx-g2plot": "^0.0.8", 48 | "rxjs": "~6.5.5", 49 | "screenfull": "^5.0.2", 50 | "tslib": "^2.0.0", 51 | "zone.js": "~0.10.3" 52 | }, 53 | "devDependencies": { 54 | "@angular-devkit/build-angular": "~0.1000.5", 55 | "@angular/cli": "~10.0.5", 56 | "@angular/compiler-cli": "~10.0.6", 57 | "@angularclass/hmr": "^2.1.3", 58 | "@delon/testing": "^10.0.0-beta.2", 59 | "@types/jasmine": "~3.5.0", 60 | "@types/jasminewd2": "~2.0.3", 61 | "@types/jszip": "^3.1.7", 62 | "@types/node": "^12.11.1", 63 | "antd-theme-generator": "1.2.2", 64 | "codelyzer": "^6.0.0", 65 | "husky": "^4.2.3", 66 | "jasmine-core": "~3.5.0", 67 | "jasmine-spec-reporter": "~5.0.0", 68 | "karma": "~5.0.0", 69 | "karma-chrome-launcher": "~3.1.0", 70 | "karma-coverage-istanbul-reporter": "~3.0.2", 71 | "karma-jasmine": "~3.3.0", 72 | "karma-jasmine-html-reporter": "^1.5.0", 73 | "less-plugin-clean-css": "^1.5.1", 74 | "less-plugin-npm-import": "^2.1.0", 75 | "ng-alain": "^10.0.0-beta.2", 76 | "ng-alain-codelyzer": "^0.0.1", 77 | "prettier": "^2.0.5", 78 | "pretty-quick": "^2.0.1", 79 | "protractor": "~7.0.0", 80 | "stylelint": "^13.3.1", 81 | "stylelint-config-prettier": "^8.0.1", 82 | "stylelint-config-rational-order": "^0.1.2", 83 | "stylelint-config-standard": "^20.0.0", 84 | "stylelint-declaration-block-no-ignored-properties": "^2.3.0", 85 | "stylelint-order": "^4.0.0", 86 | "ts-node": "~8.3.0", 87 | "tslint": "~6.1.0", 88 | "tslint-config-prettier": "^1.18.0", 89 | "tslint-language-service": "^0.9.9", 90 | "typescript": "~3.9.5", 91 | "webpack-bundle-analyzer": "^3.6.1", 92 | "xlsx": "^0.16.1" 93 | }, 94 | "husky": { 95 | "hooks": { 96 | "pre-commit": "pretty-quick --staged" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://cashwarden-core-api.yii.work/v1", 4 | "secure": false, 5 | "changeOrigin": true, 6 | "pathRewrite": { 7 | "^/api": "" 8 | }, 9 | "logLevel": "debug" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /scripts/color-less.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { 4 | generateTheme 5 | } = require('antd-theme-generator'); 6 | 7 | // Specify the name of the theme variables to be changed, default is `@primary-color` 8 | // Can be set all antd & ng-alain custom theme variables 9 | const themeVariables = [ 10 | '@primary-color' 11 | ]; 12 | 13 | const root = path.resolve(__dirname, '../'); 14 | const tmpVarFilePath = path.join(root, 'scripts/var.less'); 15 | const outputFilePath = path.join(root, './src/assets/alain-default.less'); 16 | 17 | const options = { 18 | stylesDir: path.join(root, './src'), 19 | antdStylesDir: path.join(root, './node_modules/ng-zorro-antd'), 20 | varFile: path.join(root, './scripts/var.less'), 21 | mainLessFile: path.join(root, './src/styles.less'), 22 | themeVariables, 23 | outputFilePath, 24 | }; 25 | 26 | function genVarFile() { 27 | const ALLVAR = ` 28 | @import '~@delon/theme/theme-default'; 29 | @import '../src/styles/theme.less'; 30 | `; 31 | 32 | fs.writeFileSync(tmpVarFilePath, ALLVAR); 33 | } 34 | 35 | function removeVarFile() { 36 | fs.unlinkSync(tmpVarFilePath); 37 | } 38 | 39 | function removeOutputFile() { 40 | if (fs.existsSync(outputFilePath)) { 41 | fs.unlinkSync(outputFilePath); 42 | } 43 | } 44 | 45 | genVarFile(); 46 | removeOutputFile(); 47 | generateTheme(options) 48 | .then(() => { 49 | removeVarFile(); 50 | console.log('Theme generated successfully'); 51 | }) 52 | .catch(error => { 53 | removeVarFile(); 54 | console.log('Error', error); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule], 9 | }) 10 | export class AppRoutingModule {} 11 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, Renderer2 } from '@angular/core'; 2 | import { NavigationEnd, Router } from '@angular/router'; 3 | import { TitleService, VERSION as VERSION_ALAIN } from '@delon/theme'; 4 | import { SettingsService } from '@delon/theme'; 5 | import { NzModalService } from 'ng-zorro-antd/modal'; 6 | import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version'; 7 | import { filter } from 'rxjs/operators'; 8 | 9 | declare let gtag: Function; 10 | 11 | @Component({ 12 | selector: 'app-root', 13 | template: ` `, 14 | }) 15 | export class AppComponent implements OnInit { 16 | constructor( 17 | el: ElementRef, 18 | renderer: Renderer2, 19 | private router: Router, 20 | private titleSrv: TitleService, 21 | private modalSrv: NzModalService, 22 | private settings: SettingsService, 23 | ) { 24 | renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full); 25 | renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full); 26 | } 27 | 28 | ngOnInit() { 29 | this.router.events.pipe(filter((evt) => evt instanceof NavigationEnd)).subscribe((e: NavigationEnd) => { 30 | gtag('config', this.settings.app.google_analytics, { page_path: e.urlAfterRedirects }); 31 | this.titleSrv.setTitle(); 32 | this.modalSrv.closeAll(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/core/README.md: -------------------------------------------------------------------------------- 1 | ### CoreModule 2 | 3 | **应** 仅只留 `providers` 属性。 4 | 5 | **作用:** 一些通用服务,例如:用户消息、HTTP数据访问。 6 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { throwIfAlreadyLoaded } from './module-import-guard'; 3 | 4 | import { I18NService } from './i18n/i18n.service'; 5 | 6 | @NgModule({ 7 | providers: [ 8 | I18NService 9 | ] 10 | }) 11 | export class CoreModule { 12 | constructor( @Optional() @SkipSelf() parentModule: CoreModule) { 13 | throwIfAlreadyLoaded(parentModule, 'CoreModule'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/i18n/i18n.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, TestBedStatic } from '@angular/core/testing'; 2 | import { DelonLocaleService, SettingsService } from '@delon/theme'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { NzI18nService } from 'ng-zorro-antd/i18n'; 5 | import { of } from 'rxjs'; 6 | 7 | import { I18NService } from './i18n.service'; 8 | 9 | describe('Service: I18n', () => { 10 | let injector: TestBedStatic; 11 | let srv: I18NService; 12 | const MockSettingsService = { 13 | layout: { 14 | lang: null, 15 | }, 16 | }; 17 | const MockNzI18nService = { 18 | setLocale: () => {}, 19 | setDateLocale: () => {}, 20 | }; 21 | const MockDelonLocaleService = { 22 | setLocale: () => {}, 23 | }; 24 | const MockTranslateService = { 25 | getBrowserLang: jasmine.createSpy('getBrowserLang'), 26 | addLangs: () => {}, 27 | setLocale: () => {}, 28 | getDefaultLang: () => '', 29 | use: (lang: string) => of(lang), 30 | instant: jasmine.createSpy('instant'), 31 | }; 32 | 33 | function genModule() { 34 | injector = TestBed.configureTestingModule({ 35 | providers: [ 36 | I18NService, 37 | { provide: SettingsService, useValue: MockSettingsService }, 38 | { provide: NzI18nService, useValue: MockNzI18nService }, 39 | { provide: DelonLocaleService, useValue: MockDelonLocaleService }, 40 | { provide: TranslateService, useValue: MockTranslateService }, 41 | ], 42 | }); 43 | srv = TestBed.inject(I18NService); 44 | } 45 | 46 | it('should working', () => { 47 | spyOnProperty(navigator, 'languages').and.returnValue(['zh-CN']); 48 | genModule(); 49 | expect(srv).toBeTruthy(); 50 | expect(srv.defaultLang).toBe('zh-CN'); 51 | const t = TestBed.inject(TranslateService); 52 | srv.fanyi('a'); 53 | srv.fanyi('a', {}); 54 | expect(t.instant).toHaveBeenCalled(); 55 | }); 56 | 57 | it('should be used layout as default language', () => { 58 | MockSettingsService.layout.lang = 'en-US'; 59 | const navSpy = spyOnProperty(navigator, 'languages'); 60 | genModule(); 61 | expect(navSpy).not.toHaveBeenCalled(); 62 | expect(srv.defaultLang).toBe('en-US'); 63 | MockSettingsService.layout.lang = null; 64 | }); 65 | 66 | it('should be used browser as default language', () => { 67 | spyOnProperty(navigator, 'languages').and.returnValue(['zh-TW']); 68 | genModule(); 69 | expect(srv.defaultLang).toBe('zh-TW'); 70 | }); 71 | 72 | it('should be trigger notify when changed language', () => { 73 | genModule(); 74 | srv.use('en-US'); 75 | srv.change.subscribe((lang) => { 76 | expect(lang).toBe('en-US'); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './i18n/i18n.service'; 2 | export * from './module-import-guard'; 3 | export * from './net/default.interceptor'; 4 | export * from './startup/startup.service'; 5 | -------------------------------------------------------------------------------- /src/app/core/module-import-guard.ts: -------------------------------------------------------------------------------- 1 | // https://angular.io/guide/styleguide#style-04-12 2 | export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) { 3 | if (parentModule) { 4 | throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/app/global-config.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { throwIfAlreadyLoaded } from '@core'; 3 | import { DelonMockModule } from '@delon/mock'; 4 | import { AlainThemeModule } from '@delon/theme'; 5 | import { AlainConfig, ALAIN_CONFIG } from '@delon/util'; 6 | 7 | // Please refer to: https://ng-alain.com/docs/global-config 8 | // #region NG-ALAIN Config 9 | 10 | import { DelonACLModule } from '@delon/acl'; 11 | 12 | const alainConfig: AlainConfig = { 13 | st: { modal: { size: 'lg' } }, 14 | pageHeader: { homeI18n: 'home' }, 15 | lodop: { 16 | license: `A59B099A586B3851E0F0D7FDBF37B603`, 17 | licenseA: `C94CEE276DB2187AE6B65D56B3FC2848`, 18 | }, 19 | auth: { login_url: '/passport/login' }, 20 | }; 21 | 22 | const alainModules = [AlainThemeModule.forRoot(), DelonACLModule.forRoot(), DelonMockModule.forRoot()]; 23 | const alainProvides = [{ provide: ALAIN_CONFIG, useValue: alainConfig }]; 24 | 25 | // mock 26 | import { environment } from '@env/environment'; 27 | import * as MOCKDATA from '../../_mock'; 28 | if (!environment.production) { 29 | alainConfig.mock = { data: MOCKDATA }; 30 | } 31 | 32 | // #region reuse-tab 33 | /** 34 | * 若需要[路由复用](https://ng-alain.com/components/reuse-tab)需要: 35 | * 1、在 `shared-delon.module.ts` 导入 `ReuseTabModule` 模块 36 | * 2、注册 `RouteReuseStrategy` 37 | * 3、在 `src/app/layout/default/default.component.html` 修改: 38 | * ```html 39 | *
40 | * 41 | * 42 | *
43 | * ``` 44 | */ 45 | // import { RouteReuseStrategy } from '@angular/router'; 46 | // import { ReuseTabService, ReuseTabStrategy } from '@delon/abc/reuse-tab'; 47 | // alainProvides.push({ 48 | // provide: RouteReuseStrategy, 49 | // useClass: ReuseTabStrategy, 50 | // deps: [ReuseTabService], 51 | // } as any); 52 | 53 | // #endregion 54 | 55 | // #endregion 56 | 57 | // Please refer to: https://ng.ant.design/docs/global-config/en#how-to-use 58 | // #region NG-ZORRO Config 59 | 60 | import { NzConfig, NZ_CONFIG } from 'ng-zorro-antd/core/config'; 61 | 62 | const ngZorroConfig: NzConfig = {}; 63 | 64 | const zorroProvides = [{ provide: NZ_CONFIG, useValue: ngZorroConfig }]; 65 | 66 | // #endregion 67 | 68 | @NgModule({ 69 | imports: [...alainModules], 70 | }) 71 | export class GlobalConfigModule { 72 | constructor(@Optional() @SkipSelf() parentModule: GlobalConfigModule) { 73 | throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule'); 74 | } 75 | 76 | static forRoot(): ModuleWithProviders { 77 | return { 78 | ngModule: GlobalConfigModule, 79 | providers: [...alainProvides, ...zorroProvides], 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/layout/default/default.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /src/app/layout/default/default.component.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { 3 | AfterViewInit, 4 | Component, 5 | ComponentFactoryResolver, 6 | ElementRef, 7 | Inject, 8 | OnDestroy, 9 | OnInit, 10 | Renderer2, 11 | ViewChild, 12 | ViewContainerRef, 13 | } from '@angular/core'; 14 | import { NavigationCancel, NavigationEnd, NavigationError, RouteConfigLoadEnd, RouteConfigLoadStart, Router } from '@angular/router'; 15 | import { SettingsService } from '@delon/theme'; 16 | import { updateHostClass } from '@delon/util'; 17 | import { environment } from '@env/environment'; 18 | import { NzMessageService } from 'ng-zorro-antd/message'; 19 | import { Subject } from 'rxjs'; 20 | import { takeUntil } from 'rxjs/operators'; 21 | 22 | import { SettingDrawerComponent } from './setting-drawer/setting-drawer.component'; 23 | 24 | @Component({ 25 | selector: 'layout-default', 26 | templateUrl: './default.component.html', 27 | }) 28 | export class LayoutDefaultComponent implements OnInit, AfterViewInit, OnDestroy { 29 | private unsubscribe$ = new Subject(); 30 | @ViewChild('settingHost', { read: ViewContainerRef, static: true }) 31 | private settingHost: ViewContainerRef; 32 | isFetching = false; 33 | 34 | constructor( 35 | router: Router, 36 | msgSrv: NzMessageService, 37 | private resolver: ComponentFactoryResolver, 38 | private settings: SettingsService, 39 | private el: ElementRef, 40 | private renderer: Renderer2, 41 | @Inject(DOCUMENT) private doc: any, 42 | ) { 43 | // scroll to top in change page 44 | router.events.pipe(takeUntil(this.unsubscribe$)).subscribe((evt) => { 45 | if (!this.isFetching && evt instanceof RouteConfigLoadStart) { 46 | this.isFetching = true; 47 | } 48 | if (evt instanceof NavigationError || evt instanceof NavigationCancel) { 49 | this.isFetching = false; 50 | if (evt instanceof NavigationError) { 51 | msgSrv.error(`无法加载${evt.url}路由`, { nzDuration: 1000 * 3 }); 52 | } 53 | return; 54 | } 55 | if (!(evt instanceof NavigationEnd || evt instanceof RouteConfigLoadEnd)) { 56 | return; 57 | } 58 | if (this.isFetching) { 59 | setTimeout(() => { 60 | this.isFetching = false; 61 | }, 100); 62 | } 63 | }); 64 | } 65 | 66 | private setClass() { 67 | const { el, doc, renderer, settings } = this; 68 | const layout = settings.layout; 69 | updateHostClass(el.nativeElement, renderer, { 70 | ['alain-default']: true, 71 | [`alain-default__fixed`]: layout.fixed, 72 | [`alain-default__collapsed`]: layout.collapsed, 73 | }); 74 | 75 | doc.body.classList[layout.colorWeak ? 'add' : 'remove']('color-weak'); 76 | } 77 | 78 | ngAfterViewInit(): void { 79 | // Setting componet for only developer 80 | if (!environment.production) { 81 | setTimeout(() => { 82 | const settingFactory = this.resolver.resolveComponentFactory(SettingDrawerComponent); 83 | this.settingHost.createComponent(settingFactory); 84 | }, 22); 85 | } 86 | } 87 | 88 | ngOnInit() { 89 | const { settings, unsubscribe$ } = this; 90 | settings.notify.pipe(takeUntil(unsubscribe$)).subscribe(() => this.setClass()); 91 | this.setClass(); 92 | } 93 | 94 | ngOnDestroy() { 95 | const { unsubscribe$ } = this; 96 | unsubscribe$.next(); 97 | unsubscribe$.complete(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/app/layout/default/header/components/fullscreen.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core'; 2 | import * as screenfull from 'screenfull'; 3 | 4 | @Component({ 5 | selector: 'header-fullscreen', 6 | template: ` 7 | 8 | {{ (status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | translate }} 9 | `, 10 | // tslint:disable-next-line: no-host-metadata-property 11 | host: { 12 | '[class.d-block]': 'true', 13 | }, 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class HeaderFullScreenComponent { 17 | status = false; 18 | private get sf(): screenfull.Screenfull { 19 | return screenfull as screenfull.Screenfull; 20 | } 21 | 22 | @HostListener('window:resize') 23 | _resize() { 24 | this.status = this.sf.isFullscreen; 25 | } 26 | 27 | @HostListener('click') 28 | _click() { 29 | if (this.sf.isEnabled) { 30 | this.sf.toggle(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/layout/default/header/components/i18n.component.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; 3 | import { ALAIN_I18N_TOKEN, SettingsService } from '@delon/theme'; 4 | import { InputBoolean } from '@delon/util'; 5 | 6 | import { I18NService } from '@core'; 7 | 8 | @Component({ 9 | selector: 'header-i18n', 10 | template: ` 11 |
12 | 13 | {{ 'menu.lang' | translate }} 14 | 15 |
16 | 24 | 25 |
    26 |
  • 32 | {{ item.abbr }} 33 | {{ item.text }} 34 |
  • 35 |
36 |
37 | `, 38 | changeDetection: ChangeDetectionStrategy.OnPush, 39 | }) 40 | export class HeaderI18nComponent { 41 | /** Whether to display language text */ 42 | @Input() @InputBoolean() showLangText = true; 43 | 44 | get langs() { 45 | return this.i18n.getLangs(); 46 | } 47 | 48 | get curLangCode() { 49 | return this.settings.layout.lang; 50 | } 51 | 52 | constructor( 53 | private settings: SettingsService, 54 | @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService, 55 | @Inject(DOCUMENT) private doc: any, 56 | ) {} 57 | 58 | change(lang: string) { 59 | const spinEl = this.doc.createElement('div'); 60 | spinEl.setAttribute('class', `page-loading ant-spin ant-spin-lg ant-spin-spinning`); 61 | spinEl.innerHTML = ``; 62 | this.doc.body.appendChild(spinEl); 63 | 64 | this.i18n.use(lang); 65 | this.settings.setLayout('lang', lang); 66 | setTimeout(() => this.doc.location.reload()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/layout/default/header/components/icon.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'header-icon', 5 | template: ` 6 |
14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 | Calendar 23 |
24 |
25 | 26 | Files 27 |
28 |
29 | 30 | Cloud 31 |
32 |
33 | 34 | Star 35 |
36 |
37 | 38 | Team 39 |
40 |
41 | 42 | QR 43 |
44 |
45 | 46 | Pay 47 |
48 |
49 | 50 | Print 51 |
52 |
53 |
54 |
55 |
56 | `, 57 | changeDetection: ChangeDetectionStrategy.OnPush, 58 | }) 59 | export class HeaderIconComponent { 60 | loading = true; 61 | 62 | constructor(private cdr: ChangeDetectorRef) {} 63 | 64 | change() { 65 | setTimeout(() => { 66 | this.loading = false; 67 | this.cdr.detectChanges(); 68 | }, 500); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/layout/default/header/components/search.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | ElementRef, 7 | HostBinding, 8 | Input, 9 | OnDestroy, 10 | } from '@angular/core'; 11 | import { BehaviorSubject } from 'rxjs'; 12 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; 13 | 14 | @Component({ 15 | selector: 'header-search', 16 | template: ` 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 34 | 35 | 36 | {{ i }} 37 | 38 | `, 39 | changeDetection: ChangeDetectionStrategy.OnPush, 40 | }) 41 | export class HeaderSearchComponent implements AfterViewInit, OnDestroy { 42 | q: string; 43 | qIpt: HTMLInputElement; 44 | options: string[] = []; 45 | search$ = new BehaviorSubject(''); 46 | loading = false; 47 | 48 | @HostBinding('class.alain-default__search-focus') 49 | focus = false; 50 | @HostBinding('class.alain-default__search-toggled') 51 | searchToggled = false; 52 | 53 | @Input() 54 | set toggleChange(value: boolean) { 55 | if (typeof value === 'undefined') { 56 | return; 57 | } 58 | this.searchToggled = true; 59 | this.focus = true; 60 | setTimeout(() => this.qIpt.focus(), 300); 61 | } 62 | 63 | constructor(private el: ElementRef, private cdr: ChangeDetectorRef) {} 64 | 65 | ngAfterViewInit(): void { 66 | this.qIpt = this.el.nativeElement.querySelector('.ant-input') as HTMLInputElement; 67 | this.search$.pipe(debounceTime(500), distinctUntilChanged()).subscribe((value) => { 68 | this.options = value ? [value, value + value, value + value + value] : []; 69 | this.loading = false; 70 | this.cdr.detectChanges(); 71 | }); 72 | } 73 | 74 | qFocus(): void { 75 | this.focus = true; 76 | } 77 | 78 | qBlur(): void { 79 | this.focus = false; 80 | this.searchToggled = false; 81 | } 82 | 83 | search(ev: KeyboardEvent): void { 84 | if (ev.key === 'Enter') { 85 | return; 86 | } 87 | this.loading = true; 88 | this.search$.next((ev.target as HTMLInputElement).value); 89 | } 90 | 91 | ngOnDestroy(): void { 92 | this.search$.complete(); 93 | this.search$.unsubscribe(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/layout/default/header/components/storage.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core'; 2 | import { NzMessageService } from 'ng-zorro-antd/message'; 3 | import { NzModalService } from 'ng-zorro-antd/modal'; 4 | 5 | @Component({ 6 | selector: 'header-storage', 7 | template: ` 8 | 9 | {{ 'menu.clear.local.storage' | translate }} 10 | `, 11 | // tslint:disable-next-line: no-host-metadata-property 12 | host: { 13 | '[class.d-block]': 'true', 14 | }, 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | }) 17 | export class HeaderStorageComponent { 18 | constructor(private modalSrv: NzModalService, private messageSrv: NzMessageService) {} 19 | 20 | @HostListener('click') 21 | _click() { 22 | this.modalSrv.confirm({ 23 | nzTitle: 'Make sure clear all local storage?', 24 | nzOnOk: () => { 25 | localStorage.clear(); 26 | this.messageSrv.success('Clear Finished!'); 27 | }, 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/layout/default/header/components/task.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'header-task', 5 | template: ` 6 |
14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 | cipchk 29 |

Please tell me what happened in a few words, don't go into details.

30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 | はなさき 38 |

ハルカソラトキヘダツヒカリ

39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 | 苏先生 47 |

请告诉我,我应该说点什么好?

48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 | Kent 56 |

Please tell me what happened in a few words, don't go into details.

57 |
58 |
59 |
60 |
61 | 62 |
63 |
64 | Jefferson 65 |

Please tell me what happened in a few words, don't go into details.

66 |
67 |
68 |
69 |
70 | See All 71 |
72 |
73 |
74 |
75 |
76 | `, 77 | changeDetection: ChangeDetectionStrategy.OnPush, 78 | }) 79 | export class HeaderTaskComponent { 80 | loading = true; 81 | 82 | constructor(private cdr: ChangeDetectorRef) {} 83 | 84 | change() { 85 | setTimeout(() => { 86 | this.loading = false; 87 | this.cdr.detectChanges(); 88 | }, 500); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/layout/default/header/components/user.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth'; 4 | import { SettingsService, User } from '@delon/theme'; 5 | 6 | @Component({ 7 | selector: 'header-user', 8 | template: ` 9 |
10 | 11 | {{ user.name }} 12 |
13 | 14 |
15 |
16 | 17 | {{ 'menu.account.settings' | translate }} 18 |
19 |
  • 20 |
    21 | 22 | {{ 'menu.account.logout' | translate }} 23 |
    24 |
    25 |
    26 | `, 27 | changeDetection: ChangeDetectionStrategy.OnPush, 28 | }) 29 | export class HeaderUserComponent { 30 | get user(): User { 31 | return this.settings.user; 32 | } 33 | 34 | constructor(private settings: SettingsService, private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {} 35 | 36 | logout() { 37 | this.tokenService.clear(); 38 | this.router.navigateByUrl(this.tokenService.login_url); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/layout/default/header/header.component.html: -------------------------------------------------------------------------------- 1 | 7 |
    8 |
      9 | 10 |
    • 11 |
      12 | 13 |
      14 |
    • 15 | 16 | 21 | 22 | 27 |
    28 | 29 |
      30 | 31 |
    • 32 |
      33 | 34 |
      35 | 36 |
      37 |
      38 | 39 |
      40 |
      41 | 42 |
      43 | 46 |
      47 |
      48 |
    • 49 |
    • 50 | 51 |
    • 52 |
    53 |
    54 | -------------------------------------------------------------------------------- /src/app/layout/default/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { App, SettingsService } from '@delon/theme'; 3 | 4 | @Component({ 5 | selector: 'layout-header', 6 | templateUrl: './header.component.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | }) 9 | export class HeaderComponent { 10 | searchToggleStatus: boolean; 11 | 12 | get app(): App { 13 | return this.settings.app; 14 | } 15 | 16 | get collapsed(): boolean { 17 | return this.settings.layout.collapsed; 18 | } 19 | 20 | constructor(private settings: SettingsService) {} 21 | 22 | toggleCollapsedSidebar() { 23 | this.settings.setLayout('collapsed', !this.settings.layout.collapsed); 24 | } 25 | 26 | searchToggleChange() { 27 | this.searchToggleStatus = !this.searchToggleStatus; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/layout/default/header/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | component: app-header 3 | title: 顶部菜单 4 | --- 5 | 6 | 顶部菜单组件允许通过 `components` 目录下的组件进行按需组装。 7 | 8 | ## 组件列表 9 | 10 | 组件名 | 说明 11 | ----|------ 12 | `header-fullscreen` | 全屏切换 13 | `header-icon` | 应用图标 14 | `header-langs` | 语言切换 15 | `header-notify` | 菜单通知 16 | `header-search` | 搜索框 17 | `header-storage` | 清除 LocalStorage 缓存 18 | `header-task` | 任务通知 19 | `header-theme` | 主题切换 20 | `header-user` | 用户菜单 21 | -------------------------------------------------------------------------------- /src/app/layout/default/setting-drawer/setting-drawer-item.component.html: -------------------------------------------------------------------------------- 1 | {{ i.label }}{{ i.tip }} 4 |
    5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
    34 | -------------------------------------------------------------------------------- /src/app/layout/default/setting-drawer/setting-drawer-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | // tslint:disable-next-line:component-selector 5 | selector: 'setting-drawer-item', 6 | templateUrl: './setting-drawer-item.component.html', 7 | // tslint:disable-next-line: no-host-metadata-property 8 | host: { 9 | '[class.setting-drawer__body-item]': 'true', 10 | }, 11 | }) 12 | export class SettingDrawerItemComponent { 13 | i: any = {}; 14 | 15 | @Input() 16 | set data(val: any) { 17 | this.i = val; 18 | if (val.type === 'px') { 19 | this.pxVal = +val.value.replace('px', ''); 20 | } 21 | } 22 | 23 | pxVal: number; 24 | pxChange(val: number) { 25 | this.i.value = `${val}px`; 26 | } 27 | format = (value) => `${value} px`; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/layout/default/setting-drawer/setting-drawer.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |

    主题色

    5 | 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
    47 |
    48 | 49 |
    50 |
    51 | 固定头和侧边栏 52 | 53 |
    54 |
    55 | 色弱模式 56 | 57 |
    58 |
    59 | 60 | 61 | 62 | 63 | 68 |
    69 |
    70 |
    71 | 72 |
    73 | -------------------------------------------------------------------------------- /src/app/layout/default/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 8 |
    9 | 10 |
    11 | -------------------------------------------------------------------------------- /src/app/layout/default/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { SettingsService, User } from '@delon/theme'; 3 | 4 | @Component({ 5 | selector: 'layout-sidebar', 6 | templateUrl: './sidebar.component.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | }) 9 | export class SidebarComponent { 10 | get user(): User { 11 | return this.settings.user; 12 | } 13 | 14 | constructor(private settings: SettingsService) {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/layout/fullscreen/fullscreen.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/layout/fullscreen/fullscreen.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'layout-fullscreen', 5 | templateUrl: './fullscreen.component.html', 6 | // tslint:disable-next-line: no-host-metadata-property 7 | host: { 8 | '[class.alain-fullscreen]': 'true', 9 | }, 10 | }) 11 | export class LayoutFullScreenComponent {} 12 | -------------------------------------------------------------------------------- /src/app/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | import { LayoutDefaultComponent } from './default/default.component'; 4 | import { HeaderFullScreenComponent } from './default/header/components/fullscreen.component'; 5 | import { HeaderI18nComponent } from './default/header/components/i18n.component'; 6 | import { HeaderIconComponent } from './default/header/components/icon.component'; 7 | import { HeaderNotifyComponent } from './default/header/components/notify.component'; 8 | import { HeaderSearchComponent } from './default/header/components/search.component'; 9 | import { HeaderStorageComponent } from './default/header/components/storage.component'; 10 | import { HeaderTaskComponent } from './default/header/components/task.component'; 11 | import { HeaderUserComponent } from './default/header/components/user.component'; 12 | import { HeaderComponent } from './default/header/header.component'; 13 | import { SettingDrawerItemComponent } from './default/setting-drawer/setting-drawer-item.component'; 14 | import { SettingDrawerComponent } from './default/setting-drawer/setting-drawer.component'; 15 | import { SidebarComponent } from './default/sidebar/sidebar.component'; 16 | import { LayoutFullScreenComponent } from './fullscreen/fullscreen.component'; 17 | 18 | const SETTINGDRAWER = [SettingDrawerComponent, SettingDrawerItemComponent]; 19 | 20 | const COMPONENTS = [ 21 | LayoutDefaultComponent, 22 | LayoutFullScreenComponent, 23 | HeaderComponent, 24 | SidebarComponent, 25 | ...SETTINGDRAWER 26 | ]; 27 | 28 | const HEADERCOMPONENTS = [ 29 | HeaderSearchComponent, 30 | HeaderNotifyComponent, 31 | HeaderTaskComponent, 32 | HeaderIconComponent, 33 | HeaderFullScreenComponent, 34 | HeaderI18nComponent, 35 | HeaderStorageComponent, 36 | HeaderUserComponent 37 | ]; 38 | 39 | // passport 40 | import { LayoutPassportComponent } from './passport/passport.component'; 41 | const PASSPORT = [ 42 | LayoutPassportComponent 43 | ]; 44 | 45 | @NgModule({ 46 | imports: [SharedModule], 47 | declarations: [ 48 | ...COMPONENTS, 49 | ...HEADERCOMPONENTS, 50 | ...PASSPORT 51 | ], 52 | exports: [ 53 | ...COMPONENTS, 54 | ...PASSPORT 55 | ] 56 | }) 57 | export class LayoutModule { } 58 | -------------------------------------------------------------------------------- /src/app/layout/passport/passport.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 |
    6 | 7 | CashWarden 8 |
    9 |
    为您提供开源的资产管理
    10 |
    11 | 12 | 13 | Copyright 14 | 2020 CashWarden 出品 15 | 16 |
    17 |
    18 | -------------------------------------------------------------------------------- /src/app/layout/passport/passport.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host ::ng-deep { 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | min-height: 100%; 7 | background: #f0f2f5; 8 | } 9 | .langs { 10 | width: 100%; 11 | height: 40px; 12 | line-height: 44px; 13 | text-align: right; 14 | .anticon { 15 | margin-top: 24px; 16 | margin-right: 24px; 17 | font-size: 14px; 18 | vertical-align: top; 19 | cursor: pointer; 20 | } 21 | } 22 | .wrap { 23 | flex: 1; 24 | padding: 32px 0; 25 | } 26 | .ant-form-item { 27 | display: flex; 28 | justify-content: space-between; 29 | margin-bottom: 24px; 30 | } 31 | 32 | @media (min-width: @screen-md-min) { 33 | .container { 34 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg'); 35 | background-repeat: no-repeat; 36 | background-position: center 110px; 37 | background-size: 100%; 38 | } 39 | .wrap { 40 | padding: 32px 0 24px; 41 | } 42 | } 43 | .top { 44 | text-align: center; 45 | } 46 | .header { 47 | height: 44px; 48 | line-height: 44px; 49 | a { 50 | text-decoration: none; 51 | } 52 | } 53 | .logo { 54 | height: 44px; 55 | margin-right: 16px; 56 | } 57 | .title { 58 | position: relative; 59 | color: @heading-color; 60 | font-weight: 600; 61 | font-size: 33px; 62 | font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; 63 | vertical-align: middle; 64 | } 65 | .desc { 66 | margin-top: 12px; 67 | margin-bottom: 40px; 68 | color: @text-color-secondary; 69 | font-size: @font-size-base; 70 | } 71 | } 72 | 73 | [data-theme='dark'] { 74 | :host ::ng-deep { 75 | .container { 76 | background: #141414; 77 | } 78 | .title { 79 | color: fade(@white, 85%); 80 | } 81 | .desc { 82 | color: fade(@white, 45%); 83 | } 84 | @media (min-width: @screen-md-min) { 85 | .container { 86 | background-image: none; 87 | } 88 | } 89 | } 90 | } 91 | 92 | [data-theme='compact'] { 93 | :host ::ng-deep { 94 | .ant-form-item { 95 | margin-bottom: 16px; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/layout/passport/passport.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth'; 3 | 4 | @Component({ 5 | selector: 'layout-passport', 6 | templateUrl: './passport.component.html', 7 | styleUrls: ['./passport.component.less'], 8 | }) 9 | export class LayoutPassportComponent implements OnInit { 10 | links = [ 11 | { 12 | title: '帮助', 13 | href: '', 14 | }, 15 | { 16 | title: '隐私', 17 | href: '', 18 | }, 19 | { 20 | title: '条款', 21 | href: '', 22 | }, 23 | ]; 24 | 25 | constructor(@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {} 26 | 27 | ngOnInit(): void { 28 | this.tokenService.clear(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/routes/account/account-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AccountIndexComponent } from './index/index.component'; 4 | 5 | const routes: Routes = [{ path: 'index', component: AccountIndexComponent }]; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule], 10 | }) 11 | export class AccountRoutingModule {} 12 | -------------------------------------------------------------------------------- /src/app/routes/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | import { AccountRoutingModule } from './account-routing.module'; 4 | import { AccountFormComponent } from './form/form.component'; 5 | import { AccountIndexComponent } from './index/index.component'; 6 | import { AccountViewComponent } from './view/view.component'; 7 | 8 | const COMPONENTS = [AccountIndexComponent]; 9 | const COMPONENTS_NOROUNT = [AccountFormComponent, 10 | AccountViewComponent]; 11 | 12 | @NgModule({ 13 | imports: [SharedModule, AccountRoutingModule], 14 | declarations: [...COMPONENTS, ...COMPONENTS_NOROUNT], 15 | }) 16 | export class AccountModule {} 17 | -------------------------------------------------------------------------------- /src/app/routes/account/form/form.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 | {{ i.name }} 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 | 39 | 40 | 41 | 42 |
    43 | -------------------------------------------------------------------------------- /src/app/routes/account/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { _HttpClient } from '@delon/theme'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { NzModalRef } from 'ng-zorro-antd/modal'; 5 | 6 | @Component({ 7 | selector: 'app-account-form', 8 | templateUrl: './form.component.html', 9 | }) 10 | export class AccountFormComponent implements OnInit { 11 | record: any = {}; 12 | accountTypes: []; 13 | 14 | form = { 15 | name: '', 16 | type: 'general_account', 17 | currency_balance: '', 18 | currency_code: 'CNY', 19 | status: 'active', 20 | default: false, 21 | exclude_from_stats: false, 22 | }; 23 | ngOnInit(): void { 24 | if (this.record.id) { 25 | this.form = this.record; 26 | } 27 | } 28 | 29 | constructor(private http: _HttpClient, private modal: NzModalRef, private msgSrv: NzMessageService) {} 30 | 31 | save(value: any) { 32 | const url = this.record.id ? `/${this.record.id}` : ''; 33 | const method = this.record.id ? 'put' : 'post'; 34 | this.http.request(method, `/api/accounts${url}`, { body: value }).subscribe((res: any) => { 35 | if (res.code !== 0) { 36 | this.msgSrv.warning(res.message); 37 | return; 38 | } 39 | this.msgSrv.success('保存成功'); 40 | this.modal.close(res.data); 41 | }); 42 | } 43 | 44 | close() { 45 | this.modal.destroy(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/routes/account/index/index.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | 净资产 6 | {{ overview?.net_asset }} 7 | 8 |
    9 |
    10 | 负债 11 | {{ overview?.liabilities }} 12 | 13 |
    14 |
    15 | 总资产 16 | {{ overview?.total_assets }} 17 |
    18 |
    19 | 账户数 20 | {{ overview?.count }} 21 |
    22 |
    23 |
    24 |
    25 | 26 | 27 |
    28 | 29 | 30 | 31 | 32 | 33 | 34 | {{ i.name }} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
    47 | 51 |
    52 | 53 | 54 | 55 | 56 | 57 |

    {{ item.name }}

    58 |
    59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 75 | 76 | 77 | 78 | {{ item.type_name }} 79 | 默认账户 80 | 冻结 81 | 82 |
    83 |
    84 |
    85 | -------------------------------------------------------------------------------- /src/app/routes/account/index/index.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | -------------------------------------------------------------------------------- /src/app/routes/account/index/index.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { ModalHelper, _HttpClient } from '@delon/theme'; 4 | import { NzMessageService } from 'ng-zorro-antd/message'; 5 | import { AccountFormComponent } from './../form/form.component'; 6 | 7 | @Component({ 8 | selector: 'app-account-index', 9 | templateUrl: './index.component.html', 10 | styleUrls: ['./index.component.less'], 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class AccountIndexComponent implements OnInit { 14 | q: any = { 15 | page: 1, 16 | pageSize: 50, 17 | }; 18 | accountSorts = [ 19 | { value: '-balance_cent', label: '余额倒序' }, 20 | { value: 'balance_cent', label: '余额正序' }, 21 | ]; 22 | accountTypes: any[] = []; 23 | list: Array<{ id: number; name: string; type: string; color: string; balance: string }> = []; 24 | 25 | loading = true; 26 | overview: { count: number; net_asset: number; total_assets: number; liabilities: number }; 27 | 28 | constructor( 29 | private http: _HttpClient, 30 | private msg: NzMessageService, 31 | private modal: ModalHelper, 32 | private cdr: ChangeDetectorRef, 33 | private router: Router, 34 | ) {} 35 | 36 | ngOnInit(): void { 37 | this.getOverview(); 38 | this.getData(); 39 | this.getAccountTypes(); 40 | } 41 | 42 | getData(): void { 43 | this.loading = true; 44 | const q = {}; 45 | Object.entries(this.q) 46 | .filter(([, value]) => value !== null) 47 | .map(([key, value]) => (q[key] = value)); 48 | this.q = q; 49 | this.http.get('/api/accounts', this.q).subscribe((res) => { 50 | this.list = res.data.items; 51 | this.loading = false; 52 | this.cdr.detectChanges(); 53 | }); 54 | } 55 | 56 | getAccountTypes(): void { 57 | this.http.get('/api/accounts/types').subscribe((res) => { 58 | if (res.code !== 0) { 59 | this.msg.warning(res.message); 60 | return; 61 | } 62 | if (res.data) { 63 | this.accountTypes = res.data; 64 | this.cdr.detectChanges(); 65 | } 66 | }); 67 | } 68 | 69 | getOverview(): void { 70 | this.http.get('/api/accounts/overview').subscribe((res) => { 71 | this.overview = res.data; 72 | this.cdr.detectChanges(); 73 | }); 74 | } 75 | 76 | search(): void { 77 | this.getData(); 78 | } 79 | 80 | reset(): void { 81 | this.q = { 82 | page: 1, 83 | pageSize: 50, 84 | }; 85 | this.getData(); 86 | } 87 | 88 | to(item: { key: string }) { 89 | this.router.navigateByUrl(`/account/view/${item.key}`); 90 | } 91 | 92 | form(record: { id?: number } = {}): void { 93 | this.modal.create(AccountFormComponent, { record, accountTypes: this.accountTypes }, { size: 'md' }).subscribe((res) => { 94 | this.getData(); 95 | this.getOverview(); 96 | this.cdr.detectChanges(); 97 | }); 98 | } 99 | 100 | delete(record: any): void { 101 | this.http.delete(`/api/accounts/${record.id}`).subscribe((res) => { 102 | if (res?.code !== 0) { 103 | this.msg.warning(res?.message); 104 | return; 105 | } 106 | this.getData(); 107 | this.getOverview(); 108 | this.msg.success('删除成功'); 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/app/routes/account/view/view.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | {{ i.owner }} 7 | 32943898021309809423 8 | 3321944288191034921 9 | 18112345678 10 | 曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口 11 |
    12 | 15 | -------------------------------------------------------------------------------- /src/app/routes/account/view/view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NzModalRef } from 'ng-zorro-antd/modal'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { _HttpClient } from '@delon/theme'; 5 | 6 | @Component({ 7 | selector: 'app-account-view', 8 | templateUrl: './view.component.html', 9 | }) 10 | export class AccountViewComponent implements OnInit { 11 | record: any = {}; 12 | i: any; 13 | 14 | constructor( 15 | private modal: NzModalRef, 16 | public msgSrv: NzMessageService, 17 | public http: _HttpClient 18 | ) { } 19 | 20 | ngOnInit(): void { 21 | this.http.get(`/user/${this.record.id}`).subscribe(res => this.i = res); 22 | } 23 | 24 | close() { 25 | this.modal.destroy(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/routes/analysis/analysis-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AnalysisIndexComponent } from './index/index.component'; 4 | 5 | const routes: Routes = [{ path: 'index', component: AnalysisIndexComponent }]; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule], 10 | }) 11 | export class AnalysisRoutingModule {} 12 | -------------------------------------------------------------------------------- /src/app/routes/analysis/analysis.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | import { AnalysisRoutingModule } from './analysis-routing.module'; 4 | import { AnalysisIndexComponent } from './index/index.component'; 5 | 6 | const COMPONENTS = [AnalysisIndexComponent]; 7 | const COMPONENTS_NOROUNT = []; 8 | 9 | @NgModule({ 10 | imports: [SharedModule, AnalysisRoutingModule], 11 | declarations: [...COMPONENTS, ...COMPONENTS_NOROUNT], 12 | }) 13 | export class AnalysisModule {} 14 | -------------------------------------------------------------------------------- /src/app/routes/analysis/index/index.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 |
    6 | 7 | 17 | 18 | 28 | 29 | 30 | 31 | 32 |
    33 | 39 |
    40 |
    41 | 47 |
    48 |
    49 |
    50 |
    51 |
    52 | 53 |
    54 | 55 | 56 | 65 | 66 | 67 | 合计 68 | 69 | {{ (data?.total)[type] | _currency }} 70 | 71 | 72 | 73 | 结余 74 | 75 | {{ data.total.surplus | _currency }} 76 | 77 | 78 | 79 | 80 | 81 | 82 |
    83 |
    84 |
    85 | -------------------------------------------------------------------------------- /src/app/routes/analysis/index/index.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host ::ng-deep { 3 | .record-sum-card { 4 | .bar { 5 | padding: 0 24px 30px 24px; 6 | } 7 | .ant-tabs-nav-list { 8 | padding-left: 24px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/routes/analysis/index/index.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common'; 2 | import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; 3 | import { STColumn, STComponent } from '@delon/abc/st'; 4 | import { G2BarData } from '@delon/chart/bar'; 5 | import { G2PieData } from '@delon/chart/pie'; 6 | import { _HttpClient } from '@delon/theme'; 7 | import { deepCopy, getTimeDistance } from '@delon/util'; 8 | import { yuan } from '@shared'; 9 | 10 | @Component({ 11 | selector: 'app-analysis-index', 12 | styleUrls: ['./index.component.less'], 13 | templateUrl: './index.component.html', 14 | }) 15 | export class AnalysisIndexComponent implements OnInit { 16 | q: any = {}; 17 | data: { total: { expense: number; income: number; surplus: number }; expense: []; income: [] }; 18 | pieData: { total: { expense: number; income: number; surplus: number }; expense: G2PieData[]; income: G2PieData[] }; 19 | recordSumData: { total: { expense: number; income: number; surplus: number }; expense: G2BarData[]; income: G2BarData[] }; 20 | date: Date[] = []; 21 | loading = true; 22 | types = ['expense', 'income']; 23 | 24 | @ViewChild('st', { static: false }) st: STComponent; 25 | columns: STColumn[] = [ 26 | { title: '分类', index: 'category_name' }, 27 | { title: '金额', type: 'number', index: 'currency_amount' }, 28 | ]; 29 | 30 | tabs: Array<{ key: string; name: string; show?: boolean }> = [ 31 | { key: 'day', name: '日视图', show: true }, 32 | { key: 'month', name: '月视图' }, 33 | { key: 'year', name: '年视图' }, 34 | ]; 35 | 36 | constructor(private http: _HttpClient, private cdr: ChangeDetectorRef, private datePipe: DatePipe) {} 37 | 38 | ngOnInit() { 39 | this.date = getTimeDistance('month'); 40 | if (this.date) { 41 | this.q.date = this.date.map((item: any) => this.datePipe.transform(item, 'yyyy-MM-dd')).join('~'); 42 | } 43 | this.getData(); 44 | this.getRecordSumData(); 45 | } 46 | 47 | getData(): void { 48 | this.loading = true; 49 | const q = {}; 50 | Object.entries(this.q) 51 | .filter(([, value]) => value !== null) 52 | .map(([key, value]) => (q[key] = value)); 53 | this.q = q; 54 | 55 | this.http.get('/api/analysis/category', this.q).subscribe((res) => { 56 | this.data = deepCopy(res.data); 57 | this.pieData = deepCopy(res.data); 58 | this.pieData.expense = this.pieData.expense.map((item: any) => ({ x: item.category_name, y: item.currency_amount })); 59 | this.pieData.income = this.pieData.income.map((item: any) => ({ x: item.category_name, y: item.currency_amount })); 60 | 61 | this.loading = false; 62 | this.cdr.detectChanges(); 63 | }); 64 | } 65 | 66 | getRecordSumData(): void { 67 | const q = {}; 68 | Object.entries(this.q) 69 | .filter(([, value]) => value !== null) 70 | .map(([key, value]) => (q[key] = value)); 71 | this.q = q; 72 | this.http.get('/api/analysis/date', this.q).subscribe((res) => { 73 | this.recordSumData = res.data; 74 | this.recordSumData.expense = this.recordSumData.expense.map((item: any) => ({ 75 | x: item.date, 76 | y: item.currency_amount, 77 | color: '#f50', 78 | })); 79 | 80 | this.recordSumData.income = this.recordSumData.income.map((item: any) => ({ x: item.date, y: item.currency_amount })); 81 | this.cdr.detectChanges(); 82 | }); 83 | } 84 | 85 | reloadData(value: {}) { 86 | if (value) { 87 | this.q = value; 88 | this.getData(); 89 | this.getRecordSumData(); 90 | } 91 | } 92 | 93 | changeTab(idx: number): void { 94 | this.tabs[idx].show = true; 95 | this.q.group_type = this.tabs[idx].key; 96 | 97 | this.getRecordSumData(); 98 | this.cdr.detectChanges(); 99 | } 100 | 101 | handlePieValueFormat(value: string | number): string { 102 | return yuan(value); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/routes/callback/callback.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { SocialService } from '@delon/auth'; 4 | import { SettingsService } from '@delon/theme'; 5 | 6 | @Component({ 7 | selector: 'app-callback', 8 | template: ``, 9 | providers: [SocialService], 10 | }) 11 | export class CallbackComponent implements OnInit { 12 | type: string; 13 | 14 | constructor( 15 | private socialService: SocialService, 16 | private settingsSrv: SettingsService, 17 | private route: ActivatedRoute, 18 | ) {} 19 | 20 | ngOnInit(): void { 21 | this.type = this.route.snapshot.params.type; 22 | this.mockModel(); 23 | } 24 | 25 | private mockModel() { 26 | const info = { 27 | token: '123456789', 28 | name: 'cipchk', 29 | email: `${this.type}@${this.type}.com`, 30 | id: 10000, 31 | time: +new Date(), 32 | }; 33 | this.settingsSrv.setUser({ 34 | ...this.settingsSrv.user, 35 | ...info, 36 | }); 37 | this.socialService.callback(info); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/routes/dashboard/dashboard.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | 3 | :host ::ng-deep { 4 | .my-card-body { 5 | height: 250px; 6 | } 7 | .sales-card { 8 | .bar { 9 | padding: 0 0 32px 32px; 10 | } 11 | .rank { 12 | padding: 0 32px 32px 72px; 13 | } 14 | .ant-tabs-bar { 15 | padding-left: 16px; 16 | .ant-tabs-nav .ant-tabs-tab { 17 | padding-top: 16px; 18 | padding-bottom: 14px; 19 | line-height: 24px; 20 | } 21 | } 22 | .ant-tabs-extra-content { 23 | padding-right: 24px; 24 | line-height: 55px; 25 | } 26 | .ant-card-head { 27 | position: relative; 28 | } 29 | .ant-card-head-title { 30 | align-items: normal; 31 | } 32 | } 33 | 34 | .rank { 35 | min-height: 208px; 36 | padding: 0 10px; 37 | } 38 | .rank-list { 39 | margin: 25px 0 0; 40 | padding: 0; 41 | list-style: none; 42 | li { 43 | .clearfix(); 44 | 45 | display: flex; 46 | align-items: center; 47 | margin-top: 16px; 48 | text-align: left; 49 | span { 50 | font-size: 14px; 51 | line-height: 22px; 52 | } 53 | .title { 54 | flex: 1; 55 | margin-right: 8px; 56 | padding-right: 10px; 57 | overflow: hidden; 58 | font-weight: bold; 59 | white-space: nowrap; 60 | text-overflow: ellipsis; 61 | } 62 | } 63 | } 64 | .g2-pie__legend-block .g2-pie__chart { 65 | margin: 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/routes/exception/403.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'exception-403', 5 | template: ` `, 6 | }) 7 | export class Exception403Component {} 8 | -------------------------------------------------------------------------------- /src/app/routes/exception/404.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'exception-404', 5 | template: ` `, 6 | }) 7 | export class Exception404Component {} 8 | -------------------------------------------------------------------------------- /src/app/routes/exception/500.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'exception-500', 5 | template: ` `, 6 | }) 7 | export class Exception500Component {} 8 | -------------------------------------------------------------------------------- /src/app/routes/exception/exception-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { Exception403Component } from './403.component'; 5 | import { Exception404Component } from './404.component'; 6 | import { Exception500Component } from './500.component'; 7 | import { ExceptionTriggerComponent } from './trigger.component'; 8 | 9 | const routes: Routes = [ 10 | { path: '403', component: Exception403Component }, 11 | { path: '404', component: Exception404Component }, 12 | { path: '500', component: Exception500Component }, 13 | { path: 'trigger', component: ExceptionTriggerComponent }, 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forChild(routes)], 18 | exports: [RouterModule], 19 | }) 20 | export class ExceptionRoutingModule {} 21 | -------------------------------------------------------------------------------- /src/app/routes/exception/exception.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | 4 | import { ExceptionRoutingModule } from './exception-routing.module'; 5 | 6 | import { Exception403Component } from './403.component'; 7 | import { Exception404Component } from './404.component'; 8 | import { Exception500Component } from './500.component'; 9 | import { ExceptionTriggerComponent } from './trigger.component'; 10 | 11 | const COMPONENTS = [Exception403Component, Exception404Component, Exception500Component, ExceptionTriggerComponent]; 12 | const COMPONENTS_NOROUNT = []; 13 | 14 | @NgModule({ 15 | imports: [SharedModule, ExceptionRoutingModule], 16 | declarations: [...COMPONENTS, ...COMPONENTS_NOROUNT], 17 | entryComponents: COMPONENTS_NOROUNT, 18 | }) 19 | export class ExceptionModule {} 20 | -------------------------------------------------------------------------------- /src/app/routes/exception/trigger.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth'; 3 | import { _HttpClient } from '@delon/theme'; 4 | 5 | @Component({ 6 | selector: 'exception-trigger', 7 | template: ` 8 |
    9 | 10 | 11 | 12 | 13 |
    14 | `, 15 | }) 16 | export class ExceptionTriggerComponent { 17 | types = [401, 403, 404, 500]; 18 | 19 | constructor(private http: _HttpClient, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {} 20 | 21 | go(type: number): void { 22 | this.http.get(`/api/${type}`).subscribe(); 23 | } 24 | 25 | refresh(): void { 26 | this.tokenService.set({ token: 'invalid-token' }); 27 | // 必须提供一个后端地址,无法通过 Mock 来模拟 28 | this.http.post(`https://localhost:5001/auth`).subscribe( 29 | (res) => console.warn('成功', res), 30 | (err) => { 31 | console.log('最后结果失败', err); 32 | }, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/routes/passport/lock/lock.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 |
    6 |
    7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /src/app/routes/passport/lock/lock.component.less: -------------------------------------------------------------------------------- 1 | :host ::ng-deep { 2 | .ant-card-body { 3 | position: relative; 4 | margin-top: 80px; 5 | } 6 | .avatar { 7 | position: absolute; 8 | top: -20px; 9 | left: 50%; 10 | margin-left: -20px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/routes/passport/lock/lock.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth'; 5 | import { SettingsService, User } from '@delon/theme'; 6 | 7 | @Component({ 8 | selector: 'passport-lock', 9 | templateUrl: './lock.component.html', 10 | styleUrls: ['./lock.component.less'], 11 | }) 12 | export class UserLockComponent { 13 | f: FormGroup; 14 | 15 | get user(): User { 16 | return this.settings.user; 17 | } 18 | 19 | constructor( 20 | fb: FormBuilder, 21 | @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService, 22 | private settings: SettingsService, 23 | private router: Router, 24 | ) { 25 | this.f = fb.group({ 26 | password: [null, Validators.required], 27 | }); 28 | } 29 | 30 | submit() { 31 | // tslint:disable-next-line:forin 32 | for (const i in this.f.controls) { 33 | this.f.controls[i].markAsDirty(); 34 | this.f.controls[i].updateValueAndValidity(); 35 | } 36 | if (this.f.valid) { 37 | console.log('Valid!'); 38 | console.log(this.f.value); 39 | this.tokenService.set({ 40 | token: '123', 41 | }); 42 | this.router.navigate(['dashboard']); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/routes/passport/login/login.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{ 'app.login.forgot-password' | translate }} 27 | 28 | 29 | 30 | 33 | {{ 'app.login.signup' | translate }} 34 | 35 |
    36 | -------------------------------------------------------------------------------- /src/app/routes/passport/login/login.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host { 3 | display: block; 4 | width: 368px; 5 | margin: 0 auto; 6 | ::ng-deep { 7 | .ant-tabs .ant-tabs-bar { 8 | margin-bottom: 24px; 9 | text-align: center; 10 | border-bottom: 0; 11 | } 12 | .ant-tabs-tab { 13 | font-size: 16px; 14 | line-height: 24px; 15 | } 16 | .ant-input-affix-wrapper .ant-input:not(:first-child) { 17 | padding-left: 4px; 18 | } 19 | .icon { 20 | margin-left: 16px; 21 | color: rgba(0, 0, 0, 0.2); 22 | font-size: 24px; 23 | vertical-align: middle; 24 | cursor: pointer; 25 | transition: color 0.3s; 26 | &:hover { 27 | color: @primary-color; 28 | } 29 | } 30 | .submit { 31 | width: 50%; 32 | } 33 | .register { 34 | float: right; 35 | line-height: @btn-height-lg; 36 | } 37 | } 38 | } 39 | 40 | [data-theme='dark'] { 41 | :host ::ng-deep { 42 | .icon { 43 | color: rgba(255, 255, 255, 0.2); 44 | &:hover { 45 | color: #fff; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/routes/passport/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnDestroy, Optional } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { StartupService } from '@core'; 5 | import { ReuseTabService } from '@delon/abc/reuse-tab'; 6 | import { DA_SERVICE_TOKEN, ITokenService, SocialOpenType, SocialService } from '@delon/auth'; 7 | import { SettingsService, _HttpClient } from '@delon/theme'; 8 | import { environment } from '@env/environment'; 9 | import { NzMessageService } from 'ng-zorro-antd/message'; 10 | 11 | @Component({ 12 | selector: 'passport-login', 13 | templateUrl: './login.component.html', 14 | styleUrls: ['./login.component.less'], 15 | providers: [SocialService], 16 | }) 17 | export class UserLoginComponent implements OnDestroy { 18 | constructor( 19 | fb: FormBuilder, 20 | private router: Router, 21 | private settingsService: SettingsService, 22 | private socialService: SocialService, 23 | @Optional() 24 | @Inject(ReuseTabService) 25 | private reuseTabService: ReuseTabService, 26 | @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService, 27 | private startupSrv: StartupService, 28 | public http: _HttpClient, 29 | public msg: NzMessageService, 30 | ) { 31 | this.form = fb.group({ 32 | email: [null, [Validators.required, Validators.email]], 33 | password: [null, [Validators.required]], 34 | remember: [true], 35 | }); 36 | } 37 | 38 | // #region fields 39 | 40 | get email() { 41 | return this.form.controls.email; 42 | } 43 | get password() { 44 | return this.form.controls.password; 45 | } 46 | 47 | form: FormGroup; 48 | error = ''; 49 | type = 0; 50 | 51 | switch({ index }: { index: number }): void { 52 | this.type = index; 53 | } 54 | 55 | // #endregion 56 | 57 | submit() { 58 | this.error = ''; 59 | if (this.type === 0) { 60 | this.email.markAsDirty(); 61 | this.email.updateValueAndValidity(); 62 | this.password.markAsDirty(); 63 | this.password.updateValueAndValidity(); 64 | if (this.email.invalid || this.password.invalid) { 65 | return; 66 | } 67 | } else { 68 | return; 69 | } 70 | 71 | // 默认配置中对所有HTTP请求都会强制 [校验](https://ng-alain.com/auth/getting-started) 用户 Token 72 | // 然一般来说登录请求不需要校验,因此可以在请求URL加上:`/login?_allow_anonymous=true` 表示不触发用户 Token 校验 73 | this.http 74 | .post('/api/login?_allow_anonymous=true', { 75 | username: this.email.value, 76 | password: this.password.value, 77 | }) 78 | .subscribe((res) => { 79 | if (res.code !== 0) { 80 | this.error = res.message; 81 | return; 82 | } 83 | // 清空路由复用信息 84 | this.reuseTabService.clear(); 85 | // 设置用户Token信息 86 | this.tokenService.set({ token: res.data.token }); 87 | const user = { name: res.data.user.username, email: res.data.user.email, avatar: res.data.user.avatar }; 88 | this.settingsService.setUser(user); 89 | // 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响 90 | this.startupSrv.load().then(() => { 91 | let url = this.tokenService.referrer.url || '/'; 92 | if (url.includes('/passport')) { 93 | url = '/'; 94 | } 95 | this.router.navigateByUrl(url); 96 | }); 97 | }); 98 | } 99 | 100 | ngOnDestroy(): void {} 101 | } 102 | -------------------------------------------------------------------------------- /src/app/routes/passport/register-result/register-result.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | {{ 'app.register-result.msg' | translate: params }} 5 |
    6 |
    7 | 10 | 13 |
    14 | -------------------------------------------------------------------------------- /src/app/routes/passport/register-result/register-result.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | 5 | @Component({ 6 | selector: 'passport-register-result', 7 | templateUrl: './register-result.component.html', 8 | }) 9 | export class UserRegisterResultComponent { 10 | params = { email: '' }; 11 | email = ''; 12 | constructor(route: ActivatedRoute, public msg: NzMessageService) { 13 | this.params.email = this.email = route.snapshot.queryParams.email || 'ng-alain@example.com'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/routes/passport/register/register.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host { 3 | display: block; 4 | width: 368px; 5 | margin: 0 auto; 6 | ::ng-deep { 7 | h3 { 8 | margin-bottom: 20px; 9 | font-size: 16px; 10 | } 11 | .submit { 12 | width: 50%; 13 | } 14 | .login { 15 | float: right; 16 | line-height: @btn-height-lg; 17 | } 18 | } 19 | } 20 | ::ng-deep { 21 | .register-password-cdk { 22 | .success, 23 | .warning, 24 | .error { 25 | transition: color 0.3s; 26 | } 27 | .success { 28 | color: @success-color; 29 | } 30 | .warning { 31 | color: @warning-color; 32 | } 33 | .error { 34 | color: @error-color; 35 | } 36 | .progress-pass > .progress { 37 | .ant-progress-bg { 38 | background-color: @warning-color; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/routes/passport/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy } from '@angular/core'; 2 | import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { _HttpClient } from '@delon/theme'; 5 | import { NzMessageService } from 'ng-zorro-antd/message'; 6 | 7 | @Component({ 8 | selector: 'passport-register', 9 | templateUrl: './register.component.html', 10 | styleUrls: ['./register.component.less'], 11 | }) 12 | export class UserRegisterComponent implements OnDestroy { 13 | constructor(fb: FormBuilder, private router: Router, public http: _HttpClient, public msg: NzMessageService) { 14 | this.form = fb.group({ 15 | email: [null, [Validators.required, Validators.email]], 16 | username: [null, [Validators.required, Validators.minLength(3)]], 17 | password: [null, [Validators.required, Validators.minLength(6), UserRegisterComponent.checkPassword.bind(this)]], 18 | confirm: [null, [Validators.required, Validators.minLength(6), UserRegisterComponent.passwordEquar]], 19 | base_currency_code: [null, [Validators.required]], 20 | }); 21 | } 22 | 23 | // #region fields 24 | get username() { 25 | return this.form.controls.username; 26 | } 27 | get email() { 28 | return this.form.controls.email; 29 | } 30 | get password() { 31 | return this.form.controls.password; 32 | } 33 | 34 | form: FormGroup; 35 | error = ''; 36 | visible = false; 37 | status = 'pool'; 38 | progress = 0; 39 | passwordProgressMap = { 40 | ok: 'success', 41 | pass: 'normal', 42 | pool: 'exception', 43 | }; 44 | 45 | // #endregion 46 | 47 | static checkPassword(control: FormControl) { 48 | if (!control) { 49 | return null; 50 | } 51 | const self: any = this; 52 | self.visible = !!control.value; 53 | if (control.value && control.value.length > 9) { 54 | self.status = 'ok'; 55 | } else if (control.value && control.value.length > 5) { 56 | self.status = 'pass'; 57 | } else { 58 | self.status = 'pool'; 59 | } 60 | 61 | if (self.visible) { 62 | self.progress = control.value.length * 10 > 100 ? 100 : control.value.length * 10; 63 | } 64 | } 65 | 66 | static passwordEquar(control: FormControl) { 67 | if (!control || !control.parent) { 68 | return null; 69 | } 70 | if (control.value !== control.parent.get('password').value) { 71 | return { equar: true }; 72 | } 73 | return null; 74 | } 75 | 76 | // #endregion 77 | 78 | submit() { 79 | this.error = ''; 80 | Object.keys(this.form.controls).forEach((key) => { 81 | this.form.controls[key].markAsDirty(); 82 | this.form.controls[key].updateValueAndValidity(); 83 | }); 84 | if (this.form.invalid) { 85 | return; 86 | } 87 | 88 | const data = this.form.value; 89 | this.http.post('/api/join?_allow_anonymous=true', data).subscribe((res) => { 90 | if (res.code !== 0) { 91 | this.error = res.message; 92 | return; 93 | } 94 | this.msg.success('注册成功,请登录'); 95 | this.router.navigateByUrl('/passport/login', { 96 | queryParams: { email: data.email }, 97 | }); 98 | }); 99 | } 100 | 101 | ngOnDestroy(): void {} 102 | } 103 | -------------------------------------------------------------------------------- /src/app/routes/record/create-by-desc/create-by-desc.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 | -------------------------------------------------------------------------------- /src/app/routes/record/create-by-desc/create-by-desc.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { _HttpClient } from '@delon/theme'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | 5 | @Component({ 6 | selector: 'app-record-create-by-desc', 7 | templateUrl: './create-by-desc.component.html', 8 | }) 9 | export class RecordCreateByDescComponent { 10 | @Output() created = new EventEmitter<{}>(); 11 | 12 | i: { description?: string } = {}; 13 | 14 | constructor(public msgSrv: NzMessageService, public http: _HttpClient) {} 15 | 16 | submit() { 17 | const data = { description: this.i.description }; 18 | this.http.post('/api/transactions/by-description', data).subscribe((res) => { 19 | if (res.code !== 0) { 20 | this.msgSrv.warning(res.message); 21 | return; 22 | } 23 | this.msgSrv.success('添加成功'); 24 | this.i.description = ''; 25 | const q = { page: 1, pageSize: 50 }; 26 | this.created.emit(q); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/routes/record/form/form.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ i.name }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{ i.name }} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{ i.name }} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
    63 | -------------------------------------------------------------------------------- /src/app/routes/record/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import { CacheService } from '@delon/cache'; 3 | import { _HttpClient } from '@delon/theme'; 4 | import { toDate } from '@delon/util'; 5 | import format from 'date-fns/format'; 6 | import { NzMessageService } from 'ng-zorro-antd/message'; 7 | import { NzModalRef } from 'ng-zorro-antd/modal'; 8 | 9 | @Component({ 10 | selector: 'app-record-form', 11 | templateUrl: './form.component.html', 12 | }) 13 | export class RecordFormComponent implements OnInit { 14 | record: any = {}; 15 | selectData: any = {}; 16 | selectCacheKey = 'RECORD_SEARCH_SELECT_CACHE_KEY'; 17 | 18 | form = { 19 | type: 'expense', 20 | from_account_id: '', 21 | to_account_id: '', 22 | currency_amount: '', 23 | currency_code: 'CNY', 24 | category_id: '', 25 | tags: [], 26 | remark: '', 27 | date: new Date(), 28 | reimbursement_status: 'none', 29 | exclude_from_stats: false, 30 | status: 'done', 31 | }; 32 | 33 | constructor( 34 | private http: _HttpClient, 35 | private modal: NzModalRef, 36 | private msgSrv: NzMessageService, 37 | private cdr: ChangeDetectorRef, 38 | private cache: CacheService, 39 | ) {} 40 | 41 | ngOnInit(): void { 42 | if (this.record) { 43 | this.form = Object.assign({}, this.record); 44 | this.form.date = toDate(this.record.date); 45 | } 46 | this.selectData = this.cache.getNone(this.selectCacheKey); 47 | this.changeTransactionType(this.form.type); 48 | } 49 | 50 | save(value: any) { 51 | const url = this.record?.id ? `/${this.record.id}` : ''; 52 | const method = this.record?.id ? 'put' : 'post'; 53 | value.date = format(new Date(value.date), 'yyyy-MM-dd HH:mm'); 54 | 55 | this.http.request(method, `/api/transactions${url}`, { body: value }).subscribe((res: any) => { 56 | if (res.code !== 0) { 57 | this.msgSrv.warning(res.message); 58 | return; 59 | } 60 | this.msgSrv.success('保存成功'); 61 | this.modal.close(true); 62 | }); 63 | } 64 | 65 | changeTransactionType(type: string) { 66 | this.http.get('/api/categories', { transaction_type: type }).subscribe((res: any) => { 67 | if (res.code !== 0) { 68 | this.msgSrv.warning(res.message); 69 | return; 70 | } 71 | this.selectData.category_id = res.data.items.map((item: any) => ({ id: item.id, name: item.name, icon: item.icon_name })); 72 | if (!this.record) { 73 | this.form.category_id = this.selectData.category_id[0].id; 74 | } else { 75 | this.form.category_id = this.form.type !== this.record.type ? this.selectData.category_id[0].id : this.record.category_id; 76 | } 77 | this.cdr.detectChanges(); 78 | }); 79 | } 80 | 81 | close() { 82 | this.modal.destroy(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/routes/record/import/import.component.html: -------------------------------------------------------------------------------- 1 | 4 |

    5 | 6 | 下载模版文件 7 | 8 |

    9 | 10 | 11 | 12 | 13 | 14 | 15 | 列名 16 | 描述 17 | 是否必须 18 | 19 | 20 | 21 | 22 | {{ data.column_name }} 23 | {{ data.description }} 24 | {{ data.required }} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

    33 | 34 |

    35 |

    点击或拖动文件到此区域上传

    36 |

    37 | 支持多次上传,每次上传的文件要么全部失败,要么全部成功导入数据。 38 |

    39 |
    40 | -------------------------------------------------------------------------------- /src/app/routes/record/import/import.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { _HttpClient } from '@delon/theme'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { NzModalRef } from 'ng-zorro-antd/modal'; 5 | import { NzUploadChangeParam } from 'ng-zorro-antd/upload'; 6 | 7 | @Component({ 8 | selector: 'app-record-import', 9 | templateUrl: './import.component.html', 10 | }) 11 | export class RecordImportComponent { 12 | dataSet = [ 13 | { column_name: '账单日期', description: '消费时间, 常见的时间格式都支持,如:「2020-09-08 20:35」 「2020-09-08」', required: '是' }, 14 | { column_name: '类别', description: '账单类别,必须已经存在的分类', required: '是' }, 15 | { column_name: '类型', description: '账单类型,目前只有「支出」、「收入」、「转账」', required: '是' }, 16 | { column_name: '金额', description: '消费金额,整数或者最多两位小数', required: '是' }, 17 | { column_name: '标签', description: '多个标签用「/」分开', required: '否' }, 18 | { column_name: '描述', description: '账单描述', required: '否' }, 19 | { column_name: '备注', description: '账单备注', required: '否' }, 20 | { 21 | column_name: '账户1', 22 | description: '收入或者支出的账户,或者是转账类型的转出账户。如果为空,则使用默认账户,否则填写的账户必须已存在', 23 | required: '否', 24 | }, 25 | { column_name: '账户2', description: '转账类型时的转入账户', required: '交易类型为「转账」时必填' }, 26 | ]; 27 | 28 | constructor(private modal: NzModalRef, private msgSrv: NzMessageService, public http: _HttpClient) {} 29 | 30 | handleChange({ file, fileList }: NzUploadChangeParam): void { 31 | const status = file.status; 32 | if (status === 'done') { 33 | if (file.response.code === 0) { 34 | if (file.response.data.fail === 0) { 35 | this.msgSrv.success(`${file.name} 导入成功`); 36 | } else { 37 | const error = file.response.data.fail_list[0]; 38 | this.msgSrv.error(`${file.name} 导入失败:[${error.data.toString()}] ${error.reason}`); 39 | } 40 | } else { 41 | this.msgSrv.error(`${file.name} 导入失败:${file.response.message}`); 42 | } 43 | } else if (status === 'error') { 44 | this.msgSrv.error(`${file.name} 文件上传失败`); 45 | } 46 | } 47 | 48 | close() { 49 | this.modal.destroy(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/routes/record/index/index.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | 6 |
    7 |
    8 | 12 |
    13 |
    14 | 15 | 16 | 17 | 18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{ item.date }} / 支出:{{ item.out }} / 收入:{{ item.in }} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {{ record?.category?.name }} 48 | 49 | 50 |

    51 | {{ record.transaction?.description }} 52 | ({{ record.transaction.remark }}) 53 |

    54 |
    55 | 56 | 57 | 58 |
    59 | 60 |
    61 |

    62 |

    来源:{{ record.source_text }}

    63 |
    64 |
    65 |
    66 | {{ tag }} 67 |
    68 |
    69 |
    70 |

    71 | {{ record.direction === 'expense' ? '-' : '' }} 72 | {{ record.currency_code }} 73 | {{ record.currency_amount }} 74 | 不计入 75 |

    76 |
    77 |
    78 |
    79 | 80 |
    81 |
    82 | 83 |
    84 |
    85 |
    86 | -------------------------------------------------------------------------------- /src/app/routes/record/index/index.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host ::ng-deep { 3 | .select-icon { 4 | padding-right: 4px; 5 | } 6 | 7 | .loadmore { 8 | height: 32px; 9 | margin-top: 12px; 10 | line-height: 32px; 11 | text-align: center; 12 | } 13 | 14 | .float-right { 15 | float: right; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/routes/record/record-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { RecordIndexComponent } from './index/index.component'; 4 | 5 | const routes: Routes = [{ path: 'index', component: RecordIndexComponent }]; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule], 10 | }) 11 | export class RecordRoutingModule {} 12 | -------------------------------------------------------------------------------- /src/app/routes/record/record.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | import { RecordCreateByDescComponent } from './create-by-desc/create-by-desc.component'; 4 | import { RecordFormComponent } from './form/form.component'; 5 | import { RecordImportComponent } from './import/import.component'; 6 | import { RecordIndexComponent } from './index/index.component'; 7 | import { RecordRoutingModule } from './record-routing.module'; 8 | 9 | const COMPONENTS = [RecordIndexComponent]; 10 | const COMPONENTS_NOROUNT = [RecordFormComponent, RecordCreateByDescComponent, RecordImportComponent]; 11 | 12 | @NgModule({ 13 | imports: [SharedModule, RecordRoutingModule], 14 | declarations: [...COMPONENTS, ...COMPONENTS_NOROUNT], 15 | }) 16 | export class RecordModule {} 17 | -------------------------------------------------------------------------------- /src/app/routes/recurrence/form/form.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 记录详情 7 | {{ transaction.type_text }} 8 | {{ transaction.fromAccount?.name }} 9 | {{ transaction.toAccount?.name }} 10 | {{ transaction.category?.name }} 11 | {{ transaction.amount }} 12 | {{ transaction.tags }} 13 | {{ transaction.remark }} 14 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /src/app/routes/recurrence/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; 2 | import { SFComponent, SFDateWidgetSchema, SFRadioWidgetSchema, SFSchema, SFSelectWidgetSchema, SFUISchema } from '@delon/form'; 3 | import { _HttpClient } from '@delon/theme'; 4 | import { NzMessageService } from 'ng-zorro-antd/message'; 5 | import { NzModalRef } from 'ng-zorro-antd/modal'; 6 | import { map, tap } from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 'app-recurrence-form', 10 | templateUrl: './form.component.html', 11 | }) 12 | export class RecurrenceFormComponent implements OnInit { 13 | @ViewChild('sf', { static: false }) private sf: SFComponent; 14 | record: any = {}; 15 | transaction: any = {}; 16 | url = `/api/recurrences`; 17 | schema: SFSchema = { 18 | properties: { 19 | name: { type: 'string', title: '名称' }, 20 | frequency: { 21 | type: 'string', 22 | title: '频率', 23 | default: null, 24 | ui: { 25 | widget: 'select', 26 | asyncData: () => { 27 | return this.http.get('/api/recurrences/frequencies').pipe( 28 | map((res) => { 29 | return res.data.map((item: any) => { 30 | return { value: item.type, label: item.name }; 31 | }); 32 | }), 33 | ); 34 | }, 35 | } as SFSelectWidgetSchema, 36 | }, 37 | schedule: { 38 | type: 'string', 39 | title: '时间值', 40 | description: '每周范围:1~7;每月范围1~31;每年范围:01-01~12-31', 41 | ui: { 42 | visibleIf: { frequency: ['week', 'month', 'year'] }, 43 | } as SFSelectWidgetSchema, 44 | }, 45 | started_at: { 46 | type: 'string', 47 | title: '开始时间', 48 | format: 'date', 49 | default: new Date(), 50 | ui: { 51 | widget: 'date', 52 | disabledDate: (date) => new Date(date.toDateString()) < new Date(new Date().toDateString()), 53 | } as SFDateWidgetSchema, 54 | }, 55 | status: { 56 | type: 'string', 57 | title: '状态', 58 | enum: [ 59 | { value: 'active', label: '开启' }, 60 | { value: 'unactivated', label: '停用' }, 61 | ], 62 | ui: { 63 | widget: 'radio', 64 | styleType: 'button', 65 | buttonStyle: 'solid', 66 | } as SFRadioWidgetSchema, 67 | default: 'active', 68 | }, 69 | }, 70 | required: ['name', 'frequency', 'started_at'], 71 | }; 72 | ui: SFUISchema = { 73 | '*': { 74 | spanLabelFixed: 100, 75 | grid: { span: 24 }, 76 | }, 77 | }; 78 | 79 | constructor(private modal: NzModalRef, private msgSrv: NzMessageService, public http: _HttpClient, private cdr: ChangeDetectorRef) {} 80 | 81 | ngOnInit() { 82 | this.getTransaction(); 83 | } 84 | 85 | save(value: any) { 86 | const url = this.record.id ? `/${this.record.id}` : ''; 87 | const method = this.record.id ? 'put' : 'post'; 88 | this.http.request(method, `${this.url}${url}`, { body: value }).subscribe((res: any) => { 89 | if (res.code !== 0) { 90 | this.msgSrv.warning(res.message); 91 | return; 92 | } 93 | this.msgSrv.success('保存成功'); 94 | this.modal.close(res.data); 95 | }); 96 | } 97 | 98 | getTransaction(): void { 99 | this.http.get(`/api/transactions/${this.record.transaction_id}`, { expand: 'toAccount,category,fromAccount' }).subscribe((res) => { 100 | this.transaction = res.data; 101 | this.cdr.detectChanges(); 102 | }); 103 | } 104 | 105 | close() { 106 | this.modal.destroy(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/routes/recurrence/index/index.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | 6 |
    7 |
    8 |
    9 |
    10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/routes/recurrence/recurrence-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { RecurrenceIndexComponent } from './index/index.component'; 4 | 5 | const routes: Routes = [{ path: 'index', component: RecurrenceIndexComponent }]; 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule], 10 | }) 11 | export class RecurrenceRoutingModule {} 12 | -------------------------------------------------------------------------------- /src/app/routes/recurrence/recurrence.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | import { RecurrenceRoutingModule } from './recurrence-routing.module'; 4 | import { RecurrenceIndexComponent } from './index/index.component'; 5 | import { RecurrenceFormComponent } from './form/form.component'; 6 | 7 | const COMPONENTS = [RecurrenceIndexComponent]; 8 | const COMPONENTS_NOROUNT = [RecurrenceFormComponent]; 9 | 10 | @NgModule({ 11 | imports: [SharedModule, RecurrenceRoutingModule], 12 | declarations: [...COMPONENTS, ...COMPONENTS_NOROUNT], 13 | }) 14 | export class RecurrenceModule {} 15 | -------------------------------------------------------------------------------- /src/app/routes/routes-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { JWTGuard } from '@delon/auth'; 4 | import { environment } from '@env/environment'; 5 | // layout 6 | import { LayoutDefaultComponent } from '../layout/default/default.component'; 7 | import { LayoutFullScreenComponent } from '../layout/fullscreen/fullscreen.component'; 8 | import { LayoutPassportComponent } from '../layout/passport/passport.component'; 9 | // single pages 10 | import { CallbackComponent } from './callback/callback.component'; 11 | // dashboard pages 12 | import { DashboardComponent } from './dashboard/dashboard.component'; 13 | import { UserLockComponent } from './passport/lock/lock.component'; 14 | // passport pages 15 | import { UserLoginComponent } from './passport/login/login.component'; 16 | import { UserRegisterResultComponent } from './passport/register-result/register-result.component'; 17 | import { UserRegisterComponent } from './passport/register/register.component'; 18 | 19 | const routes: Routes = [ 20 | { 21 | path: '', 22 | component: LayoutDefaultComponent, 23 | canActivate: [JWTGuard], 24 | children: [ 25 | { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, 26 | { path: 'dashboard', component: DashboardComponent, data: { title: '仪表盘' } }, 27 | { path: 'account', loadChildren: () => import('./account/account.module').then((m) => m.AccountModule) }, 28 | { path: 'record', loadChildren: () => import('./record/record.module').then((m) => m.RecordModule) }, 29 | { path: 'recurrence', loadChildren: () => import('./recurrence/recurrence.module').then((m) => m.RecurrenceModule) }, 30 | { path: 'analysis', loadChildren: () => import('./analysis/analysis.module').then((m) => m.AnalysisModule) }, 31 | { path: 'exception', loadChildren: () => import('./exception/exception.module').then((m) => m.ExceptionModule) }, 32 | { path: 'settings', loadChildren: () => import('./settings/settings.module').then((m) => m.SettingsModule) }, 33 | // 业务子模块 34 | // { path: 'widgets', loadChildren: () => import('./widgets/widgets.module').then(m => m.WidgetsModule) }, 35 | ], 36 | }, 37 | // 全屏布局 38 | // { 39 | // path: 'fullscreen', 40 | // component: LayoutFullScreenComponent, 41 | // children: [ 42 | // ] 43 | // }, 44 | // passport 45 | { 46 | path: 'passport', 47 | component: LayoutPassportComponent, 48 | children: [ 49 | { path: 'login', component: UserLoginComponent, data: { title: '登录' } }, 50 | { path: 'register', component: UserRegisterComponent, data: { title: '注册' } }, 51 | { path: 'register-result', component: UserRegisterResultComponent, data: { title: '注册结果' } }, 52 | { path: 'lock', component: UserLockComponent, data: { title: '锁屏' } }, 53 | ], 54 | }, 55 | // 单页不包裹Layout 56 | { path: 'callback/:type', component: CallbackComponent }, 57 | { path: '**', redirectTo: 'exception/404' }, 58 | ]; 59 | 60 | @NgModule({ 61 | imports: [ 62 | RouterModule.forRoot(routes, { 63 | useHash: environment.useHash, 64 | // NOTICE: If you use `reuse-tab` component and turn on keepingScroll you can set to `disabled` 65 | // Pls refer to https://ng-alain.com/components/reuse-tab 66 | scrollPositionRestoration: 'top', 67 | }), 68 | ], 69 | exports: [RouterModule], 70 | }) 71 | export class RouteRoutingModule {} 72 | -------------------------------------------------------------------------------- /src/app/routes/routes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SharedModule } from '@shared'; 4 | // single pages 5 | import { CallbackComponent } from './callback/callback.component'; 6 | // dashboard pages 7 | import { DashboardComponent } from './dashboard/dashboard.component'; 8 | import { UserLockComponent } from './passport/lock/lock.component'; 9 | // passport pages 10 | import { UserLoginComponent } from './passport/login/login.component'; 11 | import { UserRegisterResultComponent } from './passport/register-result/register-result.component'; 12 | import { UserRegisterComponent } from './passport/register/register.component'; 13 | import { RouteRoutingModule } from './routes-routing.module'; 14 | 15 | const COMPONENTS = [ 16 | DashboardComponent, 17 | // passport pages 18 | UserLoginComponent, 19 | UserRegisterComponent, 20 | UserRegisterResultComponent, 21 | // single pages 22 | CallbackComponent, 23 | UserLockComponent, 24 | ]; 25 | const COMPONENTS_NOROUNT = []; 26 | 27 | @NgModule({ 28 | imports: [SharedModule, RouteRoutingModule], 29 | declarations: [...COMPONENTS, ...COMPONENTS_NOROUNT], 30 | }) 31 | export class RoutesModule {} 32 | -------------------------------------------------------------------------------- /src/app/routes/settings/categories/categories.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | 6 |
    7 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 16 | 17 | {{ c.title.text }} 18 | 19 | 20 | {{ item.name }} 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/app/routes/settings/categories/categories.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; 2 | import { STColumn, STComponent } from '@delon/abc/st'; 3 | import { SFSchema, SFSelectWidgetSchema } from '@delon/form'; 4 | import { ModalHelper, _HttpClient } from '@delon/theme'; 5 | import { NzMessageService } from 'ng-zorro-antd/message'; 6 | import { map } from 'rxjs/operators'; 7 | import { SettingsCategoriesEditComponent } from './edit/edit.component'; 8 | 9 | @Component({ 10 | selector: 'app-settings-categories', 11 | templateUrl: './categories.component.html', 12 | }) 13 | export class SettingsCategoriesComponent implements OnInit { 14 | @ViewChild('st', { static: false }) st: STComponent; 15 | 16 | loading = true; 17 | pagination: {}; 18 | list: any[] = []; 19 | q = { 20 | page: 1, 21 | pageSize: 100, 22 | name: '', 23 | transaction_type: '', 24 | }; 25 | transactionTypes: any[] = []; 26 | 27 | searchSchema: SFSchema = { 28 | properties: { 29 | name: { 30 | type: 'string', 31 | title: '名称', 32 | }, 33 | transaction_type: { 34 | type: 'string', 35 | title: '交易类型', 36 | default: '', 37 | ui: { 38 | widget: 'select', 39 | asyncData: () => { 40 | return this.http.get('/api/transactions/types').pipe( 41 | map((res) => { 42 | if (res.code !== 0) { 43 | this.msg.warning(res.message); 44 | return; 45 | } 46 | return res.data.map((item: any) => { 47 | return { value: item.type, label: item.name }; 48 | }); 49 | }), 50 | ); 51 | }, 52 | } as SFSelectWidgetSchema, 53 | }, 54 | }, 55 | }; 56 | columns: STColumn[] = [ 57 | { title: '名称', renderTitle: 'customTitle', render: 'custom' }, 58 | { title: '交易类型', index: 'transaction_type_text' }, 59 | { title: '排序', index: 'sort' }, 60 | { title: '时间', type: 'date', index: 'updated_at' }, 61 | { 62 | title: '', 63 | buttons: [ 64 | { 65 | text: '编辑', 66 | click: (item: any) => this.form(item), 67 | iif: (record) => !['adjust', 'transfer'].includes(record.transaction_type), 68 | iifBehavior: 'disabled', 69 | }, 70 | ], 71 | }, 72 | ]; 73 | 74 | constructor(private http: _HttpClient, private modal: ModalHelper, private cdr: ChangeDetectorRef, private msg: NzMessageService) {} 75 | 76 | ngOnInit() { 77 | this.getData(); 78 | } 79 | 80 | getData(): void { 81 | this.loading = true; 82 | const data = this.http.get('/api/categories', this.q).subscribe((res) => { 83 | this.list = res.data.items; 84 | this.pagination = res.data._meta; 85 | this.loading = false; 86 | }); 87 | } 88 | 89 | form(record: { id?: number } = {}): void { 90 | this.modal.create(SettingsCategoriesEditComponent, { record }, { size: 'md' }).subscribe((res) => { 91 | if (record.id) { 92 | // record = res; 93 | this.getData(); 94 | } else { 95 | this.list.splice(0, 0, res); 96 | this.list = [...this.list]; 97 | } 98 | }); 99 | } 100 | 101 | submit(value: any): void { 102 | this.q = value; 103 | this.getData(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/app/routes/settings/categories/edit/edit.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
    30 | -------------------------------------------------------------------------------- /src/app/routes/settings/categories/edit/edit.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import { _HttpClient } from '@delon/theme'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { NzModalRef } from 'ng-zorro-antd/modal'; 5 | 6 | @Component({ 7 | selector: 'app-settings-categories-edit', 8 | templateUrl: './edit.component.html', 9 | }) 10 | export class SettingsCategoriesEditComponent implements OnInit { 11 | record: any = {}; 12 | form = { 13 | name: '', 14 | transaction_type: 'expense', 15 | sort: 99, 16 | icon_name: '', 17 | }; 18 | icons: []; 19 | 20 | constructor(private modal: NzModalRef, private msgSrv: NzMessageService, public http: _HttpClient, private cdr: ChangeDetectorRef) {} 21 | 22 | ngOnInit(): void { 23 | this.loadIcons(); 24 | if (this.record.id) { 25 | this.form = this.record; 26 | } 27 | } 28 | 29 | save(value: any) { 30 | const url = this.record.id ? `/${this.record.id}` : ''; 31 | const method = this.record.id ? 'put' : 'post'; 32 | this.http.request(method, `/api/categories${url}`, { body: value }).subscribe((res: any) => { 33 | if (res.code !== 0) { 34 | this.msgSrv.warning(res.message); 35 | return; 36 | } 37 | this.msgSrv.success('保存成功'); 38 | this.modal.close(res.data); 39 | }); 40 | } 41 | 42 | loadIcons(): void { 43 | this.http.get('/api/icons').subscribe((res: any) => { 44 | if (res.code !== 0) { 45 | this.msgSrv.warning(res.message); 46 | return; 47 | } 48 | this.icons = res.data; 49 | this.cdr.detectChanges(); 50 | }); 51 | } 52 | 53 | close() { 54 | this.modal.destroy(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/routes/settings/categories/view/view.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | {{ i.owner }} 7 | 32943898021309809423 8 | 3321944288191034921 9 | 18112345678 10 | 曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口 11 |
    12 | 15 | -------------------------------------------------------------------------------- /src/app/routes/settings/categories/view/view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NzModalRef } from 'ng-zorro-antd/modal'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { _HttpClient } from '@delon/theme'; 5 | 6 | @Component({ 7 | selector: 'app-settings-categories-view', 8 | templateUrl: './view.component.html', 9 | }) 10 | export class SettingsCategoriesViewComponent implements OnInit { 11 | record: any = {}; 12 | i: any; 13 | 14 | constructor(private modal: NzModalRef, public msgSrv: NzMessageService, public http: _HttpClient) {} 15 | 16 | ngOnInit(): void { 17 | this.http.get(`/user/${this.record.id}`).subscribe((res) => (this.i = res)); 18 | } 19 | 20 | close() { 21 | this.modal.destroy(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/base/base.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/base/base.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; 2 | import { STColumn, STComponent } from '@delon/abc/st'; 3 | import { SFSchema } from '@delon/form'; 4 | import { ModalHelper, SettingsService, TitleService, _HttpClient } from '@delon/theme'; 5 | import { NzMessageService } from 'ng-zorro-antd/message'; 6 | 7 | @Component({ 8 | selector: 'app-settings-base', 9 | templateUrl: './base.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | }) 12 | export class SettingsBaseComponent implements OnInit { 13 | schema: SFSchema = { 14 | properties: { 15 | name: { 16 | type: 'string', 17 | title: '用户名', 18 | }, 19 | email: { 20 | type: 'string', 21 | format: 'email', 22 | title: '邮箱', 23 | }, 24 | base_currency_code: { 25 | type: 'string', 26 | title: '基础货币', 27 | enum: [{ label: 'CNY', value: 'CNY' }], 28 | default: 'CNY', 29 | }, 30 | }, 31 | required: ['username', 'email', 'base_currency_code'], 32 | }; 33 | user: any; 34 | 35 | constructor( 36 | private http: _HttpClient, 37 | private settings: SettingsService, 38 | private titleSrv: TitleService, 39 | private msg: NzMessageService, 40 | ) {} 41 | 42 | ngOnInit() { 43 | this.titleSrv.setTitle('基本设置'); 44 | this.user = this.settings.user; 45 | } 46 | 47 | submit(value: any) { 48 | this.msg.success('暂未开发'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/binding/binding.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 修改 5 | 6 | 7 | 8 | 9 | 10 | 11 |
    当前未绑定 Telegram 账号
    12 |
    13 | 当前绑定的 Telegram 账号 14 | @{{ telegram?.client_username }} 15 |
    16 |
    17 |
    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/binding/binding.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import { SettingsService, TitleService, _HttpClient } from '@delon/theme'; 3 | import { environment } from '@env/environment'; 4 | import { NzIconService } from 'ng-zorro-antd/icon'; 5 | import { NzMessageService } from 'ng-zorro-antd/message'; 6 | import { NzModalService } from 'ng-zorro-antd/modal'; 7 | 8 | @Component({ 9 | selector: 'app-settings-binding', 10 | templateUrl: './binding.component.html', 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class SettingsBindingComponent implements OnInit { 14 | telegram = { client_username: '' }; 15 | 16 | constructor( 17 | private http: _HttpClient, 18 | public msg: NzMessageService, 19 | private iconService: NzIconService, 20 | private modal: NzModalService, 21 | private cdr: ChangeDetectorRef, 22 | private titleSrv: TitleService, 23 | private settings: SettingsService, 24 | ) { 25 | this.iconService.fetchFromIconfont({ 26 | scriptUrl: environment.iconfontURl, 27 | }); 28 | } 29 | 30 | ngOnInit() { 31 | this.titleSrv.setTitle('账号绑定'); 32 | this.getData(); 33 | } 34 | 35 | getData() { 36 | this.http.get('/api/users/auth-clients').subscribe((res) => { 37 | this.telegram = res.data?.telegram; 38 | this.cdr.detectChanges(); 39 | }); 40 | } 41 | 42 | bindTelegram() { 43 | const telegramBotName = this.settings.app.telegram_bot_name; 44 | this.http.post('/api/reset-token').subscribe((res) => { 45 | const code = `/bind/${res.data.reset_token}`; 46 | this.modal.info({ 47 | nzWidth: '500px', 48 | nzTitle: '绑定 Telegram 账号', 49 | nzContent: `将下面的绑定码复制发送给 Telegram 机器人 @${telegramBotName}
    ${code}`, 50 | nzOnOk: () => this.getData(), 51 | }); 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/personal.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 7 |
    8 |
    {{ title }}
    9 | 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/personal.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | 3 | :host { 4 | display: block; 5 | padding-top: 24px; 6 | ::ng-deep { 7 | .main { 8 | display: flex; 9 | width: 100%; 10 | padding-top: 16px; 11 | padding-bottom: 16px; 12 | overflow: auto; 13 | background-color: #fff; 14 | } 15 | 16 | .menu { 17 | width: 224px; 18 | border-right: @border-width-base @border-style-base @border-color-split; 19 | .ant-menu-inline { 20 | border: none; 21 | } 22 | .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { 23 | font-weight: bold; 24 | } 25 | } 26 | 27 | .content { 28 | flex: 1; 29 | padding-top: 8px; 30 | padding-right: 40px; 31 | padding-bottom: 8px; 32 | padding-left: 40px; 33 | .title { 34 | margin-bottom: 12px; 35 | color: @heading-color; 36 | font-weight: 500; 37 | font-size: 20px; 38 | line-height: 28px; 39 | } 40 | .ant-list-split .ant-list-item:last-child { 41 | border-bottom: 1px solid #e8e8e8; 42 | } 43 | .ant-list-item { 44 | padding-top: 14px; 45 | padding-bottom: 14px; 46 | } 47 | } 48 | 49 | @media screen and (max-width: @mobile-max) { 50 | .main { 51 | flex-direction: column; 52 | .menu { 53 | width: 100%; 54 | border: none; 55 | } 56 | .content { 57 | padding: 40px; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | [data-theme='dark'] { 65 | :host ::ng-deep { 66 | .main { 67 | background-color: #141414; 68 | } 69 | .content { 70 | .title { 71 | color: rgba(255, 255, 255, 0.65); 72 | } 73 | } 74 | .menu { 75 | border-right-color: #303030; 76 | } 77 | .content .ant-list-split .ant-list-item:last-child { 78 | border-bottom-color: #303030; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/personal.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy } from '@angular/core'; 2 | import { ActivationEnd, Router } from '@angular/router'; 3 | import { _HttpClient } from '@delon/theme'; 4 | import { fromEvent, Subscription } from 'rxjs'; 5 | import { debounceTime, filter } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'app-settings-personal', 9 | templateUrl: './personal.component.html', 10 | styleUrls: ['./personal.component.less'], 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class SettingsPersonalComponent implements AfterViewInit, OnDestroy { 14 | private resize$: Subscription; 15 | private router$: Subscription; 16 | mode = 'inline'; 17 | title: string; 18 | menus: Array<{ key: string; title: string; selected?: boolean }> = [ 19 | { 20 | key: 'base', 21 | title: '基本设置', 22 | }, 23 | // { 24 | // key: 'security', 25 | // title: '安全设置', 26 | // }, 27 | { 28 | key: 'binding', 29 | title: '账号绑定', 30 | }, 31 | ]; 32 | constructor(private router: Router, private cdr: ChangeDetectorRef, private el: ElementRef) { 33 | this.router$ = this.router.events.pipe(filter((e) => e instanceof ActivationEnd)).subscribe(() => this.setActive()); 34 | } 35 | 36 | private setActive(): void { 37 | const key = this.router.url.substr(this.router.url.lastIndexOf('/') + 1); 38 | this.menus.forEach((i) => { 39 | i.selected = i.key === key; 40 | }); 41 | this.title = this.menus.find((w) => w.selected).title; 42 | } 43 | 44 | to(item: { key: string }): void { 45 | this.router.navigateByUrl(`/settings/personal/${item.key}`); 46 | } 47 | 48 | private resize(): void { 49 | const el = this.el.nativeElement; 50 | let mode = 'inline'; 51 | const { offsetWidth } = el; 52 | if (offsetWidth < 641 && offsetWidth > 400) { 53 | mode = 'horizontal'; 54 | } 55 | if (window.innerWidth < 768 && offsetWidth > 400) { 56 | mode = 'horizontal'; 57 | } 58 | this.mode = mode; 59 | this.cdr.detectChanges(); 60 | } 61 | 62 | ngAfterViewInit(): void { 63 | this.resize$ = fromEvent(window, 'resize') 64 | .pipe(debounceTime(200)) 65 | .subscribe(() => this.resize()); 66 | } 67 | 68 | ngOnDestroy(): void { 69 | this.resize$.unsubscribe(); 70 | this.router$.unsubscribe(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/security/security.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/routes/settings/personal/security/security.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { SFSchema } from '@delon/form'; 3 | import { ModalHelper, SettingsService, _HttpClient, TitleService } from '@delon/theme'; 4 | 5 | @Component({ 6 | selector: 'app-settings-security', 7 | templateUrl: './security.component.html', 8 | }) 9 | export class SettingsSecurityComponent implements OnInit { 10 | schema: SFSchema = { 11 | properties: { 12 | name: { 13 | type: 'string', 14 | title: '用户名', 15 | }, 16 | email: { 17 | type: 'string', 18 | format: 'email', 19 | title: '邮箱', 20 | }, 21 | base_currency_code: { 22 | type: 'string', 23 | title: '基础货币', 24 | enum: [{ label: 'CNY', value: 'CNY' }], 25 | default: 'CNY', 26 | }, 27 | }, 28 | required: ['username', 'email', 'base_currency_code'], 29 | }; 30 | user: any; 31 | 32 | constructor(private http: _HttpClient, private settings: SettingsService, private titleSrv: TitleService) {} 33 | 34 | ngOnInit() { 35 | this.titleSrv.setTitle('安全设置'); 36 | this.user = this.settings.user; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/routes/settings/rules/form/form.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/routes/settings/rules/rules.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | 6 |
    7 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/routes/settings/settings-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { SettingsBaseComponent } from './personal/base/base.component'; 4 | import { SettingsBindingComponent } from './personal/binding/binding.component'; 5 | import { SettingsPersonalComponent } from './personal/personal.component'; 6 | import { SettingsSecurityComponent } from './personal/security/security.component'; 7 | import { SettingsRulesComponent } from './rules/rules.component'; 8 | import { SettingsTagsComponent } from './tags/tags.component'; 9 | import { SettingsCategoriesComponent } from './categories/categories.component'; 10 | 11 | const routes: Routes = [ 12 | { path: 'rules', component: SettingsRulesComponent }, 13 | { 14 | path: 'personal', 15 | component: SettingsPersonalComponent, 16 | children: [ 17 | { path: '', redirectTo: 'base', pathMatch: 'full' }, 18 | { path: 'base', component: SettingsBaseComponent }, 19 | { path: 'binding', component: SettingsBindingComponent }, 20 | { path: 'security', component: SettingsSecurityComponent }, 21 | ], 22 | }, 23 | { path: 'tags', component: SettingsTagsComponent }, 24 | { path: 'categories', component: SettingsCategoriesComponent }, 25 | ]; 26 | 27 | @NgModule({ 28 | imports: [RouterModule.forChild(routes)], 29 | exports: [RouterModule], 30 | }) 31 | export class SettingsRoutingModule {} 32 | -------------------------------------------------------------------------------- /src/app/routes/settings/settings.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from '@shared'; 3 | import { SettingsBaseComponent } from './personal/base/base.component'; 4 | import { SettingsBindingComponent } from './personal/binding/binding.component'; 5 | import { SettingsPersonalComponent } from './personal/personal.component'; 6 | import { SettingsRulesFormComponent } from './rules/form/form.component'; 7 | import { SettingsRulesComponent } from './rules/rules.component'; 8 | import { SettingsRoutingModule } from './settings-routing.module'; 9 | import { SettingsSecurityComponent } from './personal/security/security.component'; 10 | import { SettingsTagsComponent } from './tags/tags.component'; 11 | import { SettingsTagsEditComponent } from './tags/edit/edit.component'; 12 | import { SettingsTagsViewComponent } from './tags/view/view.component'; 13 | import { SettingsCategoriesComponent } from './categories/categories.component'; 14 | import { SettingsCategoriesEditComponent } from './categories/edit/edit.component'; 15 | import { SettingsCategoriesViewComponent } from './categories/view/view.component'; 16 | 17 | const COMPONENTS = [ 18 | SettingsRulesComponent, 19 | SettingsPersonalComponent, 20 | SettingsBaseComponent, 21 | SettingsBindingComponent, 22 | SettingsSecurityComponent, 23 | SettingsTagsComponent, 24 | SettingsCategoriesComponent, 25 | ]; 26 | const COMPONENTS_NOROUNT = [ 27 | SettingsRulesFormComponent, 28 | SettingsTagsEditComponent, 29 | SettingsTagsViewComponent, 30 | SettingsCategoriesEditComponent, 31 | SettingsCategoriesViewComponent, 32 | ]; 33 | 34 | @NgModule({ 35 | imports: [SharedModule, SettingsRoutingModule], 36 | declarations: [...COMPONENTS, ...COMPONENTS_NOROUNT], 37 | }) 38 | export class SettingsModule {} 39 | -------------------------------------------------------------------------------- /src/app/routes/settings/tags/edit/edit.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/routes/settings/tags/edit/edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { SFComponent, SFSchema, SFUISchema } from '@delon/form'; 3 | import { _HttpClient } from '@delon/theme'; 4 | import { NzMessageService } from 'ng-zorro-antd/message'; 5 | import { NzModalRef } from 'ng-zorro-antd/modal'; 6 | 7 | @Component({ 8 | selector: 'app-settings-tags-edit', 9 | templateUrl: './edit.component.html', 10 | }) 11 | export class SettingsTagsEditComponent implements OnInit { 12 | @ViewChild('sf', { static: false }) private sf: SFComponent; 13 | record: any = {}; 14 | schema: SFSchema = { 15 | properties: { 16 | name: { type: 'string', title: '名称' }, 17 | }, 18 | required: ['name'], 19 | }; 20 | ui: SFUISchema = { 21 | '*': { 22 | spanLabelFixed: 100, 23 | grid: { span: 24 }, 24 | }, 25 | }; 26 | 27 | constructor(private modal: NzModalRef, private msgSrv: NzMessageService, public http: _HttpClient) {} 28 | 29 | ngOnInit(): void {} 30 | 31 | save(value: any) { 32 | const url = this.record.id ? `/${this.record.id}` : ''; 33 | const method = this.record.id ? 'put' : 'post'; 34 | this.http.request(method, `/api/tags${url}`, { body: value }).subscribe((res: any) => { 35 | if (res.code !== 0) { 36 | this.msgSrv.warning(res.message); 37 | return; 38 | } 39 | this.msgSrv.success('保存成功'); 40 | this.modal.close(res.data); 41 | }); 42 | } 43 | 44 | close() { 45 | this.modal.destroy(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/routes/settings/tags/tags.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 | 6 |
    7 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/routes/settings/tags/tags.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; 2 | import { STColumn, STComponent } from '@delon/abc/st'; 3 | import { SFSchema, SFSelectWidgetSchema } from '@delon/form'; 4 | import { ModalHelper, _HttpClient } from '@delon/theme'; 5 | import { NzMessageService } from 'ng-zorro-antd/message'; 6 | import { SettingsTagsEditComponent } from './edit/edit.component'; 7 | 8 | @Component({ 9 | selector: 'app-settings-tags', 10 | templateUrl: './tags.component.html', 11 | }) 12 | export class SettingsTagsComponent implements OnInit { 13 | @ViewChild('st', { static: false }) st: STComponent; 14 | 15 | loading = true; 16 | pagination: {}; 17 | list: any[] = []; 18 | q = { 19 | page: 1, 20 | pageSize: 100, 21 | name: '', 22 | }; 23 | 24 | searchSchema: SFSchema = { 25 | properties: { 26 | name: { 27 | type: 'string', 28 | title: '名称', 29 | }, 30 | }, 31 | }; 32 | columns: STColumn[] = [ 33 | { title: '名称', index: 'name' }, 34 | { title: '次数', index: 'count' }, 35 | { title: '时间', type: 'date', index: 'updated_at' }, 36 | { 37 | title: '', 38 | buttons: [ 39 | { 40 | text: '编辑', 41 | click: (item: any) => this.form(item), 42 | }, 43 | { 44 | text: '删除', 45 | pop: { 46 | title: '确定要删除吗?', 47 | okType: 'danger', 48 | }, 49 | click: (record, _modal, comp) => { 50 | this.delete(record, comp); 51 | }, 52 | }, 53 | ], 54 | }, 55 | ]; 56 | 57 | constructor(private http: _HttpClient, private modal: ModalHelper, private cdr: ChangeDetectorRef, private msg: NzMessageService) {} 58 | 59 | ngOnInit() { 60 | this.getData(); 61 | } 62 | 63 | getData(): void { 64 | this.loading = true; 65 | const data = this.http.get('/api/tags', this.q).subscribe((res) => { 66 | this.list = res.data.items; 67 | this.pagination = res.data._meta; 68 | this.loading = false; 69 | }); 70 | } 71 | 72 | form(record: { id?: number } = {}): void { 73 | this.modal.create(SettingsTagsEditComponent, { record }, { size: 'md' }).subscribe((res) => { 74 | if (record.id) { 75 | // record = res; 76 | this.getData(); 77 | } else { 78 | this.list.splice(0, 0, res); 79 | this.list = [...this.list]; 80 | } 81 | }); 82 | } 83 | 84 | delete(record: any, comp): void { 85 | this.http.delete(`/api/tags/${record.id}`).subscribe((res) => { 86 | if (res?.code !== 0) { 87 | this.msg.warning(res?.message); 88 | return; 89 | } 90 | // tslint:disable-next-line: no-non-null-assertion 91 | comp!.removeRow(record); 92 | // this.getData(); 93 | this.msg.success('删除成功'); 94 | }); 95 | } 96 | 97 | submit(value: any): void { 98 | if (value.name) { 99 | this.q.name = value.name; 100 | this.getData(); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/app/routes/settings/tags/view/view.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | {{ i.owner }} 7 | 32943898021309809423 8 | 3321944288191034921 9 | 18112345678 10 | 曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口 11 |
    12 | 15 | -------------------------------------------------------------------------------- /src/app/routes/settings/tags/view/view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NzModalRef } from 'ng-zorro-antd/modal'; 3 | import { NzMessageService } from 'ng-zorro-antd/message'; 4 | import { _HttpClient } from '@delon/theme'; 5 | 6 | @Component({ 7 | selector: 'app-settings-tags-view', 8 | templateUrl: './view.component.html', 9 | }) 10 | export class SettingsTagsViewComponent implements OnInit { 11 | record: any = {}; 12 | i: any; 13 | 14 | constructor(private modal: NzModalRef, public msgSrv: NzMessageService, public http: _HttpClient) {} 15 | 16 | ngOnInit(): void { 17 | this.http.get(`/user/${this.record.id}`).subscribe((res) => (this.i = res)); 18 | } 19 | 20 | close() { 21 | this.modal.destroy(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | // Components 2 | 3 | // Utils 4 | export * from './utils/yuan'; 5 | 6 | // Module 7 | export * from './shared.module'; 8 | export * from './json-schema/json-schema.module'; 9 | -------------------------------------------------------------------------------- /src/app/shared/json-schema/json-schema.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DelonFormModule, WidgetRegistry } from '@delon/form'; 3 | import { SharedModule } from '../shared.module'; 4 | 5 | // import { TinymceWidget } from './widgets/tinymce/tinymce.widget'; 6 | // import { UEditorWidget } from './widgets/ueditor/ueditor.widget'; 7 | 8 | export const SCHEMA_THIRDS_COMPONENTS = [ 9 | // TinymceWidget, 10 | // UEditorWidget 11 | ]; 12 | 13 | @NgModule({ 14 | declarations: SCHEMA_THIRDS_COMPONENTS, 15 | imports: [ 16 | SharedModule, 17 | DelonFormModule.forRoot() 18 | ], 19 | exports: [ 20 | ...SCHEMA_THIRDS_COMPONENTS 21 | ] 22 | }) 23 | export class JsonSchemaModule { 24 | constructor(widgetRegistry: WidgetRegistry) { 25 | // widgetRegistry.register(TinymceWidget.KEY, TinymceWidget); 26 | // widgetRegistry.register(UEditorWidget.KEY, UEditorWidget); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/search/search.component.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ i.name }} 11 | 12 | 13 | 14 | 15 | 16 | 今天 / 本周 / 17 | 本月 / 本年 18 | 19 | 20 | 21 | 23 |
    24 |
    25 | -------------------------------------------------------------------------------- /src/app/shared/search/search.component.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/index'; 2 | :host ::ng-deep { 3 | .data-extra { 4 | padding-left: 6px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/search/search.component.ts: -------------------------------------------------------------------------------- 1 | import { DatePipe } from '@angular/common'; 2 | import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 3 | import { CacheService } from '@delon/cache'; 4 | import { _HttpClient } from '@delon/theme'; 5 | import { deepCopy, getTimeDistance } from '@delon/util'; 6 | 7 | @Component({ 8 | selector: 'app-record-search', 9 | styleUrls: ['./search.component.less'], 10 | templateUrl: './search.component.html', 11 | }) 12 | export class RecordSearchComponent implements OnInit { 13 | @Output() searched = new EventEmitter<{}>(); 14 | @Output() reseted = new EventEmitter<{}>(); 15 | @Input() q: any = {}; 16 | @Input() date: Date[] = []; 17 | 18 | selectCacheKey = 'RECORD_SEARCH_SELECT_CACHE_KEY'; 19 | selectData: any = {}; 20 | 21 | initQ: any; 22 | loading = true; 23 | selectLabels: any = [ 24 | { key: 'account_id', label: '账户' }, 25 | { key: 'category_id', label: '分类' }, 26 | { key: 'transaction_type', label: '类型' }, 27 | { key: 'source', label: '来源' }, 28 | ]; 29 | 30 | constructor(private http: _HttpClient, private cdr: ChangeDetectorRef, private datePipe: DatePipe, private cache: CacheService) {} 31 | 32 | ngOnInit() { 33 | this.initQ = deepCopy(this.q); 34 | this.loadSelect('/api/accounts?status=active', 'account_id'); 35 | this.loadSelect('/api/categories', 'category_id'); 36 | this.loadSelect('/api/tags', 'tags'); 37 | this.loadSelect('/api/transactions/types', 'transaction_type'); 38 | this.loadSelect('/api/records/sources', 'source'); 39 | } 40 | 41 | loadSelect(url: string, key: string) { 42 | this.loading = true; 43 | this.http.get(url, { pageSize: 50 }).subscribe((res: any) => { 44 | if (res.data) { 45 | if (key === 'tags') { 46 | this.selectData[key] = res.data.items.map((item: any) => ({ id: item.name, name: item.name })); 47 | } else if (['transaction_type', 'source'].includes(key)) { 48 | this.selectData[key] = res.data.map((item: any) => ({ id: item.type, name: item.name })); 49 | } else if (['account_id', 'category_id'].includes(key)) { 50 | this.selectData[key] = res.data.items.map((item: any) => ({ id: item.id, name: item.name, icon: item.icon_name })); 51 | } else { 52 | this.selectData[key] = res.data.items.map((item: any) => ({ id: item.id, name: item.name })); 53 | } 54 | this.loading = false; 55 | this.cache.set(this.selectCacheKey, this.selectData); 56 | this.cdr.detectChanges(); 57 | } 58 | }); 59 | } 60 | 61 | reset(): void { 62 | this.date = []; 63 | this.q = this.initQ; 64 | this.reseted.emit(this.q); 65 | } 66 | 67 | search(): void { 68 | if (this.date) { 69 | this.q.date = this.date.map((item: any) => this.datePipe.transform(item, 'yyyy-MM-dd')).join('~'); 70 | } 71 | this.searched.emit(this.q); 72 | } 73 | 74 | setDate(type: 'today' | 'week' | 'month' | 'year'): void { 75 | this.date = getTimeDistance(type); 76 | setTimeout(() => this.cdr.detectChanges()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/shared/shared-delon.module.ts: -------------------------------------------------------------------------------- 1 | import { DownFileModule } from '@delon/abc/down-file'; 2 | import { ExceptionModule } from '@delon/abc/exception'; 3 | import { GlobalFooterModule } from '@delon/abc/global-footer'; 4 | import { NoticeIconModule } from '@delon/abc/notice-icon'; 5 | import { PageHeaderModule } from '@delon/abc/page-header'; 6 | import { ResultModule } from '@delon/abc/result'; 7 | // import { ReuseTabModule } from '@delon/abc/reuse-tab'; 8 | import { SEModule } from '@delon/abc/se'; 9 | import { SidebarNavModule } from '@delon/abc/sidebar-nav'; 10 | import { STModule } from '@delon/abc/st'; 11 | import { SVModule } from '@delon/abc/sv'; 12 | import { TagSelectModule } from '@delon/abc/tag-select'; 13 | import { XlsxModule } from '@delon/abc/xlsx'; 14 | import { G2CustomModule } from '@delon/chart/custom'; 15 | import { TrendModule } from '@delon/chart/trend'; 16 | 17 | export const SHARED_DELON_MODULES = [ 18 | PageHeaderModule, 19 | ResultModule, 20 | ExceptionModule, 21 | NoticeIconModule, 22 | SidebarNavModule, 23 | GlobalFooterModule, 24 | STModule, 25 | SEModule, 26 | SVModule, 27 | TagSelectModule, 28 | DownFileModule, 29 | TrendModule, 30 | G2CustomModule, 31 | XlsxModule, 32 | // ReuseTabModule, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/app/shared/shared-zorro.module.ts: -------------------------------------------------------------------------------- 1 | import { NzAlertModule } from 'ng-zorro-antd/alert'; 2 | import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete'; 3 | import { NzAvatarModule } from 'ng-zorro-antd/avatar'; 4 | import { NzBadgeModule } from 'ng-zorro-antd/badge'; 5 | import { NzButtonModule } from 'ng-zorro-antd/button'; 6 | import { NzCardModule } from 'ng-zorro-antd/card'; 7 | import { NzCheckboxModule } from 'ng-zorro-antd/checkbox'; 8 | import { NzCollapseModule } from 'ng-zorro-antd/collapse'; 9 | import { NzDatePickerModule } from 'ng-zorro-antd/date-picker'; 10 | import { NzDividerModule } from 'ng-zorro-antd/divider'; 11 | import { NzDrawerModule } from 'ng-zorro-antd/drawer'; 12 | import { NzDropDownModule } from 'ng-zorro-antd/dropdown'; 13 | import { NzFormModule } from 'ng-zorro-antd/form'; 14 | import { NzGridModule } from 'ng-zorro-antd/grid'; 15 | import { NzIconModule } from 'ng-zorro-antd/icon'; 16 | import { NzInputModule } from 'ng-zorro-antd/input'; 17 | import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; 18 | import { NzListModule } from 'ng-zorro-antd/list'; 19 | import { NzMessageModule } from 'ng-zorro-antd/message'; 20 | import { NzModalModule } from 'ng-zorro-antd/modal'; 21 | import { NzPaginationModule } from 'ng-zorro-antd/pagination'; 22 | import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm'; 23 | import { NzPopoverModule } from 'ng-zorro-antd/popover'; 24 | import { NzProgressModule } from 'ng-zorro-antd/progress'; 25 | import { NzRadioModule } from 'ng-zorro-antd/radio'; 26 | import { NzSelectModule } from 'ng-zorro-antd/select'; 27 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 28 | import { NzSwitchModule } from 'ng-zorro-antd/switch'; 29 | import { NzTableModule } from 'ng-zorro-antd/table'; 30 | import { NzTabsModule } from 'ng-zorro-antd/tabs'; 31 | import { NzTagModule } from 'ng-zorro-antd/tag'; 32 | import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; 33 | import { NzTypographyModule } from 'ng-zorro-antd/typography'; 34 | import { NzUploadModule } from 'ng-zorro-antd/upload'; 35 | 36 | export const SHARED_ZORRO_MODULES = [ 37 | NzFormModule, 38 | NzGridModule, 39 | NzButtonModule, 40 | NzInputModule, 41 | NzInputNumberModule, 42 | NzAlertModule, 43 | NzProgressModule, 44 | NzSelectModule, 45 | NzAvatarModule, 46 | NzCardModule, 47 | NzDropDownModule, 48 | NzMessageModule, 49 | NzSpinModule, 50 | NzPopconfirmModule, 51 | NzTableModule, 52 | NzPaginationModule, 53 | NzPopoverModule, 54 | NzDrawerModule, 55 | NzModalModule, 56 | NzTabsModule, 57 | NzBadgeModule, 58 | NzToolTipModule, 59 | NzIconModule, 60 | NzDividerModule, 61 | NzSwitchModule, 62 | NzListModule, 63 | NzRadioModule, 64 | NzCheckboxModule, 65 | NzAutocompleteModule, 66 | NzTagModule, 67 | NzDatePickerModule, 68 | NzTypographyModule, 69 | NzUploadModule, 70 | NzCollapseModule, 71 | ]; 72 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { DelonACLModule } from '@delon/acl'; 6 | import { DelonFormModule } from '@delon/form'; 7 | import { AlainThemeModule } from '@delon/theme'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | 10 | import { SHARED_DELON_MODULES } from './shared-delon.module'; 11 | import { SHARED_ZORRO_MODULES } from './shared-zorro.module'; 12 | 13 | import { G2BarModule } from '@delon/chart/bar'; 14 | import { G2CardModule } from '@delon/chart/card'; 15 | import { G2GaugeModule } from '@delon/chart/gauge'; 16 | import { G2MiniAreaModule } from '@delon/chart/mini-area'; 17 | import { G2PieModule } from '@delon/chart/pie'; 18 | import { G2TagCloudModule } from '@delon/chart/tag-cloud'; 19 | import { G2TimelineModule } from '@delon/chart/timeline'; 20 | import { G2WaterWaveModule } from '@delon/chart/water-wave'; 21 | import { NgxG2plotModule } from 'ngx-g2plot'; 22 | import { RecordSearchComponent } from './search/search.component'; 23 | 24 | // #region third libs 25 | 26 | const THIRDMODULES = [NgxG2plotModule]; 27 | 28 | // #endregion 29 | 30 | // #region your componets & directives 31 | 32 | const COMPONENTS = [RecordSearchComponent]; 33 | const DIRECTIVES = []; 34 | 35 | // #endregion 36 | 37 | @NgModule({ 38 | imports: [ 39 | CommonModule, 40 | FormsModule, 41 | RouterModule, 42 | ReactiveFormsModule, 43 | AlainThemeModule.forChild(), 44 | DelonACLModule, 45 | DelonFormModule, 46 | G2CardModule, 47 | G2PieModule, 48 | G2BarModule, 49 | G2TagCloudModule, 50 | G2GaugeModule, 51 | G2WaterWaveModule, 52 | G2MiniAreaModule, 53 | G2TimelineModule, 54 | ...SHARED_DELON_MODULES, 55 | ...SHARED_ZORRO_MODULES, 56 | // third libs 57 | ...THIRDMODULES, 58 | ], 59 | declarations: [ 60 | // your components 61 | ...COMPONENTS, 62 | ...DIRECTIVES, 63 | ], 64 | exports: [ 65 | CommonModule, 66 | FormsModule, 67 | ReactiveFormsModule, 68 | RouterModule, 69 | AlainThemeModule, 70 | DelonACLModule, 71 | DelonFormModule, 72 | TranslateModule, 73 | G2CardModule, 74 | G2PieModule, 75 | G2BarModule, 76 | G2TagCloudModule, 77 | G2GaugeModule, 78 | G2WaterWaveModule, 79 | G2MiniAreaModule, 80 | G2TimelineModule, 81 | ...SHARED_DELON_MODULES, 82 | ...SHARED_ZORRO_MODULES, 83 | // third libs 84 | ...THIRDMODULES, 85 | // your components 86 | ...COMPONENTS, 87 | ...DIRECTIVES, 88 | ], 89 | }) 90 | export class SharedModule {} 91 | -------------------------------------------------------------------------------- /src/app/shared/st-widget/st-widget.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | // import { STWidgetRegistry } from '@delon/abc/st'; 3 | import { SharedModule } from '../shared.module'; 4 | 5 | export const STWIDGET_COMPONENTS = []; 6 | 7 | @NgModule({ 8 | declarations: STWIDGET_COMPONENTS, 9 | imports: [SharedModule], 10 | exports: [...STWIDGET_COMPONENTS], 11 | }) 12 | export class STWidgetModule { 13 | // constructor(widgetRegistry: STWidgetRegistry) { 14 | // widgetRegistry.register(STImgWidget.KEY, STImgWidget); 15 | // } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/utils/yuan.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 转化成RMB元字符串 3 | * @param digits 当数字类型时,允许指定小数点后数字的个数,默认2位小数 4 | */ 5 | export function yuan(value: number | string, digits: number = 2): string { 6 | if (typeof value === 'number') { 7 | value = value.toFixed(digits); 8 | } 9 | return `¥ ${value}`; 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/import-template.csv: -------------------------------------------------------------------------------- 1 | 账单日期,类别,类型,金额,标签,描述,备注,账户1,账户2 2 | 2020-07-31,餐饮食品,支出,10,早餐/三餐,昨天晚餐10,示例数据,, 3 | 2020-07-31,转账,转账,15,,,示例数据,测试账户,测试 -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/tmp/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/1.png -------------------------------------------------------------------------------- /src/assets/tmp/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/2.png -------------------------------------------------------------------------------- /src/assets/tmp/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/3.png -------------------------------------------------------------------------------- /src/assets/tmp/img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/4.png -------------------------------------------------------------------------------- /src/assets/tmp/img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/5.png -------------------------------------------------------------------------------- /src/assets/tmp/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/6.png -------------------------------------------------------------------------------- /src/assets/tmp/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/avatar.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg1.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg10.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg2.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg3.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg4.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg5.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg6.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg7.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg8.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/bg9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/bg9.jpg -------------------------------------------------------------------------------- /src/assets/tmp/img/half-float-bg-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/assets/tmp/img/half-float-bg-1.jpg -------------------------------------------------------------------------------- /src/assets/zorro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/environments/environment.hmr.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | SERVER_URL: `./`, 7 | production: false, 8 | useHash: true, 9 | hmr: true, 10 | iconfontURl: 'https://at.alicdn.com/t/font_2007518_gto6oizgx6s.js', 11 | }; 12 | 13 | /* 14 | * In development mode, to ignore zone related error stack frames such as 15 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 16 | * import the following file, but please comment it out in production mode 17 | * because it will have performance impact when throw error 18 | */ 19 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 20 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | SERVER_URL: ``, 3 | production: true, 4 | useHash: true, 5 | hmr: false, 6 | iconfontURl: 'https://at.alicdn.com/t/font_2007518_gto6oizgx6s.js', 7 | }; 8 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | SERVER_URL: ``, 7 | production: false, 8 | useHash: true, 9 | hmr: false, 10 | iconfontURl: 'https://at.alicdn.com/t/font_2007518_gto6oizgx6s.js', 11 | }; 12 | 13 | /* 14 | * In development mode, to ignore zone related error stack frames such as 15 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 16 | * import the following file, but please comment it out in production mode 17 | * because it will have performance impact when throw error 18 | */ 19 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 20 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cashwarden/web/cdbda7f93359f8d168e9f14d84e16de67418397a/src/favicon.ico -------------------------------------------------------------------------------- /src/hmr.ts: -------------------------------------------------------------------------------- 1 | import { NgModuleRef, ApplicationRef } from '@angular/core'; 2 | import { createNewHosts } from '@angularclass/hmr'; 3 | import { NzModalService } from 'ng-zorro-antd/modal'; 4 | 5 | export const hmrBootstrap = ( 6 | module: any, 7 | bootstrap: () => Promise>, 8 | ) => { 9 | let ngModule: NgModuleRef; 10 | module.hot.accept(); 11 | bootstrap().then(mod => (ngModule = mod)); 12 | module.hot.dispose(() => { 13 | const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef); 14 | const modalService = ngModule.injector.get(NzModalService, null) as NzModalService; 15 | if (modalService) modalService.closeAll(); 16 | const elements = appRef.components.map(c => c.location.nativeElement); 17 | const makeVisible = createNewHosts(elements); 18 | ngModule.destroy(); 19 | makeVisible(); 20 | }); 21 | }; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CashwardenWeb 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 |
    22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, ViewEncapsulation } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | import { preloaderFinished } from '@delon/theme'; 8 | preloaderFinished(); 9 | 10 | import { hmrBootstrap } from './hmr'; 11 | 12 | if (environment.production) { 13 | enableProdMode(); 14 | } 15 | 16 | const bootstrap = () => { 17 | return platformBrowserDynamic() 18 | .bootstrapModule(AppModule, { 19 | defaultEncapsulation: ViewEncapsulation.Emulated, 20 | }) 21 | .then(res => { 22 | if ((window as any).appBootstrap) { 23 | (window as any).appBootstrap(); 24 | } 25 | return res; 26 | }); 27 | }; 28 | 29 | if (environment.hmr) { 30 | // tslint:disable-next-line: no-string-literal 31 | if (module['hot']) { 32 | hmrBootstrap(module, bootstrap); 33 | } else { 34 | console.error('HMR is not enabled for webpack-dev-server!'); 35 | console.log('Are you using the --hmr flag for ng serve?'); 36 | } 37 | } else { 38 | bootstrap(); 39 | } -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/style-icons-auto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Automatically generated by 'ng g ng-alain:plugin icon' 3 | * @see https://ng-alain.com/cli/plugin#icon 4 | */ 5 | 6 | import { 7 | AppstoreOutline, 8 | ArrowDownOutline, 9 | CloudOutline, 10 | CopyrightOutline, 11 | DeleteOutline, 12 | FieldTimeOutline, 13 | FullscreenExitOutline, 14 | FullscreenOutline, 15 | GithubOutline, 16 | GlobalOutline, 17 | LockOutline, 18 | LogoutOutline, 19 | MailOutline, 20 | MenuFoldOutline, 21 | MenuUnfoldOutline, 22 | PayCircleOutline, 23 | PrinterOutline, 24 | ScanOutline, 25 | SettingOutline, 26 | StarOutline, 27 | TeamOutline, 28 | ToolOutline, 29 | UserOutline, 30 | EditOutline, 31 | } from '@ant-design/icons-angular/icons'; 32 | 33 | export const ICONS_AUTO = [ 34 | AppstoreOutline, 35 | ArrowDownOutline, 36 | CloudOutline, 37 | CopyrightOutline, 38 | FullscreenExitOutline, 39 | FullscreenOutline, 40 | GithubOutline, 41 | GlobalOutline, 42 | LockOutline, 43 | LogoutOutline, 44 | MailOutline, 45 | MenuFoldOutline, 46 | MenuUnfoldOutline, 47 | PayCircleOutline, 48 | PrinterOutline, 49 | ScanOutline, 50 | SettingOutline, 51 | StarOutline, 52 | TeamOutline, 53 | ToolOutline, 54 | UserOutline, 55 | FieldTimeOutline, 56 | DeleteOutline, 57 | EditOutline, 58 | ]; 59 | -------------------------------------------------------------------------------- /src/style-icons.ts: -------------------------------------------------------------------------------- 1 | // Custom icon static resources 2 | 3 | import { 4 | AccountBookOutline, 5 | AppstoreOutline, 6 | AreaChartOutline, 7 | BulbOutline, 8 | DashboardOutline, 9 | DashOutline, 10 | DatabaseOutline, 11 | DeleteOutline, 12 | DownloadOutline, 13 | EditOutline, 14 | ExceptionOutline, 15 | GroupOutline, 16 | InfoOutline, 17 | LineChartOutline, 18 | LinkOutline, 19 | PlusOutline, 20 | ProfileOutline, 21 | SettingOutline, 22 | } from '@ant-design/icons-angular/icons'; 23 | 24 | export const ICONS = [ 25 | InfoOutline, 26 | BulbOutline, 27 | ProfileOutline, 28 | ExceptionOutline, 29 | LinkOutline, 30 | PlusOutline, 31 | EditOutline, 32 | DeleteOutline, 33 | AppstoreOutline, 34 | DashOutline, 35 | AccountBookOutline, 36 | DatabaseOutline, 37 | AreaChartOutline, 38 | GroupOutline, 39 | DashboardOutline, 40 | SettingOutline, 41 | AppstoreOutline, 42 | LineChartOutline, 43 | DownloadOutline, 44 | ]; 45 | -------------------------------------------------------------------------------- /src/styles.less: -------------------------------------------------------------------------------- 1 | @import '~@delon/theme/system/index'; 2 | @import '~@delon/abc/index'; 3 | @import '~@delon/chart/index'; 4 | @import '~@delon/theme/layout/default/index'; 5 | @import '~@delon/theme/layout/fullscreen/index'; 6 | 7 | @import './styles/index'; 8 | @import './styles/theme'; 9 | 10 | // You can directly set the default theme 11 | // - `dark` Import the official dark less style file 12 | // - `compact` Import the official compact less style file 13 | // @import '~@delon/theme/theme-dark.less'; 14 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/styles/theme.less: -------------------------------------------------------------------------------- 1 | // 可以通过 https://ng-alain.github.io/ng-alain/ 获取主题参数代码 2 | // The theme paraments can be generated at https://ng-alain.github.io/ng-alain/ 3 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // # 3rd Party Library 2 | // If the library doesn't have typings available at `@types/`, 3 | // you can still use it by manually adding typings for it 4 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [ 6 | "node" 7 | ] 8 | }, 9 | "files": [ 10 | "src/main.ts", 11 | "src/polyfills.ts" 12 | ], 13 | "include": [ 14 | "src/**/*.d.ts" 15 | ] 16 | } -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "target": "es2015", 13 | "module": "es2020", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ], 18 | "paths": { 19 | "@shared": [ 20 | "src/app/shared/index" 21 | ], 22 | "@core": [ 23 | "src/app/core/index" 24 | ], 25 | "@env/*": [ 26 | "src/environments/*" 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.spec.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------