Compare commits

..

50 Commits
v2.1.0 ... dev

Author SHA1 Message Date
禾几海
b00c6e5a2b chore: 更新依赖版本 2020-09-16 16:22:06 +08:00
禾几海
13c0529def feat: Upload a Build Artifact 2020-09-13 15:40:37 +08:00
禾几海
d59eb47f03 Merge branch 'dev' 2020-09-13 15:33:42 +08:00
禾几海
25cf2290b9 refactor: 修改copyright 2020-09-13 15:33:16 +08:00
禾几海
130e135e03 Merge branch 'dev' 2020-09-13 12:24:17 +08:00
禾几海
f8e30d8823 refactor: 修改copyright 2020-09-13 12:22:03 +08:00
禾几海
b42d3b7d33 Update issue templates 2020-09-07 12:20:57 +08:00
禾几海
138b93da09 fix(commonTable): 请求参数的传递 2020-09-05 17:19:07 +08:00
禾几海
072eaa12e8 fix: 参数异常 2020-09-05 17:09:10 +08:00
禾几海
d263cc132d feat(访客管理): 添加对位置信息的展示 2020-09-05 16:54:06 +08:00
禾几海
f044e9d01b fix(commonTable): 未设置action列导致的空值异常 2020-09-04 08:24:17 +08:00
禾几海
c14f495f8e chore: 本地存储favicon.ico和logo图片 2020-08-31 22:17:36 +08:00
禾几海
ab8a9154ec remove console.log() 2020-08-28 22:32:01 +08:00
禾几海
6da84e259e feature(commonTable): dynamic detection of data changes 2020-08-28 14:53:11 +08:00
禾几海
e95cd8fe23 fix(commonTable): error cause by shallow copy 2020-08-28 14:46:55 +08:00
禾几海
d8e6e9c5d9 refactor(commonTable): remove unused field 2020-08-28 11:36:42 +08:00
禾几海
990548b8d5 feat(commonTable): adjust column's width 2020-08-28 11:35:29 +08:00
禾几海
449adc4cee feat(commonTable): editable field 2020-08-28 11:25:45 +08:00
禾几海
22480569a2 Merge branch 'dev' 2020-08-28 09:38:15 +08:00
禾几海
58655bed94 fix(token): token signature error 2020-08-28 09:36:21 +08:00
禾几海
32e5c4daf0 Update build.yml 2020-08-27 21:58:48 +08:00
禾几海
6ea2f792db Update build.yml 2020-08-27 21:24:37 +08:00
禾几海
ab6d056d3a Update build.yml 2020-08-27 21:09:30 +08:00
禾几海
670b028384 手机端布局异常,移除i标签 2020-08-08 11:26:53 +08:00
禾几海
2e1fa1eb6a Update README.md 2020-08-07 22:20:35 +08:00
禾几海
c6aebd8d68 Create LICENSE 2020-08-07 22:16:04 +08:00
禾几海
1e8acd91c2 调整友链页面,友链管理页面 2020-08-07 22:05:03 +08:00
禾几海
398716e3ff 补全表单缺失项 2020-08-07 21:45:39 +08:00
禾几海
58d11c4fa8 icon 图标的颜色 2020-08-07 21:31:24 +08:00
禾几海
5454a747a7 加入网站图标预览和友链页链接自动补写 2020-08-07 21:29:30 +08:00
禾几海
953fba5b17 调整友链页面样式 2020-08-07 21:10:20 +08:00
禾几海
fee6f45ea9 新增获取随机颜色的方法 2020-08-07 21:10:10 +08:00
禾几海
7803629cd3 添加维护页面,修复bug 2020-08-05 23:20:38 +08:00
禾几海
a81f41e15c 友链添加按钮 2020-08-05 22:36:09 +08:00
禾几海
86378a46f2 typo 2020-08-05 22:33:53 +08:00
禾几海
53ddfe6e4c 全局异常提示 2020-08-05 22:30:07 +08:00
禾几海
44f251135c 路由跳转 2020-08-05 22:16:30 +08:00
禾几海
6fb62adb91 清理代码 2020-08-05 21:38:21 +08:00
禾几海
01c21f5732 删除ErrDispatch.ts 2020-08-05 21:35:10 +08:00
禾几海
66d523e69c 使用service过度error处理 2020-08-05 21:34:26 +08:00
禾几海
3ff80e5b54 解除继承关系 2020-08-05 21:31:24 +08:00
禾几海
8fb268f4cf 显示小header 2020-08-05 12:55:58 +08:00
禾几海
34ff380731 维护页面入口 2020-08-05 12:55:12 +08:00
禾几海
392c7f3996 添加维护页面 2020-08-05 12:54:58 +08:00
禾几海
0c8eed9243 ... 2020-08-01 21:43:36 +08:00
禾几海
7e0b2b0e78 Merge pull request #24 from xiaohai2271/feature-#23
调整友链申请
2020-08-01 21:27:19 +08:00
禾几海
6b63e7b02b 修复接口异常 2020-08-01 19:21:56 +08:00
禾几海
5d3b55b8b7 调整弹窗 2020-08-01 19:21:37 +08:00
禾几海
a6c3f16ddf 调整 2020-08-01 13:31:10 +08:00
禾几海
c75b17fa05 调整 2020-08-01 13:30:57 +08:00
43 changed files with 2240 additions and 2112 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -21,6 +21,12 @@ jobs:
- run: npm install -g @angular/cli
- run: bash build.sh
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.1.4
with:
name: dist
path: ./dist/index/*
- name: SCP
uses: appleboy/scp-action@master
with:
@@ -29,12 +35,12 @@ jobs:
password: ${{ secrets.SSH_PASSWORD }}
port: ${{ secrets.SSH_PORT }}
source: "index.tar"
target: "/www/wwwroot/celess.cn"
target: "/www/wwwroot/www.celess.cn"
- name: Run SSH command
uses: garygrossgarten/github-action-ssh@v0.5.0
with:
command: cd /www/wwwroot/celess.cn && bash deploy.sh
command: cd /www/wwwroot/www.celess.cn && bash deploy.sh
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
password: ${{ secrets.SSH_PASSWORD }}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 禾几海
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -51,15 +51,13 @@
##### 构建
>
>
> > 1. 进入index目录
> > 2. npm install
> > 3. 修改环境数据中的host
> >
> > 1. npm install
> > 2. 修改环境数据中的host
> >
> > - ` /src/environments/environment.ts` (本地开发环境)
> > - `/src/environments/environment-prod.ts`(线上发布环境)
> > 4. ng build --prod
> > 3. ng build --prod
>
>
>可使用项目根目录的`build.sh` 脚本进行构建,但是 两个项目中的环境里面的变量仍需自己修改
@@ -68,7 +66,7 @@
##### 发布
-`index/dist/index`下的全部文件上传到网站根目录
-`dist/index`下的全部文件上传到网站根目录
- 目录结构如下:

3193
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,41 +11,41 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^10.0.3",
"@angular/common": "^10.0.3",
"@angular/compiler": "^10.0.3",
"@angular/core": "^10.0.3",
"@angular/forms": "^10.0.3",
"@angular/platform-browser": "^10.0.3",
"@angular/platform-browser-dynamic": "^10.0.3",
"@angular/router": "^10.0.3",
"@angular/service-worker": "^10.0.3",
"@angular/animations": "^10.1.1",
"@angular/common": "^10.1.1",
"@angular/compiler": "^10.1.1",
"@angular/core": "^10.1.1",
"@angular/forms": "^10.1.1",
"@angular/platform-browser": "^10.1.1",
"@angular/platform-browser-dynamic": "^10.1.1",
"@angular/router": "^10.1.1",
"@angular/service-worker": "^10.1.1",
"jquery": "^3.5.1",
"ng-zorro-antd": "^9.3.0",
"nrm": "^1.2.1",
"rxjs": "^6.6.0",
"tslib": "^2.0.0",
"zone.js": "^0.10.3"
"rxjs": "^6.6.3",
"tslib": "^2.0.1",
"zone.js": "^0.11.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.1000.2",
"@angular/cli": "^10.0.2",
"@angular/compiler-cli": "^10.0.3",
"@angular/language-service": "^10.0.3",
"@types/jasmine": "^3.5.11",
"@angular-devkit/build-angular": "^0.1001.1",
"@angular/cli": "^10.1.1",
"@angular/compiler-cli": "^10.1.1",
"@angular/language-service": "^10.1.1",
"@types/jasmine": "^3.5.14",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^14.0.22",
"@types/node": "^14.10.2",
"codelyzer": "^6.0.0",
"jasmine-core": "^3.5.0",
"jasmine-core": "^3.6.0",
"jasmine-spec-reporter": "^5.0.2",
"karma": "^5.1.0",
"karma": "^5.2.2",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "^3.3.1",
"karma-jasmine": "^4.0.1",
"karma-jasmine-html-reporter": "^1.5.4",
"protractor": "^7.0.0",
"ts-node": "^8.10.2",
"tslint": "^6.1.2",
"typescript": "^3.9.6"
"ts-node": "^9.0.0",
"tslint": "^6.1.3",
"typescript": "^4.0.2"
}
}

View File

@@ -1,39 +1,29 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {forwardRef, Inject, Injectable} from '@angular/core';
import {Article} from '../class/Article';
import {HttpService} from './http/http.service';
import {PageList} from '../class/HttpReqAndResp';
import {ErrDispatch} from '../class/ErrDispatch';
import {ArticleReq} from '../class/Article';
import {Category, Tag} from '../class/Tag';
import {Comment} from '../class/Comment';
import {CommentReq} from '../class/Comment';
import {Link} from '../class/Link';
import {ApplyLinkReq, Link} from '../class/Link';
import {User} from '../class/User';
import {LoginReq} from '../class/User';
import {LocalStorageService} from '../services/local-storage.service';
import {Visitor} from '../class/Visitor';
import {UpdateInfo} from '../class/UpdateInfo';
@Injectable({
providedIn: 'root'
})
export class ApiService extends HttpService {
export class ApiService {
constructor(httpClient: HttpClient,
localStorageService: LocalStorageService) {
super(httpClient, localStorageService);
}
setErrDispatch(errDispatch: ErrDispatch) {
super.setErrDispatch(errDispatch);
constructor(private httpService: HttpService) {
}
createArticle(article: ArticleReq) {
article.id = null;
return super.Service<Article>({
return this.httpService.Service<Article>({
path: '/admin/article/create',
contentType: 'application/json',
method: 'POST',
@@ -42,7 +32,7 @@ export class ApiService extends HttpService {
}
deleteArticle(id: number) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: '/admin/article/del',
method: 'DELETE',
queryParam: {articleID: id}
@@ -50,7 +40,7 @@ export class ApiService extends HttpService {
}
articles(pageNumber: number = 1, pageSize: number = 5) {
return super.Service<PageList<Article>>({
return this.httpService.Service<PageList<Article>>({
path: '/articles',
method: 'GET',
queryParam: {
@@ -61,7 +51,7 @@ export class ApiService extends HttpService {
}
adminArticles(pageNumber: number = 1, pageSize: number = 10) {
return super.Service<PageList<Article>>({
return this.httpService.Service<PageList<Article>>({
path: '/admin/articles',
method: 'GET',
queryParam: {
@@ -72,7 +62,7 @@ export class ApiService extends HttpService {
}
updateArticle(article: ArticleReq) {
return super.Service<Article>({
return this.httpService.Service<Article>({
path: '/admin/article/update',
method: 'PUT',
contentType: 'application/json',
@@ -81,7 +71,7 @@ export class ApiService extends HttpService {
}
getArticle(articleId: number, is4Update: boolean = false) {
return super.Service<Article>({
return this.httpService.Service<Article>({
path: `/article/articleID/${articleId}`,
method: 'GET',
queryParam: {update: is4Update},
@@ -89,7 +79,7 @@ export class ApiService extends HttpService {
}
articlesByCategory(category: string, pageNumber: number = 1, pageSize: number = 10) {
return super.Service<PageList<Article>>({
return this.httpService.Service<PageList<Article>>({
path: `/articles/category/${category}`,
method: 'GET',
queryParam: {
@@ -100,7 +90,7 @@ export class ApiService extends HttpService {
}
articlesByTag(tag: string, pageNumber: number = 1, pageSize: number = 10) {
return super.Service<PageList<Article>>({
return this.httpService.Service<PageList<Article>>({
path: `/articles/tag/${tag}`,
method: 'GET',
queryParam: {
@@ -111,14 +101,14 @@ export class ApiService extends HttpService {
}
categories() {
return super.Service<PageList<Category>>({
return this.httpService.Service<PageList<Category>>({
path: '/categories',
method: 'GET'
});
}
createCategory(nameStr: string) {
return super.Service<Category>({
return this.httpService.Service<Category>({
path: '/admin/category/create',
method: 'POST',
queryParam: {name: nameStr}
@@ -126,7 +116,7 @@ export class ApiService extends HttpService {
}
deleteCategory(categoryId: number) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: '/admin/category/del',
method: 'DELETE',
queryParam: {id: categoryId}
@@ -134,7 +124,7 @@ export class ApiService extends HttpService {
}
updateCategory(categoryId: number, nameStr: string) {
return super.Service<Category>({
return this.httpService.Service<Category>({
path: '/admin/category/update',
method: 'PUT',
queryParam: {id: categoryId, name: nameStr}
@@ -142,7 +132,7 @@ export class ApiService extends HttpService {
}
tags(pageNumber: number = 1, pageSize: number = 10) {
return super.Service<PageList<Tag>>({
return this.httpService.Service<PageList<Tag>>({
path: '/tags',
method: 'GET',
queryParam: {
@@ -153,14 +143,14 @@ export class ApiService extends HttpService {
}
tagsNac() {
return super.Service<{ name: string, size: number }[]>({
return this.httpService.Service<{ name: string, size: number }[]>({
path: '/tags/nac',
method: 'GET'
});
}
createTag(nameStr: string) {
return super.Service<Tag>({
return this.httpService.Service<Tag>({
path: '/admin/tag/create',
method: 'POST',
queryParam: {name: nameStr}
@@ -168,7 +158,7 @@ export class ApiService extends HttpService {
}
deleteTag(TagId: number) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: '/admin/tag/del',
method: 'DELETE',
queryParam: {id: TagId}
@@ -176,7 +166,7 @@ export class ApiService extends HttpService {
}
updateTag(TagId: number, nameStr: string) {
return super.Service<Tag>({
return this.httpService.Service<Tag>({
path: '/admin/tag/update',
method: 'PUT',
queryParam: {id: TagId, name: nameStr}
@@ -184,7 +174,7 @@ export class ApiService extends HttpService {
}
getCommentByTypeForAdmin(pagePath: string, pageNumber: number = 1, pageSize: number = 10) {
return super.Service<PageList<Comment>>({
return this.httpService.Service<PageList<Comment>>({
path: `/admin/comment/pagePath/${pagePath}`,
method: 'GET',
queryParam: {
@@ -195,7 +185,7 @@ export class ApiService extends HttpService {
}
getCommentByTypeForUser(pagePath: string, pageNumber: number = 1, pageSize: number = 10) {
return super.Service<PageList<Comment>>({
return this.httpService.Service<PageList<Comment>>({
path: `/user/comment/pagePath/${pagePath}`,
method: 'GET',
queryParam: {
@@ -206,7 +196,7 @@ export class ApiService extends HttpService {
}
deleteComment(idNumer: number) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: `/user/comment/del`,
method: 'DELETE',
queryParam: {id: idNumer}
@@ -214,7 +204,7 @@ export class ApiService extends HttpService {
}
updateComment(commentReq: CommentReq) {
return super.Service<Comment>({
return this.httpService.Service<Comment>({
path: `/user/comment/update`,
method: 'PUT',
data: commentReq,
@@ -223,7 +213,7 @@ export class ApiService extends HttpService {
}
comments(pagePath: string, pageSize: number = 10, pageNumber: number = 1) {
return super.Service<PageList<Comment>>({
return this.httpService.Service<PageList<Comment>>({
path: `/comment/pagePath/${pagePath}`,
method: 'GET',
queryParam: {
@@ -234,7 +224,7 @@ export class ApiService extends HttpService {
}
createComment(commentReq: CommentReq) {
return super.Service<Comment>({
return this.httpService.Service<Comment>({
path: '/user/comment/create',
method: 'POST',
contentType: 'application/json',
@@ -244,7 +234,7 @@ export class ApiService extends HttpService {
counts() {
return super.Service<{
return this.httpService.Service<{
articleCount: number,
visitorCount: number,
categoryCount: number,
@@ -257,7 +247,7 @@ export class ApiService extends HttpService {
}
adminLinks(pageSize: number = 10, pageNumber: number = 1) {
return super.Service<PageList<Link>>({
return this.httpService.Service<PageList<Link>>({
path: '/admin/links',
method: 'GET',
queryParam: {
@@ -268,7 +258,7 @@ export class ApiService extends HttpService {
}
createLink(linkReq: Link) {
return super.Service<Link>({
return this.httpService.Service<Link>({
path: '/admin/links/create',
method: 'POST',
data: linkReq,
@@ -277,14 +267,14 @@ export class ApiService extends HttpService {
}
deleteLink(idNumber: number) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: `/admin/links/del/${idNumber}`,
method: 'DELETE',
});
}
updateLink(linkReq: Link) {
return super.Service<Link>({
return this.httpService.Service<Link>({
path: '/admin/links/update',
method: 'PUT',
data: linkReq,
@@ -292,26 +282,34 @@ export class ApiService extends HttpService {
});
}
applyLink(link: Link) {
return super.Service<string>({
applyLink(link: ApplyLinkReq) {
return this.httpService.Service<string>({
path: '/apply',
method: 'POST',
data: link,
contentType: 'application/json'
});
}
reapplyLink(keyStr: string) {
return this.httpService.Service<string>({
path: '/reapply',
method: 'POST',
queryParam: {
name: link.name,
url: link.url
key: keyStr
}
});
}
links() {
return super.Service<Link[]>({
return this.httpService.Service<Link[]>({
path: '/links',
method: 'GET',
});
}
verifyImgCode(codeStr: string) {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/verCode',
method: 'POST',
queryParam: {code: codeStr}
@@ -320,7 +318,7 @@ export class ApiService extends HttpService {
login(loginReq: LoginReq) {
return super.Service<User>({
return this.httpService.Service<User>({
path: '/login',
method: 'POST',
contentType: 'application/json',
@@ -329,14 +327,14 @@ export class ApiService extends HttpService {
}
logout() {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/logout',
method: 'GET',
});
}
registration(emailStr: string, pwd: string) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: '/registration',
method: 'POST',
queryParam: {
@@ -347,7 +345,7 @@ export class ApiService extends HttpService {
}
resetPwd(idStr: string, emailStr: string, pwdStr: string) {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/resetPwd',
method: 'POST',
queryParam: {
@@ -359,7 +357,7 @@ export class ApiService extends HttpService {
}
emailVerify(idStr: string, emailStr: string) {
return super.Service<void>({
return this.httpService.Service<void>({
path: '/emailVerify',
method: 'POST',
queryParam: {
@@ -371,7 +369,7 @@ export class ApiService extends HttpService {
sendResetPwdEmail(emailStr: string) {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/sendResetPwdEmail',
method: 'POST',
queryParam: {email: emailStr}
@@ -379,7 +377,7 @@ export class ApiService extends HttpService {
}
sendVerifyEmail(emailStr: string) {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/sendVerifyEmail',
method: 'POST',
queryParam: {email: emailStr}
@@ -387,14 +385,14 @@ export class ApiService extends HttpService {
}
userInfo() {
return super.Service<User>({
return this.httpService.Service<User>({
path: '/user/userInfo',
method: 'GET',
});
}
adminUpdateUser(user: User) {
return super.Service<User>({
return this.httpService.Service<User>({
path: '/admin/user',
method: 'PUT',
data: user,
@@ -403,14 +401,14 @@ export class ApiService extends HttpService {
}
deleteUser(id: number) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: `/admin/user/delete/${id}`,
method: 'DELETE',
});
}
multipleDeleteUser(idArray: number[]) {
return super.Service<{ id: number; msg: string; status: boolean }[]>({
return this.httpService.Service<{ id: number; msg: string; status: boolean }[]>({
path: `/admin/user/delete`,
method: 'DELETE',
data: idArray,
@@ -420,14 +418,14 @@ export class ApiService extends HttpService {
// 获取邮件是否已注册
emailStatus(email: string) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: `/emailStatus/${email}`,
method: 'GET'
})
}
updateUserInfo(descStr: string, disPlayNameStr: string) {
return super.Service<User>({
return this.httpService.Service<User>({
path: '/user/userInfo/update',
method: 'PUT',
queryParam: {
@@ -438,7 +436,7 @@ export class ApiService extends HttpService {
}
adminUsers(pageSize: number = 10, pageNumber: number = 1) {
return super.Service<PageList<User>>({
return this.httpService.Service<PageList<User>>({
path: '/admin/users',
method: 'GET',
queryParam: {
@@ -449,14 +447,14 @@ export class ApiService extends HttpService {
}
visit() {
return super.Service<Visitor>({
return this.httpService.Service<Visitor>({
path: '/visit',
method: 'POST'
});
}
adminVisitors(location: boolean = false, pageSize: number = 10, pageNumber: number = 1) {
return super.Service<PageList<Visitor>>({
return this.httpService.Service<PageList<Visitor>>({
path: '/admin/visitor/page',
method: 'GET',
queryParam: {
@@ -468,42 +466,42 @@ export class ApiService extends HttpService {
}
dayVisitCount() {
return super.Service<number>({
return this.httpService.Service<number>({
path: '/dayVisitCount',
method: 'GET',
});
}
getLocalIp() {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/ip',
method: 'GET',
});
}
getIpLocation(ip: string) {
return super.Service<string>({
return this.httpService.Service<string>({
path: `/ip/${ip}`,
method: 'GET',
});
}
visitorCount() {
return super.Service<number>({
return this.httpService.Service<number>({
path: `/visitor/count`,
method: 'GET',
});
}
webUpdate() {
return super.Service<{ id: number, info: string, time: string }[]>({
return this.httpService.Service<{ id: number, info: string, time: string }[]>({
path: '/webUpdate',
method: 'GET'
});
}
webUpdatePage(pageSize: number = 10, pageNumber: number = 1) {
return super.Service<PageList<{ id: number, info: string, time: string }>>({
return this.httpService.Service<PageList<{ id: number, info: string, time: string }>>({
path: '/webUpdate/pages',
method: 'GET',
queryParam: {
@@ -514,7 +512,7 @@ export class ApiService extends HttpService {
}
lastestUpdate() {
return super.Service<{
return this.httpService.Service<{
lastUpdateTime: string;
lastUpdateInfo: string;
lastCommit: string;
@@ -528,7 +526,7 @@ export class ApiService extends HttpService {
}
createWebUpdateInfo(infoStr: string) {
return super.Service<UpdateInfo>({
return this.httpService.Service<UpdateInfo>({
path: '/admin/webUpdate/create',
method: 'POST',
queryParam: {info: infoStr}
@@ -536,14 +534,14 @@ export class ApiService extends HttpService {
}
deleteWebUpdateInfo(idNumber: number) {
return super.Service<boolean>({
return this.httpService.Service<boolean>({
path: `/admin/webUpdate/del/${idNumber}`,
method: 'DELETE',
});
}
updateWebUpdateInfo(idNumber: number, infoStr: string) {
return super.Service<UpdateInfo>({
return this.httpService.Service<UpdateInfo>({
path: '/admin/webUpdate/update',
method: 'PUT',
queryParam: {id: idNumber, info: infoStr}
@@ -551,14 +549,14 @@ export class ApiService extends HttpService {
}
bingPic() {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/bingPic',
method: 'GET'
});
}
setPwd(pwdStr: string, newPwdStr: string, confirmPwdStr: string,) {
return super.Service<string>({
return this.httpService.Service<string>({
path: '/user/setPwd',
method: 'POST',
queryParam: {

View File

@@ -1,28 +1,27 @@
import {Injectable} from '@angular/core';
import {RequestObj} from '../../class/HttpReqAndResp';
import {Injectable, Injector} from '@angular/core';
import {RequestObj, Response} from '../../class/HttpReqAndResp';
import {HttpClient, HttpResponse} from '@angular/common/http';
import {environment} from '../../../environments/environment';
import {LocalStorageService} from '../../services/local-storage.service';
import {Response} from '../../class/HttpReqAndResp';
import {Observable, Observer, Subject} from 'rxjs';
import {ErrDispatch} from '../../class/ErrDispatch';
import {Observable, Observer, Subscription} from 'rxjs';
import {ErrorService} from '../../services/error.service';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class HttpService {
constructor(private httpClient: HttpClient,
protected localStorageService: LocalStorageService) {
private localStorageService: LocalStorageService,
private injector: Injector) {
}
private errorDispatch: ErrDispatch;
private subscriptionQueue: Subscription[] = [];
setErrDispatch(errDispatch: ErrDispatch) {
this.errorDispatch = errDispatch;
}
public getSubscriptionQueue = () => this.subscriptionQueue;
Service<T>(request: RequestObj) {
const errorService = this.injector.get(ErrorService);
request.url = null;
// 设置默认值
request.contentType = request.contentType == null ? 'application/x-www-form-urlencoded' : request.contentType;
@@ -55,25 +54,32 @@ export class HttpService {
const oob = new Observable<Response<T>>(o => observer = o);
observable.subscribe(o => {
const tokenFromReps = o.headers.get('Authorization');
if (tokenFromReps) {
this.localStorageService.setToken(tokenFromReps);
}
if (o.body.code !== 0) {
observer.error(o.body);
if (this.errorDispatch) {
this.errorDispatch.errHandler(o.body.code, o.body.msg, request);
const subscription = observable.subscribe({
next: o => {
const tokenFromReps = o.headers.get('Authorization');
if (tokenFromReps) {
this.localStorageService.setToken(tokenFromReps);
}
} else {
observer.next(o.body);
}
observer.complete();
if (o.body.code !== 0) {
observer.error(o.body);
errorService.httpException(o.body, request)
} else {
observer.next(o.body);
}
observer.complete();
},
error: err => {
errorService.httpError(err,request);
errorService.checkConnection();
this.subscriptionQueue.splice(this.subscriptionQueue.indexOf(subscription), 1)
},
complete: () => this.subscriptionQueue.splice(this.subscriptionQueue.indexOf(subscription), 1)
});
this.subscriptionQueue.push(subscription);
return oob;
}
private get<T>(request: RequestObj) {
get<T>(request: RequestObj) {
return this.httpClient.get<T>(request.url,
{
headers: request.header,
@@ -82,7 +88,7 @@ export class HttpService {
});
}
private post<T>(request: RequestObj) {
post<T>(request: RequestObj) {
return this.httpClient.post<T>(request.url, request.data,
{
headers: request.header,
@@ -91,7 +97,7 @@ export class HttpService {
});
}
private put<T>(request: RequestObj) {
put<T>(request: RequestObj) {
return this.httpClient.put<T>(request.url, request.data,
{
headers: request.header,
@@ -100,7 +106,7 @@ export class HttpService {
});
}
private delete<T>(request: RequestObj) {
delete<T>(request: RequestObj) {
return this.httpClient.delete<T>(request.url,
{
headers: request.header,

View File

@@ -11,17 +11,15 @@ const routes: Routes = [
{path: 'resetPwd', loadChildren: () => import('./view/reset-pwd/reset-pwd.module').then(mod => mod.ResetPwdModule)},
{path: 'write', loadChildren: () => import('./view/write/write.module').then(mod => mod.WriteModule)},
{path: 'links', loadChildren: () => import('./view/link/link.module').then(mod => mod.LinkModule)},
{path: 'admin', loadChildren: () => import('./view/admin/admin.module').then(mod => mod.AdminModule)},
{path: 'maintain', loadChildren: () => import('./view/maintain/maintain.module').then(mod => mod.MaintainModule)},
{
path: 'emailVerify',
loadChildren: () => import('./view/email-verify/email-verify.module').then(mod => mod.EmailVerifyModule)
},
{
path: 'user', loadChildren: () => import('./view/login-registration/login-registration.module')
.then(mod => mod.LoginRegistrationModule)
},
{
path: 'admin',
loadChildren: () => import('./view/admin/admin.module').then(mod => mod.AdminModule),
path: 'user',
loadChildren: () => import('./view/login-registration/login-registration.module').then(mod => mod.LoginRegistrationModule)
},
{
path: '**',

View File

@@ -1,5 +1,5 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {forwardRef, NgModule} from '@angular/core';
import {AppComponent} from './app.component';
import {NgZorroAntdModule, NZ_I18N, zh_CN} from 'ng-zorro-antd';
import {FormsModule} from '@angular/forms';
@@ -12,8 +12,14 @@ import {FooterComponent} from './components/footer/footer.component';
import {AppRoutingModule} from './app-routing.module';
import {LoginRegistrationModule} from './view/login-registration/login-registration.module';
import {AdminModule} from './view/admin/admin.module';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import {ServiceWorkerModule} from '@angular/service-worker';
import {environment} from '../environments/environment';
import {HttpService} from './api/http/http.service';
import {ErrorService} from './services/error.service';
import {ComponentStateService} from './services/component-state.service';
import {GlobalUserService} from './services/global-user.service';
import {LocalStorageService} from './services/local-storage.service';
import {ApiService} from './api/api.service';
registerLocaleData(zh);
@@ -33,9 +39,17 @@ registerLocaleData(zh);
BrowserAnimationsModule,
LoginRegistrationModule,
AdminModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
],
providers: [
ComponentStateService,
GlobalUserService,
LocalStorageService,
HttpService,
ApiService,
ErrorService,
{provide: NZ_I18N, useValue: zh_CN},
],
providers: [{provide: NZ_I18N, useValue: zh_CN}],
exports: [],
bootstrap: [AppComponent]
})

View File

@@ -1,5 +0,0 @@
import {RequestObj} from './HttpReqAndResp';
export interface ErrDispatch {
errHandler(code: number, msg: string, request?: RequestObj): void;
}

View File

@@ -6,7 +6,7 @@ export class RequestObj {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: {};
contentType?: 'application/json' | 'application/x-www-form-urlencoded';
queryParam?: {};
queryParam?: { [key: string]: any };
header?: HttpHeaders | {
[header: string]: string | string[];
};

View File

@@ -6,3 +6,13 @@ export class Link {
iconPath: string;
desc: string;
}
export class ApplyLinkReq {
desc: string;
email: string;
iconPath: string;
linkUrl: string;
name: string;
url: string
}

View File

@@ -5,8 +5,8 @@
鄂ICP备18023929号
</a>
<div>
© 2019 <a href="https://www.celess.cn">小海博客</a> -
<span>郑海 </span> <span *ngIf="gName">& {{gName}} </span>版权所有
© 2020 <a href="https://www.celess.cn">小海博客</a> -
<span>{{bName}} </span> <span *ngIf="gName">& {{gName}} </span>版权所有
</div>
</div>
</div>

View File

@@ -11,7 +11,8 @@ export class FooterComponent implements OnInit {
constructor(public componentStateService: ComponentStateService) {
}
readonly gName: string;
readonly gName: string = '何梦幻';
readonly bName: string = '郑海';
ngOnInit() {
}

View File

@@ -37,7 +37,8 @@ export class HeaderComponent implements OnInit {
});
// 订阅一级路由的变化
componentStateService.watchRouterChange().subscribe(prefix => {
if (prefix === '/user' || prefix === '/write' || prefix === '/update') {
// TODO:: 使用service来获取 size
if (prefix === '/user' || prefix === '/write' || prefix === '/update' || prefix === '/maintain') {
this.size = 'default';
} else {
this.size = 'large';

View File

@@ -0,0 +1,67 @@
import {Injectable, Injector} from '@angular/core';
import {RequestObj, Response} from '../class/HttpReqAndResp';
import {environment} from '../../environments/environment';
import {Router} from '@angular/router';
import {ComponentStateService} from './component-state.service';
import {NzNotificationService} from 'ng-zorro-antd';
import {HttpService} from '../api/http/http.service';
import {LocalStorageService} from './local-storage.service';
@Injectable({
providedIn: 'root'
})
export class ErrorService {
constructor(/*private httpService: HttpService,*/
private router: Router,
private injector: Injector,
private componentStateService: ComponentStateService,
private notification: NzNotificationService,
private localStorageService: LocalStorageService) {
}
private static HTTP_ERROR_COUNT: number = 0;
private readonly MAINTAIN_PAGE_PREFIX = '/maintain'
private readonly ADMIN_PAGE_PREFIX = '/admin'
public httpError(err: any, request: RequestObj) {
if (!environment.production) {
console.log('error=>', err, request)
}
ErrorService.HTTP_ERROR_COUNT++;
// this.httpService.getSubscriptionQueue().map(a => a.unsubscribe())
}
public httpException(response: Response<any>, request: RequestObj) {
if (!environment.production)
console.log('exception=>', response, request)
if (response.code === -1 && response.msg === '重复请求') return
if (this.componentStateService.currentPath === this.ADMIN_PAGE_PREFIX) {
this.notification.create('error', `请求失败<${response.code}>`, `${response.msg}`);
}
// 3830 token签名错误
if (response.code === 3830) {
this.localStorageService.removeToken();
}
}
public checkConnection() {
// The HTTP_ERROR_COUNT is start with 1 in this function
if (ErrorService.HTTP_ERROR_COUNT === 1) {
const req: RequestObj = {
path: '/headerInfo',
method: 'GET',
url: environment.host + '/headerInfo'
}
this.injector.get(HttpService).get(req).subscribe({
next: () => null,
error: () => {
if (this.componentStateService.currentPath !== this.MAINTAIN_PAGE_PREFIX) {
this.router.navigateByUrl(this.MAINTAIN_PAGE_PREFIX)
}
ErrorService.HTTP_ERROR_COUNT = 0;
}
})
}
}
}

View File

@@ -1,4 +1,9 @@
export const ColorList: { bgColor: string, fontColor: string }[] = [
export class Color {
bgColor: string;
fontColor: string
}
export const ColorList: Color[] = [
{bgColor: '#7bcfa6', fontColor: '#000000'}, // 石青
{bgColor: '#bce672', fontColor: '#000000'}, // 松花色
{bgColor: '#ff8936', fontColor: '#000000'}, // 橘黄
@@ -7,3 +12,27 @@ export const ColorList: { bgColor: string, fontColor: string }[] = [
{bgColor: '#3eede7', fontColor: '#000000'}, // 碧蓝
{bgColor: '#177cb0', fontColor: '#ffffff'}, // 靛青
];
export const ColorListLength = ColorList.length
/**
* 获取一组随机颜色
* @param count 数量
*/
export function RandomColor(count: number = 1): Color[] {
const map = new Map<number, number>();
ColorList.forEach((color, index) => map.set(index, 0))
const colorArray: Color[] = [];
const oneRandomColor = () => {
const minValue = Math.min.apply(null, Array.from(map.values()))
const keys = Array.from(map.keys()).filter(key => map.get(key) === minValue);
const keyIndex = Math.floor(Math.random() * keys.length);
const index = keys[keyIndex];
map.set(index, minValue + 1);
return ColorList[index]
};
for (let i = 0; i < count; i++) {
colorArray.push(oneRandomColor());
}
return colorArray;
}

View File

@@ -4,6 +4,7 @@
[headData]="headData"
[template]="{open:{temp:open,param:{true:'可见',false:'不可见'}},delete:{temp:deleteTemp,param:{true:'已删除',false:'未删除'}}}"
>
<button nz-button (click)="addLink()">添加</button>
</common-table>
<ng-template #open let-value="value">
@@ -19,14 +20,14 @@
(nzOnCancel)="modalVisible = false" [nzClosable]="true" [nzOkDisabled]="!formGroup.valid">
<form nz-form [formGroup]="formGroup">
<nz-form-item>
<nz-form-label nzRequired>网站名称</nz-form-label>
<nz-form-control nzErrorTip="网站名称不可为空">
<nz-form-label nzFlex="80px" nzRequired>网站名称</nz-form-label>
<nz-form-control nzFlex="auto" nzErrorTip="网站名称不可为空">
<input nz-input formControlName="name">
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired>网站链接</nz-form-label>
<nz-form-control [nzErrorTip]="nameErrTip">
<nz-form-label nzFlex="80px" nzRequired>网站链接</nz-form-label>
<nz-form-control nzFlex="auto" [nzErrorTip]="nameErrTip">
<input nz-input formControlName="url">
<ng-template #nameErrTip>
<div *ngIf="formGroup.controls.url.hasError('required')">网站链接不可为空</div>
@@ -36,13 +37,32 @@
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzRequired>是否公开</nz-form-label>
<nz-form-control nzErrorTip="不可为空">
<nz-form-label nzFlex="80px" nzRequired>是否公开</nz-form-label>
<nz-form-control nzFlex="auto" nzErrorTip="不可为空">
<nz-select nzPlaceHolder="请选择" formControlName="open" [nzAllowClear]="true">
<nz-option [nzValue]="true" nzLabel="公开"></nz-option>
<nz-option [nzValue]="false" nzLabel="不公开"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFlex="80px">网站图标</nz-form-label>
<nz-form-control nzFlex="auto" nzErrorTip="链接格式不正确">
<nz-input-group [nzSuffix]="icon" nzSize="large">
<input nz-input formControlName="iconPath">
</nz-input-group>
<ng-template #icon>
<img style="width: 25px;height: 25px" *ngIf="formGroup.value.iconPath"
[src]="formGroup.value.iconPath" alt="icon">
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFlex="80px">网站描述</nz-form-label>
<nz-form-control nzFlex="auto" nzErrorTip="可输入最大文字长度为255">
<textarea nz-input formControlName="desc" [nzAutosize]="{ minRows: 2, maxRows: 6 }"></textarea>
</nz-form-control>
</nz-form-item>
</form>
</nz-modal>

View File

@@ -22,6 +22,8 @@ export class AdminLinkComponent implements OnInit {
name: new FormControl(null, [Validators.required]),
url: new FormControl(null, [Validators.required, Validators.pattern(/^(https:\/\/|http:\/\/|)([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/)]),
open: new FormControl(null, [Validators.required]),
desc: new FormControl(null, [Validators.maxLength(255)]),
iconPath: new FormControl(null, [Validators.pattern(/^(https:\/\/|http:\/\/|)([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/)]),
oper: new FormControl(null)
})
}
@@ -83,13 +85,7 @@ export class AdminLinkComponent implements OnInit {
modalConfirm() {
this.modalVisible = false;
const linkReq: Link = new Link();
linkReq.name = this.formGroup.value.name;
linkReq.url = this.formGroup.value.url;
linkReq.open = this.formGroup.value.open;
// 暂时设置未空
linkReq.desc = '';
linkReq.iconPath = '';
const linkReq: Link = this.formGroup.value
const oper = this.formGroup.value.oper;
let observable: Observable<Response<Link>>;
if (oper === 'edit') {

View File

@@ -3,7 +3,15 @@ import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {AdminLinkComponent} from './admin-link.component';
import {CommonTableModule} from '../components/common-table/common-table.module';
import {NzCheckboxModule, NzFormModule, NzInputModule, NzModalModule, NzSelectModule, NzTagModule} from 'ng-zorro-antd';
import {
NzButtonModule,
NzCheckboxModule,
NzFormModule,
NzInputModule,
NzModalModule,
NzSelectModule,
NzTagModule
} from 'ng-zorro-antd';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@@ -23,6 +31,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
NzInputModule,
NzSelectModule,
NzTagModule,
NzButtonModule,
]
})

View File

@@ -17,14 +17,6 @@ export class AdminVisitorComponent implements OnInit {
headData: Data<Visitor>[];
request: RequestObj
/***
* browserName: "Chrome 8"
browserVersion: "83.0.4103.116"
date: "2020-07-11 09:30:13"
id: 3131
ip: "127.0.0.1"
osname: "Windows 10"
*/
ngOnInit(): void {
this.title.setTitle('小海博客 | 访客信息管理')
this.request = {
@@ -33,14 +25,15 @@ export class AdminVisitorComponent implements OnInit {
queryParam: {
count: 1,
page: 10,
showLocation: location
showLocation: true
}
}
this.headData = [
{fieldValue: 'id', title: '主键', show: false, primaryKey: true},
{fieldValue: 'date', title: '访问日期', show: true},
{fieldValue: 'browserName', title: '浏览器', show: true},
{fieldValue: 'ip', title: 'ip地址', show: true},
{fieldValue: 'location', title: '位置', show: true},
{fieldValue: 'browserName', title: '浏览器', show: true},
{fieldValue: 'browserVersion', title: '浏览器版本', show: true},
{fieldValue: 'osname', title: '系统', show: true}
]

View File

@@ -22,7 +22,6 @@ export class AdminComponent implements OnInit {
complete: () => null,
error: (err) => null,
next: data => {
console.log('更新user')
this.user = data.result
if (data.result) this.initHelloWords()
}
@@ -68,7 +67,6 @@ export class AdminComponent implements OnInit {
checkSamePwd = () => {
return (control: AbstractControl): { [key: string]: any } | null => {
console.log('a')
const newPwd = this.resetPwdFormGroup && this.resetPwdFormGroup.value.newPwd;
return control.value !== newPwd ? {pwdNotSame: true} : null;
};

View File

@@ -1,10 +1,12 @@
<nz-card *ngIf="cardTitle" nzSize="small" [nzExtra]="refresh" [nzTitle]="cardTitle" [nzLoading]="loading">
<nz-card *ngIf="cardTitle" nzSize="small" [nzExtra]="extra" [nzTitle]="cardTitle" [nzLoading]="loading">
<ng-container *ngTemplateOutlet="table"></ng-container>
</nz-card>
<ng-container [ngTemplateOutlet]="table" *ngIf="!cardTitle"></ng-container>
<ng-template #refresh>
<ng-template #extra>
<i nz-icon nzType="setting" nzTheme="outline" title="设置" (click)="showFieldSetting()"
style="cursor: pointer;margin-right: 10px"></i>
<i nz-icon nzType="reload" nzTheme="outline" (click)="getData()" title="刷新" style="cursor: pointer"></i>
</ng-template>
@@ -17,12 +19,12 @@
[nzPageSize]="dataList.pageSize"
(nzPageIndexChange)="getData()"
nzFrontPagination="false"
[nzScroll]="{x:'1300px'}"
[nzScroll]="{x:visibleFieldLength*100+'px'}"
[nzLoading]="loading">
<thead>
<tr>
<ng-container *ngFor="let data of headData">
<th *ngIf="data.show">
<ng-container *ngFor="let data of filedData">
<th *ngIf="data.show" [nzWidth]="data.isActionColumns?data.action.length*80+'px':null">
{{data.title}}
</th>
</ng-container>
@@ -30,7 +32,7 @@
</thead>
<tbody>
<tr *ngFor="let t of dataList.list;let index = index">
<ng-container *ngFor="let data of headData">
<ng-container *ngFor="let data of filedData">
<td *ngIf="data.show"
nz-typography
nzEllipsis
@@ -70,3 +72,27 @@
</tbody>
</nz-table>
</ng-template>
<nz-modal [(nzVisible)]="settingModalVisible"
[nzClosable]="true"
(nzOnCancel)="cancel()"
(nzOnOk)="ok()"
nzTitle="表格字段设置(可拖动排序)"
>
<button nz-button nzType="primary" (click)="reset()" [disabled]="!changed">重置</button>
<nz-table [nzData]="filedData" nzSize="small" nzPageSize="10000" nzShowPagination="false">
<tbody cdkDropList (cdkDropListDropped)="drop($event)">
<ng-template ngFor [ngForOf]="filedData" let-item let-index="index">
<tr *ngIf="!item.isActionColumns" cdkDrag (click)="click()">
<td>{{index + 1}}</td>
<td style="text-align: center">{{item.title}}</td>
<td style="text-align: center">{{item.fieldValue}}</td>
<td style="text-align: right">
<nz-switch [(ngModel)]="item.show" nzSize="small"></nz-switch>
</td>
</tr>
</ng-template>
</tbody>
</nz-table>
</nz-modal>

View File

@@ -0,0 +1,14 @@
ul {
list-style: none;
margin: 0;
padding: 0;
li {
margin: 8px 0;
padding: 0;
}
}
td {
border-bottom: none !important;
}

View File

@@ -2,6 +2,7 @@ import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges
import {Data} from './data';
import {PageList, RequestObj} from '../../../../class/HttpReqAndResp';
import {HttpService} from '../../../../api/http/http.service';
import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop';
@Component({
selector: 'common-table',
@@ -14,10 +15,7 @@ export class CommonTableComponent<T> implements OnInit, OnChanges {
}
/**
* 设置readonly data 因为后面有使用eval 为了安全
*/
@Input() headData: Data<T>[];
@Input() private headData: Data<T>[];
@Input() request: RequestObj;
@Input() cardTitle: string | null;
@Input() template: {
@@ -30,8 +28,20 @@ export class CommonTableComponent<T> implements OnInit, OnChanges {
loading: boolean = true;
dataList: PageList<T> = new PageList<T>();
settingModalVisible: boolean = false;
filedData: Data<T>[];
changed: boolean = false;
visibleFieldLength: number = 0;
ngOnInit(): void {
if (localStorage.getItem(this.request.path)) {
this.filedData = this.cloneData(localStorage.getItem(this.request.path));
this.changed = true;
} else {
this.filedData = this.cloneData(this.headData)
}
this.calculateVisibleFieldLength();
if (!this.template) this.template = {}
this.headData.forEach(dat => {
if (!dat.action) return;
@@ -49,10 +59,13 @@ export class CommonTableComponent<T> implements OnInit, OnChanges {
this.loading = true;
const pageValue = this.dataList.pageNum ? this.dataList.pageNum : 1;
const countValue = this.dataList.pageSize ? this.dataList.pageSize : 10
this.request.queryParam = {
page: pageValue,
count: countValue
}
this.request.queryParam.page = pageValue;
this.request.queryParam.count = countValue;
// this.request.queryParam = {
// page: pageValue,
// count: countValue
// }
this.pageInfo.emit({page: pageValue, pageSize: countValue})
return this.httpService.Service<PageList<T>>(this.request).subscribe({
next: resp => {
@@ -65,7 +78,6 @@ export class CommonTableComponent<T> implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
if (changes.request && !changes.request.isFirstChange()) {
console.log(changes.request)
this.request = changes.request.currentValue;
this.getData().unsubscribe();
this.getData();
@@ -100,4 +112,63 @@ export class CommonTableComponent<T> implements OnInit, OnChanges {
}
return context;
}
showFieldSetting = () => this.settingModalVisible = true;
cancel = () => this.settingModalVisible = false;
calculateVisibleFieldLength = () => this.filedData.filter(value => value.show).length;
ok() {
this.calculateVisibleFieldLength();
this.settingModalVisible = !this.settingModalVisible;
if (!this.changed) {
return
}
this.dataList = JSON.parse(JSON.stringify(this.dataList));
localStorage.setItem(this.request.path, JSON.stringify(this.filedData))
this.changed = true;
}
drop(event: CdkDragDrop<T, any>) {
this.changed = true;
moveItemInArray(this.filedData, event.previousIndex, event.currentIndex);
}
reset = () => {
localStorage.removeItem(this.request.path);
this.filedData = this.cloneData(this.headData);
this.changed = false;
this.calculateVisibleFieldLength();
}
cloneData = (source: Data<T>[] | string): Data<T>[] => {
let dist: Data<T>[];
if (typeof source === 'string') {
dist = JSON.parse(source);
} else {
dist = JSON.parse(JSON.stringify(source));
}
const action = this.headData.filter(value => value.isActionColumns).pop();
if (!action) {
return dist;
}
const del = dist.filter(value => value.isActionColumns).pop()
dist.splice(dist.indexOf(del), 1);
dist.push(action);
return dist;
}
/**
* 字段编辑项被点击
*/
click = () => {
this.changed = false;
for (let i = 0; i < this.filedData.length; i++) {
const d1 = this.filedData[i];
const d2 = this.headData[i];
if (d1.fieldValue !== d2.fieldValue || d1.show !== d2.show) {
this.changed = true;
}
}
}
}

View File

@@ -2,14 +2,16 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {CommonTableComponent} from './common-table.component';
import {
NzButtonModule,
NzCardModule,
NzDividerModule,
NzIconModule, NzOutletModule, NzPopconfirmModule,
NzTableModule,
NzIconModule, NzModalModule, NzOutletModule, NzPopconfirmModule, NzSwitchModule,
NzTableModule, NzTagModule,
NzToolTipModule,
NzTypographyModule
} from 'ng-zorro-antd';
import {FormsModule} from '@angular/forms';
import {DragDropModule} from '@angular/cdk/drag-drop'
@NgModule({
declarations: [
@@ -27,7 +29,13 @@ import {
NzCardModule,
NzIconModule,
NzOutletModule,
NzPopconfirmModule
NzPopconfirmModule,
NzModalModule,
NzTagModule,
NzSwitchModule,
FormsModule,
DragDropModule,
NzButtonModule
]
})
export class CommonTableModule {

View File

@@ -12,6 +12,7 @@ export class Data<T> {
[value: string]: string
}
};
// order?: number;
action ?: {
name: string,
color?: string,

View File

@@ -3,11 +3,9 @@ import {ApiService} from '../../api/api.service';
import {Article} from '../../class/Article';
import {NzIconService, NzMessageService} from 'ng-zorro-antd';
import {SvgIconUtil} from '../../utils/svgIconUtil';
import {PageList} from '../../class/HttpReqAndResp';
import {ErrDispatch} from '../../class/ErrDispatch';
import {RequestObj} from '../../class/HttpReqAndResp';
import {PageList, RequestObj} from '../../class/HttpReqAndResp';
import {Router} from '@angular/router';
import {Category, Tag} from '../../class/Tag';
import {Category} from '../../class/Tag';
import {Title} from '@angular/platform-browser';
@Component({
@@ -16,7 +14,7 @@ import {Title} from '@angular/platform-browser';
styleUrls: ['./index.component.less'],
providers: [ApiService]
})
export class IndexComponent implements OnInit, ErrDispatch {
export class IndexComponent implements OnInit {
constructor(private apiService: ApiService,
private iconService: NzIconService,
@@ -24,7 +22,6 @@ export class IndexComponent implements OnInit, ErrDispatch {
private router: Router,
private title: Title) {
this.iconService.addIconLiteral('blog:location', SvgIconUtil.locationIcon);
apiService.setErrDispatch(this);
title.setTitle('小海博客');
}

View File

@@ -1,18 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {LinkComponent} from './link.component';
const routes: Routes = [
{path: '**', component: LinkComponent}
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [RouterModule]
})
export class LinkRoutingModule {
}

View File

@@ -3,50 +3,143 @@
<i nz-icon nzType="smile" nzTheme="outline" class="titleTag"></i><span class="title">友情链接</span>
</div>
<ul class="partner-sites">
<li *ngFor="let link of linkList">
<i nz-icon nzType="link" nzTheme="outline"></i>
<a [href]="link.url" target="_blank" [title]="link.name">{{link.name}}</a>
<li *ngFor="let link of linkList;let i = index" [style.background]="colors[i].bgColor"
[style.color]="colors[i].fontColor">
<a [href]="link.url" target="_blank" [title]="link.desc||link.name" [style.color]="colors[i].fontColor">
<div class="link-name">
<i nz-icon nzType="link" nzTheme="outline" [style.color]="colors[i].fontColor"></i>
{{link.name}}
</div>
<div class="link-info" [style.color]="colors[i].fontColor">
<div class="link-icon">
<img *ngIf="link.iconPath" [src]="link.iconPath" [alt]="link.iconPath">
<i *ngIf="!link.iconPath" nz-icon nzType="link" nzTheme="outline"
[style.color]="colors[i].fontColor"></i>
</div>
<p>
{{link.desc || '该站长暂时未留下网站简介'}}
</p>
</div>
</a>
</li>
<li class="applylink" (click)="showModal=!showModal">申请友链</li>
</ul>
</div>
<div class="placard am-animation-slide-bottom">
<div class="title">
<i nz-icon nzType="smile" nzTheme="outline" class="titleTag"></i><span class="title">友链公告</span>
</div>
<br>
<p style="padding-left: 30px;">
✔ 原创优先&nbsp;&nbsp;✔ 技术优先&nbsp;&nbsp;❌ 经常宕机&nbsp;&nbsp;❌ 不合法规&nbsp;&nbsp;❌ 插边球站&nbsp;&nbsp;❌ 红标报毒&nbsp;
</p>
<ul class="placard-content">
<li>请确认贵站可正常访问</li>
<li>原创博客、技术博客、游记博客优先</li>
<li>博客内容时常更新</li>
<li><strong>提交申请时若为https链接请带上https否则将视为http</strong></li>
<li>交换友链请先在您的网站添加本站链接</li>
<p style="margin: 20px;">
本站信息 <br>
名称:小海博客<br>
网址https://www.celess.cn/<br>
图标https://www.celess.cn/assets/logo.jpg<br>
描述:小海博客,记录学习成长历程,主要关注与java后端的技术学习
</p>
<li>本站的友链申请会自动进行抓取并在12h内进行审核</li>
</ul>
</div>
<nz-modal [(nzVisible)]="showModal" [nzTitle]="modalTitle" [nzContent]="modalContent" [nzFooter]="modalFooter"
(nzOnCancel)="cancel()">
(nzOnCancel)="cancel()" nzWidth="650">
<ng-template #modalTitle>
<h2 style="text-align: center">申请友链</h2>
</ng-template>
<ng-template #modalContent>
<div class="am-modal-bd">
<div class="article-setting">
<label>网站名称:
<input nz-input placeholder="请输入网站名称" nzSize="large" [(ngModel)]="link.name">
</label>
<br>
<br>
<label>网站链接:
<input nz-input placeholder="请输入网站链接" nzSize="large" [(ngModel)]="link.url">
</label>
</div>
</div>
<form nz-form [formGroup]="applyFormGroup">
<nz-form-item>
<nz-form-label nzFlex="100px" nzRequired>网站名称</nz-form-label>
<nz-form-control nzFlex="auto" [nzErrorTip]="nameErrTip">
<input nz-input formControlName="name">
<ng-template #nameErrTip>
<div *ngIf="applyFormGroup.controls.name.hasError('required')">网站名称不可为空</div>
<div *ngIf="applyFormGroup.controls.name.hasError('maxlength')">最大长度为255</div>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFlex="100px" nzRequired>站长邮箱</nz-form-label>
<nz-form-control nzFlex="auto" [nzErrorTip]="emailErrTip">
<input nz-input formControlName="email">
<ng-template #emailErrTip>
<div *ngIf="applyFormGroup.controls.email.hasError('required')">站长邮箱不可为空</div>
<div *ngIf="applyFormGroup.controls.email.hasError('pattern')">邮箱格式不正确</div>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFlex="100px" nzRequired>首页链接</nz-form-label>
<nz-form-control nzFlex="auto" [nzErrorTip]="urlErrTip">
<nz-input-group [nzAddOnBefore]="protocol" nzCompact>
<ng-template #protocol>
<nz-select formControlName="urlProtocol">
<nz-option nzLabel="Http://" nzValue="http://"></nz-option>
<nz-option nzLabel="Https://" nzValue="https://"></nz-option>
</nz-select>
</ng-template>
<input nz-input formControlName="url">
<ng-template #urlErrTip>
<div *ngIf="applyFormGroup.controls.url.hasError('required')">首页链接不可为空</div>
<div *ngIf="applyFormGroup.controls.url.hasError('pattern')">链接格式不正确</div>
</ng-template>
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFlex="100px" nzRequired>友链页链接</nz-form-label>
<nz-form-control nzFlex="auto" [nzErrorTip]="urlLinkErrTip">
<nz-input-group [nzAddOnBefore]="protocol">
<ng-template #protocol>
<nz-select formControlName="urlLinkProtocol">
<nz-option nzLabel="Http://" nzValue="http://"></nz-option>
<nz-option nzLabel="Https://" nzValue="https://"></nz-option>
</nz-select>
</ng-template>
<input nz-input formControlName="linkUrl">
<ng-template #urlLinkErrTip>
<div *ngIf="applyFormGroup.controls.linkUrl.hasError('required')">首页链接不可为空</div>
<div *ngIf="applyFormGroup.controls.linkUrl.hasError('pattern')">链接格式不正确</div>
</ng-template>
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFlex="100px">网站图标</nz-form-label>
<nz-form-control nzFlex="auto" nzErrorTip="链接格式不正确">
<nz-input-group [nzSuffix]="icon" nzSize="large">
<input nz-input formControlName="iconPath">
</nz-input-group>
<ng-template #icon>
<img style="width: 25px;height: 25px" *ngIf="applyFormGroup.value.iconPath"
[src]="applyFormGroup.value.iconPath" alt="icon">
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFlex="100px">网站描述</nz-form-label>
<nz-form-control nzFlex="auto" nzErrorTip="可输入最大文字长度为255">
<textarea nz-input formControlName="desc" [nzAutosize]="{ minRows: 2, maxRows: 6 }"></textarea>
</nz-form-control>
</nz-form-item>
</form>
</ng-template>
<ng-template #modalFooter>
<button nz-button (click)="cancel()">取消</button>
<button nz-button nzType="primary" (click)="apply()">提交</button>
<button nz-button nzType="primary" (click)="apply()" [disabled]="!applyFormGroup.valid" [nzLoading]="loading">
提交
</button>
</ng-template>
</nz-modal>

View File

@@ -16,15 +16,100 @@ i {
list-style: none;
display: flex;
flex-wrap: wrap;
padding: 20px 10px;
justify-content: space-between;
align-items: flex-start;
li {
width: 25%;
margin: 10px 0;
height: 30px;
line-height: 30px;
text-align: center
flex: 1;
width: 20%;
min-width: 20%; // 加入这两个后每个item的宽度就生效了
max-width: 20%; // 加入这两个后每个item的宽度就生效了
height: 80px;
text-align: center;
overflow: hidden;
margin: 10px;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, .1);
.link-icon {
display: none;
transition: 0.3s;
}
.link-name, .link-info {
height: 80px;
width: 100%;
margin: 0;
transition: 0.3s;
padding: 0;
}
.link-name {
// background: #eed1b3;
line-height: 80px;
cursor: pointer;
i {
display: inline;
}
}
.link-info {
// background: #00d95a;
}
}
li:hover {
transition: 0.3s;
.link-icon {
//width: 80px;
height: 56px;
line-height: 56px;
// background: #00aaaa;
transition: 0.3s;
display: inline-block;
width: 56px;
float: left;
img {
height: 80%;
width: 80%;
border-radius: 50%;
line-height: 56px;
//padding: 3px;
}
}
.link-name {
height: 24px;
line-height: 24px;
i {
display: none;
transition: 0.3s;
}
}
.link-info {
border-top: 1px solid rgba(150, 150, 150, .1);
height: 56px;
line-height: 56px;
padding-bottom: 2px;
p {
width: 100%;
height: 100%;
text-align: left;
font-size: xx-small;
}
}
}
}
.placard {
@@ -32,14 +117,20 @@ i {
margin: 0 auto;
}
.site-middle, .placard {
background: #fff;
padding: 20px;
}
.placard-content {
margin-top: 30px;
margin-left: 30px;
border-left: 5px solid #aaa4a4;
border-left: 3px solid #6bc30d;
padding: 0;
list-style: none;
li {
padding-left: 15px;
padding-left: 20px;
font-size: 1.2em;
margin: 5px 0;
}
@@ -48,9 +139,12 @@ i {
.applylink {
float: right;
border: none;
background: white;
background: #f6f1f1;
border-radius: 5px;
width: 150px;
height: 80px;
line-height: 80px;
font-size: x-large;
}
.applylink:hover {
@@ -77,8 +171,14 @@ i {
width: 96%;
}
.partner-sites{
padding: 0 30px;
}
.partner-sites li {
float: left;
width: 100%;
max-width: 100%;
min-width: 80%;
margin-right: 10px;
}
}

View File

@@ -1,8 +1,10 @@
import {Component, OnInit} from '@angular/core';
import {NzMessageService} from 'ng-zorro-antd';
import {NzMessageService, NzModalService} from 'ng-zorro-antd';
import {Title} from '@angular/platform-browser';
import {ApiService} from '../../api/api.service';
import {Link} from '../../class/Link';
import {ApplyLinkReq, Link} from '../../class/Link';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {Color, RandomColor} from '../../utils/color';
@Component({
selector: 'view-link',
@@ -13,7 +15,9 @@ export class LinkComponent implements OnInit {
constructor(private message: NzMessageService,
private titleService: Title,
private apiService: ApiService) {
private apiService: ApiService,
private fb: FormBuilder,
private modal: NzModalService) {
titleService.setTitle('小海博客 | 友链');
}
@@ -23,39 +27,74 @@ export class LinkComponent implements OnInit {
link: Link;
linkList: Link[];
loading: boolean = false;
applyFormGroup: FormGroup;
colors: Color[];
private lastUrl: string = '';
ngOnInit() {
window.scrollTo(0, 0);
this.link = new Link();
this.apiService.links().subscribe(data => {
this.linkList = data.result;
this.apiService.links().subscribe({
next: data => this.linkList = data.result,
error: err => this.message.error(err.msg),
complete: () => this.colors = RandomColor(this.linkList.length)
});
this.applyFormGroup = this.fb.group({
urlLinkProtocol: ['http://'],
urlProtocol: ['http://'],
desc: [null, [Validators.maxLength(255)]],
email: [null, [Validators.required, Validators.pattern(/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/)]],
iconPath: [null, [Validators.pattern(/^(https:\/\/|http:\/\/|)([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/)]],
linkUrl: [null, [Validators.required, Validators.pattern(/^([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/)]],
name: [null, [Validators.required, Validators.maxLength(255)]],
url: [null, [Validators.required, Validators.pattern(/^([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/)]]
});
this.applyFormGroup.controls.url.valueChanges.subscribe({
next: data => {
const linkUrlData: string = this.applyFormGroup.value.linkUrl || '';
this.applyFormGroup.patchValue({linkUrl: linkUrlData.replace(this.lastUrl, data)});
this.lastUrl = data;
},
error => {
this.message.error(error.msg);
});
})
}
apply() {
if (this.link.name === '') {
this.message.error('网站名称不能为空');
return;
}
if (this.link.url === '') {
this.message.error('网站链接不能为空');
return;
}
const regExp = /^(https:\/\/|http:\/\/|)([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/;
if (!regExp.test(this.link.url)) {
this.message.error('网站链接输入不合法');
return;
}
this.showModal = false;
this.apiService.applyLink(this.link).subscribe(data => {
const value = this.applyFormGroup.value;
value.url = value.urlProtocol + value.url;
value.linkUrl = value.urlLinkProtocol + value.linkUrl;
const req: ApplyLinkReq = value;
this.loading = true;
this.apiService.applyLink(req).subscribe({
next: data => {
this.message.success('提交成功,请稍等,即将为你处理');
this.loading = false;
this.showModal = false;
this.applyFormGroup.reset()
},
error => {
this.message.error('提交失败,原因:' + error.msg);
});
error: err => {
if (err.code === 7200) {
const key = err.result;
this.modal.create({
nzTitle: '抓取站点失败',
nzContent: '暂未在您的网站友链页抓取到本站链接,是否确认已添加并重新提交邮件申请?',
nzClosable: false,
nzOnOk: () => {
this.apiService.reapplyLink(key).subscribe({
next: data1 => this.message.success('提交成功,请稍等,即将为你处理'),
error: err1 => this.message.error('提交失败,原因:' + err.msg)
})
}
});
} else {
this.message.error('提交失败,原因:' + err.msg);
}
this.loading = false;
this.showModal = false;
this.applyFormGroup.reset()
}
});
}
cancel() {

View File

@@ -1,21 +1,24 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {LinkComponent} from './link.component';
import {LinkRoutingModule} from './link-routing.module';
import {NzButtonModule, NzIconModule, NzInputModule, NzModalModule} from 'ng-zorro-antd';
import {FormsModule} from '@angular/forms';
import {NzButtonModule, NzFormModule, NzIconModule, NzInputModule, NzModalModule, NzSelectModule} from 'ng-zorro-antd';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
@NgModule({
declarations: [LinkComponent],
imports: [
CommonModule,
LinkRoutingModule,
NzIconModule,
NzModalModule,
FormsModule,
NzButtonModule,
NzInputModule
NzInputModule,
RouterModule.forChild([{path: '**', component: LinkComponent}]),
NzFormModule,
ReactiveFormsModule,
NzSelectModule
]
})
export class LinkModule {

View File

@@ -3,7 +3,6 @@ import {environment} from '../../../../../environments/environment';
import {ApiService} from '../../../../api/api.service';
import {NzMessageService} from 'ng-zorro-antd';
import {Router} from '@angular/router';
import {ErrDispatch} from '../../../../class/ErrDispatch';
import {RequestObj} from '../../../../class/HttpReqAndResp';
import {LoginReq} from '../../../../class/User';
import {Title} from '@angular/platform-browser';
@@ -14,13 +13,12 @@ import {Title} from '@angular/platform-browser';
styleUrls: ['./registration.component.less'],
providers: [ApiService]
})
export class RegistrationComponent implements OnInit, ErrDispatch {
export class RegistrationComponent implements OnInit {
constructor(private apiService: ApiService,
private nzMessageService: NzMessageService,
private router: Router,
private title: Title) {
apiService.setErrDispatch(this);
this.title.setTitle('小海博客 | 注册');
}

View File

@@ -0,0 +1,5 @@
<nz-result nzStatus="500" nzTitle="暂时无法连接到后台服务器,可能正在维护更新">
<div nz-result-extra>
<button nz-button nzType="primary" routerLink="/">返回首页</button>
</div>
</nz-result>

View File

@@ -0,0 +1,16 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-maintain',
templateUrl: './maintain.component.html',
styleUrls: ['./maintain.component.less']
})
export class MaintainComponent implements OnInit {
constructor() {
}
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,18 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MaintainComponent} from './maintain.component';
import {RouterModule} from '@angular/router';
import {NzButtonModule, NzResultModule} from 'ng-zorro-antd';
@NgModule({
declarations: [MaintainComponent],
imports: [
CommonModule,
RouterModule.forChild([{path: '', component: MaintainComponent}]),
NzResultModule,
NzButtonModule
]
})
export class MaintainModule {
}

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
src/assets/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -5,7 +5,7 @@
export const environment = {
production: false,
logger: true,
host: 'http://celess.cn:8082/'
host: 'http://127.0.0.1/'
};
/*

View File

@@ -7,7 +7,7 @@
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta name="keywords" content="小海博客,个人博客,java博客,学习,IT,生活,前端,后端,移动端,java,spring,springboot,angular">
<meta name="description" content="小海博客,记录学习成长历程,主要关注与java后端的技术学习">
<link rel="icon" type="image/x-icon" href="https://56462271.oss-cn-beijing.aliyuncs.com/web/logo.ico">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
</head>