Commit c4a520f2 by 牛晓林

latest mobile

parent e892a611
# Editor configuration, see https://editorconfig.org
# Editor configuration, see http://editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2
......
......@@ -4,15 +4,11 @@
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
/src/assets/alain-*.less
# dependencies
/node_modules
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
/yarn.lock
# IDEs and editors
/.idea
......@@ -29,7 +25,6 @@ speed-measure-plugin.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
......@@ -37,9 +32,13 @@ speed-measure-plugin.json
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
yarn-error.log
/_all.less
# e2e
/e2e/*.map
# System Files
.DS_Store
......
.github
node_modules
dist
tmp
# add files you wish to ignore here
**/*.md
**/*.svg
**/*.html
**/test.ts
node_modules/
coverage/
dist/
package.json
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"useTabs": false,
"overrides": [
{
"files": ".prettierrc",
"options": {
"parser": "json"
}
}
]
}
{
"extends": [
"stylelint-config-standard",
"./node_modules/prettier-stylelint/config.js"
],
"rules": {
"comment-empty-line-before": null,
"declaration-empty-line-before": null,
"function-comma-newline-after": null,
"function-name-case": null,
"function-parentheses-newline-inside": null,
"function-max-empty-lines": null,
"function-whitespace-after": null,
"indentation": null,
"number-leading-zero": null,
"number-no-trailing-zeros": null,
"rule-empty-line-before": null,
"selector-combinator-space-after": null,
"selector-list-comma-newline-after": null,
"selector-pseudo-element-colon-notation": null,
"unit-no-unknown": null,
"value-list-max-empty-lines": null,
"string-no-newline": null,
"selector-type-no-unknown": null,
"no-descending-specificity": null,
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": [
"ng-deep"
]
}
]
}
}
sudo: required
dist: trusty
language: node_js
node_js:
- '10.9.0'
addons:
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
- google-chrome-beta
git:
depth: 1
jobs:
include:
- env: "MODE=build"
- env: "MODE=lint"
- env: "MODE=test-coverage"
- env: "DEPLOY_MODE=artifacts"
if: branch = master
matrix:
allow_failures:
- env: "DEPLOY_MODE=artifacts"
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
install:
- chmod +x -R scripts
- yarn
script:
- ./scripts/_ci/travis.sh
cache:
yarn: true
directories:
- ./node_modules/
{
"recommendations": [
"cipchk.ng-alain-extension-pack"
]
}
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceRoot}",
"sourceMaps": true
}
]
}
{
"typescript.tsdk": "./node_modules/typescript/lib",
// 保存时自动格式化,需要安装:https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
// "editor.formatOnSave": true,
// Angular schematics 插件: https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics
"ngschematics.schematics": [
"ng-alain"
],
"editor.tabSize": 2
}
# Contributing to ng-alain
We would love for you to contribute to ng-alain and help make it even better than it is
today! As a contributor, here are the guidelines we would like you to follow:
- [Issues and Bugs](#issue)
- [Feature Requests](#feature)
- [Submission Guidelines](#submit)
- [Coding Rules](#rules)
- [Commit Message Guidelines](#commit)
## <a name="issue"></a> Found a Bug?
If you find a bug in the source code, you can help us by
[submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can
[submit a Pull Request](#submit-pr) with a fix.
## <a name="feature"></a> Missing a Feature?
You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub
Repository. If you would like to *implement* a new feature, please submit an issue with
a for your work first, to be sure that we can use it.
Please consider what kind of change it is:
* For a **Major Feature**, first open an issue and outline your proposal so that it can be
discussed. This will also allow us to better coordinate our efforts, prevent duplication of work,
and help you to craft the change so that it is successfully accepted into the project.
* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
## <a name="submit"></a> Submission Guidelines
### <a name="submit-issue"></a> Submitting an Issue
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
- version of ng-alain used
- 3rd-party libraries and their versions
- and most importantly - a use-case that fails
A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm a bug (or point out coding problem) as well as confirm that we are fixing the right problem. If plunker is not a suitable way to demonstrate the problem (for example for issues related to our npm packaging), please create a standalone git repository demonstrating the problem.
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
You can file new issues by filling out our [new issue form](https://github.com/ng-alain/ng-alain/issues/new).
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
Before you submit your Pull Request (PR) consider the following guidelines:
* Search [GitHub](https://github.com/ng-alain/ng-alain/pulls) for an open or closed PR
that relates to your submission. You don't want to duplicate effort.
* Make your changes in a new git branch:
```shell
git checkout -b my-fix-branch master
```
* Create your patch, **including appropriate test cases**.
* Follow our [Coding Rules](#rules).
* Run the full ng-alain test suite <!-- , as described in the [developer documentation][dev-doc] -->, and ensure that all tests pass.
* Commit your changes using a descriptive commit message that follows our
[commit message conventions](#commit). Adherence to these conventions
is necessary because release notes are automatically generated from these messages.
```shell
git commit -a
```
Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
* Push your branch to GitHub:
```shell
git push origin my-fix-branch
```
* In GitHub, send a pull request to `ng-alain:master`.
* If we suggest changes then:
* Make the required updates.
* Re-run the ng-alain test suites to ensure tests are still passing.
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
```shell
git rebase master -i
git push -f
```
That's it! Thank you for your contribution!
#### After your pull request is merged
After your pull request is merged, you can safely delete your branch and pull the changes
from the main (upstream) repository:
* Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
```shell
git push origin --delete my-fix-branch
```
* Check out the master branch:
```shell
git checkout master -f
```
* Delete the local branch:
```shell
git branch -D my-fix-branch
```
* Update your master with the latest upstream version:
```shell
git pull --ff upstream master
```
## <a name="rules"></a> Coding Rules
To ensure consistency throughout the source code, keep these rules in mind as you are working:
* All features or bug fixes **must be tested** by one or more specs (unit-tests).
* All public API methods **must be documented**.
## <a name="commit"></a> Commit Message Guidelines
We have very precise rules over how our git commit messages can be formatted. This leads to **more
readable messages** that are easy to follow when looking through the **project history**. But also,
we use the git commit messages to **generate the ng-alain change log**.
### Commit Message Format
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
format that includes a **type**, a **scope** and a **subject**:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
Samples: (even more [samples](https://github.com/ng-alain/ng-alain/commits/master))
```
docs(changelog): update change log to beta.5
```
```
fix(release): need to depend on latest rxjs and zone.js
The version in our package.json gets copied to the one we publish, and users need the latest of these.
```
### Revert
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
### Type
Must be one of the following:
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
* **docs**: Documentation only changes
* **feat**: A new feature
* **fix**: A bug fix
* **perf**: A code change that improves performance
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
* **test**: Adding missing tests or correcting existing tests
### Subject
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
A detailed explanation can be found in this [document][commit-message-format].
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#
[github]: https://github.com/ng-alain/ng-alain
[plunker]: http://plnkr.co/edit
MIT License
Copyright (c) 2018-present 卡色<cipchk@qq.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<p align="center">
<a href="https://ng-alain.com">
<img width="100" src="https://ng-alain.com/assets/img/logo-color.svg">
</a>
</p>
<h1 align="center">NG-ALAIN</h1>
<div align="center">
一个基于 Antd 中后台前端解决方案,提供更多通用性业务模块,让开发者更加专注于业务。
[![Build Status](https://img.shields.io/travis/ng-alain/ng-alain/master.svg?style=flat-square)](https://travis-ci.org/ng-alain/ng-alain)
[![Dependency Status](https://david-dm.org/ng-alain/ng-alain/status.svg?style=flat-square)](https://david-dm.org/ng-alain/ng-alain)
[![GitHub Release Date](https://img.shields.io/github/release-date/ng-alain/ng-alain.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/releases)
[![NPM version](https://img.shields.io/npm/v/ng-alain.svg?style=flat-square)](https://www.npmjs.com/package/ng-alain)
[![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://prettier.io/)
[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/blob/master/LICENSE)
[![Gitter](https://img.shields.io/gitter/room/ng-alain/ng-alain.svg?style=flat-square)](https://gitter.im/ng-alain/ng-alain)
[![ng-zorro-vscode](https://img.shields.io/badge/ng--zorro-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-zorro-vscode)
[![ng-alain-vscode](https://img.shields.io/badge/ng--alain-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-alain-vscode)
</div>
[English](README.md) | 简体中文
## 快速入门
```bash
# 确保使用的是最新版本 Angular cli
ng new demo --style less
cd demo
ng add ng-alain
ng s -o
# 或 HMR 模式
npm run hmr
```
> 请参考[命令行工具](https://ng-alain.com/cli)了解更多细节。
>
> [vscode] 建议安装 [ng-zorro-vscode](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-zorro-vscode) 和 [ng-alain-vscode](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-alain-vscode) 插件,开发更爽。
## 链接
+ [文档](https://ng-alain.com)
+ [@delon](https://github.com/ng-alain/delon)
+ [DEMO](https://ng-alain.github.io/ng-alain/)
## 特性
+ 基于 `ng-zorro-antd`
+ 响应式
+ 国际化
+ 基建类库 [@delon](https://github.com/ng-alain/delon)(包括:业务组件、ACL访问控制、缓存、授权、动态表单等)
+ 延迟加载及良好的启用画面
+ 良好的UI路由设计
+ 定制主题
+ Less预编译
+ 良好的目录组织结构
+ 简单升级
+ 支持Docker部署
## Architecture
![Architecture](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/architecture.png)
> [delon](https://github.com/ng-alain/delon) 是基于 Ant Design 设计理念的企业级中后台前端业务型组件库。
## 应用截图
![desktop](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/desktop.png)
![ipad](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/ipad.png)
![iphone](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/iphone.png)
## 赞助
ng-alain是MIT协议的开源项目。为了项目能够更好的持续的发展,我们期望获得更多的支持者,你可以通过如下任何一种方式支持我们:
- [patreon](https://www.patreon.com/cipchk)
- [opencollective](https://opencollective.com/ng-alain)
- [paypal](https://www.paypal.me/cipchk)
- [支付宝或微信](https://ng-alain.com/assets/donate.png)
或购买我们 [商品主题](https://e.ng-alain.com/)
## Backers
Thank you to all our backers! 🙏
<a href="https://opencollective.com/ng-alain#backers" target="_blank"><img src="https://opencollective.com/ng-alain/backers.svg?width=890"></a>
### License
The MIT License (see the [LICENSE](https://github.com/ng-alain/ng-alain/blob/master/LICENSE) file for the full text)
# AdminMobile
<p align="center">
<a href="https://ng-alain.com">
<img width="100" src="https://ng-alain.com/assets/img/logo-color.svg">
</a>
</p>
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.8.
<h1 align="center">NG-ALAIN</h1>
## Development server
<div align="center">
Out-of-box UI solution for enterprise applications, Let developers focus on business.
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
[![Build Status](https://img.shields.io/travis/ng-alain/ng-alain/master.svg?style=flat-square)](https://travis-ci.org/ng-alain/ng-alain)
[![Dependency Status](https://david-dm.org/ng-alain/ng-alain/status.svg?style=flat-square)](https://david-dm.org/ng-alain/ng-alain)
[![GitHub Release Date](https://img.shields.io/github/release-date/ng-alain/ng-alain.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/releases)
[![NPM version](https://img.shields.io/npm/v/ng-alain.svg?style=flat-square)](https://www.npmjs.com/package/ng-alain)
[![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://prettier.io/)
[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/blob/master/LICENSE)
[![Gitter](https://img.shields.io/gitter/room/ng-alain/ng-alain.svg?style=flat-square)](https://gitter.im/ng-alain/ng-alain)
[![ng-zorro-vscode](https://img.shields.io/badge/ng--zorro-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-zorro-vscode)
[![ng-alain-vscode](https://img.shields.io/badge/ng--alain-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-alain-vscode)
## Code scaffolding
</div>
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
English | [简体中文](README-zh_CN.md)
## Build
## Quick start
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
```bash
# Make sure you are using the latest version of Angular cli
ng new demo --style less
cd demo
ng add ng-alain
ng s -o
# or HMR mode
npm run hmr
```
## Running unit tests
> Please refer to [Cli](https://ng-alain.com/cli) for more details.
>
> [vscode] recommended install [ng-zorro-vscode](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-zorro-vscode) & [ng-alain-vscode](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-alain-vscode) plugins.
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Links
## Running end-to-end tests
+ [Document](https://ng-alain.com)
+ [DEMO](https://ng-alain.github.io/ng-alain/)
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Features
## Further help
+ `ng-zorro-antd` based
+ Responsive Layout
+ I18n
+ [@delon](https://github.com/ng-alain/delon)
+ Lazy load Assets
+ UI Router States
+ Customize Theme
+ Less preprocessor
+ Well organized & commented code
+ Simple upgrade
+ Support Docker deploy
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
## Architecture
![Architecture](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/architecture.png)
> [delon](https://github.com/ng-alain/delon) is a production-ready solution for admin business components packages, Built on the design principles developed by Ant Design.
## App Shots
![desktop](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/desktop.png)
![ipad](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/ipad.png)
![iphone](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/iphone.png)
## Donation
ng-alain is an MIT-licensed open source project. In order to achieve better and sustainable development of the project, we expect to gain more backers. You can support us in any of the following ways:
- [patreon](https://www.patreon.com/cipchk)
- [opencollective](https://opencollective.com/ng-alain)
- [paypal](https://www.paypal.me/cipchk)
- [支付宝或微信](https://ng-alain.com/assets/donate.png)
Or purchasing our [business theme](https://e.ng-alain.com/).
## Backers
Thank you to all our backers! 🙏
<a href="https://opencollective.com/ng-alain#backers" target="_blank"><img src="https://opencollective.com/ng-alain/backers.svg?width=890"></a>
### License
The MIT License (see the [LICENSE](https://github.com/ng-alain/ng-alain/blob/master/LICENSE) file for the full text)
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { <%= componentName %> } from './<%= dasherize(name) %>.component';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
describe('<%= componentName %>', () => {
let component: <%= componentName %>;
let fixture: ComponentFixture<<%= componentName %>>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
declarations: [ <%= componentName %> ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
fixture = TestBed.createComponent(<%= componentName %>);
component = fixture.componentInstance;
fixture.detectChanges();
});
......
import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
import { _HttpClient } from '@delon/theme';
import { NzMessageService } from 'ng-zorro-antd';
@Component({
selector: '<%= selector %>',
templateUrl: './<%= dasherize(name) %>.component.html',<% if(!inlineStyle) { %><% } else { %>
styleUrls: ['./<%= dasherize(name) %>.component.<%= styleext %>']<% } %><% if(!!viewEncapsulation) { %>,
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
})
export class <%= componentName %> implements OnInit {
constructor(private http: _HttpClient, private msg: NzMessageService) { }
ngOnInit() { }
}
[Document](https://ng-alain.com/mock)
import { MockRequest, MockStatusError } from '@delon/mock';
// region: mock data
const titles = ['Alipay', 'Angular', 'Ant Design', 'Ant Design Pro', 'Bootstrap', 'React', 'Vue', 'Webpack'];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const covers = [
'https://gw.alipayobjects.com/zos/rmsportal/HrxcVbrKnCJOZvtzSqjN.png',
'https://gw.alipayobjects.com/zos/rmsportal/alaPpKWajEbIYEUvvVNf.png',
'https://gw.alipayobjects.com/zos/rmsportal/RLwlKSYGSXGHuWSojyvp.png',
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
];
const desc = [
'那是一种内在的东西, 他们到达不了,也无法触及的',
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
'生命就像一盒巧克力,结果往往出人意料',
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
'那时候我只会想自己想要什么,从不想自己拥有什么',
];
const user = [
'卡色',
'cipchk',
'付小小',
'曲丽丽',
'林东东',
'周星星',
'吴加好',
'朱偏右',
'鱼酱',
'乐哥',
'谭小仪',
'仲尼',
];
// endregion
function getFakeList(count: number = 20): any[] {
const list: any[] = [];
for (let i = 0; i < count; i += 1) {
list.push({
id: `fake-list-${i}`,
owner: user[i % 10],
title: titles[i % 8],
avatar: avatars[i % 8],
cover: parseInt((i / 4).toString(), 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
status: ['active', 'exception', 'normal'][i % 3],
percent: Math.ceil(Math.random() * 50) + 50,
logo: avatars[i % 8],
href: 'https://ant.design',
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
subDescription: desc[i % 5],
description:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
activeUser: Math.ceil(Math.random() * 100000) + 100000,
newUser: Math.ceil(Math.random() * 1000) + 1000,
star: Math.ceil(Math.random() * 100) + 100,
like: Math.ceil(Math.random() * 100) + 100,
message: Math.ceil(Math.random() * 10) + 10,
content:
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
members: [
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
name: '曲丽丽',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
name: '王昭君',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
name: '董娜娜',
},
],
});
}
return list;
}
function getNotice(): any[] {
return [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西, 他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
];
}
function getActivities(): any[] {
return [
{
id: 'trend-1',
updatedAt: new Date(),
user: {
name: '林东东',
avatar: avatars[0],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-2',
updatedAt: new Date(),
user: {
name: '付小小',
avatar: avatars[1],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-3',
updatedAt: new Date(),
user: {
name: '曲丽丽',
avatar: avatars[2],
},
group: {
name: '中二少女团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-4',
updatedAt: new Date(),
user: {
name: '周星星',
avatar: avatars[3],
},
project: {
name: '5 月日常迭代',
link: 'http://github.com/',
},
template: '将 @{project} 更新至已发布状态',
},
{
id: 'trend-5',
updatedAt: new Date(),
user: {
name: '朱偏右',
avatar: avatars[4],
},
project: {
name: '工程效能',
link: 'http://github.com/',
},
comment: {
name: '留言',
link: 'http://github.com/',
},
template: '在 @{project} 发布了 @{comment}',
},
{
id: 'trend-6',
updatedAt: new Date(),
user: {
name: '乐哥',
avatar: avatars[5],
},
group: {
name: '程序员日常',
link: 'http://github.com/',
},
project: {
name: '品牌迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
];
}
export const APIS = {
'/api/list': (req: MockRequest) => getFakeList(req.queryString.count),
'/api/notice': () => getNotice(),
'/api/activities': () => getActivities(),
'/api/401': () => {
throw new MockStatusError(401);
},
'/api/403': () => {
throw new MockStatusError(403);
},
'/api/404': () => {
throw new MockStatusError(404);
},
'/api/500': () => {
throw new MockStatusError(500);
},
};
// tslint:disable
import * as Mock from 'mockjs';
import { format } from 'date-fns';
import { deepCopy } from '@delon/util';
// region: mock data
const visitData: any[] = [];
const beginDay = new Date().getTime();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2: any[] = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData: any[] = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}月`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData: any[] = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '个护健康',
y: 188,
},
{
x: '服饰箱包',
y: 344,
},
{
x: '母婴产品',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData: any[] = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `门店${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData: any[] = [];
for (let i = 0; i < 20; i += 1) {
offlineChartData.push({
x: new Date().getTime() + 1000 * 60 * 30 * i,
y1: Math.floor(Math.random() * 100) + 10,
y2: Math.floor(Math.random() * 100) + 10,
});
}
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
//
const radarData: any[] = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach(item => {
Object.keys(item).forEach(key => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key],
value: item[key],
});
}
});
});
// endregion
export const CHARTS = {
'/chart': JSON.parse(
JSON.stringify({
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
}),
),
'/chart/visit': JSON.parse(JSON.stringify(visitData)),
'/chart/tags': Mock.mock({
'list|100': [{ x: '@city', 'value|1-100': 150, 'category|0-2': 1 }],
}),
};
import { MockRequest } from '@delon/mock';
const DATA = [
{
name: '上海',
id: '310000',
},
{
name: '市辖区',
id: '310100',
},
{
name: '北京',
id: '110000',
},
{
name: '市辖区',
id: '110100',
},
{
name: '浙江省',
id: '330000',
},
{
name: '杭州市',
id: '330100',
},
{
name: '宁波市',
id: '330200',
},
{
name: '温州市',
id: '330300',
},
{
name: '嘉兴市',
id: '330400',
},
{
name: '湖州市',
id: '330500',
},
{
name: '绍兴市',
id: '330600',
},
{
name: '金华市',
id: '330700',
},
{
name: '衢州市',
id: '330800',
},
{
name: '舟山市',
id: '330900',
},
{
name: '台州市',
id: '331000',
},
{
name: '丽水市',
id: '331100',
},
];
export const GEOS = {
'/geo/province': () => DATA.filter(w => w.id.endsWith('0000')),
'/geo/:id': (req: MockRequest) => {
const pid = (req.params.id || '310000').slice(0, 2);
return DATA.filter(w => w.id.slice(0, 2) === pid && !w.id.endsWith('0000'));
},
};
export const POIS = {
'/pois': {
total: 2,
list: [
{
id: 10000,
user_id: 1,
name: '测试品牌',
branch_name: '测试分店',
geo: 310105,
country: '中国',
province: '上海',
city: '上海市',
district: '长宁区',
address: '中山公园',
tel: '15900000000',
categories: '美食,粤菜,湛江菜',
lng: 121.41707989151003,
lat: 31.218656214644792,
recommend: '推荐品',
special: '特色服务',
introduction: '商户简介',
open_time: '营业时间',
avg_price: 260,
reason: null,
status: 1,
status_str: '待审核',
status_wx: 1,
modified: 1505826527288,
created: 1505826527288,
},
{
id: 10001,
user_id: 2,
name: '测试品牌2',
branch_name: '测试分店2',
geo: 310105,
country: '中国',
province: '上海',
city: '上海市',
district: '长宁区',
address: '中山公园',
tel: '15900000000',
categories: '美食,粤菜,湛江菜',
lng: 121.41707989151003,
lat: 31.218656214644792,
recommend: '推荐品',
special: '特色服务',
introduction: '商户简介',
open_time: '营业时间',
avg_price: 260,
reason: null,
status: 1,
status_str: '待审核',
status_wx: 1,
modified: 1505826527288,
created: 1505826527288,
},
],
},
};
const basicGoods = [
{
id: '1234561',
name: '矿泉水 550ml',
barcode: '12421432143214321',
price: '2.00',
num: '1',
amount: '2.00',
},
{
id: '1234562',
name: '凉茶 300ml',
barcode: '12421432143214322',
price: '3.00',
num: '2',
amount: '6.00',
},
{
id: '1234563',
name: '好吃的薯片',
barcode: '12421432143214323',
price: '7.00',
num: '4',
amount: '28.00',
},
{
id: '1234564',
name: '特别好吃的蛋卷',
barcode: '12421432143214324',
price: '8.50',
num: '3',
amount: '25.50',
},
];
const basicProgress = [
{
key: '1',
time: '2017-10-01 14:10',
rate: '联系客户',
status: 'processing',
operator: '取货员 ID1234',
cost: '5mins',
},
{
key: '2',
time: '2017-10-01 14:05',
rate: '取货员出发',
status: 'success',
operator: '取货员 ID1234',
cost: '1h',
},
{
key: '3',
time: '2017-10-01 13:05',
rate: '取货员接单',
status: 'success',
operator: '取货员 ID1234',
cost: '5mins',
},
{
key: '4',
time: '2017-10-01 13:00',
rate: '申请审批通过',
status: 'success',
operator: '系统',
cost: '1h',
},
{
key: '5',
time: '2017-10-01 12:00',
rate: '发起退货申请',
status: 'success',
operator: '用户',
cost: '5mins',
},
];
const advancedOperation1 = [
{
key: 'op1',
type: '订购关系生效',
name: '曲丽丽',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
{
key: 'op2',
type: '财务复审',
name: '付小小',
status: 'reject',
updatedAt: '2017-10-03 19:23:12',
memo: '不通过原因',
},
{
key: 'op3',
type: '部门初审',
name: '周毛毛',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
{
key: 'op4',
type: '提交订单',
name: '林东东',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '很棒',
},
{
key: 'op5',
type: '创建订单',
name: '汗牙牙',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const advancedOperation2 = [
{
key: 'op1',
type: '订购关系生效',
name: '曲丽丽',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const advancedOperation3 = [
{
key: 'op1',
type: '创建订单',
name: '汗牙牙',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
export const PROFILES = {
'GET /profile/progress': basicProgress,
'GET /profile/goods': basicGoods,
'GET /profile/advanced': {
advancedOperation1,
advancedOperation2,
advancedOperation3,
},
};
import { HttpRequest } from '@angular/common/http';
import { MockRequest } from '@delon/mock';
const list: any[] = [];
for (let i = 0; i < 46; i += 1) {
list.push({
key: i,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
no: `TradeCode ${i}`,
title: `一个任务名称 ${i}`,
owner: '曲丽丽',
description: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: new Date(`2017-07-${i < 18 ? '0' + (Math.floor(i / 2) + 1) : Math.floor(i / 2) + 1}`),
createdAt: new Date(`2017-07-${i < 18 ? '0' + (Math.floor(i / 2) + 1) : Math.floor(i / 2) + 1}`),
progress: Math.ceil(Math.random() * 100),
});
}
function getRule(params: any) {
let ret = [...list];
if (params.sorter) {
const s = params.sorter.split('_');
ret = ret.sort((prev, next) => {
if (s[1] === 'descend') {
return next[s[0]] - prev[s[0]];
}
return prev[s[0]] - next[s[0]];
});
}
if (params.statusList && params.statusList.length > 0) {
ret = ret.filter(data => params.statusList.indexOf(data.status) > -1);
}
if (params.no) {
ret = ret.filter(data => data.no.indexOf(params.no) > -1);
}
return ret;
}
function removeRule(nos: string): boolean {
nos.split(',').forEach(no => {
const idx = list.findIndex(w => w.no === no);
if (idx !== -1) list.splice(idx, 1);
});
return true;
}
function saveRule(description: string) {
const i = Math.ceil(Math.random() * 10000);
list.unshift({
key: i,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
no: `TradeCode ${i}`,
title: `一个任务名称 ${i}`,
owner: '曲丽丽',
description,
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 2,
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
});
}
export const RULES = {
'/rule': (req: MockRequest) => getRule(req.queryString),
'DELETE /rule': (req: MockRequest) => removeRule(req.queryString.nos),
'POST /rule': (req: MockRequest) => saveRule(req.body.description),
};
import { MockRequest } from '@delon/mock';
const list: any[] = [];
const total = 50;
for (let i = 0; i < total; i += 1) {
list.push({
id: i + 1,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
no: `TradeCode ${i}`,
title: `一个任务名称 ${i}`,
owner: '曲丽丽',
description: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
progress: Math.ceil(Math.random() * 100),
});
}
function genData(params: any) {
let ret = [...list];
const pi = +params.pi,
ps = +params.ps,
start = (pi - 1) * ps;
if (params.no) {
ret = ret.filter(data => data.no.indexOf(params.no) > -1);
}
return { total: ret.length, list: ret.slice(start, ps * pi) };
}
function saveData(id: number, value: any) {
const item = list.find(w => w.id === id);
if (!item) return { msg: '无效用户信息' };
Object.assign(item, value);
return { msg: 'ok' };
}
export const USERS = {
'/user': (req: MockRequest) => genData(req.queryString),
'/user/:id': (req: MockRequest) => list.find(w => w.id === +req.params.id),
'POST /user/:id': (req: MockRequest) => saveData(+req.params.id, req.body),
'/user/current': {
name: 'Cipchk',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'cipchk@qq.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注撩妹',
},
{
key: '2',
label: '帅~',
},
{
key: '3',
label: '通吃',
},
{
key: '4',
label: '专职后端',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
country: 'China',
geographic: {
province: {
label: '上海',
key: '330000',
},
city: {
label: '市辖区',
key: '330100',
},
},
address: 'XX区XXX路 XX 号',
phone: '你猜-你猜你猜猜猜',
},
'POST /user/avatar': 'ok',
'POST /login/account': (req: MockRequest) => {
const data = req.body;
if (!(data.userName === 'admin' || data.userName === 'user') || data.password !== 'ng-alain.com') {
return { msg: `Invalid username or password(admin/ng-alain.com)` };
}
return {
msg: 'ok',
user: {
token: '123456789',
name: data.userName,
email: `${data.userName}@qq.com`,
id: 10000,
time: +new Date(),
},
};
},
'POST /register': {
msg: 'ok',
},
};
export * from './_profile';
export * from './_rule';
export * from './_api';
export * from './_chart';
export * from './_pois';
export * from './_user';
export * from './_geo';
......@@ -3,34 +3,39 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"admin-mobile": {
"ng-alain": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"style": "less"
"styleext": "less"
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/admin-mobile",
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/favicon.ico",
"src/assets"
"src/assets",
"src/favicon.ico"
],
"styles": [
"src/styles.less"
],
"scripts": [],
"es5BrowserSupport": true
"scripts": [
"node_modules/@antv/g2/dist/g2.min.js",
"node_modules/@antv/data-set/dist/data-set.min.js",
"node_modules/@antv/g2-plugin-slider/dist/g2-plugin-slider.min.js",
"node_modules/ajv/dist/ajv.bundle.js",
"node_modules/qrious/dist/qrious.min.js"
]
},
"configurations": {
"production": {
......@@ -56,39 +61,55 @@
"maximumError": "5mb"
}
]
},
"hmr": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.hmr.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "admin-mobile:build"
"browserTarget": "ng-alain:build"
},
"configurations": {
"production": {
"browserTarget": "admin-mobile:build:production"
"browserTarget": "ng-alain:build:production"
},
"hmr": {
"browserTarget": "ng-alain:build:hmr",
"hmr": true
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "admin-mobile:build"
"browserTarget": "ng-alain:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"karmaConfig": "./src/karma.conf.js",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"scripts": [
"node_modules/@antv/g2/dist/g2.min.js",
"node_modules/@antv/data-set/dist/data-set.min.js",
"node_modules/@antv/g2-plugin-slider/dist/g2-plugin-slider.min.js",
"node_modules/ajv/dist/ajv.bundle.js",
"node_modules/qrious/dist/qrious.min.js"
],
"styles": [
"src/styles.less"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
]
}
......@@ -107,7 +128,7 @@
}
}
},
"admin-mobile-e2e": {
"ng-alain-e2e": {
"root": "e2e/",
"projectType": "application",
"prefix": "",
......@@ -116,11 +137,11 @@
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "admin-mobile:serve"
"devServerTarget": "ng-alain:serve"
},
"configurations": {
"production": {
"devServerTarget": "admin-mobile:serve:production"
"devServerTarget": "ng-alain:serve:production"
}
}
},
......@@ -136,5 +157,5 @@
}
}
},
"defaultProject": "admin-mobile"
}
\ No newline at end of file
"defaultProject": "ng-alain"
}
......@@ -25,4 +25,4 @@ exports.config = {
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};
\ No newline at end of file
};
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
......@@ -10,14 +9,6 @@ describe('workspace-project App', () => {
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to admin-mobile!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
expect(page.getTitleText()).toEqual('Welcome to ng7!');
});
});
......@@ -2,10 +2,10 @@ import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
return browser.get('/');
}
getTitleText() {
return element(by.css('app-root h1')).getText() as Promise<string>;
return element(by.css('app-root h1')).getText();
}
}
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
......@@ -10,4 +11,4 @@
"node"
]
}
}
\ No newline at end of file
}
......@@ -3655,12 +3655,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
......@@ -3675,17 +3677,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
......@@ -3802,7 +3807,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
......@@ -3814,6 +3820,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
......@@ -3828,6 +3835,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
......@@ -3835,12 +3843,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
......@@ -3859,6 +3869,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
......@@ -3939,7 +3950,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
......@@ -3951,6 +3963,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
......@@ -4072,6 +4085,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
......
{
"name": "admin-mobile",
"version": "0.0.0",
"name": "ng-alain",
"version": "7.2.0",
"description": "ng-zorro-antd admin panel front-end framework",
"author": "cipchk",
"homepage": "https://ng-alain.github.io/ng-alain/",
"bugs": {
"url": "https://github.com/ng-alain/ng-alain/issues"
},
"license": "MIT",
"keywords": [
"delon",
"antd",
"ng-zorro-antd",
"angular",
"component",
"scaffold"
],
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
"start": "npm run color-less && ng serve -o",
"hmr": "npm run color-less && ng serve -c=hmr",
"build": "npm run color-less && ng build --prod --build-optimizer",
"analyze": "ng build --prod --build-optimizer --stats-json",
"lint": "npm run lint:ts && npm run lint:style",
"lint:ts": "tslint -p src/tsconfig.app.json -c tslint.json 'src/**/*.ts'",
"lint:style": "stylelint \"{src}/**/*.less\" --syntax less",
"lint-staged": "lint-staged",
"tslint-check": "tslint-config-prettier-check ./tslint.json",
"e2e": "ng e2e",
"test": "ng test --watch",
"test-coverage": "ng test --code-coverage --watch=false",
"color-less": "node scripts/color-less.js",
"icon": "ng g ng-alain:plugin icon"
},
"private": true,
"dependencies": {
"@angular/animations": "~7.2.0",
"@angular/common": "~7.2.0",
......@@ -20,10 +43,31 @@
"@angular/platform-browser-dynamic": "~7.2.0",
"@angular/router": "~7.2.0",
"core-js": "^2.5.4",
"ng-zorro-antd-mobile": "^0.11.9",
"rxjs": "~6.3.3",
"tslib": "^1.9.0",
"zone.js": "~0.8.26"
"zone.js": "~0.8.26",
"@antv/data-set": "^0.10.2",
"@antv/g2": "^3.5.1",
"@antv/g2-plugin-slider": "^2.1.1",
"@ngx-translate/core": "^11.0.1",
"@ngx-translate/http-loader": "^4.0.0",
"ajv": "^6.9.2",
"@delon/abc": "^7.2.0",
"@delon/acl": "^7.2.0",
"@delon/auth": "^7.2.0",
"@delon/cache": "^7.2.0",
"@delon/chart": "^7.2.0",
"@delon/form": "^7.2.0",
"@delon/mock": "^7.2.0",
"@delon/theme": "^7.2.0",
"@delon/util": "^7.2.0",
"file-saver": "^2.0.0",
"ng-zorro-antd": "^7.2.0",
"ngx-countdown": "^3.2.0",
"ngx-tinymce": "^7.0.0",
"ngx-ueditor": "^2.1.3",
"screenfull": "^4.2.0",
"qrious": "^4.0.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.0",
......@@ -44,6 +88,51 @@
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.2.2"
"typescript": "~3.2.2",
"@angularclass/hmr": "^2.1.3",
"@types/jszip": "^3.1.5",
"@types/mockjs": "^1.0.2",
"codecov": "^3.2.0",
"tslint-config-prettier": "^1.18.0",
"tslint-language-service": "^0.9.9",
"editorconfig-tools": "^0.1.1",
"husky": "^1.3.1",
"gh-pages": "^2.0.1",
"lint-staged": "^8.1.0",
"less-bundle-promise": "^1.0.7",
"mockjs": "^1.0.1-beta3",
"prettier": "^1.16.1",
"prettier-stylelint": "^0.4.2",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0",
"webpack-bundle-analyzer": "^3.1.0",
"xlsx": "^0.14.0",
"@delon/testing": "^7.2.0",
"ng-alain": "^7.2.0",
"ng-alain-codelyzer": "^0.0.1"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-staged"
}
},
"lint-staged": {
"*.{cmd,html,json,md,sh,txt,xml,yml}": [
"editorconfig-tools fix",
"git add"
],
"*.ts": [
"npm run lint:ts",
"prettier --write",
"git add"
],
"*.less": [
"npm run lint:style",
"prettier --write",
"git add"
],
"ignore": [
"src/assets/*"
]
}
}
# Only for CI, you can delete it
#!/usr/bin/env bash
set -e
GH=false
DAY_RELEASE=false
for ARG in "$@"; do
case "$ARG" in
-gh)
GH=true
;;
-dr)
DAY_RELEASE=true
;;
esac
done
cd $(dirname $0)/../..
ROOT_DIR="$(pwd)"
DIST_DIR="$(pwd)/dist"
VERSION=$(node -p "require('./package.json').version")
echo "Start build version: ${VERSION}"
echo ""
echo "Generate color less"
echo ""
node ./scripts/color-less.js
echo '===== need mock'
sed -i "s/const MOCK_MODULES = !environment.production/const MOCK_MODULES = true/g" ${ROOT_DIR}/src/app/delon.module.ts
sed -i "s/if (!environment.production)/if (true)/g" ${ROOT_DIR}/src/app/layout/default/default.component.ts
if [[ ${DAY_RELEASE} == true ]]; then
NG_ALAIN_VERSION=$(node -p "require('./node_modules/ng-alain/package.json').version")
echo "Current ng-alain version: ${NG_ALAIN_VERSION}"
echo ""
echo "Day Build, Muse be download @delon build packages"
git clone --depth 1 https://github.com/ng-alain/delon-builds.git
rm -rf node_modules/@delon
rm -rf node_modules/ng-alain
echo "Copies"
rsync -am delon-builds/ node_modules/
NG_ALAIN_VERSION=$(node -p "require('./node_modules/ng-alain/package.json').version")
echo "After ng-alain version: ${NG_ALAIN_VERSION}"
fi
echo ""
echo "Build angular"
echo ""
if [[ ${GH} == true ]]; then
$(npm bin)/ng build --prod --build-optimizer --base-href /ng-alain/
else
$(npm bin)/ng build --prod --build-optimizer
fi
cp -f ${DIST_DIR}/index.html ${DIST_DIR}/404.html
if [[ ${GH} == true ]]; then
commitAuthorName=$(git --no-pager show -s --format='%an' HEAD)
if [[ ${commitAuthorName} != 'cipchk' ]] && [[ ${commitAuthorName} != '卡色' ]]; then
echo "Warning: Just only cipchk or 卡色 user (current: ${commitAuthorName})"
exit 0
fi
if [ -z ${NG_ALAIN_BUILDS_TOKEN} ]; then
echo "Error: No access token for GitHub could be found." \
"Please set the environment variable 'NG_ALAIN_BUILDS_TOKEN'."
exit 0
fi
echo ""
echo "Deploy by gh-pages"
echo ""
$(npm bin)/gh-pages -d dist -r "https://${NG_ALAIN_BUILDS_TOKEN}@github.com/ng-alain/ng-alain.git"
fi
echo "Finished"
#!/bin/bash
set -e
readonly thisDir=$(cd $(dirname $0); pwd)
cd ${thisDir}
if [[ "${MODE}" ]]; then
echo ""
echo "Running mode: ${MODE}"
echo ""
npm run ${MODE}
elif [[ "${DEPLOY_MODE}" ]]; then
./deploy.sh -gh -dr
fi
const path = require('path');
const fs = require('fs');
const bundle = require('less-bundle-promise');
const root = path.resolve(__dirname, '../');
const allLessPath = path.join(root, '_all.less');
const target = path.join(root, 'src/assets/alain-default.less');
const content = `
@import 'node_modules/@delon/theme/styles/index';
@import 'node_modules/@delon/abc/index';
@import 'node_modules/@delon/chart/index';
@import 'node_modules/@delon/theme/styles/layout/default/index';
@import 'node_modules/@delon/theme/styles/layout/fullscreen/index';
@import 'src/styles/index';
@import 'src/styles/theme';
`;
fs.writeFileSync(allLessPath, content);
bundle({
src: allLessPath,
}).then(colorsLess => {
fs.writeFileSync(target, colorsLess);
fs.unlinkSync(allLessPath);
});
import { Component } from '@angular/core';
import { Component, OnInit, Renderer2, ElementRef } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { TitleService } from '@delon/theme';
import { VERSION as VERSION_ALAIN } from '@delon/theme';
import { VERSION as VERSION_ZORRO, NzModalService } from 'ng-zorro-antd';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
template: `
<router-outlet></router-outlet>
`,
})
export class AppComponent {
title = 'admin-mobile';
export class AppComponent implements OnInit {
constructor(
el: ElementRef,
renderer: Renderer2,
private router: Router,
private titleSrv: TitleService,
private modalSrv: NzModalService,
) {
renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full);
renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full);
}
ngOnInit() {
this.router.events.pipe(filter(evt => evt instanceof NavigationEnd)).subscribe(() => {
this.titleSrv.setTitle();
this.modalSrv.closeAll();
});
}
}
import { BrowserModule } from "@angular/platform-browser";
import { NgModule, APP_INITIALIZER } from "@angular/core";
import { AppComponent } from "./app.component";
import { ShareModule } from "./share";
import { RoutesModule } from "./routes/routes.module";
import { CoreModule } from "./core/core.module";
import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { StartupService } from "./core/startup/startup.service";
import { DefaultInterceptor } from "./core/net/default.interceptor";
import { LayoutModule } from './layout/layout.module';
import { NgModule, LOCALE_ID, APP_INITIALIZER } from '@angular/core';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
// #region default language
// 参考:https://ng-alain.com/docs/i18n
import { default as ngLang } from '@angular/common/locales/zh';
import { NZ_I18N, zh_CN as zorroLang } from 'ng-zorro-antd';
import { DELON_LOCALE, zh_CN as delonLang } from '@delon/theme';
const LANG = {
abbr: 'zh',
ng: ngLang,
zorro: zorroLang,
delon: delonLang,
};
// register angular
import { registerLocaleData } from '@angular/common';
registerLocaleData(LANG.ng, LANG.abbr);
const LANG_PROVIDES = [
{ provide: LOCALE_ID, useValue: LANG.abbr },
{ provide: NZ_I18N, useValue: LANG.zorro },
{ provide: DELON_LOCALE, useValue: LANG.delon },
];
// #endregion
// #region i18n services
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { I18NService } from '@core';
// 加载i18n语言文件
export function I18nHttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, `assets/tmp/i18n/`, '.json');
}
const I18NSERVICE_MODULES = [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: I18nHttpLoaderFactory,
deps: [HttpClient],
},
}),
];
const I18NSERVICE_PROVIDES = [{ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false }];
// #endregion
// #region global third module
const GLOBAL_THIRD_MODULES = [];
export function StartupServiceFactory(
startupService: StartupService
): Function {
return () => startupService.load().then(_ => {});
// #endregion
// #region JSON Schema form (using @delon/form)
import { JsonSchemaModule } from '@shared/json-schema/json-schema.module';
const FORM_MODULES = [JsonSchemaModule];
// #endregion
// #region Http Interceptors
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { SimpleInterceptor } from '@delon/auth';
import { DefaultInterceptor } from '@core';
const INTERCEPTOR_PROVIDES = [
{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
];
// #endregion
// #region Startup Service
import { StartupService } from '@core';
export function StartupServiceFactory(startupService: StartupService): Function {
return () => startupService.load();
}
const APPINIT_PROVIDES = [
StartupService,
{
provide: APP_INITIALIZER,
useFactory: StartupServiceFactory,
deps: [StartupService],
multi: true,
},
];
// #endregion
import { DelonModule } from './delon.module';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
import { RoutesModule } from './routes/routes.module';
import { LayoutModule } from './layout/layout.module';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
ShareModule,
RoutesModule,
CoreModule,
BrowserAnimationsModule,
HttpClientModule,
LayoutModule
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
StartupService,
{
provide: APP_INITIALIZER,
useFactory: StartupServiceFactory,
deps: [StartupService],
multi: true
}
DelonModule.forRoot(),
CoreModule,
SharedModule,
LayoutModule,
RoutesModule,
...I18NSERVICE_MODULES,
...GLOBAL_THIRD_MODULES,
...FORM_MODULES,
],
bootstrap: [AppComponent]
providers: [...LANG_PROVIDES, ...INTERCEPTOR_PROVIDES, ...I18NSERVICE_PROVIDES, ...APPINIT_PROVIDES],
bootstrap: [AppComponent],
})
export class AppModule {}
### CoreModule
**应** 仅只留 `providers` 属性。
**作用:** 一些通用服务,例如:用户消息、HTTP数据访问。
import { NgModule, Optional, SkipSelf } from "@angular/core";
import { throwIfAlreadyLoaded } from "./module-import-guard";
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { throwIfAlreadyLoaded } from './module-import-guard';
@NgModule({
providers: []
providers: [],
})
export class CoreModule {
constructor(
@Optional()
@SkipSelf()
parentModule: CoreModule
) {
throwIfAlreadyLoaded(parentModule, "CoreModule");
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}
import { TestBed, TestBedStatic } from '@angular/core/testing';
import { TranslateService } from '@ngx-translate/core';
import { SettingsService, DelonLocaleService } from '@delon/theme';
import { NzI18nService } from 'ng-zorro-antd';
import { of } from 'rxjs';
import { I18NService } from './i18n.service';
describe('Service: I18n', () => {
let injector: TestBedStatic;
let srv: I18NService;
const MockSettingsService = {
layout: {
lang: null,
},
};
const MockNzI18nService = {
setLocale: () => {},
};
const MockDelonLocaleService = {
setLocale: () => {},
};
const MockTranslateService = {
getBrowserLang: jasmine.createSpy('getBrowserLang'),
addLangs: () => {},
setLocale: () => {},
getDefaultLang: () => '',
use: () => of(),
instant: jasmine.createSpy('instant'),
};
function genModule() {
injector = TestBed.configureTestingModule({
providers: [
I18NService,
{ provide: SettingsService, useValue: MockSettingsService },
{ provide: NzI18nService, useValue: MockNzI18nService },
{ provide: DelonLocaleService, useValue: MockDelonLocaleService },
{ provide: TranslateService, useValue: MockTranslateService },
],
});
srv = injector.get(I18NService);
}
it('should working', () => {
genModule();
expect(srv).toBeTruthy();
expect(srv.defaultLang).toBe('zh-CN');
srv.fanyi('a');
srv.fanyi('a', {});
const t = injector.get(TranslateService) as TranslateService;
expect(t.instant).toHaveBeenCalled();
});
it('should be used layout as default language', () => {
MockSettingsService.layout.lang = 'en-US';
genModule();
expect(srv.defaultLang).toBe('en-US');
MockSettingsService.layout.lang = null;
});
it('should be used browser as default language', () => {
MockTranslateService.getBrowserLang.and.returnValue('zh-TW');
genModule();
expect(srv.defaultLang).toBe('zh-TW');
});
it('should be trigger notify when changed language', () => {
genModule();
srv.use('en-US');
srv.change.subscribe(lang => {
expect(lang).toBe('en-US');
});
});
});
// 请参考:https://ng-alain.com/docs/i18n
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { registerLocaleData } from '@angular/common';
import ngZh from '@angular/common/locales/zh';
import ngEn from '@angular/common/locales/en';
import ngZhTw from '@angular/common/locales/zh-Hant';
import { en_US, zh_CN, zh_TW, NzI18nService } from 'ng-zorro-antd';
import * as df_en from 'date-fns/locale/en';
import * as df_zh_cn from 'date-fns/locale/zh_cn';
import * as df_zh_tw from 'date-fns/locale/zh_tw';
import { TranslateService } from '@ngx-translate/core';
import {
SettingsService,
AlainI18NService,
DelonLocaleService,
en_US as delonEnUS,
zh_CN as delonZhCn,
zh_TW as delonZhTw,
} from '@delon/theme';
interface LangData {
text: string;
ng: any;
zorro: any;
dateFns: any;
delon: any;
abbr: string;
}
const DEFAULT = 'zh-CN';
const LANGS: { [key: string]: LangData } = {
'zh-CN': {
text: '简体中文',
ng: ngZh,
zorro: zh_CN,
dateFns: df_zh_cn,
delon: delonZhCn,
abbr: '🇨🇳',
},
'zh-TW': {
text: '繁体中文',
ng: ngZhTw,
zorro: zh_TW,
dateFns: df_zh_tw,
delon: delonZhTw,
abbr: '🇭🇰',
},
'en-US': {
text: 'English',
ng: ngEn,
zorro: en_US,
dateFns: df_en,
delon: delonEnUS,
abbr: '🇬🇧',
},
};
@Injectable({ providedIn: 'root' })
export class I18NService implements AlainI18NService {
private _default = DEFAULT;
private change$ = new BehaviorSubject<string | null>(null);
private _langs = Object.keys(LANGS).map(code => {
const item = LANGS[code];
return { code, text: item.text, abbr: item.abbr };
});
constructor(
settings: SettingsService,
private nzI18nService: NzI18nService,
private delonLocaleService: DelonLocaleService,
private translate: TranslateService,
) {
const defaultLan = settings.layout.lang || translate.getBrowserLang();
// `@ngx-translate/core` 预先知道支持哪些语言
const lans = this._langs.map(item => item.code);
translate.addLangs(lans);
this._default = lans.includes(defaultLan) ? defaultLan : lans[0];
this.updateLangData(this._default);
}
private updateLangData(lang: string) {
const item = LANGS[lang];
registerLocaleData(item.ng);
this.nzI18nService.setLocale(item.zorro);
(window as any).__locale__ = item.dateFns;
this.delonLocaleService.setLocale(item.delon);
}
get change(): Observable<string> {
return this.change$.asObservable().pipe(filter(w => w != null)) as Observable<string>;
}
use(lang: string): void {
lang = lang || this.translate.getDefaultLang();
if (this.currentLang === lang) return;
this.updateLangData(lang);
this.translate.use(lang).subscribe(() => this.change$.next(lang));
}
/** 获取语言列表 */
getLangs() {
return this._langs;
}
/** 翻译 */
fanyi(key: string, interpolateParams?: Object) {
return this.translate.instant(key, interpolateParams);
}
/** 默认语言 */
get defaultLang() {
return this._default;
}
/** 当前语言 */
get currentLang() {
return this.translate.currentLang || this.translate.getDefaultLang() || this._default;
}
}
export * from './i18n/i18n.service';
export * from './module-import-guard';
export * from './net/default.interceptor';
export * from './startup/startup.service';
import { Injectable, Injector, Inject } from "@angular/core";
import { Router } from "@angular/router";
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpErrorResponse,
HttpSentEvent,
HttpHeaderResponse,
HttpProgressEvent,
HttpResponse,
HttpUserEvent
} from "@angular/common/http";
import { Observable, of, throwError } from "rxjs";
import { mergeMap, catchError } from "rxjs/operators";
import { environment } from "src/environments/environment";
HttpEvent,
HttpResponseBase,
} from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { mergeMap, catchError } from 'rxjs/operators';
import { NzMessageService, NzNotificationService } from 'ng-zorro-antd';
import { _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
const CODEMESSAGE = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
/**
* 默认HTTP拦截器,其注册细节见 `app.module.ts`
......@@ -22,106 +40,89 @@ import { environment } from "src/environments/environment";
export class DefaultInterceptor implements HttpInterceptor {
constructor(private injector: Injector) {}
// get msg(): NzMessageService {
// return this.injector.get(NzMessageService);
// }
private get notification(): NzNotificationService {
return this.injector.get(NzNotificationService);
}
private goTo(url: string) {
setTimeout(() => this.injector.get(Router).navigateByUrl(url));
}
private handleData(
event: HttpResponse<any> | HttpErrorResponse
): Observable<any> {
private checkStatus(ev: HttpResponseBase) {
if ((ev.status >= 200 && ev.status < 300) || ev.status === 401) {
return;
}
const errortext = CODEMESSAGE[ev.status] || ev.statusText;
this.notification.error(`请求错误 ${ev.status}: ${ev.url}`, errortext);
}
private handleData(ev: HttpResponseBase): Observable<any> {
// 可能会因为 `throw` 导出无法执行 `_HttpClient` 的 `end()` 操作
// this.injector.get(_HttpClient).end();
if (ev.status > 0) {
this.injector.get(_HttpClient).end();
}
this.checkStatus(ev);
// 业务处理:一些通用操作
switch (event.status) {
switch (ev.status) {
case 200:
case 201:
// 业务层级错误处理,以下是假定restful有一套统一输出格式(指不管成功与否都有相应的数据格式)情况下进行处理
// 例如响应内容:
// 错误内容:{ code: 1, msg: '非法参数' }
// 正确内容:{ code: 0, data: { },msg:'合法参数'}
// 则以下代码片断可直接适用
if (event instanceof HttpResponse) {
const body: any = event.body;
if (body && body.code !== 0) {
// this.msg.error(body.msg);
if (body.msg === "invalidtoken" || body.msg === "无效token") {
// this.logout();
}
// 继续抛出错误中断后续所有 Pipe、subscribe 操作,因此:
// this.http.get('/').subscribe() 并不会触发
return throwError({ msg: body.code });
} else if (body) {
// 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
return of(
new HttpResponse(Object.assign(event, { body: body.data }))
);
// 或者依然保持完整的格式
// return of(event);
} else {
// this.msg.error('server error!');
}
}
// 业务层级错误处理,以下是假定restful有一套统一输出格式(指不管成功与否都有相应的数据格式)情况下进行处理
// 例如响应内容:
// 错误内容:{ status: 1, msg: '非法参数' }
// 正确内容:{ status: 0, response: { } }
// 则以下代码片断可直接适用
// if (event instanceof HttpResponse) {
// const body: any = event.body;
// if (body && body.status !== 0) {
// this.msg.error(body.msg);
// // 继续抛出错误中断后续所有 Pipe、subscribe 操作,因此:
// // this.http.get('/').subscribe() 并不会触发
// return throwError({});
// } else {
// // 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
// return of(new HttpResponse(Object.assign(event, { body: body.response })));
// // 或者依然保持完整的格式
// return of(event);
// }
// }
break;
case 401: // 未登录状态码
this.goTo("/login");
case 401:
this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
// 清空 token 信息
(this.injector.get(DA_SERVICE_TOKEN) as ITokenService).clear();
this.goTo('/passport/login');
break;
case 403:
case 404:
case 500:
// this.goTo(`/${event.status}`);
break;
case 504:
// this.goTo("/403");
this.goTo(`/exception/${ev.status}`);
break;
default:
if (event instanceof HttpErrorResponse) {
console.warn(
"未可知错误,大部分是由于后端不支持CORS或无效配置引起",
event
);
// this.msg.error(event.message);
if (ev instanceof HttpErrorResponse) {
console.warn('未可知错误,大部分是由于后端不支持CORS或无效配置引起', ev);
return throwError(ev);
}
break;
}
return of(event);
return of(ev);
}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<
| HttpSentEvent
| HttpHeaderResponse
| HttpProgressEvent
| HttpResponse<any>
| HttpUserEvent<any>
> {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 统一加上服务端前缀
let url = req.url;
if (!url.startsWith("https://") && !url.startsWith("http://")) {
if (!url.startsWith('https://') && !url.startsWith('http://')) {
url = environment.SERVER_URL + url;
}
const newReq = req.clone({
url: url
});
const newReq = req.clone({ url });
return next.handle(newReq).pipe(
mergeMap((event: any) => {
// 允许统一对请求错误处理,这是因为一个请求若是业务上错误的情况下其HTTP请求的状态是200的情况下需要
if (
url.indexOf("api/") !== -1 &&
event instanceof HttpResponse &&
event.status === 200
)
return this.handleData(event);
// 允许统一对请求错误处理
if (event instanceof HttpResponseBase) return this.handleData(event);
// 若一切都正常,则后续操作
return of(event);
}),
catchError((err: HttpErrorResponse) => this.handleData(err))
catchError((err: HttpErrorResponse) => this.handleData(err)),
);
}
}
import { Injectable, Inject, Injector } from "@angular/core";
import { zip } from "rxjs";
import { catchError } from "rxjs/operators";
import { HttpClient } from "@angular/common/http";
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { zip } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { MenuService, SettingsService, TitleService, ALAIN_I18N_TOKEN } from '@delon/theme';
import { ACLService } from '@delon/acl';
import { TranslateService } from '@ngx-translate/core';
import { I18NService } from '../i18n/i18n.service';
import { NzIconService } from 'ng-zorro-antd';
import { ICONS_AUTO } from '../../../style-icons-auto';
import { ICONS } from '../../../style-icons';
/**
* 用于应用启动时
......@@ -9,42 +17,59 @@ import { HttpClient } from "@angular/common/http";
*/
@Injectable()
export class StartupService {
constructor(private httpClient: HttpClient) {}
private viaHttp(resolve: any, reject: any) {
zip(this.httpClient.get("assets/tmp/app-data.json"))
.pipe(
// 接收其他拦截器后产生的异常消息
catchError(([appData]) => {
resolve(null);
return [appData];
})
)
.subscribe(
([appData]) => {
// setting language data
// application data
const res: any = appData;
console.log("启动应用");
console.log(res);
// 应用信息:包括站点名、描述、年份
},
() => {},
() => {
resolve(null);
}
);
constructor(
iconSrv: NzIconService,
private menuService: MenuService,
private translate: TranslateService,
@Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
private settingService: SettingsService,
private aclService: ACLService,
private titleService: TitleService,
private httpClient: HttpClient,
) {
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
}
load(): Promise<any> {
// only works with promises
// https://github.com/angular/angular/issues/15088
return new Promise((resolve, reject) => {
this.viaHttp(resolve, reject);
// mock:请勿在生产环境中这么使用,viaMock 单纯只是为了模拟一些数据使脚手架一开始能正常运行
// this.viaMock(resolve, reject);
return new Promise(resolve => {
zip(
this.httpClient.get(`assets/tmp/i18n/${this.i18n.defaultLang}.json`),
this.httpClient.get('assets/tmp/app-data.json'),
)
.pipe(
// 接收其他拦截器后产生的异常消息
catchError(([langData, appData]) => {
resolve(null);
return [langData, appData];
}),
)
.subscribe(
([langData, appData]) => {
// setting language data
this.translate.setTranslation(this.i18n.defaultLang, langData);
this.translate.setDefaultLang(this.i18n.defaultLang);
// application data
const res: any = appData;
// 应用信息:包括站点名、描述、年份
this.settingService.setApp(res.app);
// 用户信息:包括姓名、头像、邮箱地址
this.settingService.setUser(res.user);
// ACL:设置权限为全量
this.aclService.setFull(true);
// 初始化菜单
this.menuService.add(res.menu);
// 设置页面标题的后缀
this.titleService.default = '';
this.titleService.suffix = res.app.name;
},
() => {},
() => {
resolve(null);
},
);
});
}
}
/**
* 进一步对基础模块的导入提炼
* 有关模块注册指导原则请参考:https://github.com/ng-alain/ng-alain/issues/180
*/
import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core';
import { throwIfAlreadyLoaded } from '@core';
import { AlainThemeModule } from '@delon/theme';
// #region mock
import { DelonMockModule } from '@delon/mock';
import * as MOCKDATA from '../../_mock';
import { environment } from '@env/environment';
const MOCK_MODULES = !environment.production ? [DelonMockModule.forRoot({ data: MOCKDATA })] : [];
// #endregion
// #region reuse-tab
/**
* 若需要[路由复用](https://ng-alain.com/components/reuse-tab)需要:
* 1、增加 `REUSETAB_PROVIDES`
* 2、在 `src/app/layout/default/default.component.html` 修改:
* ```html
* <section class="alain-default__content">
* <reuse-tab></reuse-tab>
* <router-outlet></router-outlet>
* </section>
* ```
*/
import { RouteReuseStrategy } from '@angular/router';
import { ReuseTabService, ReuseTabStrategy } from '@delon/abc/reuse-tab';
const REUSETAB_PROVIDES = [
// {
// provide: RouteReuseStrategy,
// useClass: ReuseTabStrategy,
// deps: [ReuseTabService],
// },
];
// #endregion
// #region global config functions
import { PageHeaderConfig } from '@delon/abc';
export function fnPageHeaderConfig(): PageHeaderConfig {
return {
...new PageHeaderConfig(),
...({ homeI18n: 'home' } as PageHeaderConfig),
};
}
import { DelonAuthConfig } from '@delon/auth';
export function fnDelonAuthConfig(): DelonAuthConfig {
return {
...new DelonAuthConfig(),
...({ login_url: '/passport/login' } as DelonAuthConfig),
};
}
import { STConfig } from '@delon/abc';
export function fnSTConfig(): STConfig {
return {
...new STConfig(),
...({
modal: { size: 'lg' },
} as STConfig),
};
}
const GLOBAL_CONFIG_PROVIDES = [
// TIPS:@delon/abc 有大量的全局配置信息,例如设置所有 `st` 的页码默认为 `20` 行
{ provide: STConfig, useFactory: fnSTConfig },
{ provide: PageHeaderConfig, useFactory: fnPageHeaderConfig },
{ provide: DelonAuthConfig, useFactory: fnDelonAuthConfig },
];
// #endregion
@NgModule({
imports: [AlainThemeModule.forRoot(), ...MOCK_MODULES],
})
export class DelonModule {
constructor(@Optional() @SkipSelf() parentModule: DelonModule) {
throwIfAlreadyLoaded(parentModule, 'DelonModule');
}
static forRoot(): ModuleWithProviders {
return {
ngModule: DelonModule,
providers: [...REUSETAB_PROVIDES, ...GLOBAL_CONFIG_PROVIDES],
};
}
}
<p>
默认布局
</p>
<router-outlet></router-outlet>
<div class="alain-default__progress-bar" *ngIf="isFetching"></div>
<layout-header class="alain-default__header"></layout-header>
<layout-sidebar class="alain-default__aside"></layout-sidebar>
<section class="alain-default__content">
<router-outlet></router-outlet>
</section>
<ng-template #settingHost></ng-template>
import { Component, OnInit } from '@angular/core';
import {
Component,
ViewChild,
ComponentFactoryResolver,
ViewContainerRef,
AfterViewInit,
OnInit,
OnDestroy,
ElementRef,
Renderer2,
Inject,
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Router, NavigationEnd, RouteConfigLoadStart, NavigationError, NavigationCancel } from '@angular/router';
import { NzMessageService } from 'ng-zorro-antd';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { updateHostClass } from '@delon/util';
import { SettingsService } from '@delon/theme';
import { environment } from '@env/environment';
import { SettingDrawerComponent } from './setting-drawer/setting-drawer.component';
@Component({
selector: 'app-default',
selector: 'layout-default',
templateUrl: './default.component.html',
styleUrls: ['./default.component.less']
})
export class DefaultComponent implements OnInit {
export class LayoutDefaultComponent implements OnInit, AfterViewInit, OnDestroy {
private unsubscribe$ = new Subject<void>();
@ViewChild('settingHost', { read: ViewContainerRef })
private settingHost: ViewContainerRef;
isFetching = false;
constructor(
router: Router,
_message: NzMessageService,
private resolver: ComponentFactoryResolver,
private settings: SettingsService,
private el: ElementRef,
private renderer: Renderer2,
@Inject(DOCUMENT) private doc: any,
) {
// scroll to top in change page
router.events.pipe(takeUntil(this.unsubscribe$)).subscribe(evt => {
if (!this.isFetching && evt instanceof RouteConfigLoadStart) {
this.isFetching = true;
}
if (evt instanceof NavigationError || evt instanceof NavigationCancel) {
this.isFetching = false;
if (evt instanceof NavigationError) {
_message.error(`无法加载${evt.url}路由`, { nzDuration: 1000 * 3 });
}
return;
}
if (!(evt instanceof NavigationEnd)) {
return;
}
setTimeout(() => {
this.isFetching = false;
}, 100);
});
}
private setClass() {
const { el, doc, renderer, settings } = this;
const layout = settings.layout;
updateHostClass(el.nativeElement, renderer, {
['alain-default']: true,
[`alain-default__fixed`]: layout.fixed,
[`alain-default__collapsed`]: layout.collapsed,
});
constructor() { }
doc.body.classList[layout.colorWeak ? 'add' : 'remove']('color-weak');
}
ngAfterViewInit(): void {
// Setting componet for only developer
if (!environment.production) {
setTimeout(() => {
const settingFactory = this.resolver.resolveComponentFactory(SettingDrawerComponent);
this.settingHost.createComponent(settingFactory);
}, 22);
}
}
ngOnInit() {
const { settings, unsubscribe$ } = this;
settings.notify.pipe(takeUntil(unsubscribe$)).subscribe(() => this.setClass());
this.setClass();
}
ngOnDestroy() {
const { unsubscribe$ } = this;
unsubscribe$.next();
unsubscribe$.complete();
}
}
import { Component, HostListener, ChangeDetectionStrategy } from '@angular/core';
import * as screenfull from 'screenfull';
@Component({
selector: 'header-fullscreen',
template: `
<i nz-icon [type]="status ? 'fullscreen-exit' : 'fullscreen'"></i>
{{ (status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | translate }}
`,
host: {
'[class.d-block]': 'true',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderFullScreenComponent {
status = false;
private get sf(): screenfull.Screenfull {
return screenfull as screenfull.Screenfull;
}
@HostListener('window:resize')
_resize() {
this.status = this.sf.isFullscreen;
}
@HostListener('click')
_click() {
if (this.sf.enabled) {
this.sf.toggle();
}
}
}
import { Component, Inject, Input, ChangeDetectionStrategy } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { SettingsService, ALAIN_I18N_TOKEN } from '@delon/theme';
import { InputBoolean } from '@delon/util';
import { I18NService } from '@core';
@Component({
selector: 'header-i18n',
template: `
<nz-dropdown nzPlacement="bottomRight">
<div *ngIf="showLangText" nz-dropdown>
<i nz-icon type="global"></i>
{{ 'menu.lang' | translate }}
<i nz-icon type="down"></i>
</div>
<i *ngIf="!showLangText" nz-dropdown nz-icon type="global"></i>
<ul nz-menu>
<li
nz-menu-item
*ngFor="let item of langs"
[nzSelected]="item.code === curLangCode"
(click)="change(item.code)"
>
<span role="img" [attr.aria-label]="item.text" class="pr-xs">{{ item.abbr }}</span>
{{ item.text }}
</li>
</ul>
</nz-dropdown>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderI18nComponent {
/** Whether to display language text */
@Input() @InputBoolean() showLangText = true;
get langs() {
return this.i18n.getLangs();
}
get curLangCode() {
return this.settings.layout.lang;
}
constructor(
private settings: SettingsService,
@Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
@Inject(DOCUMENT) private doc: any,
) {}
change(lang: string) {
const spinEl = this.doc.createElement('div');
spinEl.setAttribute('class', `page-loading ant-spin ant-spin-lg ant-spin-spinning`);
spinEl.innerHTML = `<span class="ant-spin-dot ant-spin-dot-spin"><i></i><i></i><i></i><i></i></span>`;
this.doc.body.appendChild(spinEl);
this.i18n.use(lang);
this.settings.setLayout('lang', lang);
setTimeout(() => this.doc.location.reload());
}
}
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'header-icon',
template: `
<nz-dropdown nzTrigger="click" nzPlacement="bottomRight" (nzVisibleChange)="change()">
<div class="alain-default__nav-item" nz-dropdown>
<i class="anticon anticon-appstore"></i>
</div>
<div nz-menu class="wd-xl animated jello">
<nz-spin [nzSpinning]="loading" [nzTip]="'正在读取数据...'">
<div nz-row [nzType]="'flex'" [nzJustify]="'center'" [nzAlign]="'middle'" class="app-icons">
<div nz-col [nzSpan]="6">
<i class="anticon anticon-calendar bg-error text-white"></i>
<small>Calendar</small>
</div>
<div nz-col [nzSpan]="6">
<i class="anticon anticon-file bg-geekblue text-white"></i>
<small>Files</small>
</div>
<div nz-col [nzSpan]="6">
<i class="anticon anticon-cloud bg-success text-white"></i>
<small>Cloud</small>
</div>
<div nz-col [nzSpan]="6">
<i class="anticon anticon-star-o bg-magenta text-white"></i>
<small>Star</small>
</div>
<div nz-col [nzSpan]="6">
<i class="anticon anticon-team bg-purple text-white"></i>
<small>Team</small>
</div>
<div nz-col [nzSpan]="6">
<i class="anticon anticon-scan bg-warning text-white"></i>
<small>QR</small>
</div>
<div nz-col [nzSpan]="6">
<i class="anticon anticon-pay-circle-o bg-cyan text-white"></i>
<small>Pay</small>
</div>
<div nz-col [nzSpan]="6">
<i class="anticon anticon-printer bg-grey text-white"></i>
<small>Print</small>
</div>
</div>
</nz-spin>
</div>
</nz-dropdown>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderIconComponent {
loading = true;
constructor(private cdr: ChangeDetectorRef) {}
change() {
setTimeout(() => {
this.loading = false;
this.cdr.detectChanges();
}, 500);
}
}
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import * as distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import { NzMessageService } from 'ng-zorro-antd';
import { NoticeItem, NoticeIconList } from '@delon/abc';
/**
* 菜单通知
*/
@Component({
selector: 'header-notify',
template: `
<notice-icon
[data]="data"
[count]="count"
[loading]="loading"
btnClass="alain-default__nav-item"
btnIconClass="alain-default__nav-item-icon"
(select)="select($event)"
(clear)="clear($event)"
(popoverVisibleChange)="loadData()"
></notice-icon>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderNotifyComponent {
data: NoticeItem[] = [
{
title: '通知',
list: [],
emptyText: '你已查看所有通知',
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
clearText: '清空通知',
},
{
title: '消息',
list: [],
emptyText: '您已读完所有消息',
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg',
clearText: '清空消息',
},
{
title: '待办',
list: [],
emptyText: '你已完成所有待办',
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg',
clearText: '清空待办',
},
];
count = 5;
loading = false;
constructor(private msg: NzMessageService, private cdr: ChangeDetectorRef) {}
private updateNoticeData(notices: NoticeIconList[]): NoticeItem[] {
const data = this.data.slice();
data.forEach(i => (i.list = []));
notices.forEach(item => {
const newItem = { ...item };
if (newItem.datetime)
newItem.datetime = distanceInWordsToNow(item.datetime!, {
locale: (window as any).__locale__,
});
if (newItem.extra && newItem.status) {
newItem.color = {
todo: undefined,
processing: 'blue',
urgent: 'red',
doing: 'gold',
}[newItem.status];
}
data.find(w => w.title === newItem.type)!.list.push(newItem);
});
return data;
}
loadData() {
if (this.loading) return;
this.loading = true;
setTimeout(() => {
this.data = this.updateNoticeData([
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: '通知',
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: '通知',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: '通知',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: '通知',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: '通知',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: '消息',
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
},
{
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: '待办',
},
{
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: '待办',
},
{
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: '待办',
},
{
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: '待办',
},
]);
this.loading = false;
this.cdr.detectChanges();
}, 1000);
}
clear(type: string) {
this.msg.success(`清空了 ${type}`);
}
select(res: any) {
this.msg.success(`点击了 ${res.title}${res.item.title}`);
}
}
import { Component, HostBinding, Input, ElementRef, AfterViewInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'header-search',
template: `
<nz-input-group [nzAddOnBeforeIcon]="focus ? 'anticon anticon-arrow-down' : 'anticon anticon-search'">
<input
nz-input
[(ngModel)]="q"
(focus)="qFocus()"
(blur)="qBlur()"
[placeholder]="'menu.search.placeholder' | translate"
/>
</nz-input-group>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderSearchComponent implements AfterViewInit {
q: string;
qIpt: HTMLInputElement;
@HostBinding('class.alain-default__search-focus')
focus = false;
@HostBinding('class.alain-default__search-toggled')
searchToggled = false;
@Input()
set toggleChange(value: boolean) {
if (typeof value === 'undefined') return;
this.searchToggled = true;
this.focus = true;
setTimeout(() => this.qIpt.focus(), 300);
}
constructor(private el: ElementRef) {}
ngAfterViewInit() {
this.qIpt = (this.el.nativeElement as HTMLElement).querySelector('.ant-input') as HTMLInputElement;
}
qFocus() {
this.focus = true;
}
qBlur() {
this.focus = false;
this.searchToggled = false;
}
}
import { Component, HostListener, ChangeDetectionStrategy } from '@angular/core';
import { NzModalService, NzMessageService } from 'ng-zorro-antd';
@Component({
selector: 'header-storage',
template: `
<i nz-icon type="tool"></i>
{{ 'menu.clear.local.storage' | translate }}
`,
host: {
'[class.d-block]': 'true',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderStorageComponent {
constructor(private modalSrv: NzModalService, private messageSrv: NzMessageService) {}
@HostListener('click')
_click() {
this.modalSrv.confirm({
nzTitle: 'Make sure clear all local storage?',
nzOnOk: () => {
localStorage.clear();
this.messageSrv.success('Clear Finished!');
},
});
}
}
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'header-task',
template: `
<nz-dropdown nzTrigger="click" nzPlacement="bottomRight" (nzVisibleChange)="change()">
<div class="alain-default__nav-item" nz-dropdown>
<nz-badge [nzDot]="true">
<i nz-icon type="bell" class="alain-default__nav-item-icon"></i>
</nz-badge>
</div>
<div nz-menu class="wd-lg">
<div *ngIf="loading" class="mx-lg p-lg"><nz-spin></nz-spin></div>
<nz-card *ngIf="!loading" nzTitle="Notifications" nzBordered="false" class="ant-card__body-nopadding">
<ng-template #extra><i nz-icon type="plus"></i></ng-template>
<div
nz-row
[nzType]="'flex'"
[nzJustify]="'center'"
[nzAlign]="'middle'"
class="py-sm bg-grey-lighter-h point"
>
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/1.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>cipchk</strong>
<p class="mb0">Please tell me what happened in a few words, don't go into details.</p>
</div>
</div>
<div
nz-row
[nzType]="'flex'"
[nzJustify]="'center'"
[nzAlign]="'middle'"
class="py-sm bg-grey-lighter-h point"
>
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/2.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>はなさき</strong>
<p class="mb0">ハルカソラトキヘダツヒカリ</p>
</div>
</div>
<div
nz-row
[nzType]="'flex'"
[nzJustify]="'center'"
[nzAlign]="'middle'"
class="py-sm bg-grey-lighter-h point"
>
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/3.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>苏先生</strong>
<p class="mb0">请告诉我,我应该说点什么好?</p>
</div>
</div>
<div
nz-row
[nzType]="'flex'"
[nzJustify]="'center'"
[nzAlign]="'middle'"
class="py-sm bg-grey-lighter-h point"
>
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/4.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>Kent</strong>
<p class="mb0">Please tell me what happened in a few words, don't go into details.</p>
</div>
</div>
<div
nz-row
[nzType]="'flex'"
[nzJustify]="'center'"
[nzAlign]="'middle'"
class="py-sm bg-grey-lighter-h point"
>
<div nz-col [nzSpan]="4" class="text-center">
<nz-avatar [nzSrc]="'./assets/tmp/img/5.png'"></nz-avatar>
</div>
<div nz-col [nzSpan]="20">
<strong>Jefferson</strong>
<p class="mb0">Please tell me what happened in a few words, don't go into details.</p>
</div>
</div>
<div nz-row>
<div nz-col [nzSpan]="24" class="pt-md border-top-1 text-center text-grey point">
See All
</div>
</div>
</nz-card>
</div>
</nz-dropdown>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderTaskComponent {
loading = true;
constructor(private cdr: ChangeDetectorRef) {}
change() {
setTimeout(() => {
this.loading = false;
this.cdr.detectChanges();
}, 500);
}
}
import { Component, Inject, ChangeDetectionStrategy } from '@angular/core';
import { Router } from '@angular/router';
import { SettingsService } from '@delon/theme';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
@Component({
selector: 'header-user',
template: `
<nz-dropdown nzPlacement="bottomRight">
<div class="alain-default__nav-item d-flex align-items-center px-sm" nz-dropdown>
<nz-avatar [nzSrc]="settings.user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
{{ settings.user.name }}
</div>
<div nz-menu class="width-sm">
<div nz-menu-item routerLink="/pro/account/center">
<i nz-icon type="user" class="mr-sm"></i>
{{ 'menu.account.center' | translate }}
</div>
<div nz-menu-item routerLink="/pro/account/settings">
<i nz-icon type="setting" class="mr-sm"></i>
{{ 'menu.account.settings' | translate }}
</div>
<div nz-menu-item routerLink="/exception/trigger">
<i nz-icon type="close-circle" class="mr-sm"></i>
{{ 'menu.account.trigger' | translate }}
</div>
<li nz-menu-divider></li>
<div nz-menu-item (click)="logout()">
<i nz-icon type="logout" class="mr-sm"></i>
{{ 'menu.account.logout' | translate }}
</div>
</div>
</nz-dropdown>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderUserComponent {
constructor(
public settings: SettingsService,
private router: Router,
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
) {}
logout() {
this.tokenService.clear();
this.router.navigateByUrl(this.tokenService.login_url!);
}
}
<div class="alain-default__header-logo">
<a [routerLink]="['/']" class="alain-default__header-logo-link">
<img class="alain-default__header-logo-expanded" src="./assets/logo-full.svg" alt="{{settings.app.name}}" style="max-height:40px;" />
<img class="alain-default__header-logo-collapsed" src="./assets/logo.svg" alt="{{settings.app.name}}" style="max-height:30px;" />
</a>
</div>
<div class="alain-default__nav-wrap">
<ul class="alain-default__nav">
<!-- Menu -->
<li>
<div class="alain-default__nav-item" (click)="toggleCollapsedSidebar()">
<i nz-icon type="menu-{{settings.layout.collapsed ? 'unfold' : 'fold'}}"></i>
</div>
</li>
<!-- Github Page -->
<li>
<a class="alain-default__nav-item" href="//github.com/ng-alain/ng-alain" target="_blank">
<i nz-icon type="github"></i>
</a>
</li>
<!-- Lock Page -->
<li class="hidden-mobile">
<div class="alain-default__nav-item" routerLink="/passport/lock">
<i nz-icon type="lock"></i>
</div>
</li>
<!-- Search Button -->
<li class="hidden-pc" (click)="searchToggleChange()">
<div class="alain-default__nav-item">
<i nz-icon type="search"></i>
</div>
</li>
</ul>
<header-search class="alain-default__search" [toggleChange]="searchToggleStatus"></header-search>
<ul class="alain-default__nav">
<!-- Notify -->
<li>
<header-notify></header-notify>
</li>
<!-- Task -->
<li class="hidden-mobile">
<header-task></header-task>
</li>
<!-- App Icons -->
<li class="hidden-mobile">
<header-icon></header-icon>
</li>
<!-- Settings -->
<li class="hidden-mobile">
<nz-dropdown nzTrigger="click" nzPlacement="bottomRight">
<div class="alain-default__nav-item" nz-dropdown>
<i nz-icon type="setting"></i>
</div>
<div nz-menu style="width:200px;">
<div nz-menu-item>
<header-fullscreen></header-fullscreen>
</div>
<div nz-menu-item>
<header-storage></header-storage>
</div>
<div nz-menu-item>
<header-i18n></header-i18n>
</div>
</div>
</nz-dropdown>
</li>
<li class="hidden-mobile">
<header-user></header-user>
</li>
</ul>
</div>
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { SettingsService } from '@delon/theme';
@Component({
selector: 'layout-header',
templateUrl: './header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent {
searchToggleStatus: boolean;
constructor(public settings: SettingsService) {}
toggleCollapsedSidebar() {
this.settings.setLayout('collapsed', !this.settings.layout.collapsed);
}
searchToggleChange() {
this.searchToggleStatus = !this.searchToggleStatus;
}
}
---
component: app-header
title: 顶部菜单
---
顶部菜单组件允许通过 `components` 目录下的组件进行按需组装。
## 组件列表
组件名 | 说明
----|------
`header-fullscreen` | 全屏切换
`header-icon` | 应用图标
`header-langs` | 语言切换
`header-notify` | 菜单通知
`header-search` | 搜索框
`header-storage` | 清除 LocalStorage 缓存
`header-task` | 任务通知
`header-theme` | 主题切换
`header-user` | 用户菜单
<span>{{i.label}}<span class="pl-sm text-grey">{{i.tip}}</span></span>
<div [ngSwitch]="i.type">
<ng-container *ngSwitchCase="'color'">
<input nz-input type="color" style="min-width: 88px" [(ngModel)]="i.value" [ngModelOptions]="{standalone: true}">
</ng-container>
<ng-container *ngSwitchCase="'input'">
<input nz-input style="min-width: 88px" [(ngModel)]="i.value" [ngModelOptions]="{standalone: true}">
</ng-container>
<ng-container *ngSwitchCase="'px'">
<nz-input-number [(ngModel)]="pxVal" (ngModelChange)="pxChange($event)" [nzMin]="i.min" [nzMax]="i.max"
[nzStep]="i.step || 2" [nzFormatter]="format"></nz-input-number>
</ng-container>
<ng-container *ngSwitchCase="'switch'">
<nz-switch nzSize="small" [(ngModel)]="i.value" [ngModelOptions]="{standalone: true}"></nz-switch>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-content></ng-content>
</ng-container>
</div>
import { Component, Input } from '@angular/core';
@Component({
// tslint:disable-next-line:component-selector
selector: 'setting-drawer-item',
templateUrl: './setting-drawer-item.component.html',
host: {
'[class.setting-drawer__body-item]': 'true',
},
})
export class SettingDrawerItemComponent {
i: any = {};
@Input()
set data(val: any) {
this.i = val;
if (val.type === 'px') {
this.pxVal = +val.value.replace('px', '');
}
}
pxVal: number;
pxChange(val: number) {
this.i.value = `${val}px`;
}
format = value => `${value} px`;
}
<nz-drawer [(nzVisible)]="collapse" [nzWidth]="500" (nzOnClose)="toggle()">
<div class="setting-drawer__content">
<div class="setting-drawer__body setting-drawer__theme">
<h3 class="setting-drawer__title">主题色</h3>
<span *ngFor="let c of colors" nz-tooltip [ngStyle]="{ 'background-color': c.color }" (click)="changeColor(c.color)"
nz-tooltip [nzTitle]="c.key" class="setting-drawer__theme-tag"><i *ngIf="color === c.color"
class="anticon anticon-check"></i></span>
</div>
<nz-divider></nz-divider>
<div class="setting-drawer__body">
<h3 class="setting-drawer__title">设置</h3>
<nz-tabset>
<nz-tab nzTitle="顶部">
<div class="setting-drawer__body">
<setting-drawer-item [data]="data['alain-default-header-hg']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-header-bg']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-header-padding']"></setting-drawer-item>
</div>
</nz-tab>
<nz-tab nzTitle="侧边栏">
<setting-drawer-item [data]="data['alain-default-aside-wd']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-aside-bg']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-aside-collapsed-wd']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-aside-nav-padding-top-bottom']"></setting-drawer-item>
</nz-tab>
<nz-tab nzTitle="内容">
<setting-drawer-item [data]="data['alain-default-content-bg']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-content-heading-bg']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-content-heading-border']"></setting-drawer-item>
<setting-drawer-item [data]="data['alain-default-content-padding']"></setting-drawer-item>
</nz-tab>
<nz-tab nzTitle="其它">
<setting-drawer-item [data]="data['form-state-visual-feedback-enabled']"></setting-drawer-item>
<setting-drawer-item [data]="data['preserve-white-spaces-enabled']"></setting-drawer-item>
<setting-drawer-item [data]="data['nz-table-img-radius']"></setting-drawer-item>
<setting-drawer-item [data]="data['nz-table-img-margin-right']"></setting-drawer-item>
<setting-drawer-item [data]="data['nz-table-img-max-width']"></setting-drawer-item>
<setting-drawer-item [data]="data['nz-table-img-max-height']"></setting-drawer-item>
</nz-tab>
</nz-tabset>
</div>
<nz-divider></nz-divider>
<div class="setting-drawer__body">
<div class="setting-drawer__body-item">
固定头和侧边栏
<nz-switch nzSize="small" [(ngModel)]="layout.fixed" (ngModelChange)="setLayout('fixed', layout.fixed)"></nz-switch>
</div>
<div class="setting-drawer__body-item">
色弱模式
<nz-switch nzSize="small" [(ngModel)]="layout.colorWeak" (ngModelChange)="setLayout('colorWeak', layout.colorWeak)"></nz-switch>
</div>
</div>
<nz-divider></nz-divider>
<button (click)="apply()" type="button" nz-button nzType="primary">预览</button>
<button (click)="reset()" type="button" nz-button>重置</button>
<button (click)="copyVar()" type="button" nz-button>拷贝</button>
<nz-alert class="mt-md" nzType="warning" nzMessage="配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改参数配置文件 src/styles/theme.less"></nz-alert>
</div>
</nz-drawer>
<div class="setting-drawer__handle" [ngClass]="{'setting-drawer__handle-opened': collapse}" (click)="toggle()">
<i nz-icon [type]="!collapse ? 'setting' : 'close'" class="setting-drawer__handle-icon"></i>
</div>
import { Component, ChangeDetectionStrategy, NgZone, Inject, ChangeDetectorRef } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { NzMessageService } from 'ng-zorro-antd';
import { LazyService, copy, deepCopy } from '@delon/util';
import { SettingsService } from '@delon/theme';
const ALAINDEFAULTVAR = 'alain-default-vars';
const DEFAULT_COLORS = [
{
key: 'dust',
color: '#F5222D',
},
{
key: 'volcano',
color: '#FA541C',
},
{
key: 'sunset',
color: '#FAAD14',
},
{
key: 'cyan',
color: '#13C2C2',
},
{
key: 'green',
color: '#52C41A',
},
{
key: 'daybreak',
color: '#1890ff',
},
{
key: 'geekblue',
color: '#2F54EB',
},
{
key: 'purple',
color: '#722ED1',
},
];
const DEFAULT_VARS = {
'primary-color': { label: '主颜色', type: 'color', default: '#1890ff' },
'alain-default-header-hg': {
label: '高',
type: 'px',
default: '64px',
max: 300,
min: 24,
},
'alain-default-header-bg': {
label: '背景色',
type: 'color',
default: '@primary-color',
tip: '默认同主色系',
},
'alain-default-header-padding': {
label: '顶部左右内边距',
type: 'px',
default: '16px',
},
// 侧边栏
'alain-default-aside-wd': { label: '宽度', type: 'px', default: '200px' },
'alain-default-aside-bg': {
label: '背景',
type: 'color',
default: '#ffffff',
},
'alain-default-aside-collapsed-wd': {
label: '收缩宽度',
type: 'px',
default: '64px',
},
'alain-default-aside-nav-padding-top-bottom': {
label: '项上下内边距',
type: 'px',
default: '8px',
step: 8,
},
// 主菜单
'alain-default-aside-nav-fs': {
label: '菜单字号',
type: 'px',
default: '14px',
min: 14,
max: 30,
},
'alain-default-aside-collapsed-nav-fs': {
label: '收缩菜单字号',
type: 'px',
default: '24px',
min: 24,
max: 32,
},
'alain-default-aside-nav-item-height': {
label: '菜单项高度',
type: 'px',
default: '38px',
min: 24,
max: 64,
},
'alain-default-aside-nav-text-color': {
label: '菜单文本颜色',
type: 'color',
default: 'rgba(0, 0, 0, 0.65)',
rgba: true,
},
'alain-default-aside-nav-text-hover-color': {
label: '菜单文本悬停颜色',
type: 'color',
default: '@primary-color',
tip: '默认同主色系',
},
'alain-default-aside-nav-group-text-color': {
label: '菜单分组文本颜色',
type: 'color',
default: 'rgba(0, 0, 0, 0.43)',
rgba: true,
},
'alain-default-aside-nav-selected-text-color': {
label: '菜单激活时文本颜色',
type: 'color',
default: '@primary-color',
tip: '默认同主色系',
},
'alain-default-aside-nav-selected-bg': {
label: '菜单激活时背景颜色',
type: 'color',
default: '#fcfcfc',
},
// 内容
'alain-default-content-bg': {
label: '背景色',
type: 'color',
default: '#f5f7fa',
},
'alain-default-content-heading-bg': {
label: '标题背景色',
type: 'color',
default: '#fafbfc',
},
'alain-default-content-heading-border': {
label: '标题底部边框色',
type: 'color',
default: '#efe3e5',
},
'alain-default-content-padding': {
label: '内边距',
type: 'px',
default: '24px',
min: 0,
max: 128,
step: 8,
},
// zorro组件修正
'form-state-visual-feedback-enabled': {
label: '开启表单元素的视觉反馈',
type: 'switch',
default: true,
},
'preserve-white-spaces-enabled': {
label: '开启 preserveWhitespaces',
type: 'switch',
default: true,
},
'nz-table-img-radius': {
label: '表格中:图片圆角',
type: 'px',
default: '4px',
min: 0,
max: 128,
},
'nz-table-img-margin-right': {
label: '表格中:图片右外边距',
type: 'px',
default: '4px',
min: 0,
max: 128,
},
'nz-table-img-max-width': {
label: '表格中:图片最大宽度',
type: 'px',
default: '32px',
min: 8,
max: 128,
},
'nz-table-img-max-height': {
label: '表格中:图片最大高度',
type: 'px',
default: '32px',
min: 8,
max: 128,
},
};
@Component({
// tslint:disable-next-line:component-selector
selector: 'setting-drawer',
templateUrl: './setting-drawer.component.html',
host: {
'[class.setting-drawer]': 'true',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SettingDrawerComponent {
private loadedLess = false;
collapse = false;
get layout() {
return this.settingSrv.layout;
}
data: any = {};
color: string;
colors = DEFAULT_COLORS;
constructor(
private cdr: ChangeDetectorRef,
private msg: NzMessageService,
private settingSrv: SettingsService,
private lazy: LazyService,
private zone: NgZone,
@Inject(DOCUMENT) private doc: any,
) {
this.color = this.cachedData['@primary-color'] || this.DEFAULT_PRIMARY;
this.resetData(this.cachedData, false);
}
private get cachedData() {
return this.settingSrv.layout[ALAINDEFAULTVAR] || {};
}
private get DEFAULT_PRIMARY() {
return DEFAULT_VARS['primary-color'].default;
}
private loadLess(): Promise<void> {
if (this.loadedLess) return Promise.resolve();
return this.lazy
.loadStyle('./assets/alain-default.less', 'stylesheet/less')
.then(() => {
const lessConfigNode = this.doc.createElement('script');
lessConfigNode.innerHTML = `
window.less = {
async: true,
env: 'production',
javascriptEnabled: true
};
`;
this.doc.body.appendChild(lessConfigNode);
})
.then(() => this.lazy.loadScript('https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'))
.then(() => {
this.loadedLess = true;
});
}
private genVars() {
const { data, color, validKeys } = this;
const vars: any = {
[`@primary-color`]: color,
};
validKeys.filter(key => key !== 'primary-color').forEach(key => (vars[`@${key}`] = data[key].value));
this.setLayout(ALAINDEFAULTVAR, vars);
return vars;
}
private runLess() {
const { zone, msg, cdr } = this;
const msgId = msg.loading(`正在编译主题!`, { nzDuration: 0 }).messageId;
setTimeout(() => {
zone.runOutsideAngular(() => {
this.loadLess().then(() => {
(window as any).less.modifyVars(this.genVars()).then(() => {
msg.success('成功');
msg.remove(msgId);
zone.run(() => cdr.detectChanges());
});
});
});
}, 200);
}
toggle() {
this.collapse = !this.collapse;
}
changeColor(color: string) {
this.color = color;
Object.keys(DEFAULT_VARS)
.filter(key => DEFAULT_VARS[key].default === '@primary-color')
.forEach(key => delete this.cachedData[`@${key}`]);
this.resetData(this.cachedData, false);
}
setLayout(name: string, value: any) {
this.settingSrv.setLayout(name, value);
}
private resetData(nowData?: Object, run = true) {
nowData = nowData || {};
const data = deepCopy(DEFAULT_VARS);
Object.keys(data).forEach(key => {
const value = nowData![`@${key}`] || data[key].default || '';
data[key].value = value === `@primary-color` ? this.color : value;
});
this.data = data;
if (run) {
this.cdr.detectChanges();
this.runLess();
}
}
private get validKeys(): string[] {
return Object.keys(this.data).filter(key => this.data[key].value !== this.data[key].default);
}
apply() {
this.runLess();
}
reset() {
this.color = this.DEFAULT_PRIMARY;
this.settingSrv.setLayout(ALAINDEFAULTVAR, {});
this.resetData({});
}
copyVar() {
const vars = this.genVars();
const copyContent = Object.keys(vars)
.map(key => `${key}: ${vars[key]};`)
.join('\n');
copy(copyContent);
this.msg.success('Copy success');
}
}
<div class="alain-default__aside-inner">
<nz-dropdown nzTrigger="click" class="alain-default__aside-user">
<div nz-dropdown class="user-block-dropdown">
<nz-avatar class="alain-default__aside-user-avatar" [nzSrc]="settings.user.avatar"></nz-avatar>
<div class="alain-default__aside-user-info">
<strong>{{settings.user.name}}</strong>
<p class="text-truncate mb0">{{settings.user.email}}</p>
</div>
</div>
<ul nz-menu>
<li nz-menu-item routerLink="/pro/account/center">{{ 'menu.account.center' | translate }}</li>
<li nz-menu-item routerLink="/pro/account/settings">{{ 'menu.account.settings' | translate }}</li>
</ul>
</nz-dropdown>
<sidebar-nav class="d-block py-lg"></sidebar-nav>
</div>
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { SettingsService } from '@delon/theme';
@Component({
selector: 'layout-sidebar',
templateUrl: './sidebar.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SidebarComponent {
constructor(public settings: SettingsService) {}
}
import { Component } from '@angular/core';
@Component({
selector: 'layout-fullscreen',
templateUrl: './fullscreen.component.html',
host: {
'[class.alain-fullscreen]': 'true',
},
})
export class LayoutFullScreenComponent {}
import { NgModule } from "@angular/core";
import { ShareModule } from "../share";
import { DefaultComponent } from './default/default.component';
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared';
import { LayoutDefaultComponent } from './default/default.component';
import { LayoutFullScreenComponent } from './fullscreen/fullscreen.component';
import { HeaderComponent } from './default/header/header.component';
import { SidebarComponent } from './default/sidebar/sidebar.component';
import { HeaderSearchComponent } from './default/header/components/search.component';
import { HeaderNotifyComponent } from './default/header/components/notify.component';
import { HeaderTaskComponent } from './default/header/components/task.component';
import { HeaderIconComponent } from './default/header/components/icon.component';
import { HeaderFullScreenComponent } from './default/header/components/fullscreen.component';
import { HeaderI18nComponent } from './default/header/components/i18n.component';
import { HeaderStorageComponent } from './default/header/components/storage.component';
import { HeaderUserComponent } from './default/header/components/user.component';
import { SettingDrawerComponent } from './default/setting-drawer/setting-drawer.component';
import { SettingDrawerItemComponent } from './default/setting-drawer/setting-drawer-item.component';
const SETTINGDRAWER = [SettingDrawerComponent, SettingDrawerItemComponent];
const COMPONENTS = [
LayoutDefaultComponent,
LayoutFullScreenComponent,
HeaderComponent,
SidebarComponent,
...SETTINGDRAWER,
];
const HEADERCOMPONENTS = [
HeaderSearchComponent,
HeaderNotifyComponent,
HeaderTaskComponent,
HeaderIconComponent,
HeaderFullScreenComponent,
HeaderI18nComponent,
HeaderStorageComponent,
HeaderUserComponent,
];
// passport
import { LayoutPassportComponent } from './passport/passport.component';
const PASSPORT = [LayoutPassportComponent];
@NgModule({
declarations: [DefaultComponent],
imports: [ShareModule],
exports: [DefaultComponent]
imports: [SharedModule],
entryComponents: SETTINGDRAWER,
declarations: [...COMPONENTS, ...HEADERCOMPONENTS, ...PASSPORT],
exports: [...COMPONENTS, ...PASSPORT],
})
export class LayoutModule {}
<div class="container">
<header-i18n showLangText="false" class="langs"></header-i18n>
<div class="wrap">
<div class="top">
<div class="head">
<img class="logo" src="./assets/logo-color.svg">
<span class="title">ng-alain</span>
</div>
<div class="desc">武林中最有影响力的《葵花宝典》;欲练神功,挥刀自宫</div>
</div>
<router-outlet></router-outlet>
<global-footer [links]="links">
Copyright
<i class="anticon anticon-copyright"></i> 2017
<a href="//github.com/cipchk" target="_blank">卡色</a>出品
</global-footer>
</div>
</div>
@import '~@delon/theme/styles/default';
:host {
::ng-deep {
.container {
display: flex;
flex-direction: column;
min-height: 100%;
background: #f0f2f5;
}
.langs {
text-align: right;
width: 100%;
height: 40px;
line-height: 44px;
.anticon {
margin-top: 24px;
margin-right: 24px;
font-size: 14px;
vertical-align: top;
cursor: pointer;
}
}
.wrap {
padding: 32px 0;
flex: 1;
}
.ant-form-item {
margin-bottom: 24px;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.wrap {
padding: 32px 0 24px;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 44px;
margin-right: 16px;
}
.title {
font-size: 33px;
color: @heading-color;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
vertical-align: middle;
}
.desc {
font-size: @font-size-base;
color: @text-color-secondary;
margin-top: 12px;
margin-bottom: 40px;
}
}
}
import { Component } from '@angular/core';
@Component({
selector: 'layout-passport',
templateUrl: './passport.component.html',
styleUrls: ['./passport.component.less'],
})
export class LayoutPassportComponent {
links = [
{
title: '帮助',
href: '',
},
{
title: '隐私',
href: '',
},
{
title: '条款',
href: '',
},
];
}
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { SocialService } from '@delon/auth';
import { SettingsService } from '@delon/theme';
@Component({
selector: 'app-callback',
template: ``,
providers: [SocialService],
})
export class CallbackComponent implements OnInit {
type: string;
constructor(
private socialService: SocialService,
private settingsSrv: SettingsService,
private route: ActivatedRoute,
) {}
ngOnInit(): void {
this.type = this.route.snapshot.params['type'];
this.mockModel();
}
private mockModel() {
const info = {
token: '123456789',
name: 'cipchk',
email: `${this.type}@${this.type}.com`,
id: 10000,
time: +new Date(),
};
this.settingsSrv.setUser({
...this.settingsSrv.user,
...info,
});
this.socialService.callback(info);
}
}
<div nz-row [nzGutter]="24" class="pt-lg">
<div nz-col nzXs="24" nzSm="12" nzMd="12" nzLg="6">
<g2-card [title]="'app.analysis.total-sales' | translate" total="¥ 126,560" contentHeight="44px" [action]="action1"
[footer]="footer1">
<ng-template #action1>
<nz-tooltip [nzTitle]="'app.analysis.introduce' | translate">
<i nz-tooltip nz-icon type="info-circle"></i>
</nz-tooltip>
</ng-template>
<trend flag="up" style="display:block; margin-top:2px;">
{{'app.analysis.week' | translate}}
<span class="pl-sm">12%</span>
</trend>
<trend flag="down">
{{'app.analysis.day' | translate}}
<span class="pl-sm">11%</span>
</trend>
<ng-template #footer1>
<p class="text-truncate mb0">
{{'app.analysis.day-sales' | translate}}
<span class="ml-sm">¥12,423</span>
</p>
</ng-template>
</g2-card>
</div>
<div nz-col nzXs="24" nzSm="12" nzMd="12" nzLg="6">
<g2-card [title]="'app.analysis.visits' | translate" total="8,848" contentHeight="46px" [action]="action2" [footer]="footer2">
<ng-template #action2>
<nz-tooltip [nzTitle]="'app.analysis.introduce' | translate">
<i nz-tooltip nz-icon type="info-circle"></i>
</nz-tooltip>
</ng-template>
<g2-mini-area *ngIf="data.visitData" color="#975FE4" height="46" [data]="data.visitData"></g2-mini-area>
<ng-template #footer2>
<p class="text-truncate mb0">
{{'app.analysis.day-visits' | translate}}
<span class="ml-sm">1,234</span>
</p>
</ng-template>
</g2-card>
</div>
<div nz-col nzXs="24" nzSm="12" nzMd="12" nzLg="6">
<g2-card [title]="'app.analysis.payments' | translate" total="6,560" contentHeight="46px" [action]="action3"
[footer]="footer3">
<ng-template #action3>
<nz-tooltip [nzTitle]="'app.analysis.introduce' | translate">
<i nz-tooltip nz-icon type="info-circle"></i>
</nz-tooltip>
</ng-template>
<g2-mini-bar *ngIf="data.visitData" height="46" [data]="data.visitData"></g2-mini-bar>
<ng-template #footer3>
<p class="text-truncate mb0">
{{'app.analysis.conversion-rate' | translate}}
<span class="ml-sm">60%</span>
</p>
</ng-template>
</g2-card>
</div>
<div nz-col nzXs="24" nzSm="12" nzMd="12" nzLg="6">
<g2-card [title]="'app.analysis.operational-effect' | translate" total="78%" contentHeight="46px" [action]="action4"
[footer]="footer4">
<ng-template #action4>
<nz-tooltip [nzTitle]="'app.analysis.introduce' | translate">
<i nz-tooltip nz-icon type="info-circle"></i>
</nz-tooltip>
</ng-template>
<g2-mini-progress height="46" percent="78" strokeWidth="8" target="80" color="#13C2C2"></g2-mini-progress>
<ng-template #footer4>
<div class="d-flex justify-content-between text-truncate">
<trend flag="up">
{{'app.analysis.week' | translate}}
<span class="pl-sm">12%</span>
</trend>
<trend flag="down">
{{'app.analysis.day' | translate}}
<span class="pl-sm">11%</span>
</trend>
</div>
</ng-template>
</g2-card>
</div>
</div>
<nz-card [nzLoading]="loading" [nzBordered]="false" class="ant-card__body-nopadding sales-card">
<nz-tabset [nzTabBarExtraContent]="extraTemplate" *ngIf="data.salesData" (nzSelectedIndexChange)="salesChange($event)">
<nz-tab *ngFor="let tab of saleTabs" [nzTitle]="('app.analysis.' + tab.key) | translate">
<div nz-row>
<div nz-col nzXs="24" nzSm="24" nzMd="12" nzLg="16">
<div class="bar">
<g2-bar *ngIf="tab.show" height="295" style="width: 100%" [title]="('app.analysis.' + tab.key + '-trend') | translate" [data]="data.salesData"></g2-bar>
</div>
</div>
<div nz-col nzXs="24" nzSm="24" nzMd="12" nzLg="8">
<div class="rank">
<h4 class="ranking-title">{{ ('app.analysis.' + tab.key + '-ranking') | translate}}</h4>
<ul class="rank-list">
<li *ngFor="let i of rankingListData; let idx = index">
<span class="number" [ngClass]="{'active': idx < 3}">{{idx+1}}</span>
<span class="title">{{i.title}}</span>
<span class="value">{{i.total | number: '3.0'}}</span>
</li>
</ul>
</div>
</div>
</div>
</nz-tab>
<ng-template #extraTemplate>
<div class="sales-extra-wrap">
<div class="sales-extra">
<a (click)="setDate('today')">{{'app.analysis.all-day' | translate}}</a>
<a (click)="setDate('week')">{{'app.analysis.all-week' | translate}}</a>
<a (click)="setDate('month')">{{'app.analysis.all-month' | translate}}</a>
<a (click)="setDate('year')">{{'app.analysis.all-year' | translate}}</a>
</div>
<nz-range-picker [(ngModel)]="date_range" style="display:inline-block; width: 256px;"></nz-range-picker>
</div>
</ng-template>
</nz-tabset>
</nz-card>
<div nz-row [nzGutter]="24">
<div nz-col nzXs="24" nzSm="24" nzMd="24" nzLg="12">
<nz-card [nzLoading]="loading" [nzBordered]="false" [nzTitle]="'app.analysis.online-top-search' | translate" [nzExtra]="extraOp" class="mb-0">
<ng-template #extraOp>
<nz-dropdown class="icon-group">
<i nz-dropdown nz-icon type="ellipsis"></i>
<ul nz-menu>
<li nz-menu-item>操作一</li>
<li nz-menu-item>操作二</li>
</ul>
</nz-dropdown>
</ng-template>
<div nz-row [nzGutter]="64">
<div nz-col nzXs="24" nzSm="12" class="mb-md">
<number-info total="12,321" subTotal="17.1" status="up" [subTitle]="subTitle">
<ng-template #subTitle>
{{'app.analysis.search-users' | translate}}
<nz-tooltip [nzTitle]="'app.analysis.introduce' | translate">
<i nz-tooltip nz-icon type="info-circle" class="ml-sm"></i>
</nz-tooltip>
</ng-template>
</number-info>
<g2-mini-area *ngIf="data.visitData2" [line]="true" height="45" [data]="data.visitData2"></g2-mini-area>
</div>
<div nz-col nzXs="24" nzSm="12" class="mb-md">
<number-info [subTitle]="'app.analysis.per-capita-search' | translate" total="2.7" subTotal="26.2" status="down"></number-info>
<g2-mini-area *ngIf="data.visitData2" [line]="true" height="45" [data]="data.visitData2"></g2-mini-area>
</div>
</div>
<st [data]="data.searchData" [columns]="searchColumn" size="small" ps="5" [page]="{toTopInChange:false}">
<ng-template st-row="range" let-i>
<trend [flag]="i.status === 1 ? 'down' : 'up'">
<span>{{i.range}}%</span>
</trend>
</ng-template>
</st>
</nz-card>
</div>
<div nz-col nzXs="24" nzSm="24" nzMd="24" nzLg="12">
<nz-card [nzLoading]="loading" [nzBordered]="false" [nzTitle]="'app.analysis.the-proportion-of-sales' | translate" [nzBodyStyle]="{'padding.px': 24}" [nzExtra]="extra"
class="sales-card mb-0" style="min-height: 502.5px">
<ng-template #extra>
<div class="sales-card-extra">
<nz-dropdown class="icon-group" nzPlacement="bottomLeft">
<i nz-dropdown nz-icon type="ellipsis"></i>
<ul nz-menu>
<li nz-menu-item>操作一</li>
<li nz-menu-item>操作二</li>
</ul>
</nz-dropdown>
<div class="sales-type-radio">
<nz-radio-group [(ngModel)]="salesType" (ngModelChange)="changeSaleType()">
<label nz-radio-button [nzValue]="'all'">
{{ 'app.analysis.channel.all' | translate }}
</label>
<label nz-radio-button [nzValue]="'online'">
{{ 'app.analysis.channel.online' | translate }}
</label>
<label nz-radio-button [nzValue]="'offline'">
{{ 'app.analysis.channel.stores' | translate }}
</label>
</nz-radio-group>
</div>
</div>
</ng-template>
<h4 class="margin:8px 0 32px 0;">{{'app.analysis.sales' | translate}}</h4>
<g2-pie *ngIf="salesPieData" [data]="salesPieData" [hasLegend]="true" [subTitle]="'app.analysis.sales' | translate" [height]="248" [lineWidth]="4" [total]="salesTotal"
[valueFormat]="handlePieValueFormat">
</g2-pie>
</nz-card>
</div>
</div>
<nz-card [nzLoading]="loading" [nzBordered]="false" [nzBodyStyle]="{'padding': '0 0 32px'}" class="offline-card mt-lg">
<nz-tabset *ngIf="data.offlineData" [(nzSelectedIndex)]="offlineIdx" (nzSelectedIndexChange)="offlineChange($event)">
<nz-tab *ngFor="let tab of data.offlineData; let i = index;" [nzTitle]="nzTabHeading">
<ng-template #nzTabHeading>
<div nz-row [nzGutter]="8" style="width: 138px; margin: 8px 0;">
<div nz-col [nzSpan]="12">
<number-info [title]="tab.name" [subTitle]="'app.analysis.conversion-rate' | translate" gap="2" [total]="(tab.cvr * 100) + '%'" [theme]="i !== offlineIdx && 'light'"></number-info>
</div>
<div nz-col [nzSpan]="12" style="padding-top: 36px;">
<g2-pie [animate]="false" [color]="i !== offlineIdx && '#BDE4FF'"
[inner]="0.55" [tooltip]="false"
[padding]="[0, 0, 0, 0]" [percent]="tab.cvr * 100" [height]="64">
</g2-pie>
</div>
</div>
</ng-template>
<div class="px-lg">
<g2-timeline *ngIf="tab.show" [data]="tab.chart" [titleMap]="titleMap"></g2-timeline>
</div>
</nz-tab>
</nz-tabset>
</nz-card>
@import '~@delon/theme/styles/default';
:host ::ng-deep {
.icon-group {
i {
transition: color 0.32s;
color: @text-color-secondary;
cursor: pointer;
margin-left: 16px;
&:hover {
color: @text-color;
}
}
}
.rank-list {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
.clearfix();
margin-top: 16px;
display: flex;
align-items: center;
span {
color: @text-color;
font-size: 14px;
line-height: 22px;
}
.number {
background-color: @background-color-base;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 16px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
margin-top: 1.5px;
&.active {
background-color: #314659;
color: #fff;
}
}
.title {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 8px;
}
}
}
.sales-extra {
display: inline-block;
margin-right: 24px;
a {
color: @text-color;
margin-left: 24px;
&:hover {
color: @primary-color;
}
&.currentDate {
color: @primary-color;
}
}
}
.sales-card {
.bar {
padding: 0 0 32px 32px;
}
.rank {
padding: 0 32px 32px 72px;
}
.ant-tabs-bar {
padding-left: 16px;
.ant-tabs-nav .ant-tabs-tab {
padding-top: 16px;
padding-bottom: 14px;
line-height: 24px;
}
}
.ant-tabs-extra-content {
padding-right: 24px;
line-height: 55px;
}
.ant-card-head {
position: relative;
}
.ant-card-head-title {
align-items: normal;
}
}
.sales-card-extra {
height: inherit;
}
.sales-type-radio {
position: absolute;
right: 54px;
bottom: 12px;
}
.offline-card {
.ant-tabs-ink-bar {
bottom: auto;
}
.ant-tabs-bar {
border-bottom: none;
}
.ant-tabs-nav-container-scrolling {
padding-left: 40px;
padding-right: 40px;
}
.ant-tabs-tab-prev-icon:before {
position: relative;
left: 6px;
}
.ant-tabs-tab-next-icon:before {
position: relative;
right: 6px;
}
.ant-tabs-tab-active h4 {
color: @primary-color;
}
}
.trend-text {
margin-left: 8px;
color: @heading-color;
}
@media screen and (max-width: @screen-lg) {
.sales-extra {
display: none;
}
.rank-list {
li {
span:first-child {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-md) {
.rank-title {
margin-top: 16px;
}
.sales-card .bar {
padding: 16px;
}
}
@media screen and (max-width: @screen-sm) {
.sales-extra-wrap {
display: none;
}
.sales-card {
.ant-tabs-content {
padding-top: 30px;
}
}
}
// fix pagination bottom
.ant-table-pagination {
margin-bottom: 0;
}
.g2-pie__legend-block .g2-pie__chart {
margin: 0;
}
}
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment