angular前端初始化

This commit is contained in:
iioter 2022-04-29 16:37:05 +08:00
parent 2c11c50c84
commit c753aba123
129 changed files with 57529 additions and 0 deletions

View File

@ -0,0 +1,16 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR

View File

@ -0,0 +1,10 @@
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -0,0 +1,34 @@
_cli-tpl/
dist/
coverage/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependency directories
node_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.cache/
# yarn v2
.yarn

View File

@ -0,0 +1,126 @@
const prettierConfig = require('./.prettierrc.js');
module.exports = {
root: true,
parserOptions: { ecmaVersion: 2021 },
overrides: [
{
files: ['*.ts'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['tsconfig.json'],
createDefaultProgram: true
},
plugins: ['@typescript-eslint', 'jsdoc', 'import'],
extends: [
'plugin:@angular-eslint/recommended',
'plugin:@angular-eslint/template/process-inline-templates',
'plugin:prettier/recommended'
],
rules: {
'prettier/prettier': ['error', prettierConfig],
'jsdoc/newline-after-description': 1,
'@angular-eslint/component-class-suffix': [
'error',
{
suffixes: ['Directive', 'Component', 'Base', 'Widget']
}
],
'@angular-eslint/directive-class-suffix': [
'error',
{
suffixes: ['Directive', 'Component', 'Base', 'Widget']
}
],
'@angular-eslint/component-selector': [
'off',
{
type: ['element', 'attribute'],
prefix: ['app', 'test'],
style: 'kebab-case'
}
],
'@angular-eslint/directive-selector': [
'off',
{
type: 'attribute',
prefix: ['app']
}
],
'@angular-eslint/no-attribute-decorator': 'error',
'@angular-eslint/no-conflicting-lifecycle': 'off',
'@angular-eslint/no-forward-ref': 'off',
'@angular-eslint/no-host-metadata-property': 'off',
'@angular-eslint/no-lifecycle-call': 'off',
'@angular-eslint/no-pipe-impure': 'error',
'@angular-eslint/prefer-output-readonly': 'error',
'@angular-eslint/use-component-selector': 'off',
'@angular-eslint/use-component-view-encapsulation': 'off',
'@angular-eslint/no-input-rename': 'off',
'@angular-eslint/no-output-native': 'off',
'@typescript-eslint/array-type': [
'error',
{
default: 'array-simple'
}
],
'@typescript-eslint/ban-types': [
'off',
{
types: {
String: {
message: 'Use string instead.'
},
Number: {
message: 'Use number instead.'
},
Boolean: {
message: 'Use boolean instead.'
},
Function: {
message: 'Use specific callable interface instead.'
}
}
}
],
'import/no-duplicates': 'error',
'import/no-unused-modules': 'error',
'import/no-unassigned-import': 'error',
'import/order': [
'error',
{
alphabetize: { order: 'asc', caseInsensitive: false },
'newlines-between': 'always',
groups: ['external', 'internal', ['parent', 'sibling', 'index']],
pathGroups: [],
pathGroupsExcludedImportTypes: []
}
],
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/member-ordering': 'off',
'no-irregular-whitespace': 'error',
'no-multiple-empty-lines': 'error',
'no-sparse-arrays': 'error',
'prefer-object-spread': 'error',
'prefer-template': 'error',
'prefer-const': 'off',
'max-len': 'off'
}
},
{
files: ['*.html'],
extends: ['plugin:@angular-eslint/template/recommended'],
rules: {}
},
{
files: ['*.html'],
excludedFiles: ['*inline-template-*.component.html'],
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': ['error', { parser: 'angular' }],
'@angular-eslint/template/eqeqeq': 'off'
}
}
]
};

42
IoTGateway/ClientApp/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
export NODE_OPTIONS="--max-old-space-size=4096"
npx --no-install tsc -p tsconfig.app.json --noEmit
npx --no-install lint-staged

View File

@ -0,0 +1 @@
12.14.1

View File

@ -0,0 +1,18 @@
# add files you wish to ignore here
**/*.md
**/*.svg
**/test.ts
.stylelintrc
.prettierrc
src/assets/*
src/index.html
node_modules/
.vscode/
coverage/
dist/
package.json
tslint.json
_cli-tpl/**/*

View File

@ -0,0 +1,13 @@
module.exports = {
singleQuote: true,
useTabs: false,
printWidth: 140,
tabWidth: 2,
semi: true,
htmlWhitespaceSensitivity: 'strict',
arrowParens: 'avoid',
bracketSpacing: true,
proseWrap: 'preserve',
trailingComma: 'none',
endOfLine: 'lf'
};

View File

@ -0,0 +1,38 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-rational-order",
"stylelint-config-prettier"
],
"customSyntax": "postcss-less",
"plugins": [
"stylelint-order",
"stylelint-declaration-block-no-ignored-properties"
],
"rules": {
"function-no-unknown": null,
"no-descending-specificity": null,
"plugin/declaration-block-no-ignored-properties": true,
"selector-type-no-unknown": [
true,
{
"ignoreTypes": [
"/^g2-/",
"/^nz-/",
"/^app-/"
]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": [
"ng-deep"
]
}
]
},
"ignoreFiles": [
"src/assets/**/*"
]
}

View File

@ -0,0 +1,5 @@
{
"recommendations": [
"cipchk.ng-alain-extension-pack"
]
}

View File

@ -0,0 +1,16 @@
{
// 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
}
]
}

View File

@ -0,0 +1,37 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
// For ESLint
"source.fixAll.eslint": true,
// For Stylelint
"source.fixAll.stylelint": true
},
"[markdown]": {
"editor.formatOnSave": false
},
"[javascript]": {
"editor.formatOnSave": false
},
"[json]": {
"editor.formatOnSave": false
},
"[jsonc]": {
"editor.formatOnSave": false
},
"files.watcherExclude": {
"**/.git/*/**": true,
"**/node_modules/*/**": true,
"**/dist/*/**": true,
"**/coverage/*/**": true
},
"files.associations": {
"*.json": "jsonc",
".prettierrc": "jsonc",
".stylelintrc": "jsonc"
},
// Angular schematics : https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics
"ngschematics.schematics": [
"ng-alain"
]
}

42
IoTGateway/ClientApp/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -0,0 +1,27 @@
# STEP 1: Build
FROM node:10 as builder
LABEL authors="iotgateway"
COPY package.json package-lock.json ./
RUN npm set progress=false && npm config set depth 0 && npm cache clean --force
RUN npm i && mkdir /iotgateway && cp -R ./node_modules ./iotgateway
WORKDIR /iotgateway
COPY . .
RUN npm run build
# STEP 2: Setup
FROM nginx:alpine
COPY --from=builder /iotgateway/_nginx/default.conf /etc/nginx/conf.d/default.conf
# COPY --from=builder /iotgateway/_nginx/ssl/* /etc/nginx/ssl/
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /iotgateway/dist /usr/share/nginx/html
CMD [ "nginx", "-g", "daemon off;"]

View File

@ -0,0 +1 @@
[Document](https://ng-alain.com/mock)

View File

@ -0,0 +1,122 @@
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): { total: number; list: any[] } {
let ret = [...list];
const pi = +params.pi;
const ps = +params.ps;
const 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): { msg: string } {
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: 'IoTGateway',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: '535915157@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.password !== '000000') {
return { msg: `Invalid username or passwordadmin/000000` };
}
return {
msg: 'ok',
user: {
token: '123456789',
name: data.userName,
email: `535915157@qq.com`,
id: 10000,
time: +new Date()
}
};
},
'POST /register': {
msg: 'ok'
}
};

View File

@ -0,0 +1 @@
export * from './_user';

View File

@ -0,0 +1,27 @@
server {
listen 80;
# listen 443;
# ssl on;
# ssl_certificate /etc/nginx/ssl/server.crt;
# ssl_certificate_key /etc/nginx/ssl/server.key;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -0,0 +1,171 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "yarn"
},
"newProjectRoot": "projects",
"projects": {
"iotgateway": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"skipTests": false,
"flat": false,
"inlineStyle": true,
"inlineTemplate": false,
"style": "less"
},
"@schematics/angular:application": {
"strict": true
},
"ng-alain:module": {
"routing": true,
"skipTests": false
},
"ng-alain:list": {
"skipTests": false
},
"ng-alain:edit": {
"skipTests": false,
"modal": true
},
"ng-alain:view": {
"skipTests": false,
"modal": true
},
"ng-alain:curd": {
"skipTests": false
},
"@schematics/angular:module": {
"routing": true,
"skipTests": false
},
"@schematics/angular:directive": {
"skipTests": false
},
"@schematics/angular:service": {
"skipTests": false
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/iotgateway",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.less"
],
"scripts": [
"node_modules/qrious/dist/qrious.min.js"],
"allowedCommonJsDependencies": [
"@antv/g2",
"ajv",
"ajv-formats",
"date-fns",
"file-saver"
],
"stylePreprocessorOptions": {
"includePaths": [
"node_modules/"
]
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "3mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "iotgateway:build:production"
},
"development": {
"browserTarget": "iotgateway:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"proxyConfig": "proxy.conf.js"
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "iotgateway:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "less",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.less"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"defaultProject": "iotgateway"
}

View File

@ -0,0 +1,10 @@
version: '2.1'
services:
iotgateway:
image: iotgateway
build: .
environment:
NODE_ENV: production
ports:
- 80:80

View File

@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/iotgateway'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@ -0,0 +1,13 @@
{
"$schema": "./node_modules/ng-alain/schema.json",
"theme": {
"list": [
{
"theme": "dark"
},
{
"theme": "compact"
}
]
}
}

36597
IoTGateway/ClientApp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,103 @@
{
"name": "iotgateway",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng s -o",
"build": "npm run ng-high-memory build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"ng-high-memory": "node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng",
"hmr": "ng s -o --hmr",
"analyze": "npm run ng-high-memory build -- --source-map",
"analyze:view": "source-map-explorer dist/**/*.js",
"test-coverage": "ng test --code-coverage --watch=false",
"color-less": "ng-alain-plugin-theme -t=colorLess",
"theme": "ng-alain-plugin-theme -t=themeCss",
"icon": "ng g ng-alain:plugin icon",
"prepare": "husky install",
"lint": "npm run lint:ts && npm run lint:style",
"lint:ts": "ng lint --fix",
"lint:style": "npx stylelint \"src/**/*.less\" --fix"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.3.0",
"@angular/common": "~13.3.0",
"@angular/compiler": "~13.3.0",
"@angular/core": "~13.3.0",
"@angular/forms": "~13.3.0",
"@angular/platform-browser": "~13.3.0",
"@angular/platform-browser-dynamic": "~13.3.0",
"@angular/router": "~13.3.0",
"@delon/abc": "^13.4.2",
"@delon/acl": "^13.4.2",
"@delon/auth": "^13.4.2",
"@delon/cache": "^13.4.2",
"@delon/chart": "^13.4.2",
"@delon/form": "^13.4.2",
"@delon/mock": "^13.4.2",
"@delon/theme": "^13.4.2",
"@delon/util": "^13.4.2",
"ajv": "^8.10.0",
"ajv-formats": "^2.1.1",
"monaco-editor": "^0.33.0",
"ng-alain": "13.4.2",
"ng-zorro-antd": "^13.1.1",
"ngx-tinymce": "^13.0.0",
"qrious": "^4.0.2",
"rxjs": "~7.5.0",
"screenfull": "^6.0.1",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.3.3",
"@angular-eslint/builder": "~13.1.0",
"@angular-eslint/eslint-plugin": "~13.1.0",
"@angular-eslint/eslint-plugin-template": "~13.1.0",
"@angular-eslint/schematics": "~13.1.0",
"@angular-eslint/template-parser": "~13.1.0",
"@angular/cli": "~13.3.3",
"@angular/compiler-cli": "~13.3.0",
"@angular/language-service": "~13.3.0",
"@delon/testing": "^13.4.2",
"@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "~5.15.0",
"@typescript-eslint/parser": "~5.15.0",
"eslint": "^8.11.0",
"eslint-config-prettier": "^2.6.0",
"eslint-plugin-import": "~2.25.4",
"eslint-plugin-jsdoc": "~38.0.4",
"eslint-plugin-prefer-arrow": "~1.2.3",
"eslint-plugin-prettier": "^2.6.0",
"husky": "^7.0.4",
"jasmine-core": "~4.0.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"lint-staged": "^12.3.7",
"ng-alain": "^13.4.2",
"ng-alain-plugin-theme": "^13.0.3",
"prettier": "^2.6.0",
"source-map-explorer": "^2.5.2",
"stylelint": "^14.6.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^25.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.5.0",
"stylelint-order": "^5.0.0",
"typescript": "~4.6.2"
},
"lint-staged": {
"(src)/**/*.{html,ts}": [
"eslint --fix"
],
"(src)/**/*.less": [
"npm run lint:style"
]
}
}

View File

@ -0,0 +1,17 @@
/**
* For more configuration, please refer to https://angular.io/guide/build#proxying-to-a-backend-server
*
* 更多配置描述请参考 https://angular.cn/guide/build#proxying-to-a-backend-server
*
* Note: The proxy is only valid for real requests, Mock does not actually generate requests, so the priority of Mock will be higher than the proxy
*/
module.exports = {
/**
* The following means that all requests are directed to the backend `https://localhost:9000/`
*/
// '/': {
// target: 'https://localhost:9000/',
// secure: false, // Ignore invalid SSL certificates
// changeOrigin: true
// }
};

View File

@ -0,0 +1,46 @@
import { Component, ElementRef, OnInit, Renderer2 } from '@angular/core';
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router } from '@angular/router';
import { TitleService, VERSION as VERSION_ALAIN } from '@delon/theme';
import { environment } from '@env/environment';
import { NzModalService } from 'ng-zorro-antd/modal';
import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version';
@Component({
selector: 'app-root',
template: ` <router-outlet></router-outlet> `
})
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(): void {
let configLoad = false;
this.router.events.subscribe(ev => {
if (ev instanceof RouteConfigLoadStart) {
configLoad = true;
}
if (configLoad && ev instanceof NavigationError) {
this.modalSrv.confirm({
nzTitle: `提醒`,
nzContent: environment.production ? `应用可能已发布新版本,请点击刷新才能生效。` : `无法加载路由:${ev.url}`,
nzCancelDisabled: false,
nzOkText: '刷新',
nzCancelText: '忽略',
nzOnOk: () => location.reload()
});
}
if (ev instanceof NavigationEnd) {
this.titleSrv.setTitle();
this.modalSrv.closeAll();
}
});
}
}

View File

@ -0,0 +1,108 @@
/* eslint-disable import/order */
/* eslint-disable import/no-duplicates */
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, Injector, LOCALE_ID, NgModule, Type } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NzMessageModule } from 'ng-zorro-antd/message';
import { NzNotificationModule } from 'ng-zorro-antd/notification';
import { Observable } from 'rxjs';
// #region default language
// Reference: https://ng-alain.com/docs/i18n
import { default as ngLang } from '@angular/common/locales/zh';
import { DELON_LOCALE, zh_CN as delonLang } from '@delon/theme';
import { zhCN as dateLang } from 'date-fns/locale';
import { NZ_DATE_LOCALE, NZ_I18N, zh_CN as zorroLang } from 'ng-zorro-antd/i18n';
const LANG = {
abbr: 'zh',
ng: ngLang,
zorro: zorroLang,
date: dateLang,
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: NZ_DATE_LOCALE, useValue: LANG.date },
{ provide: DELON_LOCALE, useValue: LANG.delon }
];
// #endregion
// #region i18n services
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { I18NService } from '@core';
const I18NSERVICE_PROVIDES = [{ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false }];
// #region
// #region JSON Schema form (using @delon/form)
import { JsonSchemaModule } from '@shared';
const FORM_MODULES = [JsonSchemaModule];
// #endregion
// #region Http Interceptors
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { DefaultInterceptor } from '@core';
import { SimpleInterceptor } from '@delon/auth';
const INTERCEPTOR_PROVIDES = [
{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }
];
// #endregion
// #region global third module
const GLOBAL_THIRD_MODULES: Array<Type<void>> = [];
// #endregion
// #region Startup Service
import { StartupService } from '@core';
export function StartupServiceFactory(startupService: StartupService): () => Observable<void> {
return () => startupService.load();
}
const APPINIT_PROVIDES = [
StartupService,
{
provide: APP_INITIALIZER,
useFactory: StartupServiceFactory,
deps: [StartupService],
multi: true
}
];
// #endregion
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { GlobalConfigModule } from './global-config.module';
import { LayoutModule } from './layout/layout.module';
import { RoutesModule } from './routes/routes.module';
import { SharedModule } from './shared/shared.module';
import { STWidgetModule } from './shared/st-widget/st-widget.module';
import { NgxTinymceModule } from 'ngx-tinymce';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
GlobalConfigModule.forRoot(),
CoreModule,
SharedModule,
LayoutModule,
RoutesModule,
STWidgetModule,
NzMessageModule,
NzNotificationModule,
...FORM_MODULES,
...GLOBAL_THIRD_MODULES,
NgxTinymceModule.forRoot({
baseURL: '//cdn.bootcss.com/tinymce/4.7.13/'
})
],
providers: [...LANG_PROVIDES, ...INTERCEPTOR_PROVIDES, ...I18NSERVICE_PROVIDES, ...APPINIT_PROVIDES],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@ -0,0 +1,5 @@
### CoreModule
**应** 仅只留 `providers` 属性。
**作用:** 一些通用服务例如用户消息、HTTP数据访问。

View File

@ -0,0 +1,13 @@
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { I18NService } from './i18n/i18n.service';
import { throwIfAlreadyLoaded } from './module-import-guard';
@NgModule({
providers: [I18NService]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}

View File

@ -0,0 +1,84 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { TestBed, TestBedStatic } from '@angular/core/testing';
import { DelonLocaleService, SettingsService } from '@delon/theme';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzI18nService } from 'ng-zorro-antd/i18n';
import { of } from 'rxjs';
import { I18NService } from './i18n.service';
describe('Service: I18n', () => {
let injector: TestBedStatic;
let srv: I18NService;
const MockSettingsService: NzSafeAny = {
layout: {
lang: null
}
};
const MockNzI18nService = {
setLocale: () => {},
setDateLocale: () => {}
};
const MockDelonLocaleService = {
setLocale: () => {}
};
const MockTranslateService = {
getBrowserLang: jasmine.createSpy('getBrowserLang'),
addLangs: () => {},
setLocale: () => {},
getDefaultLang: () => '',
use: (lang: string) => of(lang),
instant: jasmine.createSpy('instant')
};
function genModule(): void {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
I18NService,
{ provide: SettingsService, useValue: MockSettingsService },
{ provide: NzI18nService, useValue: MockNzI18nService },
{ provide: DelonLocaleService, useValue: MockDelonLocaleService }
]
});
srv = TestBed.inject(I18NService);
}
it('should working', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['zh-CN']);
genModule();
expect(srv).toBeTruthy();
expect(srv.defaultLang).toBe('zh-CN');
srv.fanyi('a');
srv.fanyi('a', {});
});
it('should be used layout as default language', () => {
MockSettingsService.layout.lang = 'en-US';
const navSpy = spyOnProperty(navigator, 'languages');
genModule();
expect(navSpy).not.toHaveBeenCalled();
expect(srv.defaultLang).toBe('en-US');
MockSettingsService.layout.lang = null;
});
it('should be used browser as default language', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['zh-TW']);
genModule();
expect(srv.defaultLang).toBe('zh-TW');
});
it('should be use default language when the browser language is not in the list', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['es-419']);
genModule();
expect(srv.defaultLang).toBe('zh-CN');
});
it('should be trigger notify when changed language', () => {
genModule();
srv.use('en-US', {});
srv.change.subscribe(lang => {
expect(lang).toBe('en-US');
});
});
});

View File

@ -0,0 +1,116 @@
// 请参考https://ng-alain.com/docs/i18n
import { Platform } from '@angular/cdk/platform';
import { registerLocaleData } from '@angular/common';
import ngEn from '@angular/common/locales/en';
import ngZh from '@angular/common/locales/zh';
import ngZhTw from '@angular/common/locales/zh-Hant';
import { Injectable } from '@angular/core';
import {
DelonLocaleService,
en_US as delonEnUS,
SettingsService,
zh_CN as delonZhCn,
zh_TW as delonZhTw,
_HttpClient,
AlainI18nBaseService
} from '@delon/theme';
import { AlainConfigService } from '@delon/util/config';
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw } from 'date-fns/locale';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { en_US as zorroEnUS, NzI18nService, zh_CN as zorroZhCN, zh_TW as zorroZhTW } from 'ng-zorro-antd/i18n';
import { Observable } from 'rxjs';
interface LangConfigData {
abbr: string;
text: string;
ng: NzSafeAny;
zorro: NzSafeAny;
date: NzSafeAny;
delon: NzSafeAny;
}
const DEFAULT = 'zh-CN';
const LANGS: { [key: string]: LangConfigData } = {
'zh-CN': {
text: '简体中文',
ng: ngZh,
zorro: zorroZhCN,
date: dfZhCn,
delon: delonZhCn,
abbr: '🇨🇳'
},
'zh-TW': {
text: '繁体中文',
ng: ngZhTw,
zorro: zorroZhTW,
date: dfZhTw,
delon: delonZhTw,
abbr: '🇭🇰'
},
'en-US': {
text: 'English',
ng: ngEn,
zorro: zorroEnUS,
date: dfEn,
delon: delonEnUS,
abbr: '🇬🇧'
}
};
@Injectable({ providedIn: 'root' })
export class I18NService extends AlainI18nBaseService {
protected override _defaultLang = DEFAULT;
private _langs = Object.keys(LANGS).map(code => {
const item = LANGS[code];
return { code, text: item.text, abbr: item.abbr };
});
constructor(
private http: _HttpClient,
private settings: SettingsService,
private nzI18nService: NzI18nService,
private delonLocaleService: DelonLocaleService,
private platform: Platform,
cogSrv: AlainConfigService
) {
super(cogSrv);
const defaultLang = this.getDefaultLang();
this._defaultLang = this._langs.findIndex(w => w.code === defaultLang) === -1 ? DEFAULT : defaultLang;
}
private getDefaultLang(): string {
if (!this.platform.isBrowser) {
return DEFAULT;
}
if (this.settings.layout.lang) {
return this.settings.layout.lang;
}
let res = (navigator.languages ? navigator.languages[0] : null) || navigator.language;
const arr = res.split('-');
return arr.length <= 1 ? res : `${arr[0]}-${arr[1].toUpperCase()}`;
}
loadLangData(lang: string): Observable<NzSafeAny> {
return this.http.get(`assets/tmp/i18n/${lang}.json`);
}
use(lang: string, data: Record<string, unknown>): void {
if (this._currentLang === lang) return;
this._data = this.flatData(data, []);
const item = LANGS[lang];
registerLocaleData(item.ng);
this.nzI18nService.setLocale(item.zorro);
this.nzI18nService.setDateLocale(item.date);
this.delonLocaleService.setLocale(item.delon);
this._currentLang = lang;
this._change$.next(lang);
}
getLangs(): Array<{ code: string; text: string; abbr: string }> {
return this._langs;
}
}

View File

@ -0,0 +1,4 @@
export * from './i18n/i18n.service';
export * from './module-import-guard';
export * from './net/default.interceptor';
export * from './startup/startup.service';

View File

@ -0,0 +1,6 @@
// https://angular.io/guide/styleguide#style-04-12
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string): void {
if (parentModule) {
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
}
}

View File

@ -0,0 +1,261 @@
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpHeaders,
HttpInterceptor,
HttpRequest,
HttpResponseBase
} from '@angular/common/http';
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { ALAIN_I18N_TOKEN, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, filter, mergeMap, switchMap, take } from 'rxjs/operators';
const CODEMESSAGE: { [key: number]: string } = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
};
/**
* HTTP拦截器 `app.module.ts`
*/
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
private refreshTokenEnabled = environment.api.refreshTokenEnabled;
private refreshTokenType: 're-request' | 'auth-refresh' = environment.api.refreshTokenType;
private refreshToking = false;
private refreshToken$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private injector: Injector) {
if (this.refreshTokenType === 'auth-refresh') {
this.buildAuthRefresh();
}
}
private get notification(): NzNotificationService {
return this.injector.get(NzNotificationService);
}
private get tokenSrv(): ITokenService {
return this.injector.get(DA_SERVICE_TOKEN);
}
private get http(): _HttpClient {
return this.injector.get(_HttpClient);
}
private goTo(url: string): void {
setTimeout(() => this.injector.get(Router).navigateByUrl(url));
}
private checkStatus(ev: HttpResponseBase): void {
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);
}
/**
* Token
*/
private refreshTokenRequest(): Observable<any> {
const model = this.tokenSrv.get();
return this.http.post(`/api/auth/refresh`, null, null, { headers: { refresh_token: model?.['refresh_token'] || '' } });
}
// #region 刷新Token方式一使用 401 重新刷新 Token
private tryRefreshToken(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
// 1、若请求为刷新Token请求表示来自刷新Token可以直接跳转登录页
if ([`/api/auth/refresh`].some(url => req.url.includes(url))) {
this.toLogin();
return throwError(ev);
}
// 2、如果 `refreshToking` 为 `true` 表示已经在请求刷新 Token 中,后续所有请求转入等待状态,直至结果返回后再重新发起请求
if (this.refreshToking) {
return this.refreshToken$.pipe(
filter(v => !!v),
take(1),
switchMap(() => next.handle(this.reAttachToken(req)))
);
}
// 3、尝试调用刷新 Token
this.refreshToking = true;
this.refreshToken$.next(null);
return this.refreshTokenRequest().pipe(
switchMap(res => {
// 通知后续请求继续执行
this.refreshToking = false;
this.refreshToken$.next(res);
// 重新保存新 token
this.tokenSrv.set(res);
// 重新发起请求
return next.handle(this.reAttachToken(req));
}),
catchError(err => {
this.refreshToking = false;
this.toLogin();
return throwError(err);
})
);
}
/**
* Token
*
* > `@delon/auth` Token
*/
private reAttachToken(req: HttpRequest<any>): HttpRequest<any> {
// 以下示例是以 NG-ALAIN 默认使用 `SimpleInterceptor`
const token = this.tokenSrv.get()?.token;
return req.clone({
setHeaders: {
token: `Bearer ${token}`
}
});
}
// #endregion
// #region 刷新Token方式二使用 `@delon/auth` 的 `refresh` 接口
private buildAuthRefresh(): void {
if (!this.refreshTokenEnabled) {
return;
}
this.tokenSrv.refresh
.pipe(
filter(() => !this.refreshToking),
switchMap(res => {
console.log(res);
this.refreshToking = true;
return this.refreshTokenRequest();
})
)
.subscribe(
res => {
// TODO: Mock expired value
res.expired = +new Date() + 1000 * 60 * 5;
this.refreshToking = false;
this.tokenSrv.set(res);
},
() => this.toLogin()
);
}
// #endregion
private toLogin(): void {
this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
this.goTo(this.tokenSrv.login_url!);
}
private handleData(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
this.checkStatus(ev);
// 业务处理:一些通用操作
switch (ev.status) {
case 200:
// 业务层级错误处理以下是假定restful有一套统一输出格式指不管成功与否都有相应的数据格式情况下进行处理
// 例如响应内容:
// 错误内容:{ status: 1, msg: '非法参数' }
// 正确内容:{ status: 0, response: { } }
// 则以下代码片断可直接适用
// if (ev instanceof HttpResponse) {
// const body = ev.body;
// if (body && body.status !== 0) {
// this.injector.get(NzMessageService).error(body.msg);
// // 注意这里如果继续抛出错误会被行254的 catchError 二次拦截,导致外部实现的 Pipe、subscribe 操作被中断例如this.http.get('/').subscribe() 不会触发
// // 如果你希望外部实现需要手动移除行254
// return throwError({});
// } else {
// // 忽略 Blob 文件体
// if (ev.body instanceof Blob) {
// return of(ev);
// }
// // 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
// return of(new HttpResponse(Object.assign(ev, { body: body.response })));
// // 或者依然保持完整的格式
// return of(ev);
// }
// }
break;
case 401:
if (this.refreshTokenEnabled && this.refreshTokenType === 're-request') {
return this.tryRefreshToken(ev, req, next);
}
this.toLogin();
break;
case 403:
case 404:
case 500:
// this.goTo(`/exception/${ev.status}?url=${req.urlWithParams}`);
break;
default:
if (ev instanceof HttpErrorResponse) {
console.warn(
'未可知错误大部分是由于后端不支持跨域CORS或无效配置引起请参考 https://ng-alain.com/docs/server 解决跨域问题',
ev
);
}
break;
}
if (ev instanceof HttpErrorResponse) {
return throwError(ev);
} else {
return of(ev);
}
}
private getAdditionalHeaders(headers?: HttpHeaders): { [name: string]: string } {
const res: { [name: string]: string } = {};
const lang = this.injector.get(ALAIN_I18N_TOKEN).currentLang;
if (!headers?.has('Accept-Language') && lang) {
res['Accept-Language'] = lang;
}
return res;
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 统一加上服务端前缀
let url = req.url;
if (!url.startsWith('https://') && !url.startsWith('http://')) {
const { baseUrl } = environment.api;
url = baseUrl + (baseUrl.endsWith('/') && url.startsWith('/') ? url.substring(1) : url);
}
const newReq = req.clone({ url, setHeaders: this.getAdditionalHeaders(req.headers) });
return next.handle(newReq).pipe(
mergeMap(ev => {
// 允许统一对请求错误处理
if (ev instanceof HttpResponseBase) {
return this.handleData(ev, newReq, next);
}
// 若一切都正常,则后续操作
return of(ev);
}),
catchError((err: HttpErrorResponse) => this.handleData(err, newReq, next))
);
}
}

View File

@ -0,0 +1,52 @@
import { HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { AlainAuthConfig } from '@delon/util/config';
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
import { BaseInterceptor } from '../base.interceptor';
import { CheckSimple } from '../helper';
import { DA_SERVICE_TOKEN } from '../interface';
import { SimpleTokenModel } from './simple.model';
/**
* Simple
*
* ```
* // app.module.ts
* { provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true}
* ```
*/
@Injectable()
export class SimpleInterceptor extends BaseInterceptor {
isAuth(_options: AlainAuthConfig): boolean {
this.model = this.injector.get(DA_SERVICE_TOKEN).get() as SimpleTokenModel;
return CheckSimple(this.model as SimpleTokenModel);
}
setReq(req: HttpRequest<NzSafeAny>, options: AlainAuthConfig): HttpRequest<NzSafeAny> {
const { token_send_template, token_send_key } = options;
const token = token_send_template!.replace(/\$\{([\w]+)\}/g, (_: string, g) => this.model[g]);
switch (options.token_send_place) {
case 'header':
const obj: NzSafeAny = {};
obj[token_send_key!] = token;
req = req.clone({
setHeaders: obj
});
break;
case 'body':
const body = req.body || {};
body[token_send_key!] = token;
req = req.clone({
body
});
break;
case 'url':
req = req.clone({
params: req.params.append(token_send_key!, token)
});
break;
}
return req;
}
}

View File

@ -0,0 +1,129 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { ACLService } from '@delon/acl';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { ALAIN_I18N_TOKEN, MenuService, SettingsService, TitleService } from '@delon/theme';
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzIconService } from 'ng-zorro-antd/icon';
import { Observable, zip, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ICONS } from '../../../style-icons';
import { ICONS_AUTO } from '../../../style-icons-auto';
import { I18NService } from '../i18n/i18n.service';
/**
* Used for application startup
* Generally used to get the basic data of the application, like: Menu Data, User Data, etc.
*/
@Injectable()
export class StartupService {
constructor(
iconSrv: NzIconService,
private menuService: MenuService,
@Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
private settingService: SettingsService,
private aclService: ACLService,
private titleService: TitleService,
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
private httpClient: HttpClient,
private router: Router
) {
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
}
private viaHttp(): Observable<void> {
const defaultLang = this.i18n.defaultLang;
return zip(this.i18n.loadLangData(defaultLang), this.httpClient.get('assets/tmp/app-data.json')).pipe(
catchError((res: NzSafeAny) => {
console.warn(`StartupService.load: Network request failed`, res);
setTimeout(() => this.router.navigateByUrl(`/exception/500`));
return [];
}),
map(([langData, appData]: [Record<string, string>, NzSafeAny]) => {
// setting language data
this.i18n.use(defaultLang, langData);
// Application data
// Application information: including site name, description, year
this.settingService.setApp(appData.app);
// User information: including name, avatar, email address
this.settingService.setUser(appData.user);
// ACL: Set the permissions to full, https://ng-alain.com/acl/getting-started
this.aclService.setFull(true);
// Menu data, https://ng-alain.com/theme/menu
this.menuService.add(appData.menu);
// Can be set page suffix title, https://ng-alain.com/theme/title
this.titleService.suffix = appData.app.name;
})
);
}
private viaMockI18n(): Observable<void> {
const defaultLang = this.i18n.defaultLang;
return this.i18n.loadLangData(defaultLang).pipe(
map((langData: NzSafeAny) => {
this.i18n.use(defaultLang, langData);
this.viaMock();
})
);
}
private viaMock(): Observable<void> {
// const tokenData = this.tokenService.get();
// if (!tokenData.token) {
// this.router.navigateByUrl(this.tokenService.login_url!);
// return;
// }
// mock
const app: any = {
name: `iotgateway`,
description: `Ng-zorro admin panel front-end framework`
};
const user: any = {
name: 'Admin',
avatar: './assets/tmp/img/avatar.jpg',
email: '535915157@qq.com',
token: '123456789'
};
// Application information: including site name, description, year
this.settingService.setApp(app);
// User information: including name, avatar, email address
this.settingService.setUser(user);
// ACL: Set the permissions to full, https://ng-alain.com/acl/getting-started
this.aclService.setFull(true);
// Menu data, https://ng-alain.com/theme/menu
this.menuService.add([
{
text: 'Main',
group: true,
children: [
{
text: 'Dashboard',
link: '/dashboard',
icon: { type: 'icon', value: 'appstore' }
},
{
text: 'Sys',
link: '/sys/log',
icon: { type: 'icon', value: 'appstore' }
}
]
}
]);
// Can be set page suffix title, https://ng-alain.com/theme/title
this.titleService.suffix = app.name;
return of();
}
load(): Observable<void> {
// http
// return this.viaHttp();
// mock: Dont use it in a production environment. ViaMock is just to simulate some data to make the scaffolding work normally
// mock请勿在生产环境中这么使用viaMock 单纯只是为了模拟一些数据使脚手架一开始能正常运行
return this.viaMockI18n();
}
}

View File

@ -0,0 +1,77 @@
/* eslint-disable import/order */
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { DelonACLModule } from '@delon/acl';
import { AlainThemeModule } from '@delon/theme';
import { AlainConfig, ALAIN_CONFIG } from '@delon/util/config';
import { throwIfAlreadyLoaded } from '@core';
import { environment } from '@env/environment';
// Please refer to: https://ng-alain.com/docs/global-config
// #region NG-ALAIN Config
const alainConfig: AlainConfig = {
st: { modal: { size: 'lg' } },
pageHeader: { homeI18n: 'home' },
lodop: {
license: `A59B099A586B3851E0F0D7FDBF37B603`,
licenseA: `C94CEE276DB2187AE6B65D56B3FC2848`
},
auth: { login_url: '/passport/login' }
};
const alainModules: any[] = [AlainThemeModule.forRoot(), DelonACLModule.forRoot()];
const alainProvides = [{ provide: ALAIN_CONFIG, useValue: alainConfig }];
// #region reuse-tab
/**
* [](https://ng-alain.com/components/reuse-tab)需要:
* 1 `shared-delon.module.ts` `ReuseTabModule`
* 2 `RouteReuseStrategy`
* 3 `src/app/layout/default/default.component.html`
* ```html
* <section class="alain-default__content">
* <reuse-tab #reuseTab></reuse-tab>
* <router-outlet (activate)="reuseTab.activate($event)"></router-outlet>
* </section>
* ```
*/
// import { RouteReuseStrategy } from '@angular/router';
// import { ReuseTabService, ReuseTabStrategy } from '@delon/abc/reuse-tab';
// alainProvides.push({
// provide: RouteReuseStrategy,
// useClass: ReuseTabStrategy,
// deps: [ReuseTabService],
// } as any);
// #endregion
// #endregion
// Please refer to: https://ng.ant.design/docs/global-config/en#how-to-use
// #region NG-ZORRO Config
import { NzConfig, NZ_CONFIG } from 'ng-zorro-antd/core/config';
const ngZorroConfig: NzConfig = {};
const zorroProvides = [{ provide: NZ_CONFIG, useValue: ngZorroConfig }];
// #endregion
@NgModule({
imports: [...alainModules, ...(environment.modules || [])]
})
export class GlobalConfigModule {
constructor(@Optional() @SkipSelf() parentModule: GlobalConfigModule) {
throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule');
}
static forRoot(): ModuleWithProviders<GlobalConfigModule> {
return {
ngModule: GlobalConfigModule,
providers: [...alainProvides, ...zorroProvides]
};
}
}

View File

@ -0,0 +1 @@
[Document](https://ng-alain.com/theme/default)

View File

@ -0,0 +1,85 @@
import { Component } from '@angular/core';
import { SettingsService, User } from '@delon/theme';
import { LayoutDefaultOptions } from '@delon/theme/layout-default';
import { environment } from '@env/environment';
@Component({
selector: 'layout-basic',
template: `
<layout-default [options]="options" [asideUser]="asideUserTpl" [content]="contentTpl" [customError]="null">
<layout-default-header-item direction="left">
<a layout-default-header-item-trigger href="//github.com/iioter/iotgateway" target="_blank">
<i nz-icon nzType="github"></i>
</a>
</layout-default-header-item>
<layout-default-header-item direction="left" hidden="mobile">
<a layout-default-header-item-trigger routerLink="/passport/lock">
<i nz-icon nzType="lock"></i>
</a>
</layout-default-header-item>
<layout-default-header-item direction="left" hidden="pc">
<div layout-default-header-item-trigger (click)="searchToggleStatus = !searchToggleStatus">
<i nz-icon nzType="search"></i>
</div>
</layout-default-header-item>
<layout-default-header-item direction="middle">
<header-search class="alain-default__search" [toggleChange]="searchToggleStatus"></header-search>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<div layout-default-header-item-trigger nz-dropdown [nzDropdownMenu]="settingsMenu" nzTrigger="click" nzPlacement="bottomRight">
<i nz-icon nzType="setting"></i>
</div>
<nz-dropdown-menu #settingsMenu="nzDropdownMenu">
<div nz-menu style="width: 200px;">
<div nz-menu-item>
<header-fullscreen></header-fullscreen>
</div>
<div nz-menu-item>
<header-clear-storage></header-clear-storage>
</div>
<div nz-menu-item>
<header-i18n></header-i18n>
</div>
</div>
</nz-dropdown-menu>
</layout-default-header-item>
<layout-default-header-item direction="right">
<header-user></header-user>
</layout-default-header-item>
<ng-template #asideUserTpl>
<div nz-dropdown nzTrigger="click" [nzDropdownMenu]="userMenu" class="alain-default__aside-user">
<nz-avatar class="alain-default__aside-user-avatar" [nzSrc]="user.avatar"></nz-avatar>
<div class="alain-default__aside-user-info">
<strong>{{ user.name }}</strong>
<p class="mb0">{{ user.email }}</p>
</div>
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<ul nz-menu>
<li nz-menu-item routerLink="/pro/account/center">{{ 'menu.account.center' | i18n }}</li>
<li nz-menu-item routerLink="/pro/account/settings">{{ 'menu.account.settings' | i18n }}</li>
</ul>
</nz-dropdown-menu>
</ng-template>
<ng-template #contentTpl>
<router-outlet></router-outlet>
</ng-template>
</layout-default>
<setting-drawer *ngIf="showSettingDrawer"></setting-drawer>
<theme-btn></theme-btn>
`
})
export class LayoutBasicComponent {
options: LayoutDefaultOptions = {
logoExpanded: `./assets/logo-full.svg`,
logoCollapsed: `./assets/logo.svg`
};
searchToggleStatus = false;
showSettingDrawer = !environment.production;
get user(): User {
return this.settings.user;
}
constructor(private settings: SettingsService) {}
}

View File

@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
@Component({
selector: 'header-clear-storage',
template: `
<i nz-icon nzType="tool"></i>
{{ 'menu.clear.local.storage' | i18n }}
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderClearStorageComponent {
constructor(private modalSrv: NzModalService, private messageSrv: NzMessageService) {}
@HostListener('click')
_click(): void {
this.modalSrv.confirm({
nzTitle: 'Make sure clear all local storage?',
nzOnOk: () => {
localStorage.clear();
this.messageSrv.success('Clear Finished!');
}
});
}
}

View File

@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import screenfull from 'screenfull';
@Component({
selector: 'header-fullscreen',
template: `
<i nz-icon [nzType]="status ? 'fullscreen-exit' : 'fullscreen'"></i>
{{ (status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | i18n }}
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderFullScreenComponent {
status = false;
@HostListener('window:resize')
_resize(): void {
this.status = screenfull.isFullscreen;
}
@HostListener('click')
_click(): void {
if (screenfull.isEnabled) {
screenfull.toggle();
}
}
}

View File

@ -0,0 +1,57 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, SettingsService } from '@delon/theme';
import { BooleanInput, InputBoolean } from '@delon/util/decorator';
@Component({
selector: 'header-i18n',
template: `
<div *ngIf="showLangText" nz-dropdown [nzDropdownMenu]="langMenu" nzPlacement="bottomRight">
<i nz-icon nzType="global"></i>
{{ 'menu.lang' | i18n }}
<i nz-icon nzType="down"></i>
</div>
<i *ngIf="!showLangText" nz-dropdown [nzDropdownMenu]="langMenu" nzPlacement="bottomRight" nz-icon nzType="global"></i>
<nz-dropdown-menu #langMenu="nzDropdownMenu">
<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-menu>
`,
host: {
'[class.flex-1]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderI18nComponent {
static ngAcceptInputType_showLangText: BooleanInput;
/** Whether to display language text */
@Input() @InputBoolean() showLangText = true;
get langs(): Array<{ code: string; text: string; abbr: string }> {
return this.i18n.getLangs();
}
get curLangCode(): string {
return this.settings.layout.lang;
}
constructor(private settings: SettingsService, @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService, @Inject(DOCUMENT) private doc: any) {}
change(lang: string): void {
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.loadLangData(lang).subscribe(res => {
this.i18n.use(lang, res);
this.settings.setLayout('lang', lang);
setTimeout(() => this.doc.location.reload());
});
}
}

View File

@ -0,0 +1,108 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
Output
} from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
@Component({
selector: 'header-search',
template: `
<nz-input-group [nzPrefix]="iconTpl" [nzSuffix]="loadingTpl">
<ng-template #iconTpl>
<i nz-icon [nzType]="focus ? 'arrow-down' : 'search'"></i>
</ng-template>
<ng-template #loadingTpl>
<i *ngIf="loading" nz-icon nzType="loading"></i>
</ng-template>
<input
type="text"
nz-input
[(ngModel)]="q"
[nzAutocomplete]="auto"
(input)="search($event)"
(focus)="qFocus()"
(blur)="qBlur()"
[attr.placeholder]="'' | i18n"
/>
</nz-input-group>
<nz-autocomplete nzBackfill #auto>
<nz-auto-option *ngFor="let i of options" [nzValue]="i">{{ i }}</nz-auto-option>
</nz-autocomplete>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderSearchComponent implements AfterViewInit, OnDestroy {
q = '';
qIpt: HTMLInputElement | null = null;
options: string[] = [];
search$ = new BehaviorSubject('');
loading = false;
@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 = value;
this.focus = value;
if (value) {
setTimeout(() => this.qIpt!.focus());
}
}
@Output() readonly toggleChangeChange = new EventEmitter<boolean>();
constructor(private el: ElementRef<HTMLElement>, private cdr: ChangeDetectorRef) {}
ngAfterViewInit(): void {
this.qIpt = this.el.nativeElement.querySelector('.ant-input') as HTMLInputElement;
this.search$
.pipe(
debounceTime(500),
distinctUntilChanged(),
tap({
complete: () => {
this.loading = true;
}
})
)
.subscribe(value => {
this.options = value ? [value, value + value, value + value + value] : [];
this.loading = false;
this.cdr.detectChanges();
});
}
qFocus(): void {
this.focus = true;
}
qBlur(): void {
this.focus = false;
this.searchToggled = false;
this.options.length = 0;
this.toggleChangeChange.emit(false);
}
search(ev: Event): void {
this.search$.next((ev.target as HTMLInputElement).value);
}
ngOnDestroy(): void {
this.search$.complete();
this.search$.unsubscribe();
}
}

View File

@ -0,0 +1,48 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { SettingsService, User } from '@delon/theme';
@Component({
selector: 'header-user',
template: `
<div class="alain-default__nav-item d-flex align-items-center px-sm" nz-dropdown nzPlacement="bottomRight" [nzDropdownMenu]="userMenu">
<nz-avatar [nzSrc]="user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
{{ user.name }}
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<div nz-menu class="width-sm">
<div nz-menu-item routerLink="/pro/account/center">
<i nz-icon nzType="user" class="mr-sm"></i>
{{ 'menu.account.center' | i18n }}
</div>
<div nz-menu-item routerLink="/pro/account/settings">
<i nz-icon nzType="setting" class="mr-sm"></i>
{{ 'menu.account.settings' | i18n }}
</div>
<div nz-menu-item routerLink="/exception/trigger">
<i nz-icon nzType="close-circle" class="mr-sm"></i>
{{ 'menu.account.trigger' | i18n }}
</div>
<li nz-menu-divider></li>
<div nz-menu-item (click)="logout()">
<i nz-icon nzType="logout" class="mr-sm"></i>
{{ 'menu.account.logout' | i18n }}
</div>
</div>
</nz-dropdown-menu>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderUserComponent {
get user(): User {
return this.settings.user;
}
constructor(private settings: SettingsService, private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
logout(): void {
this.tokenService.clear();
this.router.navigateByUrl(this.tokenService.login_url!);
}
}

View File

@ -0,0 +1 @@
[Document](https://ng-alain.com/theme/blank)

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'layout-blank',
template: `<router-outlet></router-outlet> `,
host: {
'[class.alain-blank]': 'true'
}
})
export class LayoutBlankComponent {}

View File

@ -0,0 +1,68 @@
/* eslint-disable import/order */
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { GlobalFooterModule } from '@delon/abc/global-footer';
import { NoticeIconModule } from '@delon/abc/notice-icon';
import { AlainThemeModule } from '@delon/theme';
import { LayoutDefaultModule } from '@delon/theme/layout-default';
import { SettingDrawerModule } from '@delon/theme/setting-drawer';
import { ThemeBtnModule } from '@delon/theme/theme-btn';
import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzBadgeModule } from 'ng-zorro-antd/badge';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzGridModule } from 'ng-zorro-antd/grid';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { LayoutBasicComponent } from './basic/basic.component';
import { HeaderClearStorageComponent } from './basic/widgets/clear-storage.component';
import { HeaderFullScreenComponent } from './basic/widgets/fullscreen.component';
import { HeaderI18nComponent } from './basic/widgets/i18n.component';
import { HeaderSearchComponent } from './basic/widgets/search.component';
import { HeaderUserComponent } from './basic/widgets/user.component';
import { LayoutBlankComponent } from './blank/blank.component';
const COMPONENTS = [LayoutBasicComponent, LayoutBlankComponent];
const HEADERCOMPONENTS = [
HeaderSearchComponent,
HeaderFullScreenComponent,
HeaderI18nComponent,
HeaderClearStorageComponent,
HeaderUserComponent
];
// passport
import { LayoutPassportComponent } from './passport/passport.component';
const PASSPORT = [LayoutPassportComponent];
@NgModule({
imports: [
CommonModule,
FormsModule,
RouterModule,
AlainThemeModule.forChild(),
ThemeBtnModule,
SettingDrawerModule,
LayoutDefaultModule,
NoticeIconModule,
GlobalFooterModule,
NzDropDownModule,
NzInputModule,
NzAutocompleteModule,
NzGridModule,
NzFormModule,
NzSpinModule,
NzBadgeModule,
NzAvatarModule,
NzIconModule
],
declarations: [...COMPONENTS, ...HEADERCOMPONENTS, ...PASSPORT],
exports: [...COMPONENTS, ...PASSPORT]
})
export class LayoutModule {}

View File

@ -0,0 +1,17 @@
<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.png" />
<span class="title">IoTGateway</span>
</div>
<div class="desc">angular版本的前端暂不可用</div>
</div>
<router-outlet></router-outlet>
<global-footer>
Copyright
<i class="anticon anticon-copyright"></i> 2022 <a href="//github.com/iioter/iotgateway" target="_blank">iioter</a>出品
</global-footer>
</div>
</div>

View File

@ -0,0 +1,112 @@
@import '@delon/theme/index';
:host ::ng-deep {
.container {
display: flex;
flex-direction: column;
min-height: 100%;
background: #f0f2f5;
}
.langs {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
.anticon {
margin-top: 24px;
margin-right: 24px;
font-size: 14px;
vertical-align: top;
cursor: pointer;
}
}
.wrap {
flex: 1;
padding: 32px 0;
}
.ant-form-item {
display: flex;
justify-content: space-between;
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 {
position: relative;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
vertical-align: middle;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
}
[data-theme='dark'] {
:host ::ng-deep {
.container {
background: #141414;
}
.title {
color: fade(@white, 85%);
}
.desc {
color: fade(@white, 45%);
}
@media (min-width: @screen-md-min) {
.container {
background-image: none;
}
}
}
}
[data-theme='compact'] {
:host ::ng-deep {
.ant-form-item {
margin-bottom: 16px;
}
}
}

View File

@ -0,0 +1,15 @@
import { Component, Inject, OnInit } from '@angular/core';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
@Component({
selector: 'layout-passport',
templateUrl: './passport.component.html',
styleUrls: ['./passport.component.less']
})
export class LayoutPassportComponent implements OnInit {
constructor(@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
ngOnInit(): void {
this.tokenService.clear();
}
}

View File

@ -0,0 +1,5 @@
<page-header></page-header>
<div>
<h2>dashboard content</h2>
</div>

View File

@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent {}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ExceptionComponent } from './exception.component';
import { ExceptionTriggerComponent } from './trigger.component';
const routes: Routes = [
{ path: '403', component: ExceptionComponent, data: { type: 403 } },
{ path: '404', component: ExceptionComponent, data: { type: 404 } },
{ path: '500', component: ExceptionComponent, data: { type: 500 } },
{ path: 'trigger', component: ExceptionTriggerComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ExceptionRoutingModule {}

View File

@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ExceptionType } from '@delon/abc/exception';
@Component({
selector: 'app-exception',
template: ` <exception [type]="type" style="min-height: 500px; height: 80%;"> </exception> `,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExceptionComponent {
get type(): ExceptionType {
return this.route.snapshot.data['type'];
}
constructor(private route: ActivatedRoute) {}
}

View File

@ -0,0 +1,15 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ExceptionModule as DelonExceptionModule } from '@delon/abc/exception';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCardModule } from 'ng-zorro-antd/card';
import { ExceptionRoutingModule } from './exception-routing.module';
import { ExceptionComponent } from './exception.component';
import { ExceptionTriggerComponent } from './trigger.component';
@NgModule({
imports: [CommonModule, DelonExceptionModule, NzButtonModule, NzCardModule, ExceptionRoutingModule],
declarations: [ExceptionComponent, ExceptionTriggerComponent]
})
export class ExceptionModule {}

View File

@ -0,0 +1,35 @@
import { Component, Inject } from '@angular/core';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { _HttpClient } from '@delon/theme';
@Component({
selector: 'exception-trigger',
template: `
<div class="pt-lg">
<nz-card>
<button *ngFor="let t of types" (click)="go(t)" nz-button nzDanger>{{ t }}</button>
<button nz-button nzType="link" (click)="refresh()">Token</button>
</nz-card>
</div>
`
})
export class ExceptionTriggerComponent {
types = [401, 403, 404, 500];
constructor(private http: _HttpClient, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
go(type: number): void {
this.http.get(`/api/${type}`).subscribe();
}
refresh(): void {
this.tokenService.set({ token: 'invalid-token' });
// 必须提供一个后端地址,无法通过 Mock 来模拟
this.http.post(`https://localhost:5001/auth`).subscribe(
res => console.warn('成功', res),
err => {
console.log('最后结果失败', err);
}
);
}
}

View File

@ -0,0 +1,35 @@
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 = '';
constructor(private socialService: SocialService, private settingsSrv: SettingsService, private route: ActivatedRoute) {}
ngOnInit(): void {
this.type = this.route.snapshot.params['type'];
this.mockModel();
}
private mockModel(): void {
const info = {
token: '123456789',
name: 'iotgateway',
email: `${this.type}@${this.type}.com`,
id: 10000,
time: +new Date()
};
this.settingsSrv.setUser({
...this.settingsSrv.user,
...info
});
this.socialService.callback(info);
}
}

View File

@ -0,0 +1,21 @@
<div class="ant-card width-lg" style="margin: 0 auto">
<div class="ant-card-body">
<div class="avatar">
<nz-avatar [nzSrc]="user.avatar" nzIcon="user" nzSize="large"></nz-avatar>
</div>
<form nz-form [formGroup]="f" (ngSubmit)="submit()" role="form" class="mt-md">
<nz-form-item>
<nz-form-control [nzErrorTip]="'validation.password.required' | i18n">
<nz-input-group nzSuffixIcon="lock">
<input type="password" nz-input formControlName="password" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-row nzType="flex" nzAlign="middle">
<nz-col [nzOffset]="12" [nzSpan]="12" style="text-align: right">
<button nz-button [disabled]="!f.valid" nzType="primary">{{ 'app.lock' | i18n }}</button>
</nz-col>
</nz-row>
</form>
</div>
</div>

View File

@ -0,0 +1,13 @@
:host ::ng-deep {
.ant-card-body {
position: relative;
margin-top: 80px;
}
.avatar {
position: absolute;
top: -20px;
left: 50%;
margin-left: -20px;
}
}

View File

@ -0,0 +1,44 @@
import { Component, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { SettingsService, User } from '@delon/theme';
@Component({
selector: 'passport-lock',
templateUrl: './lock.component.html',
styleUrls: ['./lock.component.less']
})
export class UserLockComponent {
f: FormGroup;
get user(): User {
return this.settings.user;
}
constructor(
fb: FormBuilder,
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
private settings: SettingsService,
private router: Router
) {
this.f = fb.group({
password: [null, Validators.required]
});
}
submit(): void {
for (const i in this.f.controls) {
this.f.controls[i].markAsDirty();
this.f.controls[i].updateValueAndValidity();
}
if (this.f.valid) {
console.log('Valid!');
console.log(this.f.value);
this.tokenService.set({
token: '123'
});
this.router.navigate(['dashboard']);
}
}
}

View File

@ -0,0 +1,35 @@
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
<nz-tabset [nzAnimated]="false" class="tabs" (nzSelectChange)="switch($event)">
<nz-tab [nzTitle]="'app.login.tab-login-credentials' | i18n">
<nz-alert *ngIf="error" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg"></nz-alert>
<nz-form-item>
<nz-form-control nzErrorTip="Please enter mobile number, muse be: admin">
<nz-input-group nzSize="large" nzPrefixIcon="user">
<input nz-input formControlName="userName" placeholder="username: admin" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzErrorTip="Please enter password, muse be: 000000">
<nz-input-group nzSize="large" nzPrefixIcon="lock">
<input nz-input type="password" formControlName="password" placeholder="password: 000000" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
</nz-tab>
</nz-tabset>
<nz-form-item>
<nz-col [nzSpan]="12">
<label nz-checkbox formControlName="remember">{{ 'app.login.remember-me' | i18n }}</label>
</nz-col>
</nz-form-item>
<nz-form-item>
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock>
{{ 'app.login.login' | i18n }}
</button>
</nz-form-item>
</form>
<div class="other">
{{ 'app.login.sign-in-with' | i18n }}
<!-- <a class="register" routerLink="/passport/register">{{ 'app.login.signup' | i18n }}</a> -->
</div>

View File

@ -0,0 +1,63 @@
@import '@delon/theme/index';
:host {
display: block;
width: 368px;
margin: 0 auto;
::ng-deep {
.ant-tabs .ant-tabs-bar {
margin-bottom: 24px;
text-align: center;
border-bottom: 0;
}
.ant-tabs-tab {
font-size: 16px;
line-height: 24px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 4px;
}
.icon {
margin-left: 16px;
color: rgb(0 0 0 / 20%);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
nz-tooltip {
vertical-align: middle;
}
.register {
float: right;
}
}
}
}
[data-theme='dark'] {
:host ::ng-deep {
.icon {
color: rgb(255 255 255 / 20%);
&:hover {
color: #fff;
}
}
}
}

View File

@ -0,0 +1,196 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, Optional } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { StartupService } from '@core';
import { ReuseTabService } from '@delon/abc/reuse-tab';
import { DA_SERVICE_TOKEN, ITokenService, SocialOpenType, SocialService } from '@delon/auth';
import { SettingsService, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { NzTabChangeEvent } from 'ng-zorro-antd/tabs';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'passport-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.less'],
providers: [SocialService],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserLoginComponent implements OnDestroy {
constructor(
fb: FormBuilder,
private router: Router,
private settingsService: SettingsService,
private socialService: SocialService,
@Optional()
@Inject(ReuseTabService)
private reuseTabService: ReuseTabService,
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
private startupSrv: StartupService,
private http: _HttpClient,
private cdr: ChangeDetectorRef
) {
this.form = fb.group({
userName: [null, [Validators.required, Validators.pattern(/^(admin)$/)]],
password: [null, [Validators.required, Validators.pattern(/^(000000)$/)]],
mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
captcha: [null, [Validators.required]],
remember: [true]
});
}
// #region fields
get userName(): AbstractControl {
return this.form.get('userName')!;
}
get password(): AbstractControl {
return this.form.get('password')!;
}
get mobile(): AbstractControl {
return this.form.get('mobile')!;
}
get captcha(): AbstractControl {
return this.form.get('captcha')!;
}
form: FormGroup;
error = '';
type = 0;
loading = false;
// #region get captcha
count = 0;
interval$: any;
// #endregion
switch({ index }: NzTabChangeEvent): void {
this.type = index!;
}
getCaptcha(): void {
if (this.mobile.invalid) {
this.mobile.markAsDirty({ onlySelf: true });
this.mobile.updateValueAndValidity({ onlySelf: true });
return;
}
this.count = 59;
this.interval$ = setInterval(() => {
this.count -= 1;
if (this.count <= 0) {
clearInterval(this.interval$);
}
}, 1000);
}
// #endregion
submit(): void {
this.error = '';
if (this.type === 0) {
this.userName.markAsDirty();
this.userName.updateValueAndValidity();
this.password.markAsDirty();
this.password.updateValueAndValidity();
if (this.userName.invalid || this.password.invalid) {
return;
}
} else {
this.mobile.markAsDirty();
this.mobile.updateValueAndValidity();
this.captcha.markAsDirty();
this.captcha.updateValueAndValidity();
if (this.mobile.invalid || this.captcha.invalid) {
return;
}
}
// 默认配置中对所有HTTP请求都会强制 [校验](https://ng-alain.com/auth/getting-started) 用户 Token
// 然一般来说登录请求不需要校验因此可以在请求URL加上`/login?_allow_anonymous=true` 表示不触发用户 Token 校验
this.loading = true;
this.cdr.detectChanges();
this.http
.post('/login/account?_allow_anonymous=true', {
type: this.type,
userName: this.userName.value,
password: this.password.value
})
.pipe(
finalize(() => {
this.loading = true;
this.cdr.detectChanges();
})
)
.subscribe(res => {
if (res.msg !== 'ok') {
this.error = res.msg;
this.cdr.detectChanges();
return;
}
// 清空路由复用信息
this.reuseTabService.clear();
// 设置用户Token信息
// TODO: Mock expired value
res.user.expired = +new Date() + 1000 * 60 * 5;
this.tokenService.set(res.user);
// 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响
this.startupSrv.load().subscribe(() => {
let url = this.tokenService.referrer!.url || '/';
if (url.includes('/passport')) {
url = '/';
}
this.router.navigateByUrl(url);
});
});
}
// #region social
open(type: string, openType: SocialOpenType = 'href'): void {
let url = ``;
let callback = ``;
if (environment.production) {
callback = `https://ng-alain.github.io/ng-alain/#/passport/callback/${type}`;
} else {
callback = `http://localhost:4200/#/passport/callback/${type}`;
}
switch (type) {
case 'auth0':
url = `//cipchk.auth0.com/login?client=8gcNydIDzGBYxzqV0Vm1CX_RXH-wsWo5&redirect_uri=${decodeURIComponent(callback)}`;
break;
case 'github':
url = `//github.com/login/oauth/authorize?client_id=9d6baae4b04a23fcafa2&response_type=code&redirect_uri=${decodeURIComponent(
callback
)}`;
break;
case 'weibo':
url = `https://api.weibo.com/oauth2/authorize?client_id=1239507802&response_type=code&redirect_uri=${decodeURIComponent(callback)}`;
break;
}
if (openType === 'window') {
this.socialService
.login(url, '/', {
type: 'window'
})
.subscribe(res => {
if (res) {
this.settingsService.setUser(res);
this.router.navigateByUrl('/');
}
});
} else {
this.socialService.login(url, '/', {
type: 'href'
});
}
}
// #endregion
ngOnDestroy(): void {
if (this.interval$) {
clearInterval(this.interval$);
}
}
}

View File

@ -0,0 +1,13 @@
<result type="success" [title]="title" description="{{ 'app.register-result.activation-email' | i18n }}">
<ng-template #title>
<div class="title" style="font-size: 20px">
{{ 'app.register-result.msg' | i18n: params }}
</div>
</ng-template>
<button (click)="msg.success('email')" nz-button nzSize="large" [nzType]="'primary'">
{{ 'app.register-result.view-mailbox' | i18n }}
</button>
<button routerLink="/" nz-button nzSize="large">
{{ 'app.register-result.back-home' | i18n }}
</button>
</result>

View File

@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NzMessageService } from 'ng-zorro-antd/message';
@Component({
selector: 'passport-register-result',
templateUrl: './register-result.component.html'
})
export class UserRegisterResultComponent {
params = { email: '' };
email = '';
constructor(route: ActivatedRoute, public msg: NzMessageService) {
this.params.email = this.email = route.snapshot.queryParams['email'] || 'ng-alain@example.com';
}
}

View File

@ -0,0 +1,100 @@
<h3>{{ 'app.register.register' | i18n }}</h3>
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
<nz-alert *ngIf="error" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg"></nz-alert>
<nz-form-item>
<nz-form-control [nzErrorTip]="mailErrorTip">
<nz-input-group nzSize="large" nzAddonBeforeIcon="user">
<input nz-input formControlName="mail" placeholder="Email" />
</nz-input-group>
<ng-template #mailErrorTip let-i>
<ng-container *ngIf="i.errors?.required">{{ 'validation.email.required' | i18n }}</ng-container>
<ng-container *ngIf="i.errors?.email">{{ 'validation.email.wrong-format' | i18n }}</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="'validation.password.required' | i18n">
<nz-input-group
nzSize="large"
nzAddonBeforeIcon="lock"
nz-popover
nzPopoverPlacement="right"
nzPopoverTrigger="focus"
[(nzPopoverVisible)]="visible"
nzPopoverOverlayClassName="register-password-cdk"
[nzPopoverOverlayStyle]="{ 'width.px': 240 }"
[nzPopoverContent]="pwdCdkTpl"
>
<input nz-input type="password" formControlName="password" placeholder="Password" />
</nz-input-group>
<ng-template #pwdCdkTpl>
<div style="padding: 4px 0">
<ng-container [ngSwitch]="status">
<div *ngSwitchCase="'ok'" class="success">{{ 'validation.password.strength.strong' | i18n }}</div>
<div *ngSwitchCase="'pass'" class="warning">{{ 'validation.password.strength.medium' | i18n }}</div>
<div *ngSwitchDefault class="error">{{ 'validation.password.strength.short' | i18n }}</div>
</ng-container>
<div class="progress-{{ status }}">
<nz-progress
[nzPercent]="progress"
[nzStatus]="passwordProgressMap[status]"
[nzStrokeWidth]="6"
[nzShowInfo]="false"
></nz-progress>
</div>
<p class="mt-sm">{{ 'validation.password.strength.msg' | i18n }}</p>
</div>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="confirmErrorTip">
<nz-input-group nzSize="large" nzAddonBeforeIcon="lock">
<input nz-input type="password" formControlName="confirm" placeholder="Confirm Password" />
</nz-input-group>
<ng-template #confirmErrorTip let-i>
<ng-container *ngIf="i.errors?.required">{{ 'validation.confirm-password.required' | i18n }}</ng-container>
<ng-container *ngIf="i.errors?.matchControl">{{ 'validation.password.twice' | i18n }}</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="mobileErrorTip">
<nz-input-group nzSize="large" [nzAddOnBefore]="addOnBeforeTemplate">
<ng-template #addOnBeforeTemplate>
<nz-select formControlName="mobilePrefix" style="width: 100px">
<nz-option [nzLabel]="'+86'" [nzValue]="'+86'"></nz-option>
<nz-option [nzLabel]="'+87'" [nzValue]="'+87'"></nz-option>
</nz-select>
</ng-template>
<input formControlName="mobile" nz-input placeholder="Phone number" />
</nz-input-group>
<ng-template #mobileErrorTip let-i>
<ng-container *ngIf="i.errors?.required">{{ 'validation.phone-number.required' | i18n }}</ng-container>
<ng-container *ngIf="i.errors?.pattern">{{ 'validation.phone-number.wrong-format' | i18n }}</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="'validation.verification-code.required' | i18n">
<nz-row [nzGutter]="8">
<nz-col [nzSpan]="16">
<nz-input-group nzSize="large" nzAddonBeforeIcon="mail">
<input nz-input formControlName="captcha" placeholder="Captcha" />
</nz-input-group>
</nz-col>
<nz-col [nzSpan]="8">
<button type="button" nz-button nzSize="large" (click)="getCaptcha()" [disabled]="count > 0" nzBlock [nzLoading]="loading">
{{ count ? count + 's' : ('app.register.get-verification-code' | i18n) }}
</button>
</nz-col>
</nz-row>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<button nz-button nzType="primary" nzSize="large" type="submit" [nzLoading]="loading" class="submit">
{{ 'app.register.register' | i18n }}
</button>
<a class="login" routerLink="/passport/login">{{ 'app.register.sign-in' | i18n }}</a>
</nz-form-item>
</form>

View File

@ -0,0 +1,51 @@
@import '@delon/theme/index';
:host {
display: block;
width: 368px;
margin: 0 auto;
::ng-deep {
h3 {
margin-bottom: 20px;
font-size: 16px;
}
.submit {
width: 50%;
}
.login {
float: right;
line-height: @btn-height-lg;
}
}
}
::ng-deep {
.register-password-cdk {
.success,
.warning,
.error {
transition: color 0.3s;
}
.success {
color: @success-color;
}
.warning {
color: @warning-color;
}
.error {
color: @error-color;
}
.progress-pass > .progress {
.ant-progress-bg {
background-color: @warning-color;
}
}
}
}

View File

@ -0,0 +1,139 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { _HttpClient } from '@delon/theme';
import { MatchControl } from '@delon/util/form';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'passport-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserRegisterComponent implements OnDestroy {
constructor(fb: FormBuilder, private router: Router, private http: _HttpClient, private cdr: ChangeDetectorRef) {
this.form = fb.group(
{
mail: [null, [Validators.required, Validators.email]],
password: [null, [Validators.required, Validators.minLength(6), UserRegisterComponent.checkPassword.bind(this)]],
confirm: [null, [Validators.required, Validators.minLength(6)]],
mobilePrefix: ['+86'],
mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
captcha: [null, [Validators.required]]
},
{
validators: MatchControl('password', 'confirm')
}
);
}
// #region fields
get mail(): AbstractControl {
return this.form.get('mail')!;
}
get password(): AbstractControl {
return this.form.get('password')!;
}
get confirm(): AbstractControl {
return this.form.get('confirm')!;
}
get mobile(): AbstractControl {
return this.form.get('mobile')!;
}
get captcha(): AbstractControl {
return this.form.get('captcha')!;
}
form: FormGroup;
error = '';
type = 0;
loading = false;
visible = false;
status = 'pool';
progress = 0;
passwordProgressMap: { [key: string]: 'success' | 'normal' | 'exception' } = {
ok: 'success',
pass: 'normal',
pool: 'exception'
};
// #endregion
// #region get captcha
count = 0;
interval$: any;
static checkPassword(control: FormControl): NzSafeAny {
if (!control) {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self: any = this;
self.visible = !!control.value;
if (control.value && control.value.length > 9) {
self.status = 'ok';
} else if (control.value && control.value.length > 5) {
self.status = 'pass';
} else {
self.status = 'pool';
}
if (self.visible) {
self.progress = control.value.length * 10 > 100 ? 100 : control.value.length * 10;
}
}
getCaptcha(): void {
if (this.mobile.invalid) {
this.mobile.markAsDirty({ onlySelf: true });
this.mobile.updateValueAndValidity({ onlySelf: true });
return;
}
this.count = 59;
this.cdr.detectChanges();
this.interval$ = setInterval(() => {
this.count -= 1;
this.cdr.detectChanges();
if (this.count <= 0) {
clearInterval(this.interval$);
}
}, 1000);
}
// #endregion
submit(): void {
this.error = '';
Object.keys(this.form.controls).forEach(key => {
this.form.controls[key].markAsDirty();
this.form.controls[key].updateValueAndValidity();
});
if (this.form.invalid) {
return;
}
const data = this.form.value;
this.loading = true;
this.cdr.detectChanges();
this.http
.post('/register?_allow_anonymous=true', data)
.pipe(
finalize(() => {
this.loading = false;
this.cdr.detectChanges();
})
)
.subscribe(() => {
this.router.navigate(['passport', 'register-result'], { queryParams: { email: data.mail } });
});
}
ngOnDestroy(): void {
if (this.interval$) {
clearInterval(this.interval$);
}
}
}

View File

@ -0,0 +1,67 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SimpleGuard } from '@delon/auth';
import { environment } from '@env/environment';
// layout
import { LayoutBasicComponent } from '../layout/basic/basic.component';
import { LayoutPassportComponent } from '../layout/passport/passport.component';
// dashboard pages
import { DashboardComponent } from './dashboard/dashboard.component';
// single pages
import { CallbackComponent } from './passport/callback.component';
import { UserLockComponent } from './passport/lock/lock.component';
// passport pages
import { UserLoginComponent } from './passport/login/login.component';
import { UserRegisterResultComponent } from './passport/register-result/register-result.component';
import { UserRegisterComponent } from './passport/register/register.component';
const routes: Routes = [
{
path: '',
component: LayoutBasicComponent,
canActivate: [SimpleGuard],
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent, data: { title: '仪表盘' } },
{ path: 'exception', loadChildren: () => import('./exception/exception.module').then(m => m.ExceptionModule) },
// 业务子模块
// { path: 'widgets', loadChildren: () => import('./widgets/widgets.module').then(m => m.WidgetsModule) },
{ path: 'sys', loadChildren: () => import('./sys/sys.module').then(m => m.SysModule) }
]
},
// 空白布局
// {
// path: 'blank',
// component: LayoutBlankComponent,
// children: [
// ]
// },
// passport
{
path: 'passport',
component: LayoutPassportComponent,
children: [
{ path: 'login', component: UserLoginComponent, data: { title: '登录' } },
{ path: 'register', component: UserRegisterComponent, data: { title: '注册' } },
{ path: 'register-result', component: UserRegisterResultComponent, data: { title: '注册结果' } },
{ path: 'lock', component: UserLockComponent, data: { title: '锁屏' } }
]
},
// 单页不包裹Layout
{ path: 'passport/callback/:type', component: CallbackComponent },
{ path: '**', redirectTo: 'exception/404' }
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
useHash: environment.useHash,
// NOTICE: If you use `reuse-tab` component and turn on keepingScroll you can set to `disabled`
// Pls refer to https://ng-alain.com/components/reuse-tab
scrollPositionRestoration: 'top'
})
],
exports: [RouterModule]
})
export class RouteRoutingModule {}

View File

@ -0,0 +1,30 @@
import { NgModule, Type } from '@angular/core';
import { SharedModule } from '@shared';
// dashboard pages
import { DashboardComponent } from './dashboard/dashboard.component';
// single pages
import { CallbackComponent } from './passport/callback.component';
import { UserLockComponent } from './passport/lock/lock.component';
// passport pages
import { UserLoginComponent } from './passport/login/login.component';
import { UserRegisterResultComponent } from './passport/register-result/register-result.component';
import { UserRegisterComponent } from './passport/register/register.component';
import { RouteRoutingModule } from './routes-routing.module';
const COMPONENTS: Array<Type<void>> = [
DashboardComponent,
// passport pages
UserLoginComponent,
UserRegisterComponent,
UserRegisterResultComponent,
// single pages
CallbackComponent,
UserLockComponent
];
@NgModule({
imports: [SharedModule, RouteRoutingModule],
declarations: COMPONENTS
})
export class RoutesModule {}

View File

@ -0,0 +1,11 @@
<page-header [action]="phActionTpl">
<ng-template #phActionTpl>
<button (click)="add()" nz-button nzType="primary">新建</button>
</ng-template>
</page-header>
<nz-card>
<sf mode="search" [schema]="searchSchema" (formSubmit)="st.reset($event)" (formReset)="st.reset($event)"></sf>
<st #st [data]="url" [columns]="columns"></st>
</nz-card>
<tinymce [(ngModel)]="html"></tinymce>
<!-- <image-wrapper src="https://os.alipayobjects.com/rmsportal/mgesTPFxodmIwpi.png" desc="示意图"></image-wrapper> -->

View File

@ -0,0 +1,24 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { SysLogComponent } from './log.component';
describe('SysLogComponent', () => {
let component: SysLogComponent;
let fixture: ComponentFixture<SysLogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [SysLogComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SysLogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { STColumn, STComponent } from '@delon/abc/st';
import { SFSchema } from '@delon/form';
import { ModalHelper, _HttpClient } from '@delon/theme';
@Component({
selector: 'app-sys-log',
templateUrl: './log.component.html'
})
export class SysLogComponent implements OnInit {
url = `/user`;
searchSchema: SFSchema = {
properties: {
no: {
type: 'string',
title: '编号'
}
}
};
@ViewChild('st') private readonly st!: STComponent;
columns: STColumn[] = [
{ title: '编号', index: 'no' },
{ title: '调用次数', type: 'number', index: 'callNo' },
{ title: '头像', type: 'img', width: '50px', index: 'avatar' },
{ title: '时间', type: 'date', index: 'updatedAt' },
{
title: '',
buttons: [
// { text: '查看', click: (item: any) => `/form/${item.id}` },
// { text: '编辑', type: 'static', component: FormEditComponent, click: 'reload' },
]
}
];
constructor(private http: _HttpClient, private modal: ModalHelper) {}
ngOnInit(): void {
console.log(this.html);
}
html: any;
add(): void {
console.log(this.html);
// this.modal
// .createStatic(FormEditComponent, { i: { id: 0 } })
// .subscribe(() => this.st.reload());
}
}

View File

@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SysLogComponent } from './log/log.component';
const routes: Routes = [{ path: 'log', component: SysLogComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SysRoutingModule {}

View File

@ -0,0 +1,13 @@
import { NgModule, Type } from '@angular/core';
import { SharedModule } from '@shared';
import { SysLogComponent } from './log/log.component';
import { SysRoutingModule } from './sys-routing.module';
const COMPONENTS: Array<Type<void>> = [SysLogComponent];
@NgModule({
imports: [SharedModule, SysRoutingModule],
declarations: COMPONENTS
})
export class SysModule {}

View File

@ -0,0 +1,16 @@
:host {
width: 200px;
margin: 0 auto;
padding: 0 20px 8px;
text-align: center;
background: #f2f4f5;
::ng-deep {
.img {
max-width: calc(100% - 32px);
margin: 2.4em 1em;
vertical-align: middle;
box-shadow: 0 8px 20px rgb(143 168 191 / 35%);
}
}
}

View File

@ -0,0 +1,18 @@
/* eslint-disable prettier/prettier */
import { Component, Input } from '@angular/core';
@Component({
selector: 'image-wrapper',
template: `
<div [ngStyle]="style">
<img class="img" [src]="src" [alt]="desc" />
<div *ngIf="desc" class="desc">{{ desc }}</div>
</div>
`,
styleUrls: ['./index.less']
})
export class ImageWrapperComponent {
@Input()
style!: { [key: string]: string };
@Input() src: string | undefined;
@Input() desc: string | undefined;
}

View File

@ -0,0 +1,8 @@
// Components
// Utils
export * from './utils/yuan';
// Module
export * from './shared.module';
export * from './json-schema/json-schema.module';

View File

@ -0,0 +1,3 @@
# 建议统一在 `widgets` 目录下自定义小部件
> 注:@delon/form 本身提供 nz-zorro-antd 数据录入组件的全部实现,以及若干第三方组件的代码,可从[widgets-third](https://github.com/ng-alain/delon/tree/master/packages/form/widgets-third)中获取并放置 `widgets` 目录下注册即可。

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { DelonFormModule, WidgetRegistry } from '@delon/form';
import { SharedModule } from '../shared.module';
import { TestWidget } from './test/test.widget';
export const SCHEMA_THIRDS_COMPONENTS = [TestWidget];
@NgModule({
declarations: SCHEMA_THIRDS_COMPONENTS,
imports: [SharedModule, DelonFormModule.forRoot()],
exports: SCHEMA_THIRDS_COMPONENTS
})
export class JsonSchemaModule {
constructor(widgetRegistry: WidgetRegistry) {
widgetRegistry.register(TestWidget.KEY, TestWidget);
}
}

View File

@ -0,0 +1,20 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ControlWidget } from '@delon/form';
@Component({
selector: 'test',
template: `
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
test widget
</sf-item-wrap>
`,
preserveWhitespaces: false,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestWidget extends ControlWidget implements OnInit {
static readonly KEY = 'test';
ngOnInit(): void {
console.warn('init test widget');
}
}

View File

@ -0,0 +1,7 @@
import { PageHeaderModule } from '@delon/abc/page-header';
import { ResultModule } from '@delon/abc/result';
import { SEModule } from '@delon/abc/se';
import { STModule } from '@delon/abc/st';
import { SVModule } from '@delon/abc/sv';
export const SHARED_DELON_MODULES = [PageHeaderModule, STModule, SEModule, SVModule, ResultModule];

View File

@ -0,0 +1,45 @@
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
import { NzDrawerModule } from 'ng-zorro-antd/drawer';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzGridModule } from 'ng-zorro-antd/grid';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzInputNumberModule } from 'ng-zorro-antd/input-number';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { NzPopoverModule } from 'ng-zorro-antd/popover';
import { NzProgressModule } from 'ng-zorro-antd/progress';
import { NzSelectModule } from 'ng-zorro-antd/select';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzTabsModule } from 'ng-zorro-antd/tabs';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
export const SHARED_ZORRO_MODULES = [
NzFormModule,
NzGridModule,
NzButtonModule,
NzInputModule,
NzInputNumberModule,
NzAlertModule,
NzProgressModule,
NzSelectModule,
NzAvatarModule,
NzCardModule,
NzDropDownModule,
NzPopconfirmModule,
NzTableModule,
NzPopoverModule,
NzDrawerModule,
NzModalModule,
NzTabsModule,
NzToolTipModule,
NzIconModule,
NzCheckboxModule,
NzSpinModule
];

View File

@ -0,0 +1,63 @@
import { CommonModule } from '@angular/common';
import { NgModule, Type } from '@angular/core';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { DelonACLModule } from '@delon/acl';
import { DelonFormModule } from '@delon/form';
import { AlainThemeModule } from '@delon/theme';
import { NgxTinymceModule } from 'ngx-tinymce';
import { ImageWrapperComponent } from './components/image-wrapper';
import { SHARED_DELON_MODULES } from './shared-delon.module';
import { SHARED_ZORRO_MODULES } from './shared-zorro.module';
// #region third libs
const THIRDMODULES: Array<Type<void>> = [NgxTinymceModule];
// #endregion
// #region your componets & directives
const COMPONENTS: Array<Type<void>> = [ImageWrapperComponent];
const DIRECTIVES: Array<Type<void>> = [];
// #endregion
@NgModule({
imports: [
CommonModule,
FormsModule,
RouterModule,
ReactiveFormsModule,
AlainThemeModule.forChild(),
DelonACLModule,
DelonFormModule,
...SHARED_DELON_MODULES,
...SHARED_ZORRO_MODULES,
// third libs
...THIRDMODULES
],
declarations: [
// your components
...COMPONENTS,
...DIRECTIVES
],
exports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
AlainThemeModule,
DelonACLModule,
DelonFormModule,
...SHARED_DELON_MODULES,
...SHARED_ZORRO_MODULES,
// third libs
...THIRDMODULES,
// your components
...COMPONENTS,
...DIRECTIVES
]
})
export class SharedModule {}

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
// import { STWidgetRegistry } from '@delon/abc/st';
import { SharedModule } from '../shared.module';
export const STWIDGET_COMPONENTS = [];
@NgModule({
declarations: STWIDGET_COMPONENTS,
imports: [SharedModule],
exports: [...STWIDGET_COMPONENTS]
})
export class STWidgetModule {
// constructor(widgetRegistry: STWidgetRegistry) {
// widgetRegistry.register(STImgWidget.KEY, STImgWidget);
// }
}

View File

@ -0,0 +1,11 @@
/**
* RMB元字符串
*
* @param digits 2
*/
export function yuan(value: number | string, digits: number = 2): string {
if (typeof value === 'number') {
value = value.toFixed(digits);
}
return `&yen ${value}`;
}

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="222px" height="222px" viewBox="-4.092 0 222 222" enable-background="new -4.092 0 222 222"
xml:space="preserve">
<defs>
</defs>
<path fill="#F0776F" d="M195.333,129.506c-0.446,0-0.893,0-1.488-0.148c-6.399-0.744-11.013-6.548-10.269-12.947l6.994-57.891
c0.447-3.721-1.785-7.293-5.208-8.483l-47.176-16.816c-6.103-2.232-9.228-8.78-7.144-14.882c2.232-6.102,8.78-9.227,14.882-7.144
l47.176,16.816c13.989,4.911,22.472,18.603,20.687,33.336l-6.995,57.891C206.197,125.042,201.137,129.506,195.333,129.506z"/>
<path fill="#F0776F" d="M104.851,222.222c-5.209,0-10.417-1.34-15.329-4.019l-61.016-33.931
c-8.781-4.911-14.733-13.691-15.924-23.663L0.23,60.007c-1.786-14.733,6.995-28.723,20.983-33.634L96.665,0.627
c6.102-2.083,12.799,1.19,14.882,7.292c2.084,6.103-1.19,12.799-7.292,14.883L28.803,48.547c-3.572,1.191-5.804,4.763-5.357,8.632
l12.352,100.603c0.298,2.53,1.786,4.762,4.018,6.102l61.016,33.931c2.381,1.34,5.358,1.34,7.739,0l66.076-36.163
c2.232-1.19,3.87-3.571,4.167-6.102c0.744-6.399,6.549-11.013,12.948-10.269c6.398,0.744,11.012,6.548,10.269,12.947
c-1.191,9.971-7.293,18.9-16.073,23.812l-66.076,36.163C115.268,220.882,110.059,222.222,104.851,222.222z"/>
<path fill="#F0776F" d="M157.086,131.441l-37.8-68.309l-0.149-0.149c-2.679-4.613-7.738-7.59-13.096-7.59s-10.417,2.977-13.096,7.59
l-37.8,68.458c-2.828,5.208-1.042,11.607,4.167,14.436c5.208,2.827,11.608,1.041,14.436-4.167l7.292-13.245h50.004l7.292,13.245
c1.935,3.571,5.506,5.506,9.376,5.506c1.785,0,3.571-0.446,5.06-1.339C157.979,143.049,159.914,136.501,157.086,131.441z
M92.796,107.183l13.245-23.96l13.245,23.96H92.796z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="552px" height="222px" viewBox="-4.092 0 552 222" enable-background="new -4.092 0 552 222"
xml:space="preserve">
<defs>
</defs>
<path fill="#FFFFFF" d="M195.333,129.506c-0.446,0-0.893,0-1.488-0.148c-6.399-0.744-11.013-6.548-10.269-12.947l6.994-57.891
c0.447-3.721-1.785-7.293-5.208-8.483l-47.176-16.816c-6.103-2.232-9.228-8.78-7.144-14.882c2.232-6.102,8.78-9.227,14.882-7.144
l47.176,16.816c13.989,4.911,22.472,18.603,20.687,33.336l-6.995,57.891C206.197,125.042,201.137,129.506,195.333,129.506z"/>
<path fill="#FFFFFF" d="M104.851,222.222c-5.209,0-10.417-1.34-15.329-4.019l-61.016-33.931
c-8.781-4.911-14.733-13.691-15.924-23.663L0.23,60.007c-1.786-14.733,6.995-28.723,20.983-33.634L96.665,0.627
c6.102-2.083,12.799,1.19,14.882,7.292c2.084,6.103-1.19,12.799-7.292,14.883L28.803,48.547c-3.572,1.191-5.804,4.763-5.357,8.632
l12.352,100.603c0.298,2.53,1.786,4.762,4.018,6.102l61.016,33.931c2.381,1.34,5.358,1.34,7.739,0l66.076-36.163
c2.232-1.19,3.87-3.571,4.167-6.102c0.744-6.399,6.549-11.013,12.948-10.269c6.398,0.744,11.012,6.548,10.269,12.947
c-1.191,9.971-7.293,18.9-16.073,23.812l-66.076,36.163C115.268,220.882,110.059,222.222,104.851,222.222z"/>
<path fill="#FFFFFF" d="M157.086,131.441l-37.8-68.309l-0.149-0.149c-2.679-4.613-7.738-7.59-13.096-7.59s-10.417,2.977-13.096,7.59
l-37.8,68.458c-2.828,5.208-1.042,11.607,4.167,14.436c5.208,2.827,11.608,1.041,14.436-4.167l7.292-13.245h50.004l7.292,13.245
c1.935,3.571,5.506,5.506,9.376,5.506c1.785,0,3.571-0.446,5.06-1.339C157.979,143.049,159.914,136.501,157.086,131.441z
M92.796,107.183l13.245-23.96l13.245,23.96H92.796z"/>
<path fill="#FFFFFF" d="M252.497,113.075c17.46-47.694,32.79-79.42,47.481-97.305c6.813,1.277,21.506,9.794,23.848,13.414
c-22.144,26.189-38.113,54.934-52.166,93.898c-11.923,33.216-18.311,60.896-18.311,78.78c0,4.897,0,5.11,0.213,6.175
c-3.833-0.426-7.026-2.129-7.878-4.045c-0.64-1.491-3.833-4.897-5.75-6.175c-2.555-1.703-4.471-9.795-4.471-18.95
C235.464,166.518,241.639,142.884,252.497,113.075z"/>
<path fill="#FFFFFF" d="M374.289,113.926c-11.498,23.847-19.376,48.972-19.376,63.451c0,7.878,1.704,13.414,5.536,15.543
c-3.406,2.129-9.581,3.833-13.84,3.833c-7.665,0-11.498-5.11-11.498-15.757c0-7.878,2.769-19.376,8.092-33.216
c-11.498,25.977-30.235,47.269-43.437,49.398c-10.433-0.426-17.672-11.072-17.672-25.764c0-31.726,30.66-75.161,53.655-76.439
c5.749,1.491,15.331,11.498,15.97,16.821c-22.783,3.62-50.676,41.307-50.676,67.496c0,2.981,1.065,4.472,2.981,5.11
c8.304-1.703,18.524-13.627,33.854-39.178c5.962-10.007,14.479-25.977,20.653-38.751
C364.495,106.474,374.502,111.158,374.289,113.926z"/>
<path fill="#FFFFFF" d="M391.749,137.773c1.277-5.536,7.452-27.68,20.228-34.067c4.685,1.064,14.266,5.749,19.802,14.053
c-12.988,14.479-16.82,29.596-24.061,48.333c-2.98,7.878-6.174,17.034-6.387,23.209c0,5.109,0,7.026-1.278,7.026
c-2.129,0-15.543-5.536-15.543-18.099C384.51,173.331,386.213,163.324,391.749,137.773z M425.816,81.988
c-1.49-1.491-2.555-4.685-2.555-7.026c0-4.897,2.98-18.737,6.601-27.893c6.601,0,21.505,8.517,25.764,14.905
c-6.388,5.961-21.932,24.912-24.061,29.596c-3.407-0.639-6.388-3.407-6.388-5.749C425.178,84.969,425.391,83.691,425.816,81.988z"/>
<path fill="#FFFFFF" d="M486.712,109.668c-6.388,15.543-28.745,45.778-30.448,58.766c8.943-9.581,23.422-27.467,28.318-32.576
c10.859-11.498,30.874-32.577,44.714-32.577c7.239,0,12.35,6.175,15.117,11.498c0,0.639-2.342,0.852-6.175,5.323
c-7.665,10.007-22.144,38.113-22.144,55.573c0,7.026,1.916,8.304,5.323,8.304c2.98,0,7.026-2.13,8.942-2.13
c1.064,0.64,1.916,2.981,1.916,4.259c-4.258,3.833-13.414,8.729-18.311,8.729c-16.396,0-20.228-10.007-20.228-22.569
c0-7.026,1.916-15.756,11.71-38.326c-0.639,0-8.942,4.046-17.246,12.137c-13.201,12.775-23.209,24.486-34.28,39.816
c-6.175,8.729-10.859,15.33-14.266,17.247c-5.962-3.407-8.517-10.221-9.795-16.608c0-10.646,6.388-34.493,15.97-56.424
c10.858-24.912,20.866-39.816,23.208-40.881C476.491,91.569,483.73,100.299,486.712,109.668z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]>
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
x="0px" y="0px" width="222px" height="222px" viewBox="-4.092 0 222 222" enable-background="new -4.092 0 222 222"
xml:space="preserve">
<defs>
</defs>
<path fill="#FFFFFF" d="M195.333,129.506c-0.446,0-0.893,0-1.488-0.148c-6.399-0.744-11.013-6.548-10.269-12.947l6.994-57.891
c0.447-3.721-1.785-7.293-5.208-8.483l-47.176-16.816c-6.103-2.232-9.228-8.78-7.144-14.882c2.232-6.102,8.78-9.227,14.882-7.144
l47.176,16.816c13.989,4.911,22.472,18.603,20.687,33.336l-6.995,57.891C206.197,125.042,201.137,129.506,195.333,129.506z"/>
<path fill="#FFFFFF" d="M104.851,222.222c-5.209,0-10.417-1.34-15.329-4.019l-61.016-33.931
c-8.781-4.911-14.733-13.691-15.924-23.663L0.23,60.007c-1.786-14.733,6.995-28.723,20.983-33.634L96.665,0.627
c6.102-2.083,12.799,1.19,14.882,7.292c2.084,6.103-1.19,12.799-7.292,14.883L28.803,48.547c-3.572,1.191-5.804,4.763-5.357,8.632
l12.352,100.603c0.298,2.53,1.786,4.762,4.018,6.102l61.016,33.931c2.381,1.34,5.358,1.34,7.739,0l66.076-36.163
c2.232-1.19,3.87-3.571,4.167-6.102c0.744-6.399,6.549-11.013,12.948-10.269c6.398,0.744,11.012,6.548,10.269,12.947
c-1.191,9.971-7.293,18.9-16.073,23.812l-66.076,36.163C115.268,220.882,110.059,222.222,104.851,222.222z"/>
<path fill="#FFFFFF" d="M157.086,131.441l-37.8-68.309l-0.149-0.149c-2.679-4.613-7.738-7.59-13.096-7.59s-10.417,2.977-13.096,7.59
l-37.8,68.458c-2.828,5.208-1.042,11.607,4.167,14.436c5.208,2.827,11.608,1.041,14.436-4.167l7.292-13.245h50.004l7.292,13.245
c1.935,3.571,5.506,5.506,9.376,5.506c1.785,0,3.571-0.446,5.06-1.339C157.979,143.049,159.914,136.501,157.086,131.441z
M92.796,107.183l13.245-23.96l13.245,23.96H92.796z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,353 @@
{
"app": {
"name": "Alain",
"description": "Ng-zorro admin panel front-end framework"
},
"user": {
"name": "Admin",
"avatar": "./assets/tmp/img/avatar.jpg",
"email": "535915157@qq.com"
},
"menu": [
{
"text": "主导航",
"i18n": "menu.main",
"group": true,
"hideInBreadcrumb": true,
"children": [
{
"text": "仪表盘",
"i18n": "menu.dashboard",
"icon": "anticon-dashboard",
"children": [
{
"text": "仪表盘V1",
"link": "/dashboard/v1",
"i18n": "menu.dashboard.v1"
},
{
"text": "分析页",
"link": "/dashboard/analysis",
"i18n": "menu.dashboard.analysis"
},
{
"text": "监控页",
"link": "/dashboard/monitor",
"i18n": "menu.dashboard.monitor"
},
{
"text": "工作台",
"link": "/dashboard/workplace",
"i18n": "menu.dashboard.workplace"
}
]
},
{
"text": "快捷菜单",
"i18n": "menu.shortcut",
"icon": "anticon-rocket",
"shortcutRoot": true,
"children": []
},
{
"text": "小部件",
"i18n": "menu.widgets",
"link": "/widgets",
"icon": "anticon-appstore",
"badge": 2
}
]
},
{
"text": "Alain",
"i18n": "menu.alain",
"group": true,
"hideInBreadcrumb": true,
"children": [
{
"text": "样式",
"i18n": "menu.style",
"icon": "anticon-info",
"children": [
{
"text": "Typography",
"link": "/style/typography",
"i18n": "menu.style.typography",
"shortcut": true
},
{
"text": "Grid Masonry",
"link": "/style/gridmasonry",
"i18n": "menu.style.gridmasonry"
},
{
"text": "Colors",
"link": "/style/colors",
"i18n": "menu.style.colors"
}
]
},
{
"text": "Delon",
"i18n": "menu.delon",
"icon": "anticon-bulb",
"children": [
{
"text": "Dynamic Form",
"link": "/delon/form",
"i18n": "menu.delon.form"
},
{
"text": "Simple Table",
"link": "/delon/st",
"i18n": "menu.delon.table"
},
{
"text": "Util",
"link": "/delon/util",
"i18n": "menu.delon.util",
"acl": "role-a"
},
{
"text": "Print",
"link": "/delon/print",
"i18n": "menu.delon.print",
"acl": "role-b"
},
{
"text": "QR",
"link": "/delon/qr",
"i18n": "menu.delon.qr"
},
{
"text": "ACL",
"link": "/delon/acl",
"i18n": "menu.delon.acl"
},
{
"text": "Route Guard",
"link": "/delon/guard",
"i18n": "menu.delon.guard"
},
{
"text": "Cache",
"link": "/delon/cache",
"i18n": "menu.delon.cache"
},
{
"text": "Down File",
"link": "/delon/downfile",
"i18n": "menu.delon.downfile"
},
{
"text": "Xlsx",
"link": "/delon/xlsx",
"i18n": "menu.delon.xlsx"
},
{
"text": "Zip",
"link": "/delon/zip",
"i18n": "menu.delon.zip"
}
]
}
]
},
{
"text": "Pro",
"i18n": "menu.pro",
"group": true,
"hideInBreadcrumb": true,
"children": [
{
"text": "Form Page",
"i18n": "menu.form",
"link": "/pro/form",
"icon": "anticon-edit",
"children": [
{
"text": "Basic Form",
"link": "/pro/form/basic-form",
"i18n": "menu.form.basicform",
"shortcut": true
},
{
"text": "Step Form",
"link": "/pro/form/step-form",
"i18n": "menu.form.stepform"
},
{
"text": "Advanced Form",
"link": "/pro/form/advanced-form",
"i18n": "menu.form.advancedform"
}
]
},
{
"text": "List",
"i18n": "menu.list",
"icon": "anticon-appstore",
"children": [
{
"text": "Table List",
"link": "/pro/list/table-list",
"i18n": "menu.list.searchtable",
"shortcut": true
},
{
"text": "Basic List",
"link": "/pro/list/basic-list",
"i18n": "menu.list.basiclist"
},
{
"text": "Card List",
"link": "/pro/list/card-list",
"i18n": "menu.list.cardlist"
},
{
"text": "Search List",
"i18n": "menu.list.searchlist",
"children": [
{
"link": "/pro/list/articles",
"i18n": "menu.list.searchlist.articles"
},
{
"link": "/pro/list/projects",
"i18n": "menu.list.searchlist.projects",
"shortcut": true
},
{
"link": "/pro/list/applications",
"i18n": "menu.list.searchlist.applications"
}
]
}
]
},
{
"text": "Profile",
"i18n": "menu.profile",
"icon": "anticon-profile",
"children": [
{
"text": "Basic",
"link": "/pro/profile/basic",
"i18n": "menu.profile.basic"
},
{
"text": "Advanced",
"link": "/pro/profile/advanced",
"i18n": "menu.profile.advanced",
"shortcut": true
}
]
},
{
"text": "Result",
"i18n": "menu.result",
"icon": "anticon-check-circle",
"children": [
{
"text": "Success",
"link": "/pro/result/success",
"i18n": "menu.result.success"
},
{
"text": "Fail",
"link": "/pro/result/fail",
"i18n": "menu.result.fail"
}
]
},
{
"text": "Exception",
"i18n": "menu.exception",
"link": "/",
"icon": "anticon-exception",
"children": [
{
"text": "403",
"link": "/exception/403",
"i18n": "menu.exception.not-permission",
"reuse": false
},
{
"text": "404",
"link": "/exception/404",
"i18n": "menu.exception.not-find",
"reuse": false
},
{
"text": "500",
"link": "/exception/500",
"i18n": "menu.exception.server-error",
"reuse": false
}
]
},
{
"text": "Account",
"i18n": "menu.account",
"icon": "anticon-user",
"children": [
{
"text": "center",
"link": "/pro/account/center",
"i18n": "menu.account.center"
},
{
"text": "settings",
"link": "/pro/account/settings",
"i18n": "menu.account.settings"
}
]
}
]
},
{
"text": "More",
"i18n": "menu.more",
"group": true,
"hideInBreadcrumb": true,
"children": [
{
"text": "Report",
"i18n": "menu.report",
"icon": "anticon-cloud",
"children": [
{
"text": "Relation",
"link": "/data-v/relation",
"i18n": "menu.report.relation",
"reuse": false
}
]
},
{
"text": "Extras",
"i18n": "menu.extras",
"link": "/extras",
"icon": "anticon-link",
"children": [
{
"text": "Help Center",
"link": "/extras/helpcenter",
"i18n": "menu.extras.helpcenter"
},
{
"text": "Settings",
"link": "/extras/settings",
"i18n": "menu.extras.settings"
},
{
"text": "Poi",
"link": "/extras/poi",
"i18n": "menu.extras.poi"
}
]
}
]
}
]
}

View File

@ -0,0 +1,151 @@
{
"menu.search.placeholder": "Αναζήτηση ατόμων, αρχείων, φωτογραφιών...",
"menu.fullscreen": "Πλήρης οθόνη",
"menu.fullscreen.exit": "Έξοδος από πλήρη οθόνη",
"menu.clear.local.storage": "Καθαρισμός τοπικής μνήμης",
"menu.lang": "Γλώσσα",
"menu.main": "Κύριο μενού",
"menu.dashboard": "Πίνακας διαχείρισης",
"menu.dashboard.v1": "Προεπιλογή",
"menu.dashboard.analysis": "Ανάλυση",
"menu.dashboard.monitor": "Εποπτεία",
"menu.dashboard.workplace": "Χώρος εργασίας",
"menu.shortcut": "Συντομεύσεις",
"menu.widgets": "Γραφικά στοιχεία",
"menu.alain": "Alain",
"menu.style": "Στυλ",
"menu.style.typography": "Τυπογραφία",
"menu.style.gridmasonry": "Πλέγμα Masonry",
"menu.style.colors": "Χρώματα",
"menu.delon": "Βιβλιοθήκη Delon",
"menu.delon.form": "Δυναμική φόρμα",
"menu.delon.table": "Απλός πίνακας",
"menu.delon.util": "Εργαλεία",
"menu.delon.print": "Εκτύπωση",
"menu.delon.guard": "Προστασία διαδρομής",
"menu.delon.cache": "Προσωρινή μνήμη",
"menu.delon.qr": "QR",
"menu.delon.acl": "ACL",
"menu.delon.downfile": "Λήψη αρχείου",
"menu.delon.xlsx": "Excel",
"menu.delon.zip": "Zip",
"menu.pro": "Antd Pro",
"menu.form": "Φόρμα",
"menu.form.basicform": "Βασική φόρμα",
"menu.form.stepform": "Φόρμα βημάτων",
"menu.form.stepform.info": "Φόρμα βημάτων(γράψτε πληροφορίες μεταφοράς)",
"menu.form.stepform.confirm": "Φόρμα βημάτων(επιβεβαιώστε τις πληροφορίες μεταφοράς)",
"menu.form.stepform.result": "Φόρμα βημάτων(ολοκληρωμένη)",
"menu.form.advancedform": "Σύνθετη φόρμα",
"menu.list": "Λίστα",
"menu.list.searchtable": "Πίνακας αναζήτησης",
"menu.list.basiclist": "Βασική λίστα",
"menu.list.cardlist": "Λίστα καρτών",
"menu.list.searchlist": "Λίστα αναζήτησης",
"menu.list.searchlist.articles": "Λίστα αναζήτησης (άρθρα)",
"menu.list.searchlist.projects": "Λίστα αναζήτησης (έργα)",
"menu.list.searchlist.applications": "Λίστα αναζήτησης (εφαρμογές)",
"menu.profile": "Προφίλ",
"menu.profile.basic": "Βασικό προφίλ",
"menu.profile.advanced": "Σύνθετο προφίλ",
"menu.result": "Αποτέλεσμα",
"menu.result.success": "Επιτυχία",
"menu.result.fail": "Αποτυχία",
"menu.exception": "Εξαίρεση",
"menu.exception.not-permission": "403",
"menu.exception.not-find": "404",
"menu.exception.server-error": "500",
"menu.account": "Λογαριασμός",
"menu.account.center": "Κέντρο διαχείρισης λογαριασμού",
"menu.account.settings": "Ρυθμίσεις λογαριασμού",
"menu.account.trigger": "Πρόκληση σφάλματος",
"menu.account.logout": "Αποσύνδεση",
"menu.more": "Περισσότερα",
"menu.report": "Αναφορά",
"menu.report.relation": "Χάρτης συσχετίσεων",
"menu.extras": "Επιπλέον",
"menu.extras.helpcenter": "Κέντρο βοήθειας",
"menu.extras.settings": "Ρυθμίσεις",
"menu.extras.poi": "Poi",
"app.analysis.test": "Gongzhuan Αρ.{{no}} κατάστημα",
"app.analysis.introduce": "Εισαγωγή",
"app.analysis.total-sales": "Σύνολο πωλήσεων",
"app.analysis.day-sales": "Ημερήσιες πωλήσεις",
"app.analysis.visits": "Επισκέψεις",
"app.analysis.visits-trend": "Τάση επισκεψιμότητας",
"app.analysis.visits-ranking": "Κατατάξη επισκεψιμότητας",
"app.analysis.day-visits": "Ημερήσια επισκεψιμότητα",
"app.analysis.week": "Εβδομαδιαία αναλογία",
"app.analysis.day": "Ημερήσια αναλογία",
"app.analysis.payments": "Πληρωμές",
"app.analysis.conversion-rate": "Συναλλαγματική Ισοτιμία",
"app.analysis.operational-effect": "Λειτουργική επίδραση",
"app.analysis.sales-trend": "Τάση πωλήσεων καταστημάτων",
"app.analysis.sales-ranking": "Κατάταξη πωλήσεων",
"app.analysis.all-year": "Όλο τον χρόνο",
"app.analysis.all-month": "Όλο τον μήνα",
"app.analysis.all-week": "Όλη την εβδομάδα",
"app.analysis.all-today": "Όλη μέρα",
"app.analysis.search-users": "Αναζήτηση χρηστών",
"app.analysis.per-capita-search": "Αναζήτηση ανά κεφάλαιο",
"app.analysis.online-top-search": "(Ζωντανά) Κορυφαία αναζήτηση",
"app.analysis.the-proportion-of-sales": "Το ποσοστό των πωλήσεων",
"app.analysis.channel.all": "ΟΛΑ",
"app.analysis.channel.online": "Ζωντανά",
"app.analysis.channel.stores": "Καταστήματα",
"app.analysis.sales": "Πωλήσεις",
"app.analysis.traffic": "Κίνηση",
"app.analysis.table.rank": "Κατάταξη",
"app.analysis.table.search-keyword": "Λέξη κλειδί",
"app.analysis.table.users": "Χρήστες",
"app.analysis.table.weekly-range": "Εβδομαδιαίο εύρος",
"app.monitor.trading-activity": "Δραστηριότητα συναλλαγών σε πραγματικό χρόνο",
"app.monitor.total-transactions": "Σύνολο ημερήσιων συναλλαγών",
"app.monitor.sales-target": "Ποσοστό ολοκλήρωσης στόχου πωλήσεων",
"app.monitor.remaining-time": "Εναπομείναν χρόνος δραστηριότητας",
"app.monitor.total-transactions-per-second": "Σύνολο συναλλαγών ανά δευτερόλεπτο",
"app.monitor.activity-forecast": "Πρόβλεψη δραστηριότητας",
"app.monitor.efficiency": "Αποδοτικότητα",
"app.monitor.ratio": "Αναλογία",
"app.monitor.proportion-per-category": "Ποσοστό ανά κατηγορία",
"app.monitor.fast-food": "Γρήγορο φαγητό",
"app.monitor.western-food": "Δυτικό φαγητό",
"app.monitor.hot-pot": "Ζεστό φαγητό",
"app.monitor.waiting-for-implementation": "Αναμονή για υλοποίηση",
"app.monitor.popular-searches": "Δημοφιλείς αναζητήσεις",
"app.monitor.resource-surplus": "Πλεόνασμα πόρων",
"app.monitor.fund-surplus": "Κεφαλαιακό πλεόνασμα",
"app.lock": "Κλείδωμα",
"app.login.message-invalid-credentials": "Λάθος όνομα χρήστη ή κωδικός πρόσβασηςadmin/ant.design",
"app.login.message-invalid-verification-code": "Μη έγκυρος κωδικός επιβεβαίωσης",
"app.login.tab-login-credentials": "Στοιχεία σύνδεσης",
"app.login.tab-login-mobile": "Αριθμός κινητού",
"app.login.remember-me": "Να με θυμάσαι",
"app.login.forgot-password": "Ξέχασα τον κωδικό μου",
"app.login.sign-in-with": "Σύνδεση με",
"app.login.signup": "Εγγραφή",
"app.login.login": "Σύνδεση",
"app.register.register": "Εγγραφή",
"app.register.get-verification-code": "Λήψη κωδικού",
"app.register.sign-in": "Εχετε ήδη λογαριασμό;",
"app.register-result.msg": "Λογαριασμός:εγγεγραμμένος ως {{email}}",
"app.register-result.activation-email": "Το email ενεργοποίησης έχει σταλεί στο email σας και έχει ισχύ εώς 24 ώρες. Παρακαλούμε συνδεθείτε στο email σας εγκαίρως και κάντε κλικ στο σύνδεσμο του email για να ενεργοποιήσετε το λογαριασμό.",
"app.register-result.back-home": "Επιστροφή στην αρχική σελίδα",
"app.register-result.view-mailbox": "Προβολή αλληλογραφίας",
"validation.email.required": "Παρακαλώ εισάγετε το email σας!",
"validation.email.wrong-format": "Η μορφή της διεύθυνση email δεν είναι έγκυρη!",
"validation.password.required": "Παρακαλώ εισάγετε τον κωδικό πρόσβασης!",
"validation.password.twice": "Οι κωδικοί πρόσβασης που εισαγάγατε δεν ταιριάζουν!",
"validation.password.strength.msg": "Παρακαλώ εισάγετε τουλάχιστον 6 χαρακτήρες, μην χρησιμοποιείτε αδύναμους κωδικούς",
"validation.password.strength.strong": "Ισχύς κωδικού: ισχυρός",
"validation.password.strength.medium": "Ισχύς κωδικού: μέτριος",
"validation.password.strength.short": "Ισχύς κωδικού: αδύναμος",
"validation.confirm-password.required": "Παρακαλώ επιβεβαιώστε τον κωδικό πρόσβασης!",
"validation.phone-number.required": "Παρακαλω εισάγετε τον αριθμό τηλεφώνου σας!",
"validation.phone-number.wrong-format": "O αριθμός τηλεφώνου δέν είναι έγκυρος!",
"validation.verification-code.required": "Παρακαλώ εισάγετε τον κωδικό επιβεβαίωσης!",
"validation.title.required": "Παρακαλώ εισάγετε έναν τίτλο",
"validation.date.required": "Παρακαλώ επιλέξτε την ημερομηνία έναρξης και λήξης",
"validation.goal.required": "Παρακαλώ εισάγετε την περιγραφή του στόχου",
"validation.standard.required": "Παρακαλώ εισάγετε μια μέτρηση"
}

View File

@ -0,0 +1,153 @@
{
"menu.search.placeholder": "Search for people, file, photos...",
"menu.fullscreen": "Fullscreen",
"menu.fullscreen.exit": "Exit Fullscreen",
"menu.clear.local.storage": "Clear Local Storage",
"menu.lang": "Language",
"menu.main": "Main Navigation",
"menu.dashboard": "Dashboard",
"menu.dashboard.v1": "Default",
"menu.dashboard.analysis": "Analysis",
"menu.dashboard.monitor": "Monitor",
"menu.dashboard.workplace": "Workplace",
"menu.shortcut": "Shortcut",
"menu.widgets": "Widgets",
"menu.alain": "Alain",
"menu.style": "Style",
"menu.style.typography": "Typography",
"menu.style.gridmasonry": "Grid Masonry",
"menu.style.colors": "Colors",
"menu.delon": "Delon Lib",
"menu.delon.form": "Dynamic Form",
"menu.delon.table": "Simple table",
"menu.delon.util": "Util",
"menu.delon.print": "Print",
"menu.delon.guard": "Route Guard",
"menu.delon.cache": "Cache",
"menu.delon.qr": "QR",
"menu.delon.acl": "ACL",
"menu.delon.downfile": "Download File",
"menu.delon.xlsx": "Excel",
"menu.delon.zip": "Zip",
"menu.pro": "Antd Pro",
"menu.form": "Form",
"menu.form.basicform": "Basic Form",
"menu.form.stepform": "Step Form",
"menu.form.stepform.info": "Step Form(write transfer information)",
"menu.form.stepform.confirm": "Step Form(confirm transfer information)",
"menu.form.stepform.result": "Step Form(finished)",
"menu.form.advancedform": "Advanced Form",
"menu.list": "List",
"menu.list.searchtable": "Search Table",
"menu.list.basiclist": "Basic List",
"menu.list.cardlist": "Card List",
"menu.list.searchlist": "Search List",
"menu.list.searchlist.articles": "Search List(articles)",
"menu.list.searchlist.projects": "Search List(projects)",
"menu.list.searchlist.applications": "Search List(applications)",
"menu.profile": "Profile",
"menu.profile.basic": "Basic Profile",
"menu.profile.advanced": "Advanced Profile",
"menu.result": "Result",
"menu.result.success": "Success",
"menu.result.fail": "Fail",
"menu.exception": "Exception",
"menu.exception.not-permission": "403",
"menu.exception.not-find": "404",
"menu.exception.server-error": "500",
"menu.account": "Account",
"menu.account.center": "Account Center",
"menu.account.settings": "Account Settings",
"menu.account.trigger": "Trigger Error",
"menu.account.logout": "Logout",
"menu.more": "More",
"menu.report": "Report",
"menu.report.relation": "Relation Map",
"menu.extras": "Extra",
"menu.extras.helpcenter": "Help Center",
"menu.extras.settings": "Settings",
"menu.extras.poi": "Poi",
"app.analysis.test": "Gongzhuan No.{{no}} shop",
"app.analysis.introduce": "Introduce",
"app.analysis.total-sales": "Total Sales",
"app.analysis.day-sales": "Day Sales",
"app.analysis.visits": "Visits",
"app.analysis.visits-trend": "Visits Trend",
"app.analysis.visits-ranking": "Visits Ranking",
"app.analysis.day-visits": "Day Visits",
"app.analysis.week": "Week Ratio",
"app.analysis.day": "Day Ratio",
"app.analysis.payments": "Payments",
"app.analysis.conversion-rate": "Conversion Rate",
"app.analysis.operational-effect": "Operational Effect",
"app.analysis.sales-trend": "Stores Sales Trend",
"app.analysis.sales-ranking": "Sales Ranking",
"app.analysis.all-year": "All Year",
"app.analysis.all-month": "All Month",
"app.analysis.all-week": "All Week",
"app.analysis.all-today": "All day",
"app.analysis.search-users": "Search Users",
"app.analysis.per-capita-search": "Per Capita Search",
"app.analysis.online-top-search": "Online Top Search",
"app.analysis.the-proportion-of-sales": "The Proportion Of Sales",
"app.analysis.channel.all": "ALL",
"app.analysis.channel.online": "Online",
"app.analysis.channel.stores": "Stores",
"app.analysis.sales": "Sales",
"app.analysis.traffic": "Traffic",
"app.analysis.table.rank": "Rank",
"app.analysis.table.search-keyword": "Keyword",
"app.analysis.table.users": "Users",
"app.analysis.table.weekly-range": "Weekly Range",
"app.monitor.trading-activity": "Real-Time Trading Activity",
"app.monitor.total-transactions": "Total transactions today",
"app.monitor.sales-target": "Sales target completion rate",
"app.monitor.remaining-time": "Remaining time of activity",
"app.monitor.total-transactions-per-second": "Total transactions per second",
"app.monitor.activity-forecast": "Activity forecast",
"app.monitor.efficiency": "Efficiency",
"app.monitor.ratio": "Ratio",
"app.monitor.proportion-per-category": "Proportion Per Category",
"app.monitor.fast-food": "Fast food",
"app.monitor.western-food": "Western food",
"app.monitor.hot-pot": "Hot pot",
"app.monitor.waiting-for-implementation": "Waiting for implementation",
"app.monitor.popular-searches": "Popular Searches",
"app.monitor.resource-surplus": "Resource Surplus",
"app.monitor.fund-surplus": "Fund Surplus",
"app.lock": "Lock",
"app.login.message-invalid-credentials": "Invalid username or passwordadmin/ant.design",
"app.login.message-invalid-verification-code": "Invalid verification code",
"app.login.tab-login-credentials": "Credentials",
"app.login.tab-login-mobile": "Mobile number",
"app.login.remember-me": "Remember me",
"app.login.forgot-password": "Forgot your password?",
"app.login.sign-in-with": "Sign in with",
"app.login.signup": "Sign up",
"app.login.login": "Login",
"app.register.register": "Register",
"app.register.get-verification-code": "Get code",
"app.register.sign-in": "Already have an account?",
"app.register-result.msg": "Accountregistered at {{email}}",
"app.register-result.activation-email":
"The activation email has been sent to your email address and is valid for 24 hours. Please log in to the email in time and click on the link in the email to activate the account.",
"app.register-result.back-home": "Back to home",
"app.register-result.view-mailbox": "View mailbox",
"validation.email.required": "Please enter your email!",
"validation.email.wrong-format": "The email address is in the wrong format!",
"validation.password.required": "Please enter your password!",
"validation.password.twice": "The passwords entered twice do not match!",
"validation.password.strength.msg":
"Please enter at least 6 characters and don't use passwords that are easy to guess.",
"validation.password.strength.strong": "Strength: strong",
"validation.password.strength.medium": "Strength: medium",
"validation.password.strength.short": "Strength: too short",
"validation.confirm-password.required": "Please confirm your password!",
"validation.phone-number.required": "Please enter your phone number!",
"validation.phone-number.wrong-format": "Malformed phone number!",
"validation.verification-code.required": "Please enter the verification code!",
"validation.title.required": "Please enter a title",
"validation.date.required": "Please select the start and end date",
"validation.goal.required": "Please enter a description of the goal",
"validation.standard.required": "Please enter a metric"
}

Some files were not shown because too many files have changed in this diff Show More