Compare commits

...

123 Commits
1.1 ... master

Author SHA1 Message Date
13786834a0 Merge pull request 'fix:安全问题修复。新增书签耗时长问题修复' (#22) from dev into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #22
2024-07-01 20:30:39 +08:00
fanxb
e0dccb6fd2 fix:安全问题修复。新增书签耗时长问题修复 2024-06-22 19:30:25 +08:00
845b1b077e Merge pull request 'fix:修复bing获取报错问题' (#21) from dev into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #21
2024-05-23 22:08:22 +08:00
fanxb
bb62066b82 fix:修复bing获取报错问题 2024-05-23 22:07:45 +08:00
63f1c9a54e Merge pull request 'fix:修复name过长无法导入问题' (#20) from dev into master
All checks were successful
continuous-integration/drone Build is passing
Reviewed-on: #20
2023-12-02 18:41:11 +08:00
fanxb
c9156f12d1 fix:修复name过长无法导入问题 2023-12-02 18:40:43 +08:00
325401e198 Merge pull request 'dcos:文档更新' (#19) from dev into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #19
2023-12-01 02:36:05 -05:00
fanxb
5126e31867 dcos:文档更新 2023-12-01 15:34:54 +08:00
f7994d232d Merge pull request 'fix:修复展示问题' (#18) from dev into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #18
2023-12-01 02:28:52 -05:00
fanxb
f78349a1d2 fix:修复展示问题 2023-12-01 15:27:51 +08:00
44c1ca91fc Merge pull request 'dev' (#17) from dev into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #17
2023-12-01 02:17:00 -05:00
fanxb
b0b608de5a docs:文档更新
Some checks failed
continuous-integration/drone/pr Build is failing
2023-12-01 14:58:42 +08:00
fanxb
523698a967 feat:首页图加载优化,增加OneNav导入支持 2023-12-01 13:18:44 +08:00
fanxb
405a2e05ed feat:首图下载下来 2023-11-29 23:42:33 +08:00
fanxb
6b59dfbf26 Merge branch 'master' into dev 2023-11-29 19:29:42 +08:00
5a8f805a15 更新 README.md
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-25 00:50:04 -05:00
42a2829847 Merge pull request 'master' (#16) from master into dev
Reviewed-on: #16
2023-11-25 00:49:22 -05:00
dfb39e5ee8 fix:修复执行报错
Some checks failed
continuous-integration/drone/push Build is failing
2023-11-25 00:48:57 -05:00
fanxb
946c032ba5 Merge pull request 'dev' (#15) from dev into master
Reviewed-on: #15
2023-08-13 15:37:04 +08:00
fleyx
3a77e5af4d doc:文档更新 2023-08-13 15:36:02 +08:00
fleyx
1d282b1095 Merge remote-tracking branch 'origin/master' into dev 2023-08-13 15:19:33 +08:00
fanxb
bb7deda121 Merge pull request 'feat:支持搜索引擎自定义' (#14) from dev into master
Reviewed-on: #14
2023-08-13 15:19:18 +08:00
fleyx
0d000223a2 feat:支持搜索引擎自定义 2023-08-13 15:18:48 +08:00
fanxb
51eb22adaa Merge pull request 'dev' (#13) from dev into master
Reviewed-on: #13
2023-08-13 15:07:16 +08:00
fleyx
c56ca2809c feat:支持搜索引擎自定义 2023-08-13 15:05:40 +08:00
fleyx
383450ebe4 temp 2023-08-08 23:10:59 +08:00
fanxb
ebc1da0972 更新 '.drone.yml' 2023-04-01 05:41:48 -04:00
fanxb
9dc1fe47d9 Merge pull request 'dev' (#12) from dev into master
Reviewed-on: #12
2023-04-01 05:30:11 -04:00
fanxb
35d3420470 feat:放开github登录 2023-04-01 17:29:45 +08:00
fanxb
529b42a185 更新 '.drone.yml' 2023-03-12 01:32:33 -05:00
fanxb
58a8cefaf5 Merge branch 'dev' of ssh://gitea.fleyx.com:222/fanxb/bookmark into dev 2023-03-12 10:55:06 +08:00
fanxb
b9cef34a06 refactor:vue-cli upgrade to 5.0.8 2023-03-12 10:54:54 +08:00
fanxb
928e3fac48 fix:修复bookmark缺陷 2023-02-16 19:07:40 +08:00
fanxb
c9429557b3 Merge pull request 'fix:修复bookmark缺陷' (#11) from dev into master
Reviewed-on: #11
2023-02-16 19:01:59 +08:00
fanxb
4bed4fd34d docs:修改文档 2022-12-05 19:09:28 +08:00
fanxb
5d52e389d6 Merge pull request 'docs:修改文档' (#10) from dev into master
Reviewed-on: #10
2022-12-05 19:05:42 +08:00
fanxb
767c26d89b docs:文档修改 2022-12-04 20:52:58 +08:00
fanxb
83125ae55a Merge pull request 'dev' (#9) from dev into master
Reviewed-on: #9
2022-12-04 20:49:07 +08:00
fanxb
b5f715eda6 Merge pull request 'master' (#8) from master into dev
Reviewed-on: #8
2022-12-04 19:04:45 +08:00
fanxb
cc3298f3b4 fix:修改arm上编译报错 2022-12-04 18:34:44 +08:00
fanxb
2b69814296 更新 'README.md' 2022-11-25 23:19:01 +08:00
fanxb
2880b6282c Merge pull request 'fix:修复定时任务线程池不起作用' (#7) from dev into master
Reviewed-on: #7
2022-09-24 18:50:34 +08:00
fanxb
ff560ae790 fix:修复定时任务线程池不起作用 2022-09-24 18:50:18 +08:00
fanxb
c5a35ea54e Merge pull request 'fix:修复定时任务线程池不起作用' (#6) from dev into master
Reviewed-on: #6
2022-09-24 18:44:57 +08:00
fanxb
22c2c48eea Merge branch 'master' into dev 2022-09-24 18:44:50 +08:00
fanxb
580d18a500 fix:修复定时任务线程池不起作用 2022-09-24 18:45:20 +08:00
fanxb
5868aa4a98 Merge pull request 'dev' (#5) from dev into master
Reviewed-on: #5
2022-09-24 17:19:27 +08:00
fanxb
42cbbe5999 deploy:增加drone部署 2022-09-24 17:15:51 +08:00
fanxb
9dc21ed87a Merge pull request 'master' (#3) from master into dev
Reviewed-on: #3
2022-09-24 17:00:35 +08:00
fanxb
32e63ed1fb Merge pull request 'deploy:增加drone部署' (#2) from dev into master
Reviewed-on: #2
2022-09-24 17:00:20 +08:00
fanxb
35ee930661 Merge remote-tracking branch 'origin/master' 2022-09-24 16:59:07 +08:00
fanxb
19aeab2856 deploy:增加drone部署 2022-09-24 16:56:21 +08:00
FleyX
866ce22bb5
Merge pull request #38 from FleyX/dev
Dev
2022-09-22 19:50:45 +08:00
FleyX
84c2c213a9
Merge pull request #37 from FleyX/dependabot/maven/bookMarkService/business/bookmark/org.jsoup-jsoup-1.15.3
build(deps): bump jsoup from 1.14.3 to 1.15.3 in /bookMarkService/business/bookmark
2022-09-21 16:35:44 +08:00
dependabot[bot]
9cc1a7b871
build(deps): bump jsoup in /bookMarkService/business/bookmark
Bumps [jsoup](https://github.com/jhy/jsoup) from 1.14.3 to 1.15.3.
- [Release notes](https://github.com/jhy/jsoup/releases)
- [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES)
- [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.14.3...jsoup-1.15.3)

---
updated-dependencies:
- dependency-name: org.jsoup:jsoup
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-01 23:51:05 +00:00
fanxb
5e12b5a8b1 fix:修复定时任务线程池不起作用 2022-07-18 20:25:24 +08:00
fanxb
8ab7d0c341 Merge branch 'dev' of fanxb/bookmark into master 2022-07-10 13:18:36 +08:00
fanxb
4b9adc3acd Merge branch 'dev' of ssh://git.fleyx.com:2200/fanxb/bookmark into dev 2022-07-10 13:16:47 +08:00
fanxb
a67ef95b25 feat:修改域名 2022-07-10 13:16:44 +08:00
fanxb
39e9f8221a Merge branch 'dev' of fanxb/bookmark into master 2022-07-08 10:27:54 +08:00
fanxb
40cdb1f19e fix:样式细节修复 2022-07-08 10:27:20 +08:00
fanxb
106dccb68f Merge branch 'dev' of fanxb/bookmark into master 2022-07-08 10:22:49 +08:00
fanxb
5aeae15228 feat:增加背景下载按钮 2022-07-08 10:15:52 +08:00
FleyX
837a4a7650
Merge pull request #35 from FleyX/dev
fix:修复fastjsonbug
2022-05-28 20:34:05 +08:00
fanxb
8c81842571 Merge branch 'dev' of fanxb/bookmark into master 2022-05-28 20:33:26 +08:00
fanxb
64d4504178 fix:修复fastjsonbug 2022-05-28 20:33:07 +08:00
FleyX
3072d757f0
Merge pull request #34 from FleyX/dev
Dev
2022-05-12 16:39:36 +08:00
FleyX
62c88d78fc Update README.md 2022-05-12 16:38:32 +08:00
FleyX
ff323e6eea
Update README.md 2022-05-12 16:34:44 +08:00
fanxb
e240905d58 Merge branch 'dev' of fanxb/bookmark into master 2022-05-12 16:32:43 +08:00
fanxb
f23e720fb9 fix:修改注释 2022-05-12 16:30:28 +08:00
fanxb
faffadeba9 Merge branch 'dev' of fanxb/bookmark into master 2022-05-11 17:15:11 +08:00
fanxb
b2353a0ea1 feat:细节优化 2022-05-11 17:13:50 +08:00
fanxb
d5e2b55c28 refactor:日志打印级别改为debug 2022-05-11 14:30:38 +08:00
FleyX
35910c34e1
Merge pull request #33 from FleyX/dev
Dev
2022-04-28 10:46:28 +08:00
fanxb
d19325aaad Merge branch 'dev' of fanxb/bookmark into master 2022-04-28 10:45:39 +08:00
fanxb
df5578f267 refactor:优化请求,避免白屏时间过长 2022-04-28 10:43:05 +08:00
fanxb
9a5a4cae52 Merge branch 'dev' of fanxb/bookmark into master 2022-04-23 14:27:18 +08:00
fanxb
9a689cac65 fix:修复host_icon表数据重复问题 2022-04-23 14:26:29 +08:00
fanxb
238cc21ffa Merge branch 'dev' of fanxb/bookmark into master 2022-04-19 18:10:21 +08:00
fanxb
b950c666cc feat:新增书签可选择文件夹保持 2022-04-19 18:09:51 +08:00
fanxb
828207f672 temp 2022-04-19 17:17:34 +08:00
fanxb
ce0028cc49 refactor:首页点击不打开新页面 2022-04-17 20:20:42 +08:00
fanxb
9e2c75c3ec Merge branch 'dev' of fanxb/bookmark into master 2022-04-17 18:41:43 +08:00
fanxb
d36967d852 fix:修复带端口号地址不显示问题 2022-04-17 18:40:33 +08:00
fanxb
9e3308d6ca Merge branch 'dev' of fanxb/bookmark into master 2022-04-17 18:02:54 +08:00
fanxb
85688595f8 feat:浏览器插件版本显示 2022-04-17 16:36:02 +08:00
FleyX
9a97129c79
Merge pull request #32 from FleyX/dev
Dev
2022-04-17 15:41:59 +08:00
fanxb
6444cd6ebb docs:修改文档 2022-04-17 15:41:16 +08:00
fanxb
310ae76e8c Merge branch 'dev' of fanxb/bookmark into master 2022-04-17 15:39:41 +08:00
fanxb
473da94244 fix:修复跳转问题 2022-04-17 15:39:03 +08:00
fanxb
5ed2aa1690 Merge branch 'dev' of fanxb/bookmark into master 2022-04-17 15:24:44 +08:00
fanxb
c43cf5d5ae deploy:修改部署脚本 2022-04-17 15:24:05 +08:00
fanxb
1087f5e48b feat:支持浏览器新标签页 2022-04-17 14:53:44 +08:00
fanxb
b74be9f961 feat:浏览器插件替换主页 2022-04-17 12:10:51 +08:00
fanxb
6029f9d9c0 fix:修复回车搜索报错 2022-04-17 12:02:15 +08:00
FleyX
2c10ec4831
Merge pull request #31 from FleyX/dev
Dev
2022-04-15 16:33:59 +08:00
fanxb
89ad88e359 docs:修改帮助文档 2022-04-15 16:30:49 +08:00
fanxb
a7decc0c76 Merge branch 'dev' of fanxb/bookmark into master 2022-04-15 15:58:52 +08:00
fanxb
cb5e30a7bb fix:修改插件压缩包 2022-04-15 15:57:25 +08:00
fanxb
e8a646dbe8 Merge branch 'dev' of fanxb/bookmark into master 2022-04-15 15:49:08 +08:00
fanxb
d44700a971 feat:浏览器插件新增书签功能实现 2022-04-15 15:45:19 +08:00
fanxb
50e1e0e951 temp 2022-04-14 17:10:39 +08:00
fanxb
90b1dbcf9f temp 2022-04-12 17:04:48 +08:00
fanxb
1ba7617165 temp 2022-04-11 17:42:00 +08:00
fanxb
57a6944ec5 temp 2022-04-10 21:43:25 +08:00
fanxb
d251734267 feat:新的浏览器拓展 2022-04-08 17:04:13 +08:00
FleyX
c128cba5f6
Merge pull request #30 from FleyX/dev
fix:修复背景图片填充不完全
2022-03-31 13:49:28 +08:00
fanxb
a6f1380e9c Merge branch 'dev' of fanxb/bookmark into master 2022-03-31 13:45:52 +08:00
fanxb
3d7243e276 fix:修复背景图片填充不完全 2022-03-31 13:44:14 +08:00
FleyX
bd17be5dd5
Merge pull request #29 from FleyX/dev
feat:首页背景更换为每日一图
2022-03-31 13:35:11 +08:00
fanxb
77086daeb9 Merge branch 'dev' of fanxb/bookmark into master 2022-03-31 13:33:03 +08:00
fanxb
553cec1338 feat:首页背景更换为每日一图 2022-03-31 13:31:20 +08:00
FleyX
f424187334
Merge pull request #28 from FleyX/dev
Dev
2022-03-30 17:09:21 +08:00
fanxb
8d639b6a8c Merge branch 'dev' of fanxb/bookmark into master 2022-03-30 16:57:23 +08:00
fanxb
91a78a8459 fix:修复弹窗问题 2022-03-30 16:44:43 +08:00
fanxb
14b3c6da2f Merge branch 'dev' of fanxb/bookmark into master 2022-03-30 14:21:06 +08:00
fanxb
f048496d73 fix:修复注册进入主页后显示未登录 2022-03-30 14:19:38 +08:00
fanxb
c467970e93 fix:修复首页跳转问题 2022-03-30 10:46:40 +08:00
fanxb
15ed9cab08 feat:增加使用教程地址 2022-03-29 23:27:04 +08:00
fanxb
7bb4c42cb8 fix:修复首页icon过大问题 2022-03-29 23:16:07 +08:00
fanxb
f936a6b71b Merge branch 'master' of https://github.com/FleyX/bookmark 2022-03-29 22:03:03 +08:00
fanxb
47d4698ac3 deploy:build.sh增加执行权限 2022-03-10 14:35:23 +08:00
121 changed files with 12200 additions and 1788 deletions

24
.drone.yml Normal file
View File

@ -0,0 +1,24 @@
kind: pipeline
type: docker
name: bookmarkPublish
trigger:
branch:
- master
clone:
disable: true
steps:
- name: deploy
image: appleboy/drone-ssh
settings:
host:
from_secret: devHost
port: 22
user: root
key:
from_secret: privateSsh
command_timeout: 30m
script:
- cd /root/bookmark && git pull && bash build.sh && bash syncFile.sh

View File

@ -1,10 +1,20 @@
本程序基于 docker 来进行部署,使用 docker-compose 管理服务。 本程序基于 docker 来进行部署,使用 docker-compose 管理服务。
部署过程如下: **注意,仅在 x86 环境下测试,arm 下不保证可用性(目前测试可用)**
**注意,仅在 x86 环境下测试** ## 首次部署
1. 安装新版的 docker 和 docker-compose(注意:以下操作均在项目根目录下执行) 0. 克隆代码`git clone https://github.com/FleyX/bookmark.git`
2. 执行`build.sh`编译前后端代码 1. 进入文件夹`cd bookmark`
2. 安装新版的 docker,docker-compose,zip `apt install docker docker-compose zip`
3. 修改.env 文件中的参数,改为你的实际配置 3. 修改.env 文件中的参数,改为你的实际配置
4. root 权限运行 `docker-compose up -d` 启动服务。 4. 修改`浏览器插件/bookmarkBrowserPlugin/static/js/config.js`中的 bookmarkHost改为你的实际部署路径
5. 修改`浏览器插件/bookmarkBrowserPlugin/tab/index.html`中的`<meta http-equiv="Refresh" content="0;url=https://bm.fleyx.com" />`,将 url 改为你的实际部署地址
6. 执行`build.sh`编译前后端代码 `bash build.sh`
7. root 权限运行 `docker-compose up -d` 启动服务。
## 更新系统
0. 代码库更新`cd bookmark;git pull`
1. 执行`build.sh`编译前后端代码 `bash build.sh`
2. root 权限运行 `docker-compose restart` 启动服务

16
HELP.md
View File

@ -97,3 +97,19 @@ enter/回车: 未选中书签情况下,用于发起网页搜索。选中书签
### 个人中心 ### 个人中心
通过右上角悬浮菜单进入个人中心页面,可进行头像更换,密码修改等操作 通过右上角悬浮菜单进入个人中心页面,可进行头像更换,密码修改等操作
### 浏览器插件
浏览器插件功能终于有 0.1 版本,支持鼠标右键菜单添加书签。
#### 安装插件
1. 首先下载插件压缩包,下载地址:[点击下载](https://fleyx.com/static/bookmarkBrowserPlugin.7z)
2. 安装插件(以 chrome 浏览器为例,其他支持插件的浏览器差不多)进入插件管理页面->开启开发者模式->加载已解压的拓展程序
![](https://qiniupic.fleyx.com/blog/202204151605709.png)
3. 之后页面点击右键->添加到书签,即可
![](https://qiniupic.fleyx.com/blog/202204151607593.png)

100
README.md
View File

@ -1,44 +1,56 @@
本项目是一个云书签的项目,取名为:签签世界. ![图片](https://s3.fleyx.com/picbed/2023/08/Snipaste_2023-08-13_15-33-16.png)
部署地址:[fleyx.com](https://fleyx.com) 本项目是一个在线书签管理的项目,名为:签签世界.
也可自己搭建,教程如下: 在线使用地址(长期提供服务):[bm.fleyx.com](https://bm.fleyx.com)
部署教程:[docker-compose 部署](https://github.com/FleyX/bookmark/blob/master/DEPLOY.md) **为获得更好的体验,建议将主页设置为 bm.fleyx.com,并安装浏览器拓展,[点击查看如何安装](https://blog.fleyx.com/blog/detail/20220329/#%e6%b5%8f%e8%a7%88%e5%99%a8%e6%8f%92%e4%bb%b6)**
# 缘由 也可自己部署搭建,教程见:[docker-compose 部署](https://github.com/FleyX/bookmark/blob/master/DEPLOY.md)
1. 主要用的是 chrome但是有时候需要用其他的浏览器Firefoxie 等。然后这些浏览器上没有书签,想进个网站还得打开 chrome 复制 url太麻烦。 # 缘由
2. chrome 必须翻墙才能同步书签,体验不是那么好。 1. 主要用的是 chrome但是有时候需要用其他的浏览器Firefoxie 等。然后这些浏览器上没有书签,想进个网站还得打开 chrome 复制 url太麻烦。
3. 如果书签全放在 chrome 上,相当于绑定死 chrome 浏览器了,很难迁移到别的优秀浏览器,比如 firfox 上。 2. chrome 必须翻墙才能同步书签,体验不是那么好。
所以有了这样这样一个项目,建立一个和平台无关的书签管理器,可在任意平台使用。 3. 如果书签全放在 chrome 上,相当于绑定死 chrome 浏览器了,很难迁移到别的优秀浏览器,比如 firfox 上。
# 主要功能 所以有了这样这样一个项目,建立一个和平台无关的书签管理器,可在任意平台使用。
使用帮助见:[使用帮助](https://github.com/FleyX/bookmark/blob/master/HELP.md) # 主要功能
1. 基础的书签增删改查功能。支持 chrome、firefox 等浏览器书签文件导入,导出。 帮助文档:[点击跳转](https://blog.fleyx.com/blog/detail/20220329/)
![](https://qiniupic.fleyx.com/blog/20220329214126.png?imageView2/2/w/1920) - 支持从 chrome,edge,firefox 等浏览器导入书签数据。
- 支持从 OneEnv 导入书签数据
2. 强大的书签检索功能,毫秒级的关键字检索。 - 树型多级目录支持
- 支持导出标准 html 书签文件
![](https://qiniupic.fleyx.com/blog/20220329214210.png?imageView2/2/w/1920) - 强大的检索功能,支持拼音检索
- 支持浏览器插件,安装插件以后可右键添加书签
3. 首页功能,参考 bing 首页实现
# 更新日志
![](https://qiniupic.fleyx.com/blog/20220329214236.png?imageView2/2/w/1920)
## 1.4.1
4. 移动端支持,手机端也可使用(部分功能比如拖拽等无法使用)
- 修复书签名过长无法导入问题
![](https://qiniupic.fleyx.com/blog/20220329214312.png?imageView2/2/w/1920)
## 1.4
# TODO
- 优化首图加载逻辑
- 主页功能 Ok! - 支持 OneEnv 备份文件导入
- 拼音检索 Ok!
- 书签导出 OK ## 1.3
- 侧边栏显示
![pic](https://s3.fleyx.com/picbed/2023/08/Snipaste_2023-08-13_15-01-20.png)
搜索引擎支持自定义[#43](https://github.com/FleyX/bookmark/issues/43)
位置:右上角个人中心-管理搜索引擎
# TODO
- [x] 主页功能
- [x] 拼音检索
- [x] 书签导出
- [x] 浏览器插件

View File

@ -22,13 +22,18 @@
<dependency> <dependency>
<groupId>org.jsoup</groupId> <groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId> <artifactId>jsoup</artifactId>
<version>1.14.3</version> <version>1.15.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.houbb</groupId> <groupId>com.github.houbb</groupId>
<artifactId>pinyin</artifactId> <artifactId>pinyin</artifactId>
<version>0.3.1</version> <version>0.3.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.44.1.0</version>
</dependency>
</dependencies> </dependencies>

View File

@ -0,0 +1,16 @@
package com.fanxb.bookmark.business.bookmark.constant;
import java.nio.file.Paths;
/**
* TODO
*
* @author fanxb
*/
public class FileConstant {
/**
* 网站icon存储路径
*/
public static final String FAVICON_PATH = Paths.get("files", "public", "favicon").toString();
}

View File

@ -69,7 +69,7 @@ public class BookmarkController {
*/ */
@RequestMapping("/uploadBookmarkFile") @RequestMapping("/uploadBookmarkFile")
public Result uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("path") String path) throws Exception { public Result uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("path") String path) throws Exception {
bookmarkService.parseBookmarkFile(UserContextHolder.get().getUserId(), file.getInputStream(), path); bookmarkService.parseBookmarkFile(UserContextHolder.get().getUserId(), file, path);
return Result.success(null); return Result.success(null);
} }

View File

@ -0,0 +1,39 @@
package com.fanxb.bookmark.business.bookmark.dao;
import org.apache.ibatis.annotations.*;
/**
* @author fanxb
*/
@Mapper
public interface HostIconDao {
/**
* 插入一条数据
*
* @param host host
* @param iconPath path
* @author fanxb
*/
@Insert("insert into host_icon(host,iconPath) value(#{host},#{iconPath})")
void insert(@Param("host") String host, @Param("iconPath") String iconPath);
/**
* 根据host获取iconPath
*
* @param host host
* @return {@link String}
* @author fanxb
*/
@Select("select iconPath from host_icon where host=#{host} limit 1")
String selectByHost(String host);
/**
* 删除一条
*
* @param host host
* @author FleyX
*/
@Delete("delete from host_icon where host=#{host}")
void deleteByHost(String host);
}

View File

@ -3,6 +3,7 @@ package com.fanxb.bookmark.business.bookmark.service;
import com.fanxb.bookmark.business.bookmark.entity.BookmarkEs; import com.fanxb.bookmark.business.bookmark.entity.BookmarkEs;
import com.fanxb.bookmark.business.bookmark.entity.MoveNodeBody; import com.fanxb.bookmark.business.bookmark.entity.MoveNodeBody;
import com.fanxb.bookmark.common.entity.po.Bookmark; import com.fanxb.bookmark.common.entity.po.Bookmark;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
@ -54,7 +55,7 @@ public interface BookmarkService {
* @author fanxb * @author fanxb
* @date 2019/7/9 18:44 * @date 2019/7/9 18:44
*/ */
void parseBookmarkFile(int userId, InputStream stream, String path) throws Exception; void parseBookmarkFile(int userId, MultipartFile file, String path) throws Exception;
/** /**
* Description: 详情 * Description: 详情

View File

@ -1,21 +1,30 @@
package com.fanxb.bookmark.business.bookmark.service.impl; package com.fanxb.bookmark.business.bookmark.service.impl;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.*;
import cn.hutool.core.util.HashUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.fanxb.bookmark.business.api.UserApi; import com.fanxb.bookmark.business.api.UserApi;
import com.fanxb.bookmark.business.bookmark.constant.FileConstant;
import com.fanxb.bookmark.business.bookmark.dao.BookmarkDao; import com.fanxb.bookmark.business.bookmark.dao.BookmarkDao;
import com.fanxb.bookmark.business.bookmark.dao.HostIconDao;
import com.fanxb.bookmark.business.bookmark.entity.BookmarkEs; import com.fanxb.bookmark.business.bookmark.entity.BookmarkEs;
import com.fanxb.bookmark.business.bookmark.entity.MoveNodeBody; import com.fanxb.bookmark.business.bookmark.entity.MoveNodeBody;
import com.fanxb.bookmark.business.bookmark.entity.redis.BookmarkDeleteMessage; import com.fanxb.bookmark.business.bookmark.entity.redis.BookmarkDeleteMessage;
import com.fanxb.bookmark.business.bookmark.entity.redis.VisitNumPlus; import com.fanxb.bookmark.business.bookmark.entity.redis.VisitNumPlus;
import com.fanxb.bookmark.business.bookmark.service.BookmarkService; import com.fanxb.bookmark.business.bookmark.service.BookmarkService;
import com.fanxb.bookmark.business.bookmark.service.PinYinService; import com.fanxb.bookmark.business.bookmark.service.PinYinService;
import com.fanxb.bookmark.common.constant.CommonConstant;
import com.fanxb.bookmark.common.constant.EsConstant; import com.fanxb.bookmark.common.constant.EsConstant;
import com.fanxb.bookmark.common.constant.RedisConstant; import com.fanxb.bookmark.common.constant.RedisConstant;
import com.fanxb.bookmark.common.entity.po.Bookmark; import com.fanxb.bookmark.common.entity.po.Bookmark;
import com.fanxb.bookmark.common.exception.CustomException;
import com.fanxb.bookmark.common.util.*; import com.fanxb.bookmark.common.util.*;
import com.mysql.cj.conf.url.SingleConnectionUrl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import okhttp3.Response;
import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder;
@ -27,10 +36,24 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.awt.print.Book;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -39,7 +62,6 @@ import java.util.stream.Collectors;
* 类功能详述 * 类功能详述
* *
* @author fanxb * @author fanxb
* @date 2019/7/8 15:00
*/ */
@Service @Service
@Slf4j @Slf4j
@ -51,29 +73,38 @@ public class BookmarkServiceImpl implements BookmarkService {
private final PinYinService pinYinService; private final PinYinService pinYinService;
private final UserApi userApi; private final UserApi userApi;
private final EsUtil esUtil; private final EsUtil esUtil;
private final HostIconDao hostIconDao;
@Autowired @Autowired
public BookmarkServiceImpl(BookmarkDao bookmarkDao, PinYinService pinYinService, UserApi userApi, EsUtil esUtil) { public BookmarkServiceImpl(BookmarkDao bookmarkDao, PinYinService pinYinService, UserApi userApi, EsUtil esUtil, HostIconDao hostIconDao) {
this.bookmarkDao = bookmarkDao; this.bookmarkDao = bookmarkDao;
this.pinYinService = pinYinService; this.pinYinService = pinYinService;
this.userApi = userApi; this.userApi = userApi;
this.esUtil = esUtil; this.esUtil = esUtil;
this.hostIconDao = hostIconDao;
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void parseBookmarkFile(int userId, InputStream stream, String path) throws Exception { public void parseBookmarkFile(int userId, MultipartFile file, String path) throws Exception {
Document doc = Jsoup.parse(stream, "utf-8", ""); List<Bookmark> bookmarks = new ArrayList<>();
Elements elements = doc.select("html>body>dl>dt");
//获取当前层sort最大值 //获取当前层sort最大值
Integer sortBase = bookmarkDao.selectMaxSort(userId, path); Integer sortBase = bookmarkDao.selectMaxSort(userId, path);
if (sortBase == null) { if (sortBase == null) {
sortBase = 0; sortBase = 0;
} }
List<Bookmark> bookmarks = new ArrayList<>(); if (file.getOriginalFilename().endsWith(".db3")) {
for (int i = 0, length = elements.size(); i < length; i++) { //处理db文件
dealBookmark(userId, elements.get(i), path, sortBase + i, bookmarks); readFromOneEnv(bookmarks, userId, file, path, sortBase);
} else {
InputStream stream = file.getInputStream();
Document doc = Jsoup.parse(stream, "utf-8", "");
Elements elements = doc.select("html>body>dl>dt");
for (int i = 0, length = elements.size(); i < length; i++) {
dealBookmark(userId, elements.get(i), path, sortBase + i, bookmarks);
}
} }
//每一千条处理插入一次,批量更新搜索字段 //每一千条处理插入一次,批量更新搜索字段
List<Bookmark> tempList = new ArrayList<>(1000); List<Bookmark> tempList = new ArrayList<>(1000);
for (int i = 0; i < bookmarks.size(); i++) { for (int i = 0; i < bookmarks.size(); i++) {
@ -100,7 +131,6 @@ public class BookmarkServiceImpl implements BookmarkService {
* @param path 节点路径不包含自身 * @param path 节点路径不包含自身
* @param sort 当前层级中的排序序号 * @param sort 当前层级中的排序序号
* @author fanxb * @author fanxb
* @date 2019/7/8 14:49
*/ */
private void dealBookmark(int userId, Element ele, String path, int sort, List<Bookmark> bookmarks) { private void dealBookmark(int userId, Element ele, String path, int sort, List<Bookmark> bookmarks) {
if (!DT.equalsIgnoreCase(ele.tagName())) { if (!DT.equalsIgnoreCase(ele.tagName())) {
@ -109,7 +139,7 @@ public class BookmarkServiceImpl implements BookmarkService {
Element first = ele.child(0); Element first = ele.child(0);
if (A.equalsIgnoreCase(first.tagName())) { if (A.equalsIgnoreCase(first.tagName())) {
//说明为链接 //说明为链接
Bookmark node = new Bookmark(userId, path, first.ownText(), first.attr("href"), first.attr("icon") Bookmark node = new Bookmark(userId, path, first.ownText(), first.attr("href"), ""
, Long.parseLong(first.attr("add_date")) * 1000, sort); , Long.parseLong(first.attr("add_date")) * 1000, sort);
//存入数据库 //存入数据库
insertOne(node); insertOne(node);
@ -133,13 +163,63 @@ public class BookmarkServiceImpl implements BookmarkService {
} }
} }
/**
* 处理oneenv的导出
*
* @param bookmarks 书签列表
* @param userId 用户id
* @param file file
* @param path path
* @param sort sort
*/
private void readFromOneEnv(List<Bookmark> bookmarks, int userId, MultipartFile file, String path, int sort) {
String filePath = CommonConstant.fileSavePath + "/files/" + IdUtil.simpleUUID() + ".db3";
try {
file.transferTo(FileUtil.newFile(filePath));
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + filePath)) {
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("select * from on_categorys");
Map<Long, Bookmark> folderMap = new HashMap<>();
Map<Long, Integer> childSortBaseMap = new HashMap<>();
while (rs.next()) {
long addTime = rs.getLong("add_time");
Bookmark folder = new Bookmark(userId, path, StrUtil.nullToEmpty(rs.getString("name")), addTime == 0 ? System.currentTimeMillis() : addTime * 1000, sort++);
int childSortBase = 0;
if (insertOne(folder)) {
childSortBase = ObjectUtil.defaultIfNull(bookmarkDao.selectMaxSort(userId, path), 0);
}
long id = rs.getLong("id");
folderMap.put(id, folder);
childSortBaseMap.put(id, childSortBase);
}
rs.close();
rs = stat.executeQuery("select * from on_links");
while (rs.next()) {
long fId = rs.getLong("fid");
long addTime = rs.getLong("add_time");
int tempSort = childSortBaseMap.get(fId);
childSortBaseMap.put(fId, tempSort + 1);
Bookmark folder = folderMap.get(fId);
String curPath = folder == null ? "" : folder.getPath() + "." + folder.getBookmarkId();
Bookmark bookmark = new Bookmark(userId, curPath, StrUtil.nullToEmpty(rs.getString("title"))
, StrUtil.nullToEmpty(rs.getString("url")), "", addTime == 0 ? System.currentTimeMillis() : addTime * 1000, tempSort);
bookmarks.add(bookmark);
insertOne(bookmark);
}
rs.close();
stat.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/** /**
* Description: 插入一条书签如果已经存在同名书签将跳过 * Description: 插入一条书签如果已经存在同名书签将跳过
* *
* @param node node * @param node node
* @return boolean 如果已经存在返回true否则false * @return boolean 如果已经存在返回true否则false
* @author fanxb * @author fanxb
* @date 2019/7/8 17:25
*/ */
private boolean insertOne(Bookmark node) { private boolean insertOne(Bookmark node) {
//先根据name,userId,parentId获取此节点id //先根据name,userId,parentId获取此节点id
@ -201,11 +281,14 @@ public class BookmarkServiceImpl implements BookmarkService {
bookmark.setUserId(userId); bookmark.setUserId(userId);
bookmark.setCreateTime(System.currentTimeMillis()); bookmark.setCreateTime(System.currentTimeMillis());
bookmark.setAddTime(bookmark.getCreateTime()); bookmark.setAddTime(bookmark.getCreateTime());
bookmark.setIcon(getIconBase64(bookmark.getUrl())); bookmark.setIcon(bookmark.getType() == 1 ? "" : getIconPath(bookmark.getUrl(), bookmark.getIcon(), bookmark.getIconUrl(), true));
//文件夹和书签都建立搜索key //文件夹和书签都建立搜索key
pinYinService.changeBookmark(bookmark); pinYinService.changeBookmark(bookmark);
bookmarkDao.insertOne(bookmark); bookmarkDao.insertOne(bookmark);
userApi.versionPlus(userId); userApi.versionPlus(userId);
if (StrUtil.isEmpty(bookmark.getIcon()) && bookmark.getType() == 0) {
updateIconAsync(bookmark.getBookmarkId(), bookmark.getUrl(), userId);
}
return bookmark; return bookmark;
} }
@ -215,13 +298,33 @@ public class BookmarkServiceImpl implements BookmarkService {
bookmark.setUserId(userId); bookmark.setUserId(userId);
if (bookmark.getType() == 0) { if (bookmark.getType() == 0) {
pinYinService.changeBookmark(bookmark); pinYinService.changeBookmark(bookmark);
bookmark.setIcon(getIconBase64(bookmark.getUrl())); bookmark.setIcon(getIconPath(bookmark.getUrl(), null, null, true));
if (StrUtil.isEmpty(bookmark.getIcon())) {
updateIconAsync(bookmark.getBookmarkId(), bookmark.getUrl(), userId);
}
} }
bookmarkDao.editBookmark(bookmark); bookmarkDao.editBookmark(bookmark);
userApi.versionPlus(userId); userApi.versionPlus(userId);
return bookmark.getIcon(); return bookmark.getIcon();
} }
/**
* 异步更新书签icon
*
* @param id 书签id
* @param url 书签url
* @param userId userId
*/
private void updateIconAsync(int id, String url, int userId) {
ThreadPoolUtil.execute(() -> {
String icon = getIconPath(url, null, null, false);
if (StrUtil.isEmpty(icon)) {
return;
}
bookmarkDao.updateIcon(id, icon);
});
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@ -271,10 +374,10 @@ public class BookmarkServiceImpl implements BookmarkService {
int size = 100; int size = 100;
int start = 0; int start = 0;
List<Bookmark> deal; List<Bookmark> deal;
while ((deal = bookmarkDao.selectUserNoIcon(userId, start, size)).size() > 0) { while (!(deal = bookmarkDao.selectUserNoIcon(userId, start, size)).isEmpty()) {
start += size; start += size;
deal.forEach(item -> { deal.forEach(item -> {
String icon = getIconBase64(item.getUrl()); String icon = getIconPath(item.getUrl(), null, null, false);
if (StrUtil.isNotEmpty(icon)) { if (StrUtil.isNotEmpty(icon)) {
bookmarkDao.updateIcon(item.getBookmarkId(), icon); bookmarkDao.updateIcon(item.getBookmarkId(), icon);
} }
@ -305,24 +408,96 @@ public class BookmarkServiceImpl implements BookmarkService {
return resPath; return resPath;
} }
private String getIconBase64(String url) { /**
if (StrUtil.isEmpty(url)) { * 获取icon,通过网络获取或者从base64还原
return ""; *
} * @param url 书签url路径
* @param icon base64编码的icon
* @param iconUrl base64编码的文件文件名,用于获取文件名后缀
* @param quick 是否快速获取
* @return {@link String}
* @author fanxb
*/
private String getIconPath(String url, String icon, String iconUrl, boolean quick) {
String host;
try { try {
URL urlObj = new URL(url); URL urlObj = new URL(url);
byte[] data = HttpUtil.download(urlIconAddress + "/icon?url=" + urlObj.getHost() + "&size=8..16..64", false); host = urlObj.getAuthority();
String base64 = new String(Base64.getEncoder().encode(data)); } catch (Exception e) {
if (StrUtil.isNotEmpty(base64)) {
return "data:image/png;base64," + base64;
} else {
log.warn("url无法获取icon:{}", url);
}
} catch (MalformedURLException e) {
log.warn("url无法解析出domain:{}", url); log.warn("url无法解析出domain:{}", url);
return "";
}
if (StrUtil.isNotBlank(icon)) {
//优先从base64还原出图片
try {
byte[] b = Base64Decoder.decode(icon.substring(icon.indexOf(",") + 1));
String iconPath = saveToFile(iconUrl, host, b);
hostIconDao.deleteByHost(host);
hostIconDao.insert(host, iconPath);
return iconPath;
} catch (Exception e) {
log.error("解析base64获取icon故障:{}", iconUrl, e);
}
}
String iconPath = hostIconDao.selectByHost(host);
if (iconPath != null) {
return iconPath;
}
//再根据url解析
iconPath = saveFile(host, urlIconAddress + "/icon?url=" + host + "&size=16..128..256", quick);
if (StrUtil.isNotEmpty(iconPath)) {
hostIconDao.insert(host, iconPath);
}
return iconPath;
}
/**
* 保存文件到icon路径
*
* @param host host
* @param url url
* @param quick 是否快速获取,快速获取超时时间1s
* @return {@link String}
* @author FleyX
*/
private String saveFile(String host, String url, boolean quick) {
try (Response res = (quick ? HttpUtil.getSHORT_CLIENT() : HttpUtil.getClient(false)).newCall(new Request.Builder().url(url)
.header("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36")
.get().build()).execute()) {
assert res.body() != null;
if (!HttpUtil.checkIsOk(res.code())) {
throw new CustomException("请求错误:" + res.code());
}
byte[] data = res.body().byteStream().readAllBytes();
if (data.length > 0) {
String iconUrl = new URL(res.request().url().toString()).getPath();
return saveToFile(iconUrl, host, data);
} else {
log.info("未获取到icon:{}", url);
}
} catch (SocketTimeoutException timeoutException) {
log.info("获取icon超时{}", host);
} catch (Exception e) { } catch (Exception e) {
log.error("url获取icon故障:{}", url, e); log.error("url获取icon故障:{}", url, e);
} }
return ""; return "";
} }
/**
* 保存到文件中
*
* @param iconUrl icon文件名
* @param host host
* @param b 数据
* @return {@link String}
* @author FleyX
*/
private String saveToFile(String iconUrl, String host, byte[] b) {
String fileName = host.replace(":", ".") + iconUrl.substring(iconUrl.lastIndexOf("."));
String filePath = Paths.get(FileConstant.FAVICON_PATH, host.replace("www", "").replaceAll("\\.", "").substring(0, 2), fileName).toString();
FileUtil.writeBytes(b, Paths.get(CommonConstant.fileSavePath, filePath).toString());
return File.separator + filePath;
}
} }

View File

@ -23,12 +23,6 @@
<artifactId>bookmark-common</artifactId> <artifactId>bookmark-common</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@ -1,5 +1,6 @@
package com.fanxb.bookmark.business.user.constant; package com.fanxb.bookmark.business.user.constant;
import com.fanxb.bookmark.common.constant.CommonConstant;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -17,6 +18,6 @@ public class FileConstant {
/** /**
* 用户头像目录 * 用户头像目录
*/ */
public static String iconPath = Paths.get("files", "public", "icon").toString(); public static String iconPath = Paths.get(CommonConstant.fileSavePath, "files", "public", "icon").toString();
} }

View File

@ -0,0 +1,47 @@
package com.fanxb.bookmark.business.user.controller;
import com.fanxb.bookmark.business.user.dao.SearchEngineDao;
import com.fanxb.bookmark.business.user.entity.SearchEngine;
import com.fanxb.bookmark.business.user.service.SearchEngineService;
import com.fanxb.bookmark.common.entity.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/searchEngine")
public class SearchEngineController {
@Autowired
private SearchEngineService searchEngineService;
/**
* 列表查询
*/
@GetMapping("/list")
public Result list() {
return Result.success(searchEngineService.list());
}
@PostMapping("/insert")
public Result insert(@RequestBody SearchEngine body){
searchEngineService.insertOne(body);
return Result.success();
}
@PostMapping("/edit")
public Result edit(@RequestBody SearchEngine body){
searchEngineService.editOne(body);
return Result.success();
}
@PostMapping("/delete")
public Result delete(@RequestBody SearchEngine body){
searchEngineService.deleteOne(body.getId());
return Result.success();
}
@PostMapping("/setChecked")
public Result setChecked(@RequestBody SearchEngine body){
searchEngineService.setChecked(body.getId());
return Result.success();
}
}

View File

@ -0,0 +1,8 @@
package com.fanxb.bookmark.business.user.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fanxb.bookmark.business.user.entity.SearchEngine;
public interface SearchEngineDao extends BaseMapper<SearchEngine> {
}

View File

@ -1,5 +1,6 @@
package com.fanxb.bookmark.business.user.dao; package com.fanxb.bookmark.business.user.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fanxb.bookmark.common.entity.po.User; import com.fanxb.bookmark.common.entity.po.User;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
@ -16,7 +17,7 @@ import java.util.List;
* @date 2019/7/6 11:36 * @date 2019/7/6 11:36
*/ */
@Component @Component
public interface UserDao { public interface UserDao extends BaseMapper<User> {
/** /**
* Description: 新增一个用户 * Description: 新增一个用户
@ -182,16 +183,6 @@ public interface UserDao {
@Select("select userId from user order by userId limit #{start},#{size}") @Select("select userId from user order by userId limit #{start},#{size}")
List<Integer> selectUserIdPage(@Param("start") int start, @Param("size") int size); List<Integer> selectUserIdPage(@Param("start") int start, @Param("size") int size);
/**
* 更新用户搜索引擎
*
* @param userId userId
* @param engine engine
* @author fanxb
* @date 2021/3/14
**/
@Update("update user set defaultSearchEngine=#{engine} where userId=#{userId}")
void updateSearchEngine(@Param("userId") int userId, @Param("engine") String engine);
/** /**
* 更新一个字段-一个条件 * 更新一个字段-一个条件

View File

@ -0,0 +1,29 @@
package com.fanxb.bookmark.business.user.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
@TableName("search_engine")
public class SearchEngine {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer userId;
private Integer checked;
/**
* 名称
*/
private String name;
/**
* url
*/
private String url;
/**
* 图标
*/
private String icon;
}

View File

@ -0,0 +1,49 @@
package com.fanxb.bookmark.business.user.service;
import com.fanxb.bookmark.business.user.entity.SearchEngine;
import java.util.List;
public interface SearchEngineService {
/**
* 列表查询
*/
List<SearchEngine> list();
/**
* delete one by id
*
* @param id id
*/
void deleteOne(int id);
/**
* insert one
*
* @param body body
*/
void insertOne(SearchEngine body);
/**
* edit one
*
* @param body body
*/
void editOne(SearchEngine body);
/**
* 设为默认搜索项
*
* @param id
*/
void setChecked(Integer id);
/**
* 新用户初始化
*
* @param userId userId
*/
void newUserInit(int userId);
}

View File

@ -87,6 +87,6 @@ public class BaseInfoServiceImpl implements BaseInfoService {
@Override @Override
public void changeDefaultSearchEngine(User user) { public void changeDefaultSearchEngine(User user) {
userDao.updateSearchEngine(user.getUserId(), user.getDefaultSearchEngine()); userDao.updateById(user);
} }
} }

View File

@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.fanxb.bookmark.business.user.dao.UserDao; import com.fanxb.bookmark.business.user.dao.UserDao;
import com.fanxb.bookmark.business.user.service.OauthService; import com.fanxb.bookmark.business.user.service.OauthService;
import com.fanxb.bookmark.business.user.service.SearchEngineService;
import com.fanxb.bookmark.business.user.service.UserService; import com.fanxb.bookmark.business.user.service.UserService;
import com.fanxb.bookmark.business.user.vo.OauthBody; import com.fanxb.bookmark.business.user.vo.OauthBody;
import com.fanxb.bookmark.common.constant.CommonConstant; import com.fanxb.bookmark.common.constant.CommonConstant;
@ -38,6 +39,8 @@ public class OauthServiceImpl implements OauthService {
private String githubClientId; private String githubClientId;
@Value("${OAuth.github.secret}") @Value("${OAuth.github.secret}")
private String githubSecret; private String githubSecret;
@Autowired
private SearchEngineService searchEngineService;
private final UserDao userDao; private final UserDao userDao;
private final UserService userService; private final UserService userService;
@ -105,6 +108,7 @@ public class OauthServiceImpl implements OauthService {
other.setLastLoginTime(System.currentTimeMillis()); other.setLastLoginTime(System.currentTimeMillis());
other.setVersion(0); other.setVersion(0);
userDao.addOne(other); userDao.addOne(other);
searchEngineService.newUserInit(other.getUserId());
return other; return other;
} else { } else {
if (!current.getEmail().equals(other.getEmail()) || !current.getGithubId().equals(other.getGithubId())) { if (!current.getEmail().equals(other.getEmail()) || !current.getGithubId().equals(other.getGithubId())) {

View File

@ -0,0 +1,88 @@
package com.fanxb.bookmark.business.user.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.druid.support.ibatis.SpringIbatisBeanNameAutoProxyCreator;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.fanxb.bookmark.business.user.dao.SearchEngineDao;
import com.fanxb.bookmark.business.user.dao.UserDao;
import com.fanxb.bookmark.business.user.entity.SearchEngine;
import com.fanxb.bookmark.business.user.service.SearchEngineService;
import com.fanxb.bookmark.common.entity.UserContext;
import com.fanxb.bookmark.common.entity.po.User;
import com.fanxb.bookmark.common.exception.CustomException;
import com.fanxb.bookmark.common.util.UserContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class SearchEngineServiceImpl implements SearchEngineService {
@Autowired
private SearchEngineDao searchEngineDao;
@Autowired
private UserDao userDao;
@Override
public List<SearchEngine> list() {
return searchEngineDao.selectList(new LambdaQueryWrapper<SearchEngine>().eq(SearchEngine::getUserId, UserContextHolder.get().getUserId()));
}
@Override
public void deleteOne(int id) {
SearchEngine engine = searchEngineDao.selectById(id);
if (engine.getUserId() != UserContextHolder.get().getUserId()) {
throw new CustomException("无法操作其他人数据");
}
if (engine.getChecked() == 1) {
throw new CustomException("默认搜索引擎无法删除");
}
searchEngineDao.deleteById(id);
}
@Override
public void insertOne(SearchEngine body) {
checkOne(body);
body.setId(null).setChecked(0).setUserId(UserContextHolder.get().getUserId());
searchEngineDao.insert(body);
}
private void checkOne(SearchEngine body) {
if (StrUtil.hasBlank(body.getIcon(), body.getUrl(), body.getName())) {
throw new CustomException("请填写完整");
}
if (!body.getUrl().contains("%s")) {
throw new CustomException("路径中必须包含%s");
}
}
@Override
public void editOne(SearchEngine body) {
SearchEngine engine = searchEngineDao.selectById(body.getId());
if (engine.getUserId() != UserContextHolder.get().getUserId()) {
throw new CustomException("无法操作其他人数据");
}
checkOne(body);
searchEngineDao.updateById(body);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void setChecked(Integer id) {
int userId = UserContextHolder.get().getUserId();
LambdaUpdateWrapper<SearchEngine> update = new LambdaUpdateWrapper<SearchEngine>().set(SearchEngine::getChecked, 0).eq(SearchEngine::getUserId, userId).eq(SearchEngine::getChecked, 1);
searchEngineDao.update(null, update);
update = new LambdaUpdateWrapper<SearchEngine>().set(SearchEngine::getChecked, 1).eq(SearchEngine::getId, id).eq(SearchEngine::getUserId, userId);
searchEngineDao.update(null, update);
}
@Override
public void newUserInit(int userId) {
searchEngineDao.insert(new SearchEngine().setUserId(userId).setIcon("icon-baidu").setName("百度").setUrl("https://www.baidu.com/s?ie=UTF-8&wd=%s").setChecked(1));
searchEngineDao.insert(new SearchEngine().setUserId(userId).setIcon("icon-bing").setName("必应").setUrl("https://www.bing.com/search?q=%s").setChecked(0));
searchEngineDao.insert(new SearchEngine().setUserId(userId).setIcon("icon-google").setName("谷歌").setUrl("https://www.google.com/search?q=%s").setChecked(0));
}
}

View File

@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
import com.fanxb.bookmark.business.api.BookmarkApi; import com.fanxb.bookmark.business.api.BookmarkApi;
import com.fanxb.bookmark.business.user.constant.FileConstant; import com.fanxb.bookmark.business.user.constant.FileConstant;
import com.fanxb.bookmark.business.user.dao.UserDao; import com.fanxb.bookmark.business.user.dao.UserDao;
import com.fanxb.bookmark.business.user.service.SearchEngineService;
import com.fanxb.bookmark.business.user.service.UserService; import com.fanxb.bookmark.business.user.service.UserService;
import com.fanxb.bookmark.business.user.vo.LoginBody; import com.fanxb.bookmark.business.user.vo.LoginBody;
import com.fanxb.bookmark.business.user.vo.RegisterBody; import com.fanxb.bookmark.business.user.vo.RegisterBody;
@ -20,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
@ -42,6 +44,8 @@ public class UserServiceImpl implements UserService {
* 登陆最大重试次数 * 登陆最大重试次数
*/ */
private static final int LOGIN_COUNT = 5; private static final int LOGIN_COUNT = 5;
@Autowired
private SearchEngineService searchEngineService;
private final UserDao userDao; private final UserDao userDao;
private final StringRedisTemplate redisTemplate; private final StringRedisTemplate redisTemplate;
@ -83,6 +87,7 @@ public class UserServiceImpl implements UserService {
* @author fanxb * @author fanxb
* @date 2019/7/6 11:30 * @date 2019/7/6 11:30
*/ */
@Transactional(rollbackFor = Exception.class)
public String register(RegisterBody body) { public String register(RegisterBody body) {
User user = userDao.selectByUsernameOrEmail(body.getUsername(), body.getEmail()); User user = userDao.selectByUsernameOrEmail(body.getUsername(), body.getEmail());
if (user != null) { if (user != null) {
@ -102,6 +107,7 @@ public class UserServiceImpl implements UserService {
user.setLastLoginTime(System.currentTimeMillis()); user.setLastLoginTime(System.currentTimeMillis());
user.setVersion(0); user.setVersion(0);
userDao.addOne(user); userDao.addOne(user);
searchEngineService.newUserInit(user.getUserId());
Map<String, String> data = new HashMap<>(1); Map<String, String> data = new HashMap<>(1);
data.put("userId", String.valueOf(user.getUserId())); data.put("userId", String.valueOf(user.getUserId()));
return JwtUtil.encode(data, CommonConstant.jwtSecret, LONG_EXPIRE_TIME); return JwtUtil.encode(data, CommonConstant.jwtSecret, LONG_EXPIRE_TIME);

View File

@ -37,16 +37,10 @@
<artifactId>commons-pool2</artifactId> <artifactId>commons-pool2</artifactId>
</dependency> </dependency>
<!--mybatis依赖-->
<!-- <dependency>-->
<!-- <groupId>org.mybatis.spring.boot</groupId>-->
<!-- <artifactId>mybatis-spring-boot-starter</artifactId>-->
<!-- <version>2.0.1</version>-->
<!-- </dependency>-->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId> <artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version> <version>3.5.3.2</version>
</dependency> </dependency>
<dependency> <dependency>
@ -59,21 +53,66 @@
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId> <artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.18</version> <version>1.2.18</version>
</dependency> </dependency>
<!--数据库版本管理--> <!--数据库版本管理-->
<dependency> <dependency>
<groupId>org.flywaydb</groupId> <groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId> <artifactId>flyway-core</artifactId>
<version>5.2.4</version> <version>9.21.1</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>9.21.1</version>
</dependency> </dependency>
<!--mysql jdbc依赖--> <!--mysql jdbc依赖-->
<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency> </dependency>
<!-- &lt;!&ndash;邮件依赖&ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-mail</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash;减负依赖&ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.projectlombok</groupId>-->
<!-- <artifactId>lombok</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash;json工具依赖&ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.83</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.elasticsearch.client</groupId>-->
<!-- <artifactId>elasticsearch-rest-high-level-client</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>cn.hutool</groupId>-->
<!-- <artifactId>hutool-all</artifactId>-->
<!-- <version>5.8.21</version>-->
<!-- </dependency>-->
<!--单元测试-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<!--mysql jdbc依赖-->
<!-- <dependency>-->
<!-- <groupId>mysql</groupId>-->
<!-- <artifactId>mysql-connector-java</artifactId>-->
<!-- <version>8.0.33</version>-->
<!-- </dependency>-->
<!--邮件依赖--> <!--邮件依赖-->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@ -89,7 +128,7 @@
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <artifactId>fastjson</artifactId>
<version>1.2.73</version> <version>1.2.83</version>
</dependency> </dependency>
@ -101,7 +140,7 @@
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>
<version>5.2.3</version> <version>5.8.25</version>
</dependency> </dependency>
<!--单元测试--> <!--单元测试-->

View File

@ -1,6 +1,9 @@
package com.fanxb.bookmark.common.configuration; package com.fanxb.bookmark.common.configuration;
import com.fanxb.bookmark.common.factory.CustomThreadFactory; import com.fanxb.bookmark.common.factory.CustomThreadFactory;
import com.fanxb.bookmark.common.factory.ThreadPoolFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@ -11,15 +14,16 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
* Created with IntelliJ IDEA * Created with IntelliJ IDEA
* *
* @author fanxb * @author fanxb
* @date 2020/1/26
*/ */
@Configuration
@Slf4j
public class ScheduleConfig implements SchedulingConfigurer { public class ScheduleConfig implements SchedulingConfigurer {
@Override @Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
ScheduledExecutorService service = new ScheduledThreadPoolExecutor(5, new CustomThreadFactory("schedule")); ScheduledExecutorService service = new ScheduledThreadPoolExecutor(5, new CustomThreadFactory("schedule"));
scheduledTaskRegistrar.setScheduler(service); scheduledTaskRegistrar.setScheduler(service);
log.info("自定义schedule线程池成功");
} }
} }

View File

@ -13,4 +13,8 @@ public class NumberConstant {
* 2^10 * 2^10
*/ */
public static final int K_SIZE = 1024; public static final int K_SIZE = 1024;
/**
* 一天的秒数
*/
public static final int S_DAY = 24 * 60 * 60;
} }

View File

@ -35,4 +35,6 @@ public class RedisConstant {
public static String getUserFailCountKey(String username) { public static String getUserFailCountKey(String username) {
return "bookmark_user_fail_count_" + username; return "bookmark_user_fail_count_" + username;
} }
public static final String BING_IMG = "bing_img";
} }

View File

@ -0,0 +1,14 @@
package com.fanxb.bookmark.common.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fanxb.bookmark.common.entity.po.GlobalConfigPo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @author fanxb
*/
@Mapper
public interface GlobalConfigDao extends BaseMapper<GlobalConfigPo> {
}

View File

@ -12,7 +12,6 @@ import java.util.List;
* 类功能详述 * 类功能详述
* *
* @author fanxb * @author fanxb
* @date 2019/7/8 11:19
*/ */
@Data @Data
public class Bookmark { public class Bookmark {
@ -36,6 +35,7 @@ public class Bookmark {
private String name; private String name;
private String url = ""; private String url = "";
private String icon = ""; private String icon = "";
private String iconUrl;
private Integer sort; private Integer sort;
private String searchKey = ""; private String searchKey = "";
private Long addTime; private Long addTime;
@ -53,7 +53,7 @@ public class Bookmark {
this.setUserId(userId); this.setUserId(userId);
this.setPath(path); this.setPath(path);
this.setType(FOLDER_TYPE); this.setType(FOLDER_TYPE);
this.setName(name); this.setName(name.length() > 2000 ? name.substring(0, 1999) : name);
this.setAddTime(addTime); this.setAddTime(addTime);
this.setSort(sort); this.setSort(sort);
this.setCreateTime(System.currentTimeMillis()); this.setCreateTime(System.currentTimeMillis());
@ -64,7 +64,7 @@ public class Bookmark {
this.setUserId(userId); this.setUserId(userId);
this.setPath(path); this.setPath(path);
this.setType(BOOKMARK_TYPE); this.setType(BOOKMARK_TYPE);
this.setName(name); this.setName(name.length() > 2000 ? name.substring(0, 1999) : name);
this.setUrl(url); this.setUrl(url);
this.setIcon(icon); this.setIcon(icon);
this.setSort(sort); this.setSort(sort);

View File

@ -0,0 +1,20 @@
package com.fanxb.bookmark.common.entity.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 全局配置表
*
* @author FleyX
*/
@Data
@TableName("global_config")
public class GlobalConfigPo {
@TableId(value = "code")
private String code;
private String value;
private String description;
}

View File

@ -2,6 +2,10 @@ package com.fanxb.bookmark.common.entity.po;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONField;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data; import lombok.Data;
@ -16,9 +20,11 @@ import java.util.Map;
* @date 2019/7/4 20:14 * @date 2019/7/4 20:14
*/ */
@Data @Data
@TableName("user")
public class User { public class User {
private int userId; @TableId(type = IdType.AUTO)
private Integer userId;
/** /**
* 第三方github登陆id,-1说明非github登陆 * 第三方github登陆id,-1说明非github登陆
*/ */
@ -30,6 +36,7 @@ public class User {
/** /**
* 是否未设置密码 * 是否未设置密码
*/ */
@TableField(exist = false)
private Boolean noPassword; private Boolean noPassword;
@JSONField(serialize = false) @JSONField(serialize = false)
private String password; private String password;
@ -42,9 +49,10 @@ public class User {
* 书签同步版本 * 书签同步版本
*/ */
private int version; private int version;
/** /**
* 默认搜索引擎 * 默认搜索引擎
*/ */
private String defaultSearchEngine; private Integer searchEngineId;
} }

View File

@ -0,0 +1,26 @@
package com.fanxb.bookmark.common.entity.vo;
import lombok.Data;
import java.util.Map;
/**
* 全局公共配置
*
* @author fanxb
*/
@Data
public class GlobalConfigVo {
/**
* 是否存在网络代理(外网访问)
*/
private boolean proxyExist;
/**
* bing每日一图地址
*/
private String bingImgSrc;
/**
* 浏览器插件版本plugin
*/
private Map<String, String> map;
}

View File

@ -142,7 +142,7 @@ public class LoginFilter implements Filter {
UserContextHolder.set(context); UserContextHolder.set(context);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
log.error("jwt解密失败{},原因:{}", jwt, e.getMessage()); log.info("jwt解密失败{},原因:{}", jwt, e.getMessage());
return false; return false;
} }
} }

View File

@ -0,0 +1,28 @@
package com.fanxb.bookmark.common.schedule;
import com.fanxb.bookmark.common.service.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author fanxb
*/
@Component
public class BingImgSchedule {
private final ConfigService configService;
@Autowired
public BingImgSchedule(ConfigService configService) {
this.configService = configService;
}
@PostConstruct
@Scheduled(cron = "* 0 0/1 * * *")
public void cache() {
configService.getCacheBingImg();
}
}

View File

@ -1,6 +1,6 @@
package com.fanxb.bookmark.common.service; package com.fanxb.bookmark.common.service;
import java.util.Map; import com.fanxb.bookmark.common.entity.vo.GlobalConfigVo;
/** /**
* 全局配置相关 * 全局配置相关
@ -17,5 +17,12 @@ public interface ConfigService {
* @author fanxb * @author fanxb
* @date 2021/9/15 下午9:58 * @date 2021/9/15 下午9:58
*/ */
Map<String, Object> getGlobalConfig(); GlobalConfigVo getGlobalConfig();
/**
* 缓存bing每日一图到redis
*
* @author fanxb
*/
String getCacheBingImg();
} }

View File

@ -1,23 +1,96 @@
package com.fanxb.bookmark.common.service.impl; package com.fanxb.bookmark.common.service.impl;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fanxb.bookmark.common.constant.CommonConstant;
import com.fanxb.bookmark.common.constant.NumberConstant;
import com.fanxb.bookmark.common.constant.RedisConstant;
import com.fanxb.bookmark.common.dao.GlobalConfigDao;
import com.fanxb.bookmark.common.entity.po.GlobalConfigPo;
import com.fanxb.bookmark.common.entity.vo.GlobalConfigVo;
import com.fanxb.bookmark.common.service.ConfigService; import com.fanxb.bookmark.common.service.ConfigService;
import com.fanxb.bookmark.common.util.HttpUtil; import com.fanxb.bookmark.common.util.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap; import java.io.File;
import java.util.Map; import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/** /**
* @author fanxb * @author fanxb
* @date 2021-09-15-下午9:59 * @date 2021-09-15-下午9:59
*/ */
@Service @Service
@Slf4j
public class ConfigServiceImpl implements ConfigService { public class ConfigServiceImpl implements ConfigService {
@Override
public Map<String, Object> getGlobalConfig() { private final StringRedisTemplate stringRedisTemplate;
Map<String, Object> res = new HashMap<>(1); private final GlobalConfigDao globalConfigDao;
res.put("proxyExist", HttpUtil.getProxyExist());
return res; @Autowired
public ConfigServiceImpl(StringRedisTemplate stringRedisTemplate, GlobalConfigDao globalConfigDao) {
this.stringRedisTemplate = stringRedisTemplate;
this.globalConfigDao = globalConfigDao;
} }
@Value("${bing.host}")
private String bingHost;
@Value("${bing.onePic}")
private String bingUrl;
@Override
public GlobalConfigVo getGlobalConfig() {
List<GlobalConfigPo> pos = globalConfigDao.selectByMap(Collections.emptyMap());
Map<String, String> map = pos.stream().collect(Collectors.toMap(GlobalConfigPo::getCode, GlobalConfigPo::getValue));
GlobalConfigVo vo = new GlobalConfigVo();
vo.setBingImgSrc(getCacheBingImg());
vo.setMap(map);
return vo;
}
@Override
public String getCacheBingImg() {
String str = stringRedisTemplate.opsForValue().get(RedisConstant.BING_IMG);
if (str != null) {
return str;
}
str = getBingImg();
stringRedisTemplate.opsForValue().set(RedisConstant.BING_IMG, str, 2, TimeUnit.HOURS);
return str;
}
private String getBingImg() {
try {
JSONObject bingObj = HttpUtil.getObj(bingHost + bingUrl, null, false);
String path = bingObj.getJSONArray("images").getJSONObject(0).getString("url");
String picUrl = bingHost + path;
Request request = new Request.Builder().url(picUrl).build();
try (Response res = HttpUtil.getClient(false).newCall(request).execute()) {
byte[] bytes = res.body().bytes();
String filePath = CommonConstant.fileSavePath + "/files/public/bing.jpg";
FileUtil.writeBytes(bytes, filePath);
} catch (Exception e) {
log.error("获取bing每日一图错误{}", e.getLocalizedMessage(), e);
}
} catch (Exception e) {
log.error("获取bing每日一图错误{}", e.getLocalizedMessage(), e);
}
return "/files/public/bing.jpg";
}
} }

View File

@ -12,8 +12,6 @@ import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Proxy; import java.net.Proxy;
import java.util.Map; import java.util.Map;
@ -51,42 +49,41 @@ public class HttpUtil {
/** /**
* 无代理环境 * 无代理环境
*/ */
private static final OkHttpClient CLIENT = new OkHttpClient.Builder().connectTimeout(2, TimeUnit.SECONDS) private static final OkHttpClient CLIENT = new OkHttpClient.Builder().connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
.build(); .build();
/**
* 超时时间1s
*/
@Getter
private static final OkHttpClient SHORT_CLIENT = new OkHttpClient.Builder().connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(1, TimeUnit.SECONDS)
.build();
/**
* 获取客户端
*
* @param proxy 是否代理
* @return {@link OkHttpClient}
* @author fanxb
*/
public static OkHttpClient getClient(boolean proxy) {
return proxy ? PROXY_CLIENT : CLIENT;
}
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
@PostConstruct @PostConstruct
public void init() { public void init() {
OkHttpClient.Builder builder = new OkHttpClient.Builder(); OkHttpClient.Builder builder = new OkHttpClient.Builder().connectTimeout(1, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS);
log.info("代理配置ip:{},port:{}", proxyIp, proxyPort); log.info("代理配置ip:{},port:{}", proxyIp, proxyPort);
if (StrUtil.isNotBlank(proxyIp) && StrUtil.isNotBlank(proxyPort)) { if (StrUtil.isNotBlank(proxyIp) && StrUtil.isNotBlank(proxyPort)) {
builder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyIp, Integer.parseInt(proxyPort)))); builder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyIp, Integer.parseInt(proxyPort))));
proxyExist = true; proxyExist = true;
} PROXY_CLIENT = builder.build();
PROXY_CLIENT = builder.connectTimeout(10, TimeUnit.SECONDS) } else {
.readTimeout(60, TimeUnit.SECONDS) PROXY_CLIENT = CLIENT;
.build();
}
/***
* 下载文件
* @author fanxb
* @param url 下载链接
* @param proxy 是否使用代理
* @return java.io.InputStream
* @date 2021/3/12
**/
public static byte[] download(String url, boolean proxy) {
try (Response res = (proxy ? PROXY_CLIENT : CLIENT).newCall(new Request.Builder().url(url).build()).execute()) {
assert res.body() != null;
if (checkIsOk(res.code())) {
return res.body().byteStream().readAllBytes();
} else {
throw new CustomException("下载出现问题:" + res.body().string());
}
} catch (Exception e) {
throw new CustomException(e);
} }
} }
@ -270,6 +267,8 @@ public class HttpUtil {
} }
return ipAddress; return ipAddress;
} }
} }

View File

@ -1,17 +0,0 @@
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks Menu</H1>
<DL><p>
<DT><A HREF="http://0.0.0.1/" ADD_DATE="1614837460" LAST_MODIFIED="1614837465">1</A>
<DT><A HREF="http://0.0.0.2/" ADD_DATE="1614837471" LAST_MODIFIED="1614837474">2</A>
<DT><H3 ADD_DATE="1614837478" LAST_MODIFIED="1614837497">f1</H3>
<DL><p>
<DT><A HREF="http://asdf/" ADD_DATE="1614837485" LAST_MODIFIED="1614837493" TAGS="ww">f11</A>
<DT><A HREF="http://f12/" ADD_DATE="1614837497" LAST_MODIFIED="1614837502">f12</A>
</DL><p>
</DL>

View File

@ -1,17 +0,0 @@
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks Menu</H1>
<DL><p>
<DT><A HREF="http://0.0.0.1/" ADD_DATE="1614837460" LAST_MODIFIED="1614837465">1</A>
<DT><A HREF="http://0.0.0.2/" ADD_DATE="1614837471" LAST_MODIFIED="1614837474">2</A>
<DT><H3 ADD_DATE="1614837478" LAST_MODIFIED="1614837497">f1</H3>
<DL><p>
<DT><A HREF="http://asdf/" ADD_DATE="1614837485" LAST_MODIFIED="1614837493" TAGS="ww">f11</A>
<DT><A HREF="http://f12/" ADD_DATE="1614837497" LAST_MODIFIED="1614837502">f12</A>
</DL><p>
</DL>

View File

@ -22,7 +22,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version> <version>2.7.17</version>
<relativePath/> <relativePath/>
</parent> </parent>

View File

@ -91,6 +91,10 @@ proxy:
# url icon服务提供地址 # url icon服务提供地址
urlIconAddress: http://localhost:11001 urlIconAddress: http://localhost:11001
bing:
host: https://cn.bing.com
onePic: /HPImageArchive.aspx?format=js&idx=0&n=1
# 管理员用户id(因为目前尚未设计分角色权限系统须指定管理员用户id) # 管理员用户id(因为目前尚未设计分角色权限系统须指定管理员用户id)
manageUserId: -1 manageUserId: -1

View File

@ -0,0 +1,10 @@
CREATE TABLE bookmark.host_icon (
id INT UNSIGNED auto_increment NOT NULL,
host varchar(300) NOT NULL COMMENT 'host',
iconPath varchar(330) NOT NULL,
CONSTRAINT host_icon_pk PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci;
CREATE INDEX host_icon_host_IDX USING BTREE ON bookmark.host_icon (host(20));

View File

@ -0,0 +1,13 @@
CREATE TABLE bookmark.global_config
(
code varchar(20) NOT NULL,
value varchar(100) NOT NULL COMMENT '',
description varchar(100) NOT NULL COMMENT '描述',
CONSTRAINT global_config_pk PRIMARY KEY (code)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_0900_ai_ci
COMMENT='全局配置表';
insert into global_config
values ("pluginVersion", "0.1.1", "浏览器插件版本");

View File

@ -0,0 +1,3 @@
update global_config
set value='0.1.2'
where code = "pluginVersion";

View File

@ -0,0 +1,22 @@
create table search_engine
(
id int auto_increment
primary key,
userId int not null,
checked tinyint not null default 0,
name varchar(20) null,
url varchar(500) null,
icon varchar(20) null
) auto_increment=1001;
create index search_engine_userId_index
on search_engine (userId);
insert into search_engine(userId, checked, name, url, icon)
select userId, if(defaultSearchEngine = 'baidu', 1, 0), '百度', 'https://www.baidu.com/s?ie=UTF-8&wd=%s', 'icon-baidu'
from user;
insert into search_engine(userId, checked, name, url, icon)
select userId, if(defaultSearchEngine = 'bing', 1, 0), '必应', 'https://www.bing.com/search?q=%s', 'icon-bing'
from user;
insert into search_engine(userId, checked, name, url, icon)
select userId, if(defaultSearchEngine = 'google', 1, 0), '谷歌', 'https://www.google.com/search?q=%s', 'icon-google'
from user;

View File

@ -0,0 +1,2 @@
alter table bookmark
modify name varchar(2000) not null;

View File

@ -3,6 +3,8 @@ node_modules
/dist /dist
package-lock.json package-lock.json
yarn.lock yarn.lock
public/files
public\static\bookmarkBrowserPlugin.zip
# local env files # local env files
.env.local .env.local
@ -22,3 +24,4 @@ pnpm-debug.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?

View File

@ -20,11 +20,11 @@
"vuex": "^3.4.0" "vuex": "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.4.0", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~4.4.0", "@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-router": "~4.4.0", "@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-vuex": "~4.4.0", "@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~4.4.0", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-airbnb": "^5.0.2", "@vue/eslint-config-airbnb": "^5.0.2",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",

9549
bookmark_front/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,20 @@
<script> <script>
export default { export default {
name: "App", name: "App",
mounted() {
window.qieziStatisticKey = "b74c4b571b644782a837433209827874";
let script = document.createElement("script");
script.type = "text/javascript";
script.defer = true;
script.src = "https://qiezi.fleyx.com/qiezijs/1.0/qiezi_statistic.min.js";
document.getElementsByTagName("head")[0].appendChild(script);
}
}; };
</script> </script>
<style lang="less"> <style lang="less">
@import "./global.less"; @import "./global.less";
html, html,
body { body {
margin: 0; margin: 0;
@ -20,6 +29,7 @@ body {
background-color: @bgColor; background-color: @bgColor;
height: initial; height: initial;
} }
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

View File

@ -1,16 +1,16 @@
<template> <template>
<div class="search"> <div class="search">
<div :class="{ listShow: focused && list.length > 0 }" class="newSearch"> <div :class="{ listShow: focused && list.length > 0 }" class="newSearch">
<input ref="searchInput" class="input" type="text" v-model="value" @keydown="keyPress" @focus="inputFocus" @blur="inputBlur" /> <input ref="searchInput" class="input" type="text" v-model="value" @keydown="keyPress" @focus="inputFocus"
@blur="inputBlur" />
<div class="action"> <div class="action">
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-tooltip title="点击切换网页搜索"> <a-tooltip title="点击切换网页搜索">
<my-icon class="icon" style="margin-right: 0.5em" :type="searchIcon" /> <my-icon class="icon" style="margin-right: 0.5em" :type="checkedSearchEngine.icon"
@click="searchIconClick" />
</a-tooltip> </a-tooltip>
<a-menu slot="overlay" @click="searchEngineChange"> <a-menu slot="overlay" @click="searchEngineChange">
<a-menu-item key="google">谷歌</a-menu-item> <a-menu-item v-for="item in searchEngineList" :key="item.id">{{ item.name }}</a-menu-item>
<a-menu-item key="bing">bing</a-menu-item>
<a-menu-item key="baidu">baidu</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
<a-icon class="icon" type="search" @click="submit(true)" /> <a-icon class="icon" type="search" @click="submit(true)" />
@ -30,7 +30,8 @@
</div> </div>
<div class="icons"> <div class="icons">
<a-tooltip title="定位到书签树中" v-if="showLocation"> <a-tooltip title="定位到书签树中" v-if="showLocation">
<my-icon style="color: white; font-size: 1.3em" type="icon-et-location" @mousedown="location($event, item)" /> <my-icon style="color: white; font-size: 1.3em" type="icon-et-location"
@mousedown="location($event, item)" />
</a-tooltip> </a-tooltip>
<a-tooltip title="复制链接"> <a-tooltip title="复制链接">
<a-icon <a-icon
@ -51,7 +52,7 @@
</div> </div>
</div> </div>
</div> </div>
<a ref="targetA" style="left: 1000000px" target="_blank" /> <a ref="targetA" style="left: 1000000px" />
</div> </div>
</template> </template>
@ -61,10 +62,11 @@ import { mapState } from "vuex";
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
import { GLOBAL_CONFIG, USER_INFO } from "@/store/modules/globalConfig"; import { GLOBAL_CONFIG, USER_INFO } from "@/store/modules/globalConfig";
import { TREE_DATA, refreshHomePinList, HOME_PIN_BOOKMARK_ID_MAP } from "@/store/modules/treeData"; import { TREE_DATA, refreshHomePinList, HOME_PIN_BOOKMARK_ID_MAP } from "@/store/modules/treeData";
export default { export default {
name: "Search", name: "Search",
props: { props: {
showLocation: Boolean, // showLocation: Boolean //
}, },
data() { data() {
return { return {
@ -74,19 +76,25 @@ export default {
// //
selectIndex: null, selectIndex: null,
copyBoard: null, // copyBoard: null, //
searchEngineList: [],
checkedSearchEngine: { icon: "icon-baidu", name: "百度", url: "https://www.baidu.com/s?ie=UTF-8&wd=%s" }
}; };
}, },
mounted() { async mounted() {
//clipboard //clipboard
this.copyBoard = new ClipboardJS(".search-copy-to-board", { this.copyBoard = new ClipboardJS(".search-copy-to-board", {
text: function (trigger) { text: function(trigger) {
return trigger.attributes.data.nodeValue; return trigger.attributes.data.nodeValue;
}, }
}); });
this.copyBoard.on("success", (e) => { this.copyBoard.on("success", (e) => {
this.$message.success("复制成功"); this.$message.success("复制成功");
e.clearSelection(); e.clearSelection();
}); });
if (this.$store.state.globalConfig.token != null) {
this.searchEngineList = await HttpUtil.get("/searchEngine/list");
this.checkedSearchEngine = this.searchEngineList.find(item => item.checked === 1);
}
}, },
destroyed() { destroyed() {
if (this.copyBoard != null) { if (this.copyBoard != null) {
@ -95,26 +103,20 @@ export default {
}, },
computed: { computed: {
...mapState("treeData", ["totalTreeData", HOME_PIN_BOOKMARK_ID_MAP]), ...mapState("treeData", ["totalTreeData", HOME_PIN_BOOKMARK_ID_MAP]),
...mapState("globalConfig", ["userInfo"]), ...mapState("globalConfig", ["userInfo"])
searchIcon() {
let search = this.userInfo != null ? this.userInfo.defaultSearchEngine : "baidu";
return search === "baidu" ? "icon-baidu" : search === "bing" ? "icon-bing" : "icon-google";
},
searchUrl() {
let search = this.userInfo && this.userInfo.defaultSearchEngine ? this.userInfo.defaultSearchEngine : "baidu";
return search === "baidu"
? "https://www.baidu.com/s?ie=UTF-8&wd="
: search === "bing"
? "https://www.bing.com/search?q="
: "https://www.google.com/search?q=";
},
}, },
watch: { watch: {
value(newVal, oldVal) { value(newVal, oldVal) {
this.search(newVal); this.search(newVal);
}, }
}, },
methods: { methods: {
searchIconClick() {
if (this.userInfo == null) {
this.searchEngineList = [];
this.$message.warning("未登录,请登录后操作");
}
},
search(content) { search(content) {
console.log(content); console.log(content);
let val = content.toLocaleLowerCase().trim(); let val = content.toLocaleLowerCase().trim();
@ -123,6 +125,7 @@ export default {
} else { } else {
this.list = this.dealSearch(val); this.list = this.dealSearch(val);
} }
this.selectIndex = null;
}, },
// //
itemClick(index) { itemClick(index) {
@ -134,7 +137,7 @@ export default {
let url; let url;
if (forceSearch || this.selectIndex == null) { if (forceSearch || this.selectIndex == null) {
//使 //使
url = this.searchUrl + encodeURIComponent(this.value); url = this.checkedSearchEngine.url.replace("%s", encodeURIComponent(this.value));
} else { } else {
// //
let bookmark = this.list[this.selectIndex]; let bookmark = this.list[this.selectIndex];
@ -175,15 +178,10 @@ export default {
}, },
// //
async searchEngineChange(item) { async searchEngineChange(item) {
if (this.userInfo == null) {
this.$message.warning("未登录,请登录后操作"); let target = this.searchEngineList.find(one => one.id === item.key);
return; await HttpUtil.post("/searchEngine/setChecked", null, { id: item.key });
} this.checkedSearchEngine = target;
if (item.key !== this.userInfo.defaultSearchEngine) {
await HttpUtil.post("/baseInfo/updateSearchEngine", null, { defaultSearchEngine: item.key });
this.userInfo.defaultSearchEngine = item.key;
this.$store.commit(GLOBAL_CONFIG + "/" + USER_INFO, this.userInfo);
}
}, },
// //
async pinBookmark(event, { bookmarkId }) { async pinBookmark(event, { bookmarkId }) {
@ -227,8 +225,8 @@ export default {
} }
console.log("阻止成功"); console.log("阻止成功");
return false; return false;
}, }
}, }
}; };
</script> </script>
@ -239,10 +237,12 @@ export default {
@listActiveBgColor: #454545; @listActiveBgColor: #454545;
.search { .search {
position: relative; position: relative;
.listShow { .listShow {
border-bottom-left-radius: 0 !important; border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important; border-bottom-right-radius: 0 !important;
} }
.newSearch { .newSearch {
display: flex; display: flex;
align-items: center; align-items: center;
@ -251,6 +251,7 @@ export default {
overflow: hidden; overflow: hidden;
font-size: 1.2em; font-size: 1.2em;
color: @textColor; color: @textColor;
.input { .input {
flex: 1; flex: 1;
border: 0; border: 0;
@ -259,11 +260,13 @@ export default {
padding-left: 0.19rem; padding-left: 0.19rem;
outline: none; outline: none;
} }
.action { .action {
padding: 0.1rem; padding: 0.1rem;
padding-right: 0.19rem; padding-right: 0.19rem;
display: flex; display: flex;
align-items: center; align-items: center;
.icon { .icon {
color: @textColor; color: @textColor;
cursor: pointer; cursor: pointer;
@ -281,6 +284,7 @@ export default {
border-bottom-left-radius: 0.18rem; border-bottom-left-radius: 0.18rem;
border-bottom-right-radius: 0.18rem; border-bottom-right-radius: 0.18rem;
overflow: hidden; overflow: hidden;
.listItem { .listItem {
font-size: 0.16rem; font-size: 0.16rem;
display: flex; display: flex;
@ -291,6 +295,7 @@ export default {
margin: 0.05rem 0 0.05rem 0; margin: 0.05rem 0 0.05rem 0;
padding: 0 0.19rem 0 0.19rem; padding: 0 0.19rem 0 0.19rem;
cursor: pointer; cursor: pointer;
.name { .name {
padding-right: 1em; padding-right: 1em;
max-width: calc(100% - 2em); max-width: calc(100% - 2em);
@ -298,17 +303,21 @@ export default {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.icons { .icons {
display: none; display: none;
align-items: center; align-items: center;
} }
} }
.listItem:hover { .listItem:hover {
background-color: @listActiveBgColor; background-color: @listActiveBgColor;
} }
.itemActive { .itemActive {
background-color: @listActiveBgColor; background-color: @listActiveBgColor;
} }
.listItem:hover .icons { .listItem:hover .icons {
display: flex; display: flex;
} }

View File

@ -6,10 +6,10 @@
</a-form-model-item> </a-form-model-item>
<template v-if="form.type !== 'file'"> <template v-if="form.type !== 'file'">
<a-form-model-item prop="name" label="名称" :required="false"> <a-form-model-item prop="name" label="名称" :required="false">
<a-input v-model="form.name" placeholder="名称" /> <a-input v-model="form.name" placeholder="名称" @pressEnter="submit" ref="inputName" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item v-if="form.type === 'bookmark'" prop="url" label="URL"> <a-form-model-item v-if="form.type === 'bookmark'" prop="url" label="URL">
<a-input v-model="form.url" placeholder="url" /> <a-input v-model="form.url" placeholder="url" @pressEnter="submit" />
</a-form-model-item> </a-form-model-item>
<div class="btns"> <div class="btns">
<a-button type="primary" @click="submit" :loading="loading" :disabled="loading">提交</a-button> <a-button type="primary" @click="submit" :loading="loading" :disabled="loading">提交</a-button>
@ -21,6 +21,7 @@
:data="{ path: form.path }" :data="{ path: form.path }"
:headers="{ 'jwt-token': token }" :headers="{ 'jwt-token': token }"
action="/bookmark/api/bookmark/uploadBookmarkFile" action="/bookmark/api/bookmark/uploadBookmarkFile"
accept=".html,.db3"
@change="fileChange" @change="fileChange"
> >
<p class="ant-upload-drag-icon"> <p class="ant-upload-drag-icon">
@ -62,7 +63,7 @@ export default {
file: null, file: null,
}, },
rules: { rules: {
name: [{ required: true, min: 1, max: 1000, message: "名称长度为1-1000", trigger: "change" }], name: [{ required: true, min: 1, max: 1000, message: "名称长度为1-200", trigger: "change" }],
url: [{ required: true, min: 1, message: "不能为空", trigger: "change" }], url: [{ required: true, min: 1, message: "不能为空", trigger: "change" }],
}, },
}; };
@ -78,6 +79,12 @@ export default {
} }
this.token = this.$store.state.globalConfig.token; this.token = this.$store.state.globalConfig.token;
this.form.path = !this.targetNode ? "" : this.targetNode.path + (this.isAdd ? "." + this.targetNode.bookmarkId : ""); this.form.path = !this.targetNode ? "" : this.targetNode.path + (this.isAdd ? "." + this.targetNode.bookmarkId : "");
this.$nextTick(() => {
if (this.$refs.inputName) {
this.$refs.inputName.focus();
}
});
}, },
methods: { methods: {
/** /**

View File

@ -1,6 +1,10 @@
<template> <template>
<div class="bottom"> <div class="bottom">
<router-link to="/public/about">关于</router-link> <router-link style="color: white" to="/public/about"><span class="text">关于</span></router-link>
<a-tooltip v-if="bgSrc">
<template #title>点击后鼠标右键-将图像另存为</template>
<a style="color: white; margin-left: 1em" :href="bgSrc" download="bing每日一图"><span class="text">下载壁纸</span></a>
</a-tooltip>
</div> </div>
</template> </template>
@ -8,6 +12,7 @@
import { mapState } from "vuex"; import { mapState } from "vuex";
export default { export default {
name: "homeTop", name: "homeTop",
props: ["bgSrc"],
data() { data() {
return {}; return {};
}, },
@ -18,6 +23,11 @@ export default {
.bottom { .bottom {
height: 0.4rem; height: 0.4rem;
padding: 0.1rem; padding: 0.1rem;
text-align: right; text-align: center;
color: black;
.text {
color: rgba(255, 255, 255, 0.9);
}
} }
</style> </style>

View File

@ -6,15 +6,18 @@
/ /
<router-link to="/public/register">注册</router-link> <router-link to="/public/register">注册</router-link>
</div> </div>
<div v-else> <div v-else class="topAction">
<a-tooltip style="margin-right: 1em">
<template #title>书签管理</template>
<router-link to="/manage">
<a-icon class="bookmarkIcon" type="setting" />
</router-link>
</a-tooltip>
<a-dropdown> <a-dropdown>
<div class="user"> <div class="user">
<img :src="userInfo.icon" class="userIcon" /> <img :src="userInfo.icon" class="userIcon" />
</div> </div>
<a-menu slot="overlay" :trigger="['hover', 'click']" @click="menuClick"> <a-menu slot="overlay" :trigger="['hover', 'click']" @click="menuClick">
<a-menu-item key="manage">
<router-link to="manage">书签管理</router-link>
</a-menu-item>
<a-menu-item key="personSpace"> <a-menu-item key="personSpace">
<router-link to="/manage/personSpace/userInfo">个人中心</router-link> <router-link to="/manage/personSpace/userInfo">个人中心</router-link>
</a-menu-item> </a-menu-item>
@ -60,6 +63,19 @@ export default {
.userIcon { .userIcon {
border-radius: 50%; border-radius: 50%;
width: 2.5em;
height: 2.5em;
}
.topAction {
display: flex;
align-items: center;
.bookmarkIcon {
font-size: 2em;
background-color: rgb(74, 74, 74, 0.5);
color: rgba(255, 255, 255, 0.8);
}
} }
} }
</style> </style>

View File

@ -1,32 +1,34 @@
import Vue from "vue"; import Vue from "vue";
import { import {
Button, Button,
FormModel, FormModel,
Input, Input,
Icon, Icon,
message, message,
Checkbox, Checkbox,
Dropdown, Dropdown,
Menu, Menu,
Tree, Tree,
Tooltip, Tooltip,
Spin, Spin,
notification, notification,
Empty, Empty,
Modal, Modal,
Radio, Radio,
Upload, Upload,
Popconfirm, Popconfirm,
AutoComplete, AutoComplete,
Select, Select,
Popover Popover,
Breadcrumb,
Table
} from "ant-design-vue"; } from "ant-design-vue";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
import store from "./store"; import store from "./store";
const IconFont = Icon.createFromIconfontCN({ const IconFont = Icon.createFromIconfontCN({
scriptUrl: "//at.alicdn.com/t/font_1261825_1cgngjf5r4f.js" scriptUrl: "//at.alicdn.com/t/c/font_1261825_v7m0rilm4hm.js"
}); });
Vue.use(Button); Vue.use(Button);
Vue.use(FormModel); Vue.use(FormModel);
@ -46,6 +48,8 @@ Vue.use(Popconfirm);
Vue.use(AutoComplete); Vue.use(AutoComplete);
Vue.use(Select); Vue.use(Select);
Vue.use(Popover); Vue.use(Popover);
Vue.use(Breadcrumb);
Vue.use(Table);
Vue.component("my-icon", IconFont); Vue.component("my-icon", IconFont);
Vue.prototype.$message = message; Vue.prototype.$message = message;
@ -54,9 +58,9 @@ Vue.prototype.$confirm = Modal.confirm;
Vue.config.productionTip = false; Vue.config.productionTip = false;
window.vueInstance = new Vue({ window.vueInstance = new Vue({
router, router,
store, store,
render: h => h(App) render: h => h(App)
}).$mount("#app"); }).$mount("#app");

View File

@ -1,65 +1,69 @@
import Vue from "vue"; import Vue from "vue";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import * as vuex from "../store/index.js"; import * as vuex from "../store/index.js";
import { GLOBAL_CONFIG, SUPPORT_NO_LOGIN, TOKEN } from "@/store/modules/globalConfig"; import { GLOBAL_CONFIG, SUPPORT_NO_LOGIN, TOKEN, setToken } from "@/store/modules/globalConfig";
import { checkJwtValid } from "@/util/UserUtil"; import { checkJwtValid } from "@/util/UserUtil";
Vue.use(VueRouter); Vue.use(VueRouter);
const routes = [ const routes = [
{ path: "/", component: () => import("@/views/home/index") }, { path: "/", component: () => import("@/views/home/index") },
{ { path: "/noHead/addBookmark", component: () => import("@/views/noHead/addBookmark/index") },
path: "/manage", {
component: () => import("@/views/manage/index"), path: "/manage",
children: [ component: () => import("@/views/manage/index"),
{ path: "", redirect: "/manage/bookmarkTree" }, children: [
{ path: "bookmarkTree", component: () => import("@/views/manage/bookmarkTree/index") }, { path: "", redirect: "/manage/bookmarkTree" },
{ path: "personSpace/userInfo", component: () => import("@/views/manage/personSpace/index") }, { path: "bookmarkTree", component: () => import("@/views/manage/bookmarkTree/index") },
] { path: "personSpace/userInfo", component: () => import("@/views/manage/personSpace/index") },
}, { path: "sso/auth", component: () => import("@/views/manage/sso/auth/index") }
{ ]
path: "/public", },
component: () => import("@/views/public/index"), {
children: [ path: "/public",
{ path: "login", component: () => import("@/views/public/login/index") }, component: () => import("@/views/public/index"),
{ path: "register", component: () => import("@/views/public/register/index") }, children: [
{ path: "resetPassword", component: () => import("@/views/public/passwordReset/index") }, { path: "login", component: () => import("@/views/public/login/index") },
{ path: "oauth/github", component: () => import("@/views/public/oauth/github/index") }, { path: "register", component: () => import("@/views/public/register/index") },
{ path: "about", component: () => import("@/views/public/about/index") }, { path: "resetPassword", component: () => import("@/views/public/passwordReset/index") },
{ path: "404", component: () => import("@/views/public/notFound/index") }, { path: "oauth/github", component: () => import("@/views/public/oauth/github/index") },
] { path: "about", component: () => import("@/views/public/about/index") },
}, { path: "404", component: () => import("@/views/public/notFound/index") }
{ path: "*", redirect: "/public/404" } ]
},
{ path: "*", redirect: "/public/404" }
]; ];
const router = new VueRouter({ const router = new VueRouter({
mode: "history", mode: "history",
routes routes
}); });
/** /**
* 在此进行登录信息判断以及重定向到登录页面 * 在此进行登录信息判断以及重定向到登录页面
*/ */
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
//进入主页面/管理页面时,确认已经进行初始化操作 if (to.query.token && checkJwtValid(to.query.token)) {
if (to.path === '/' || to.path.startsWith("/manage")) { console.log("获取到页面token", to.query.token);
await vuex.loginInit(); await vuex.default.dispatch(GLOBAL_CONFIG + "/" + setToken, to.query.token);
} }
let supportNoLogin = to.path === '/' || to.path.startsWith("/public"); //进入除/public以外的路由确认已经进行初始化操作
vuex.default.commit(GLOBAL_CONFIG + "/" + SUPPORT_NO_LOGIN, supportNoLogin); if (!to.path.startsWith("/public")) {
if (!supportNoLogin && !checkJwtValid(vuex.default.state[GLOBAL_CONFIG][TOKEN])) { await vuex.loginInit();
//如不支持未登录进入切jwt已过期直接跳转到登录页面,并清理缓存 }
await vuex.default.dispatch("treeData/clear"); let supportNoLogin = to.path === "/" || to.path.startsWith("/public");
await vuex.default.dispatch("globalConfig/clear"); vuex.default.commit(GLOBAL_CONFIG + "/" + SUPPORT_NO_LOGIN, supportNoLogin);
next({ if (!supportNoLogin && !checkJwtValid(vuex.default.state[GLOBAL_CONFIG][TOKEN])) {
path: "/public/login?to=" + btoa(location.href), //如不支持未登录进入切jwt已过期直接跳转到登录页面,并清理缓存
replace: true await vuex.default.dispatch("treeData/clear");
}); await vuex.default.dispatch("globalConfig/clear");
} else { next({
next(); path: "/public/login?to=" + btoa(to.fullPath),
} replace: true
}) });
} else {
next();
}
});
export default router; export default router;

View File

@ -8,58 +8,61 @@ import { checkJwtValid } from "@/util/UserUtil";
Vue.use(Vuex); Vue.use(Vuex);
let store = new Vuex.Store({ let store = new Vuex.Store({
state: {}, state: {},
mutations: {}, mutations: {},
actions: {}, actions: {},
modules: { modules: {
[globalConfig.GLOBAL_CONFIG]: globalConfig.store, [globalConfig.GLOBAL_CONFIG]: globalConfig.store,
[treeData.TREE_DATA]: treeData.store [treeData.TREE_DATA]: treeData.store
} }
}); });
let noLoginFinish = false; let noLoginFinish = false;
//执行各自的非登陆初始化 //执行各自的非登陆初始化
(async () => { (async () => {
await store.dispatch(globalConfig.GLOBAL_CONFIG + "/" + globalConfig.noLoginInit); await store.dispatch(globalConfig.GLOBAL_CONFIG + "/" + globalConfig.noLoginInit);
await store.dispatch(treeData.TREE_DATA + "/" + treeData.noLoginInit); //无需等待执行
noLoginFinish = true; store.dispatch(treeData.TREE_DATA + "/" + treeData.noLoginInit);
noLoginFinish = true;
})(); })();
/** /**
* 执行各模块的登陆后初始化 * 执行各模块的登陆后初始化
*/ */
export async function loginInit () { export async function loginInit() {
if (!noLoginFinish) { if (!noLoginFinish) {
await finishNoLogin(); await finishNoLogin();
} }
console.log(store.state[globalConfig.GLOBAL_CONFIG][globalConfig.TOKEN]); console.log(store.state[globalConfig.GLOBAL_CONFIG][globalConfig.TOKEN]);
if (checkJwtValid(store.state[globalConfig.GLOBAL_CONFIG][globalConfig.TOKEN])) { if (checkJwtValid(store.state[globalConfig.GLOBAL_CONFIG][globalConfig.TOKEN])) {
await store.dispatch(globalConfig.GLOBAL_CONFIG + "/" + globalConfig.loginInit); //无需等待执行完毕
await store.dispatch(treeData.TREE_DATA + "/" + treeData.loginInit); store.dispatch(globalConfig.GLOBAL_CONFIG + "/" + globalConfig.loginInit);
} store.dispatch(treeData.TREE_DATA + "/" + treeData.loginInit);
}
console.log("初始化完成");
} }
/** /**
* 推出登陆时需要清理的 * 推出登陆时需要清理的
*/ */
export async function logoutClear () { export async function logoutClear() {
await store.dispatch(globalConfig.GLOBAL_CONFIG + "/" + globalConfig.clear); await store.dispatch(globalConfig.GLOBAL_CONFIG + "/" + globalConfig.clear);
await store.dispatch(treeData.TREE_DATA + "/" + treeData.clear); await store.dispatch(treeData.TREE_DATA + "/" + treeData.clear);
} }
/** /**
* 确保未登录前要初始化的初始化完了 * 确保未登录前要初始化的初始化完了
*/ */
async function finishNoLogin () { async function finishNoLogin() {
return new Promise((resolve) => { return new Promise((resolve) => {
let timer = setInterval(() => { let timer = setInterval(() => {
if (noLoginFinish) { if (noLoginFinish) {
clearInterval(timer); clearInterval(timer);
resolve(); resolve();
} }
}, 100); }, 100);
}) });
} }
export default store; export default store;

View File

@ -11,95 +11,101 @@ export const IS_PHONE = "isPhone";
export const noLoginInit = "noLoginInit"; export const noLoginInit = "noLoginInit";
export const loginInit = "loginInit"; export const loginInit = "loginInit";
/**
* 登出清除数据
*/
export const clear = "clear"; export const clear = "clear";
/**
* 设置token
*/
export const setToken = "setToken";
/** /**
* 存储全局配置 * 存储全局配置
*/ */
const state = { const state = {
/** /**
* 用户信息 * 用户信息
*/ */
[USER_INFO]: null, [USER_INFO]: null,
/** /**
* token,null说明未获取登录凭证 * token,null说明未获取登录凭证
*/ */
[TOKEN]: null, [TOKEN]: null,
/** /**
* 是否已经初始化完成,避免多次重复初始化 * 是否已经初始化完成,避免多次重复初始化
*/ */
[IS_INIT]: false, [IS_INIT]: false,
/** /**
* 是否移动端 * 是否移动端
*/ */
[IS_PHONE]: /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent), [IS_PHONE]: /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent),
/** /**
* 是否支持未登录进入页面 * 是否支持未登录进入页面
*/ */
[SUPPORT_NO_LOGIN]: false, [SUPPORT_NO_LOGIN]: false,
/** /**
* 服务端全局配置 * 服务端全局配置
*/ */
[SERVER_CONFIG]: {} [SERVER_CONFIG]: {}
}; };
const getters = {}; const getters = {};
const actions = { const actions = {
//未登录需要进行的初始化 //未登录需要进行的初始化
async [noLoginInit] ({ commit }) { async [noLoginInit]({ commit }) {
commit(SERVER_CONFIG, await HttpUtil.get("/common/config/global")); commit(SERVER_CONFIG, await HttpUtil.get("/common/config/global"));
let token = await localforage.getItem(TOKEN); let token = await localforage.getItem(TOKEN);
if (token) { if (token) {
commit(TOKEN, token); commit(TOKEN, token);
window.jwtToken = token; window.jwtToken = token;
} }
}, },
//登陆后的,初始化数据 //登陆后的,初始化数据
async [loginInit] (context) { async [loginInit](context) {
if (context.state.isInit) { if (context.state.isInit) {
return; return;
} }
let userInfo = await HttpUtil.get("/user/currentUserInfo"); let userInfo = await HttpUtil.get("/user/currentUserInfo");
context.commit(USER_INFO, userInfo); context.commit(USER_INFO, userInfo);
context.commit(IS_INIT, true); context.commit(IS_INIT, true);
}, console.log("用户完了");
async setToken ({ commit }, token) { },
await localforage.setItem(TOKEN, token); async [setToken]({ commit }, token) {
window.jwtToken = token; await localforage.setItem(TOKEN, token);
commit(TOKEN, token); window.jwtToken = token;
}, commit(TOKEN, token);
//登出清除数据 },
async [clear] (context) { async [clear](context) {
await localforage.removeItem(TOKEN); await localforage.removeItem(TOKEN);
context.commit(USER_INFO, null); context.commit(USER_INFO, null);
context.commit(TOKEN, null); context.commit(TOKEN, null);
context.commit(IS_INIT, false); context.commit(IS_INIT, false);
}, }
}; };
const mutations = { const mutations = {
[USER_INFO] (state, userInfo) { [USER_INFO](state, userInfo) {
state[USER_INFO] = userInfo; state[USER_INFO] = userInfo;
}, },
[TOKEN] (state, token) { [TOKEN](state, token) {
state[TOKEN] = token; state[TOKEN] = token;
}, },
[IS_INIT] (state, isInit) { [IS_INIT](state, isInit) {
state[IS_INIT] = isInit; state[IS_INIT] = isInit;
}, },
[SERVER_CONFIG] (state, serverConfig) { [SERVER_CONFIG](state, serverConfig) {
state[SERVER_CONFIG] = serverConfig; state[SERVER_CONFIG] = serverConfig;
}, },
[SUPPORT_NO_LOGIN] (state, val) { [SUPPORT_NO_LOGIN](state, val) {
state[SUPPORT_NO_LOGIN] = val; state[SUPPORT_NO_LOGIN] = val;
} }
}; };
export const store = { export const store = {
namespaced: true, namespaced: true,
state, state,
getters, getters,
actions, actions,
mutations mutations
}; };

View File

@ -2,6 +2,9 @@ import localforage from "localforage";
import { checkJwtValid } from "@/util/UserUtil"; import { checkJwtValid } from "@/util/UserUtil";
import HttpUtil from "../../util/HttpUtil"; import HttpUtil from "../../util/HttpUtil";
/**书签版本检查间隔 */
const CHECK_INTERVAL = 5 * 60 * 1000;
// const CHECK_INTERVAL = 5 * 1000;
export const TREE_DATA = "treeData"; export const TREE_DATA = "treeData";
export const TOTAL_TREE_DATA = "totalTreeData"; export const TOTAL_TREE_DATA = "totalTreeData";
export const VERSION = "version"; export const VERSION = "version";
@ -19,7 +22,13 @@ export const refreshHomePinList = "refreshHomePinList";
* 通过id获取书签数据 * 通过id获取书签数据
*/ */
export const getById = "getById"; export const getById = "getById";
/**
* 登录前初始化
*/
export const noLoginInit = "noLoginInit"; export const noLoginInit = "noLoginInit";
/**
* 登陆后初始化
*/
export const loginInit = "loginInit"; export const loginInit = "loginInit";
export const refresh = "refresh"; export const refresh = "refresh";
export const clear = "clear"; export const clear = "clear";
@ -27,11 +36,19 @@ export const clear = "clear";
* 删除书签数据 * 删除书签数据
*/ */
export const deleteData = "deleteData"; export const deleteData = "deleteData";
/**
* 新增节点
*/
export const addNode = "addNode";
/** /**
* 版本检查定时调度 * 版本检查定时调度
*/ */
let timer = null; let timer = null;
/**
* 检查本地版本是否有更新
*/
let checkLocalDataTimer = null;
/** /**
* 刷新书签确认弹窗是否展示 * 刷新书签确认弹窗是否展示
*/ */
@ -41,321 +58,342 @@ let toastShow = false;
* 书签树相关配置 * 书签树相关配置
*/ */
const state = { const state = {
//全部书签数据 //全部书签数据
[TOTAL_TREE_DATA]: {}, [TOTAL_TREE_DATA]: {},
//版本 //版本
[VERSION]: null, [VERSION]: null,
//是否已经初始化书签数据 //是否已经初始化书签数据
[IS_INIT]: false, [IS_INIT]: false,
// 是否正在加载数据 // 是否正在加载数据
[IS_INITING]: false, [IS_INITING]: false,
[SHOW_REFRESH_TOAST]: false, [SHOW_REFRESH_TOAST]: false,
[HOME_PIN_LIST]: [], [HOME_PIN_LIST]: [],
[HOME_PIN_BOOKMARK_ID_MAP]: {} [HOME_PIN_BOOKMARK_ID_MAP]: {}
}; };
const getters = { const getters = {
[getById]: state => id => { [getById]: state => id => {
let arr = Object.values(state[TOTAL_TREE_DATA]); let arr = Object.values(state[TOTAL_TREE_DATA]);
for (let i in arr) { for (let i in arr) {
for (let j in arr[i]) { for (let j in arr[i]) {
if (arr[i][j].bookmarkId === id) { if (arr[i][j].bookmarkId === id) {
return arr[i][j]; return arr[i][j];
} }
} }
} }
return null; return null;
} }
}; };
const actions = { const actions = {
async [noLoginInit] () { async [noLoginInit] () { },
async [loginInit] (context) {
}, if (context.state.isInit || context.state.isIniting) {
async [loginInit] (context) { return;
if (context.state.isInit || context.state.isIniting) { }
return; await context.dispatch(refreshHomePinList);
} context.commit(IS_INITING, true);
await context.dispatch(refreshHomePinList); context.commit(TOTAL_TREE_DATA, await localforage.getItem(TOTAL_TREE_DATA));
context.commit(IS_INITING, true); context.commit(VERSION, await localforage.getItem(VERSION));
context.commit(TOTAL_TREE_DATA, await localforage.getItem(TOTAL_TREE_DATA)); await treeDataCheck(context, true);
context.commit(VERSION, await localforage.getItem(VERSION)); context.commit(IS_INIT, true);
await treeDataCheck(context, true); context.commit(IS_INITING, false);
context.commit(IS_INIT, true); timer = setInterval(() => treeDataCheck(context, false), CHECK_INTERVAL);
context.commit(IS_INITING, false); checkLocalDataTimer = setInterval(() => checkLocalData(context), 2000);
timer = setInterval(() => treeDataCheck(context, false), 5 * 60 * 1000); },
// timer = setInterval(() => treeDataCheck(context, false), 5 * 1000); /**
}, * 确保数据加载完毕
/** */
* 确保数据加载完毕 ensureDataOk (context) {
*/ return new Promise((resolve, reject) => {
ensureDataOk (context) { let timer = setInterval(() => {
return new Promise((resolve, reject) => { try {
let timer = setInterval(() => { if (context.state[IS_INIT] && context.state[IS_INITING] == false) {
try { clearInterval(timer);
if (context.state[IS_INIT] && context.state[IS_INITING] == false) { resolve();
clearInterval(timer); }
resolve(); } catch (err) {
} reject(err);
} catch (err) { }
reject(err); }, 50);
} });
}, 50); },
}); //刷新缓存数据
}, async [refresh] (context) {
//刷新缓存数据 let treeData = await HttpUtil.get("/bookmark/currentUser");
async [refresh] (context) { if (!treeData[""]) {
let treeData = await HttpUtil.get("/bookmark/currentUser"); treeData[""] = [];
if (!treeData[""]) { }
treeData[""] = []; Object.values(treeData).forEach(item =>
} item.forEach(item1 => {
Object.values(treeData).forEach(item => item1.isLeaf = item1.type === 0;
item.forEach(item1 => { item1.class = "treeNodeItem";
item1.isLeaf = item1.type === 0; item1.scopedSlots = { title: "nodeTitle" };
item1.class = "treeNodeItem"; })
item1.scopedSlots = { title: "nodeTitle" }; );
}) let version = await HttpUtil.get("/user/version");
); await context.dispatch("updateVersion", version);
let version = await HttpUtil.get("/user/version"); await context.dispatch(refreshHomePinList);
await context.dispatch("updateVersion", version); context.commit(TOTAL_TREE_DATA, treeData);
context.commit(TOTAL_TREE_DATA, treeData); await localforage.setItem(TOTAL_TREE_DATA, treeData);
await localforage.setItem(TOTAL_TREE_DATA, treeData); },
}, //清除缓存数据
//清除缓存数据 async [clear] (context) {
async [clear] (context) { context.commit(TOTAL_TREE_DATA, null);
context.commit(TOTAL_TREE_DATA, null); context.commit(VERSION, null);
context.commit(VERSION, null); context.commit(SHOW_REFRESH_TOAST, false);
context.commit(SHOW_REFRESH_TOAST, false); context.commit(IS_INIT, false);
context.commit(IS_INIT, false); context.commit(IS_INITING, false);
context.commit(IS_INITING, false); context.commit(HOME_PIN_LIST, []);
context.commit(HOME_PIN_LIST, []); if (timer != null) {
if (timer != null) { clearInterval(timer);
clearInterval(timer); }
} if (checkLocalDataTimer != null) {
await localforage.removeItem(TOTAL_TREE_DATA); clearInterval(checkLocalDataTimer);
await localforage.removeItem(VERSION); }
}, await localforage.removeItem(TOTAL_TREE_DATA);
/** await localforage.removeItem(VERSION);
* 移动节点 },
*/ /**
async moveNode (context, info) { * 移动节点
let data = context.state[TOTAL_TREE_DATA]; */
const target = info.node.dataRef; async moveNode (context, info) {
const current = info.dragNode.dataRef; let data = context.state[TOTAL_TREE_DATA];
//从原来位置中删除当前节点 const target = info.node.dataRef;
let currentList = data[current.path]; const current = info.dragNode.dataRef;
currentList.splice( //从原来位置中删除当前节点
currentList.findIndex(item => item.bookmarkId === current.bookmarkId), let currentList = data[current.path];
1 currentList.splice(
); currentList.findIndex(item => item.bookmarkId === current.bookmarkId),
//请求体 1
const body = { );
bookmarkId: current.bookmarkId, //请求体
sourcePath: current.path, const body = {
targetPath: "", bookmarkId: current.bookmarkId,
//-1 表示排在最后 sourcePath: current.path,
sort: -1 targetPath: "",
}; //-1 表示排在最后
if (info.dropToGap) { sort: -1
body.targetPath = target.path; };
//移动到目标节点的上面或者下面 if (info.dropToGap) {
let targetList = data[target.path]; body.targetPath = target.path;
//目标节点index //移动到目标节点的上面或者下面
let index = targetList.indexOf(target); let targetList = data[target.path];
//移动节点相对于目标节点位置的增量 //目标节点index
let addIndex = info.dropPosition > index ? 1 : 0; let index = targetList.indexOf(target);
body.sort = target.sort + addIndex; //移动节点相对于目标节点位置的增量
targetList.splice(index + addIndex, 0, current); let addIndex = info.dropPosition > index ? 1 : 0;
for (let i = index + 1; i < targetList.length; i++) { body.sort = target.sort + addIndex;
targetList[i].sort += 1; targetList.splice(index + addIndex, 0, current);
} for (let i = index + 1; i < targetList.length; i++) {
} else { targetList[i].sort += 1;
//移动到一个文件夹下面 }
body.targetPath = target.path + "." + target.bookmarkId; } else {
let targetList = data[body.targetPath]; //移动到一个文件夹下面
if (!targetList) { body.targetPath = target.path + "." + target.bookmarkId;
targetList = []; let targetList = data[body.targetPath];
data[body.targetPath] = targetList; if (!targetList) {
} targetList = [];
body.sort = targetList.length > 0 ? targetList[targetList.length - 1].sort + 1 : 1; data[body.targetPath] = targetList;
targetList.push(current); }
} body.sort = targetList.length > 0 ? targetList[targetList.length - 1].sort + 1 : 1;
//更新节点的path和对应子节点path targetList.push(current);
current.path = body.targetPath; }
current.sort = body.sort; //更新节点的path和对应子节点path
//如果为文件夹还要更新所有子书签的path current.path = body.targetPath;
if (body.sourcePath !== body.targetPath) { current.sort = body.sort;
let keys = Object.keys(data); //如果为文件夹还要更新所有子书签的path
//旧路径 if (body.sourcePath !== body.targetPath) {
let oldPath = body.sourcePath + "." + current.bookmarkId; let keys = Object.keys(data);
//新路径 //旧路径
let newPath = body.targetPath + "." + current.bookmarkId; let oldPath = body.sourcePath + "." + current.bookmarkId;
keys.forEach(item => { //新路径
if (!item.startsWith(oldPath)) { let newPath = body.targetPath + "." + current.bookmarkId;
return; keys.forEach(item => {
} if (!item.startsWith(oldPath)) {
let newPathStr = item.replace(oldPath, newPath); return;
let list = data[item]; }
delete data[item]; let newPathStr = item.replace(oldPath, newPath);
data[newPathStr] = list; let list = data[item];
list.forEach(item1 => (item1.path = newPathStr)); delete data[item];
}); data[newPathStr] = list;
} list.forEach(item1 => (item1.path = newPathStr));
context.commit(TOTAL_TREE_DATA, context.state[TOTAL_TREE_DATA]); });
await context.dispatch("updateVersion", null); }
await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]); context.commit(TOTAL_TREE_DATA, context.state[TOTAL_TREE_DATA]);
return body; await context.dispatch("updateVersion", null);
}, await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]);
async [refreshHomePinList] ({ commit }) { return body;
let list = await HttpUtil.get("/home/pin"); },
commit(HOME_PIN_LIST, list); async [refreshHomePinList] ({ commit }) {
let map = {}; let list = await HttpUtil.get("/home/pin");
list.filter(item => item.id).forEach(item => map[item.bookmarkId] = true); commit(HOME_PIN_LIST, list);
commit(HOME_PIN_BOOKMARK_ID_MAP, map); let map = {};
list.filter(item => item.id).forEach(item => (map[item.bookmarkId] = true));
}, commit(HOME_PIN_BOOKMARK_ID_MAP, map);
/** },
* 更新版本数据 /**
*/ * 更新版本数据
async updateVersion ({ commit, state }, version) { */
commit(VERSION, version == null ? state[VERSION] + 1 : version); async updateVersion ({ commit, state }, version) {
await localforage.setItem(VERSION, state[VERSION]); commit(VERSION, version == null ? state[VERSION] + 1 : version);
}, await localforage.setItem(VERSION, state[VERSION]);
/** },
* 新增书签文件夹 /**
*/ * 新增书签文件夹
async addNode (context, { sourceNode, targetNode }) { */
if (sourceNode === null) { async [addNode] (context, { sourceNode, targetNode }) {
if (context.state[TOTAL_TREE_DATA][""] === undefined) { if (sourceNode === null) {
context.state[TOTAL_TREE_DATA][""] = []; if (context.state[TOTAL_TREE_DATA][""] === undefined) {
} context.state[TOTAL_TREE_DATA][""] = [];
context.state[TOTAL_TREE_DATA][""].push(targetNode); }
} else { context.state[TOTAL_TREE_DATA][""].push(targetNode);
if (sourceNode.children === undefined) { } else {
sourceNode.children = []; let path = sourceNode.path + "." + sourceNode.bookmarkId;
} if (!context.state[TOTAL_TREE_DATA][path]) {
sourceNode.children.push(targetNode); context.state[TOTAL_TREE_DATA][path] = [];
} }
if (targetNode.type === 0) { if (sourceNode.children === undefined) {
context.state[TOTAL_TREE_DATA][targetNode.path + "." + targetNode.bookmarkId] = []; sourceNode.children = context.state[TOTAL_TREE_DATA][path];
} }
targetNode.isLeaf = targetNode.type === 0; sourceNode.children.push(targetNode);
targetNode.class = "treeNodeItem"; }
targetNode.scopedSlots = { title: "nodeTitle" }; if (targetNode.type === 1) {
context.commit(TOTAL_TREE_DATA, context.state[TOTAL_TREE_DATA]); context.state[TOTAL_TREE_DATA][targetNode.path + "." + targetNode.bookmarkId] = [];
await context.dispatch("updateVersion", null); }
await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]); targetNode.isLeaf = targetNode.type === 0;
}, targetNode.class = "treeNodeItem";
/** targetNode.scopedSlots = { title: "nodeTitle" };
* 删除节点数据 context.commit(TOTAL_TREE_DATA, context.state[TOTAL_TREE_DATA]);
*/ await context.dispatch("updateVersion", null);
async [deleteData] (context, { pathList, bookmarkIdList }) { await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]);
//待删除的书签 },
let bookmarkIdSet = new Set(); /**
bookmarkIdList.forEach(item => bookmarkIdSet.add(item)); * 删除节点数据
//删除子节点 */
pathList.forEach(item => { async [deleteData] (context, { pathList, bookmarkIdList }) {
delete state[TOTAL_TREE_DATA][item]; //待删除的书签
Object.keys(context.state[TOTAL_TREE_DATA]) let bookmarkIdSet = new Set();
.filter(key => key.startsWith(item + ".")) bookmarkIdList.forEach(item => bookmarkIdSet.add(item));
.forEach(key => delete state[TOTAL_TREE_DATA][key]); //删除子节点
bookmarkIdSet.add(parseInt(item.split(".").reverse())); pathList.forEach(item => {
}); delete state[TOTAL_TREE_DATA][item];
//删除直接选中的节点 Object.keys(context.state[TOTAL_TREE_DATA])
Object.keys(context.state[TOTAL_TREE_DATA]).forEach(item => { .filter(key => key.startsWith(item + "."))
let list = context.state[TOTAL_TREE_DATA][item]; .forEach(key => delete state[TOTAL_TREE_DATA][key]);
for (let i = list.length - 1; i >= 0; i--) { bookmarkIdSet.add(parseInt(item.split(".").reverse()));
if (bookmarkIdSet.has(list[i].bookmarkId)) { });
list.splice(i, 1); //删除直接选中的节点
} Object.keys(context.state[TOTAL_TREE_DATA]).forEach(item => {
} let list = context.state[TOTAL_TREE_DATA][item];
}); for (let i = list.length - 1; i >= 0; i--) {
context.commit(TOTAL_TREE_DATA, context.state[TOTAL_TREE_DATA]); if (bookmarkIdSet.has(list[i].bookmarkId)) {
await context.dispatch("updateVersion", null); list.splice(i, 1);
await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]); }
}, }
/** });
* 编辑书签节点 context.commit(TOTAL_TREE_DATA, context.state[TOTAL_TREE_DATA]);
*/ await context.dispatch("updateVersion", null);
async editNode ({ dispatch, state, commit }, { node, newName, newUrl, newIcon }) { await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]);
node.name = newName; },
node.url = newUrl; /**
node.icon = newIcon; * 编辑书签节点
commit(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]); */
await dispatch("updateVersion", null); async editNode ({ dispatch, state, commit }, { node, newName, newUrl, newIcon }) {
await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]); node.name = newName;
} node.url = newUrl;
node.icon = newIcon;
commit(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]);
await dispatch("updateVersion", null);
await localforage.setItem(TOTAL_TREE_DATA, state[TOTAL_TREE_DATA]);
}
}; };
const mutations = { const mutations = {
[TOTAL_TREE_DATA]: (state, totalTreeData) => { [TOTAL_TREE_DATA]: (state, totalTreeData) => {
state.totalTreeData = totalTreeData; state.totalTreeData = totalTreeData;
}, },
[IS_INIT] (state, isInit) { [IS_INIT] (state, isInit) {
state.isInit = isInit; state.isInit = isInit;
}, },
[IS_INITING] (state, isIniting) { [IS_INITING] (state, isIniting) {
state.isIniting = isIniting; state.isIniting = isIniting;
}, },
[VERSION]: (state, version) => { [VERSION]: (state, version) => {
state[VERSION] = version; state[VERSION] = version;
}, },
[SHOW_REFRESH_TOAST]: (state, val) => { [SHOW_REFRESH_TOAST]: (state, val) => {
state[SHOW_REFRESH_TOAST] = val; state[SHOW_REFRESH_TOAST] = val;
}, },
[HOME_PIN_LIST]: (state, val) => { [HOME_PIN_LIST]: (state, val) => {
state[HOME_PIN_LIST] = val; state[HOME_PIN_LIST] = val;
}, },
[HOME_PIN_BOOKMARK_ID_MAP]: (state, val) => { [HOME_PIN_BOOKMARK_ID_MAP]: (state, val) => {
state[HOME_PIN_BOOKMARK_ID_MAP] = val; state[HOME_PIN_BOOKMARK_ID_MAP] = val;
} }
}; };
/** /**
* 检查书签缓存是否最新 * 检查书签缓存是否最新
* *
* @param {*} context * @param {*} context
* @param {*} isFirst * @param {*} isFirst
* @returns * @returns
*/ */
async function treeDataCheck (context, isFirst) { async function treeDataCheck (context, isFirst) {
if (toastShow || !checkJwtValid(context.rootState.globalConfig.token)) { if (toastShow || !checkJwtValid(context.rootState.globalConfig.token)) {
return; return;
} }
let realVersion = await HttpUtil.get("/user/version"); let realVersion = await HttpUtil.get("/user/version");
if (realVersion !== context.state[VERSION]) { if (realVersion !== context.state[VERSION]) {
if (SHOW_REFRESH_TOAST && !isFirst) { if (context.state[SHOW_REFRESH_TOAST] && !isFirst) {
//如果在书签管理页面需要弹窗提示 //如果在书签管理页面需要弹窗提示
window.vueInstance.$confirm({ window.vueInstance.$confirm({
title: "书签数据有更新,是否立即刷新?", title: "书签数据有更新,是否立即刷新?",
cancelText: "稍后提醒", cancelText: "稍后提醒",
closable: false, closable: false,
keyboard: false, keyboard: false,
maskClosable: false, maskClosable: false,
onOk () { onOk () {
toastShow = false; toastShow = false;
return new Promise(async (resolve) => { return new Promise(async resolve => {
await context.dispatch("refresh"); await context.dispatch(refresh);
resolve(); resolve();
}); });
}, },
onCancel () { onCancel () {
toastShow = false; toastShow = false;
} }
}); });
toastShow = true; toastShow = true;
} else { } else {
await context.dispatch(refresh); await context.dispatch(refresh);
} }
} }
}
/**
* 检查本地缓存数据是否有更新
* @param {*} context
*/
async function checkLocalData (context) {
let data = await localforage.getItem(TOTAL_TREE_DATA);
let version = await localforage.getItem(VERSION);
if (!data || !version) {
return;
}
if (version > context.state[VERSION]) {
console.log("从local缓存更新数据", version);
context.commit(TOTAL_TREE_DATA, data);
context.commit(VERSION, version);
}
} }
export const store = { export const store = {
namespaced: true, namespaced: true,
state, state,
getters, getters,
actions, actions,
mutations mutations
}; };

View File

@ -12,54 +12,54 @@ import router from "../router/index";
* @param {*} redirect 接口返回未认证是否跳转到登陆 * @param {*} redirect 接口返回未认证是否跳转到登陆
* @returns 数据 * @returns 数据
*/ */
async function request (url, method, params, body, isForm, redirect) { async function request(url, method, params, body, isForm, redirect) {
let options = { let options = {
url, url,
baseURL: "/bookmark/api", baseURL: "/bookmark/api",
method, method,
params, params,
headers: { headers: {
"jwt-token": window.jwtToken "jwt-token": window.jwtToken
} }
}; };
//如果是表单类型的请求,添加请求头 //如果是表单类型的请求,添加请求头
if (isForm) { if (isForm) {
options.headers["Content-Type"] = "multipart/form-data"; options.headers["Content-Type"] = "multipart/form-data";
} }
if (body) { if (body) {
options.data = body; options.data = body;
} }
let res; let res;
try { try {
res = await http.default.request(options); res = await http.default.request(options);
} catch (err) { } catch (err) {
window.vueInstance.$message.error("网络连接异常"); window.vueInstance.$message.error("网络连接异常");
console.error(err); console.error(err);
throw err; throw err;
} }
const { code, data, message } = res.data; const { code, data, message } = res.data;
if (code === 1) { if (code === 1) {
return data; return data;
} else if (code === -1 && redirect) { } else if (code === -1 && redirect) {
//未登陆根据redirect参数判断是否需要跳转到登陆页 //未登陆根据redirect参数判断是否需要跳转到登陆页
window.vueInstance.$message.error("您尚未登陆,请先登陆"); window.vueInstance.$message.error("您尚未登陆,请先登陆");
//跳转到登录页面需要清理缓存 //跳转到登录页面需要清理缓存
await this.$store.dispatch("treeData/clear"); await this.$store.dispatch("treeData/clear");
await this.$store.dispatch("globalConfig/clear"); await this.$store.dispatch("globalConfig/clear");
router.replace(`/public/login?redirect=${encodeURIComponent(router.currentRoute.fullPath)}`); router.replace(`/public/login?redirect=${encodeURIComponent(router.currentRoute.fullPath)}`);
throw new Error(message); throw new Error(message);
} else if (code === 0) { } else if (code === 0) {
//通用异常使用error提示 //通用异常使用error提示
window.vueInstance.$notification.error({ window.vueInstance.$notification.error({
message: "异常", message: "异常",
description: message description: message
}); });
throw new Error(message); throw new Error(message);
} else if (code === -2) { } else if (code === -2) {
//表单异常使用message提示 //表单异常使用message提示
window.vueInstance.$message.error(message); window.vueInstance.$message.error(message);
throw new Error(message); throw new Error(message);
} }
} }
/** /**
@ -68,8 +68,8 @@ async function request (url, method, params, body, isForm, redirect) {
* @param {*} params url参数 * @param {*} params url参数
* @param {*} redirect 未登陆是否跳转到登陆页 * @param {*} redirect 未登陆是否跳转到登陆页
*/ */
async function get (url, params = null, redirect = true) { async function get(url, params = null, redirect = true) {
return request(url, "get", params, null, false, redirect); return request(url, "get", params, null, false, redirect);
} }
/** /**
@ -80,8 +80,8 @@ async function get (url, params = null, redirect = true) {
* @param {*} isForm 是否表单数据 * @param {*} isForm 是否表单数据
* @param {*} redirect 是否重定向 * @param {*} redirect 是否重定向
*/ */
async function post (url, params, body, isForm = false, redirect = true) { async function post(url, params, body, isForm = false, redirect = true) {
return request(url, "post", params, body, isForm, redirect); return request(url, "post", params, body, isForm, redirect);
} }
/** /**
@ -92,8 +92,8 @@ async function post (url, params, body, isForm = false, redirect = true) {
* @param {*} isForm 是否表单数据 * @param {*} isForm 是否表单数据
* @param {*} redirect 是否重定向 * @param {*} redirect 是否重定向
*/ */
async function put (url, params, body, isForm = false, redirect = true) { async function put(url, params, body, isForm = false, redirect = true) {
return request(url, "put", params, body, isForm, redirect); return request(url, "put", params, body, isForm, redirect);
} }
/** /**
@ -102,13 +102,14 @@ async function put (url, params, body, isForm = false, redirect = true) {
* @param {*} params url参数 * @param {*} params url参数
* @param {*} redirect 是否重定向 * @param {*} redirect 是否重定向
*/ */
async function deletes (url, params = null, redirect = true) { async function deletes(url, params = null, redirect = true) {
return request(url, "delete", params, null, redirect); return request(url, "delete", params, null, redirect);
} }
export default { export default {
get, get,
post, post,
put, put,
delete: deletes delete: deletes,
http
}; };

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<a href="pinObj.url" v-if="pinObj" class="pinBookmarkItem"> <a :href="pinObj.url" v-if="pinObj" class="pinBookmarkItem">
<img :src="pinObj.icon.length > 0 ? pinObj.icon : '/favicon.ico'" class="icon" /> <img :src="pinObj.icon.length > 0 ? pinObj.icon : '/favicon.ico'" class="icon" />
<span class="text" :title="pinObj.name">{{ pinObj.name }}</span> <span class="text" :title="pinObj.name">{{ pinObj.name }}</span>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">

View File

@ -1,11 +1,11 @@
<template> <template>
<div class="main"> <div class="main" :style="{ backgroundImage: backgroundImg }">
<top /> <top />
<div class="content"> <div class="content">
<search :style="{ width: isPhone ? '100%' : '60%' }" /> <search :style="{ width: isPhone ? '100%' : '60%' }" />
<div :style="{ width: isPhone ? '100%' : '70%' }"><pin-bookmark /></div> <div :style="{ width: isPhone ? '100%' : '70%' }"><pin-bookmark /></div>
</div> </div>
<bottom /> <bottom :bgSrc="serverConfig.bingImgSrc" />
</div> </div>
</template> </template>
@ -15,7 +15,7 @@ import Bottom from "@/layout/home/Bottom.vue";
import Search from "@/components/main/Search.vue"; import Search from "@/components/main/Search.vue";
import PinBookmark from "./PinBookmark.vue"; import PinBookmark from "./PinBookmark.vue";
import { mapState } from "vuex"; import { mapState } from "vuex";
import { GLOBAL_CONFIG, IS_PHONE } from "@/store/modules/globalConfig"; import { GLOBAL_CONFIG, IS_PHONE, SERVER_CONFIG } from "@/store/modules/globalConfig";
export default { export default {
name: "HOME", name: "HOME",
components: { components: {
@ -28,7 +28,11 @@ export default {
return {}; return {};
}, },
computed: { computed: {
...mapState(GLOBAL_CONFIG, [IS_PHONE, IS_PHONE]), ...mapState(GLOBAL_CONFIG, [IS_PHONE, SERVER_CONFIG]),
backgroundImg() {
let url = this.serverConfig.bingImgSrc ? this.serverConfig.bingImgSrc : "/static/img/homeBg.jpg";
return `url("${url}")`;
},
}, },
methods: {}, methods: {},
}; };
@ -38,9 +42,8 @@ export default {
.main { .main {
width: 100%; width: 100%;
height: 100vh; height: 100vh;
background: url("/static/img/homeBg.jpg") no-repeat center center; background-repeat: no-repeat;
background-size: cover; background-size: cover;
background-attachment: fixed;
.content { .content {
height: calc(~"100vh" - 1.21rem); height: calc(~"100vh" - 1.21rem);

View File

@ -65,7 +65,7 @@
@drop="onDrop" @drop="onDrop"
> >
<a-dropdown :trigger="['contextmenu']" slot="nodeTitle" slot-scope="rec"> <a-dropdown :trigger="['contextmenu']" slot="nodeTitle" slot-scope="rec">
<div class="titleContext"> <div class="titleContext" :title="rec.dataRef.url">
<a-icon type="folder" v-if="!rec.dataRef.isLeaf" /> <a-icon type="folder" v-if="!rec.dataRef.isLeaf" />
<img v-else-if="rec.dataRef.icon.length > 0" :src="rec.dataRef.icon" style="width: 16px" /> <img v-else-if="rec.dataRef.icon.length > 0" :src="rec.dataRef.icon" style="width: 16px" />
<a-icon type="book" v-else /> <a-icon type="book" v-else />

View File

@ -0,0 +1,164 @@
<template>
<div>
<a-button type="primary" @click="addOne">新增</a-button>
<a-table :columns="columns" :data-source="list" :pagination="false">
<template v-for="col in ['name', 'url']" :slot="col" slot-scope="text, record, index">
<div :key="col">
<a-input v-if="record.isEdit" style="margin: -5px 0" :value="text"
@change="e => handleChange(e.target.value, record.id, col)" />
<template v-else>{{ text }}</template>
</div>
</template>
<template slot="icon" slot-scope="text, record, index">
<div key="icon">
<a-select v-if="record.isEdit" :default-value="text" style="width: 120px"
@change="e => handleChange(e, record.id, 'icon')">
<a-select-option v-for="item in iconList" :key="item.icon" :value="item.icon">
<my-icon :type="item.icon" />
{{ item.label }}
</a-select-option>
</a-select>
<template v-else>
<my-icon style="font-size: 1.2em" :type="text" />
</template>
</div>
</template>
<template slot="checked" slot-scope="text">
<span>{{ text === 1 ? "是" : "否" }}</span>
</template>
<template slot="operation" slot-scope="text, record, index">
<div class="editable-row-operations">
<span v-if="record.isEdit">
<a @click="() => save(record.id)">保存</a>
&nbsp;<a @click="() => cancel(record.id)">取消</a>
</span>
<div v-else>
<a :disabled="currentEditCache" @click="() => edit(record.id)">编辑</a>&nbsp;
<a-popconfirm title="确认删除吗?" ok-text="" cancel-text="" @confirm="() => deleteOne(record.id)">
<a :disabled="currentEditCache">删除</a>
</a-popconfirm>&nbsp;
<a :disabled="currentEditCache || record.checked===1" @click="() => setDefault(record.id)">设为默认</a>
</div>
</div>
</template>
</a-table>
</div>
</template>
<script>
import HttpUtil from "@/util/HttpUtil";
export default {
name: "manageSearchEngine",
data() {
return {
list: [],
currentEditCache: null,
iconList: [
{ icon: "icon-baidu", label: "百度" },
{ icon: "icon-bing", label: "必应" },
{ icon: "icon-google", label: "谷歌" },
{ icon: "icon-yandex", label: "yandex" },
{ icon: "icon-sogou", label: "搜狗" },
{ icon: "icon-yahoo", label: "雅虎" },
{ icon: "icon-qita", label: "其他" }
],
columns: [
{
title: "名称",
dataIndex: "name",
width: "10em",
scopedSlots: { customRender: "name" }
}, {
title: "图标",
dataIndex: "icon",
width: "8em",
scopedSlots: { customRender: "icon" }
}, {
title: "路径(%s 会被替换为搜索内容)",
dataIndex: "url",
scopedSlots: { customRender: "url" }
}, {
title: "默认",
dataIndex: "checked",
width: "10em",
scopedSlots: { customRender: "checked" }
}, {
title: "操作",
width: "15em",
dataIndex: "operation",
scopedSlots: { customRender: "operation" }
}]
};
},
async created() {
await this.getData();
},
computed: {
//
deleteOk() {
return this.list.filter(item => item.isEdit).length === 0;
}
},
methods: {
addOne() {
let body = { id: -1, icon: "", name: "", url: "", checked: 0, isEdit: true };
this.list = [body, ...this.list];
this.currentEditCache = body;
},
handleChange(value, id, column) {
console.log(value, id, column);
const target = this.list.find(item => item.id === id);
target[column] = value;
this.list = [...this.list];
},
async edit(id) {
let target = this.list.find(item => item.id === id);
this.currentEditCache = { ...target };
target.isEdit = true;
this.list = [...this.list];
},
async save(id) {
let target = this.list.find(item => item.id === id);
if (target.id > 0) {
await HttpUtil.post("/searchEngine/edit", null, target);
} else {
await HttpUtil.post("/searchEngine/insert", null, target);
}
target.isEdit = false;
this.currentEditCache = null;
await this.getData();
},
cancel(id) {
let target = this.list.find(item => item.id === id);
Object.assign(target, this.currentEditCache);
target.isEdit = false;
this.list = [...this.list];
this.currentEditCache = null;
},
async deleteOne(id) {
await HttpUtil.post("/searchEngine/delete", null, { id });
this.list = await HttpUtil.get("/searchEngine/list");
},
async setDefault(id) {
await HttpUtil.post("/searchEngine/setChecked", null, { id });
this.list = await HttpUtil.get("/searchEngine/list");
},
async getData() {
this.list = await HttpUtil.get("/searchEngine/list");
}
}
};
</script>
<style lang="less" scoped>
.icon {
color: black;
cursor: pointer;
font-size: 1.3em;
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="userInfo"> <div v-if="userInfo" class="userInfo">
<div class="icon"> <div class="icon">
<img :src="userInfo.icon" class="full" /> <img :src="userInfo.icon" class="full" />
<label class="full"> <label class="full">
@ -12,7 +12,8 @@
<div class="baseInfo"> <div class="baseInfo">
<div class="item"> <div class="item">
<a-tooltip title="点击修改" v-if="currentAction != 'name'"> <a-tooltip title="点击修改" v-if="currentAction != 'name'">
<span style="font-size: 2em; cursor: pointer" @click="() => (this.currentAction = 'name')">{{ userInfo.username }}</span> <span style="font-size: 2em; cursor: pointer"
@click="() => (this.currentAction = 'name')">{{ userInfo.username }}</span>
</a-tooltip> </a-tooltip>
<div class="inputGroup" v-else-if="currentAction === 'name'"> <div class="inputGroup" v-else-if="currentAction === 'name'">
<a-input type="text" v-model="name" placeholder="修改昵称" /> <a-input type="text" v-model="name" placeholder="修改昵称" />
@ -26,7 +27,9 @@
<div class="item"> <div class="item">
<span style="width: 5em">密码</span> <span style="width: 5em">密码</span>
<a-tooltip title="点击修改" v-if="currentAction != 'password'"> <a-tooltip title="点击修改" v-if="currentAction != 'password'">
<span style="cursor: pointer" @click="() => (this.currentAction = 'password')">{{ userInfo.noPassword ? "设置密码" : "**********" }}</span> <span style="cursor: pointer"
@click="() => (this.currentAction = 'password')">{{ userInfo.noPassword ? "设置密码" : "**********"
}}</span>
</a-tooltip> </a-tooltip>
<div class="inputGroup" v-else-if="currentAction === 'password'"> <div class="inputGroup" v-else-if="currentAction === 'password'">
<a-input type="password" v-model="oldPassword" placeholder="旧密码(如无置空)" /> <a-input type="password" v-model="oldPassword" placeholder="旧密码(如无置空)" />
@ -58,26 +61,24 @@
<a-tooltip title="搜索框默认搜索引擎"> <a-tooltip title="搜索框默认搜索引擎">
<span style="width: 5em">搜索</span> <span style="width: 5em">搜索</span>
</a-tooltip> </a-tooltip>
<a-tooltip title="点击修改" v-if="currentAction != 'defaultSearchEngine'"> <div @click="showSearchEngineManage=true" style=" cursor: pointer;color:blue">管理搜索引擎</div>
<span style="cursor: pointer" @click="() => (this.currentAction = 'defaultSearchEngine')">{{ defaultSearchEngine }}</span>
</a-tooltip>
<div class="inputGroup" v-else-if="currentAction === 'defaultSearchEngine'">
<a-select :default-value="userInfo.defaultSearchEngine" style="width: 100%" @change="submit">
<a-select-option value="baidu">百度</a-select-option>
<a-select-option value="google">谷歌</a-select-option>
<a-select-option value="bing">Bing</a-select-option>
</a-select>
</div>
</div> </div>
</div> </div>
<a-modal v-model="showSearchEngineManage" title="管理搜索引擎" :footer="null" width="70%">
<manage-search-engine />
</a-modal>
</div> </div>
</template> </template>
<script> <script>
import manageSearchEngine from "./components/manageSearchEngine.vue";
import { mapState } from "vuex"; import { mapState } from "vuex";
import HttpUtil from "@/util/HttpUtil"; import HttpUtil from "@/util/HttpUtil";
export default { export default {
name: "UserInfo", name: "UserInfo",
components: { manageSearchEngine },
data() { data() {
return { return {
currentAction: null, //,name,password,email currentAction: null, //,name,password,email
@ -86,21 +87,11 @@ export default {
password: "", password: "",
rePassword: "", rePassword: "",
email: "", email: "",
showSearchEngineManage: false
}; };
}, },
computed: { computed: {
...mapState("globalConfig", ["userInfo"]), ...mapState("globalConfig", ["userInfo"])
defaultSearchEngine() {
switch (this.userInfo.defaultSearchEngine) {
case "baidu":
return "百度";
case "google":
return "谷歌";
case "bing":
return "Bing";
}
return "";
},
}, },
methods: { methods: {
async changeIcon(e) { async changeIcon(e) {
@ -127,21 +118,18 @@ export default {
url = "/baseInfo/password"; url = "/baseInfo/password";
body = { body = {
oldPassword: this.oldPassword, oldPassword: this.oldPassword,
password: this.password, password: this.password
}; };
} else if (this.currentAction === "email") { } else if (this.currentAction === "email") {
url = "/baseInfo/email"; url = "/baseInfo/email";
body = { oldPassword: this.oldPassword, email: this.email }; body = { oldPassword: this.oldPassword, email: this.email };
} else if (this.currentAction === "defaultSearchEngine") {
url = "/baseInfo/updateSearchEngine";
body = { defaultSearchEngine: e };
} }
await HttpUtil.post(url, null, body); await HttpUtil.post(url, null, body);
await this.$store.dispatch("globalConfig/refreshUserInfo"); await this.$store.dispatch("globalConfig/refreshUserInfo");
this.$message.success("操作成功"); this.$message.success("操作成功");
this.currentAction = null; this.currentAction = null;
}, }
}, }
}; };
</script> </script>
@ -195,6 +183,7 @@ export default {
border-bottom: 1px solid #ebebeb; border-bottom: 1px solid #ebebeb;
display: flex; display: flex;
.inputGroup { .inputGroup {
flex: 1; flex: 1;
} }

View File

@ -0,0 +1,39 @@
<template>
<div class="ssoMain">{{ message }}</div>
</template>
<script>
import { GLOBAL_CONFIG, TOKEN } from "@/store/modules/globalConfig";
export default {
name: "ssoPage",
data() {
return {
message: "loading"
};
},
mounted() {
window.addEventListener("message", event => {
if (!event.data.code) {
return;
}
console.log("收到content消息", event);
if (event.data.code == "setTokenOk") {
this.message = "登陆成功3s后关闭本页面";
setTimeout(() => window.close(), 3000);
}
});
let token = this.$store.state[GLOBAL_CONFIG][TOKEN];
window.postMessage({ code: "setToken", data: token }, "*");
},
beforeDestroy() {
window.removeEventListener("message");
}
};
</script>
<style lang="less" scoped>
.ssoMain {
text-align: center;
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="ssoAddBookmark">
<div class="body">
<div>
<a-input placeholder="标题" v-model="form.name" @pressEnter="addBookmark" ref="nameInput" />
<a-input placeholder="网址" v-model="form.url" @pressEnter="addBookmark" />
</div>
<div class="list">
<div class="path">
<div>保存路径:</div>
<a-breadcrumb>
<a-breadcrumb-item class="breadItem"><span @click="breadClick(null)"></span></a-breadcrumb-item>
<a-breadcrumb-item class="breadItem" v-for="item in breadList" :key="item.bookmarkId">
<span @click="breadClick(item)">{{ item.name.length > 4 ? item.name.substr(0, 3) + "..." : item.name }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<div class="folderList">
<div :class="{ item: true, bg: item.type == 1 }" v-for="item in dataList" :key="item.bookmarkId" :title="item.url">
<span class="text" @click="folderClick(item)">{{ item.name }}</span>
<a-popconfirm class="actionBar" placement="left" title="确认删除?" ok-text="" cancel-text="" @confirm="deleteOne(item, $event)">
<a-icon type="delete" />
</a-popconfirm>
</div>
</div>
</div>
</div>
<div class="action">
<div v-if="showAddInput" style="display: flex">
<a-input v-model="addFolderName" style="width: 8em" @pressEnter="addFolder" ref="folderInput" />
<a-button shape="circle" icon="close" @click="showAddInput = false" />
<a-button type="primary" shape="circle" icon="check" @click="addFolder" />
</div>
<a-button v-else type="link" @click="showFolderInput">新建文件夹</a-button>
<div>
<a-button style="marging-right: 1em" type="" @click="closeIframe">取消</a-button>
<a-button type="primary" @click="addBookmark">{{ breadList.length === 0 ? "保存到根" : "保存" }}</a-button>
</div>
</div>
</div>
</template>
<script>
import HttpUtil from "@/util/HttpUtil";
import { mapState } from "vuex";
import { TREE_DATA, TOTAL_TREE_DATA, addNode, deleteData } from "@/store/modules/treeData";
export default {
data() {
return {
breadList: [],
showAddInput: false,
addFolderName: "",
form: {
name: null,
url: null,
icon: null,
iconUrl: null,
type: 0,
path: "",
},
};
},
computed: {
...mapState(TREE_DATA, [TOTAL_TREE_DATA]),
dataList() {
let path = this.getCurrentPath();
return this.totalTreeData[path] ? this.totalTreeData[path] : [];
},
},
mounted() {
//
window.addEventListener("message", (event) => {
if (!event.data.code) {
return;
}
console.log("收到content消息", event);
if (event.data.code == "addBookmarkAction") {
console.log("新增书签");
this.form.name = event.data.data.name;
this.form.url = event.data.data.url;
this.form.icon = event.data.data.icon;
this.form.iconUrl = event.data.data.iconUrl;
}
});
console.log("向父节点获取数据");
window.parent.postMessage({ code: "getBookmarkData", receiver: "content" }, "*");
this.$refs.nameInput.focus();
},
methods: {
closeIframe() {
window.parent.postMessage({ code: "closeIframe", receiver: "content" }, "*");
},
//
async addBookmark() {
this.form.path = this.getCurrentPath();
let res = await HttpUtil.put("/bookmark", null, this.form);
this.$message.success("添加成功");
await this.$store.dispatch(TREE_DATA + "/" + addNode, {
sourceNode: this.breadList.length == 0 ? null : this.breadList[this.breadList.length - 1],
targetNode: res,
});
setTimeout(this.closeIframe, 500);
},
//
async breadClick(item) {
console.log(item);
//
if (item == null && this.breadList.length == 0) {
return;
}
if (item === this.breadList[this.breadList.length - 1]) {
return;
}
if (item == null) {
this.breadList = [];
} else {
let index = this.breadList.indexOf(item);
this.breadList = [...this.breadList.slice(0, index + 1)];
}
},
//
folderClick(item) {
this.form.path = item.path + "." + item.bookmarkId;
this.breadList.push(item);
},
//
async addFolder() {
let length = this.addFolderName.trim().length;
if (length == 0) {
this.$message.error("文件夹名称不为空");
return;
}
if (length > 200) {
this.$message.error("文件夹名称长度不能大于200");
return;
}
let form = {
name: this.addFolderName,
path: this.getCurrentPath(),
type: 1,
url: "",
};
let res = await HttpUtil.put("/bookmark", null, form);
await this.$store.dispatch(TREE_DATA + "/" + addNode, {
sourceNode: this.breadList.length == 0 ? null : this.breadList[this.breadList.length - 1],
targetNode: res,
});
this.addFolderName = "";
this.showAddInput = false;
this.breadList = [...this.breadList];
this.$message.success("新增成功");
},
//
getCurrentPath() {
if (this.breadList.length == 0) {
return "";
} else {
let lastOne = this.breadList[this.breadList.length - 1];
return lastOne.path + "." + lastOne.bookmarkId;
}
},
//
async deleteOne(item) {
let body = { pathList: [], bookmarkIdList: [] };
item.type == 0 ? body.bookmarkIdList.push(item.bookmarkId) : body.pathList.push(item.path + "." + item.bookmarkId);
await HttpUtil.post("/bookmark/batchDelete", null, body);
await this.$store.dispatch(TREE_DATA + "/" + deleteData, body);
this.$message.success("删除成功");
},
//
showFolderInput() {
this.showAddInput = true;
console.log(this.$refs);
this.$nextTick(() => this.$refs.folderInput.focus());
},
},
};
</script>
<style lang="less" scoped>
.ssoAddBookmark {
display: flex;
flex-direction: column;
padding: 0.5em;
padding-bottom: 1em;
background: white;
width: 100%;
height: 100vh;
.body {
flex: 1;
height: 0;
display: flex;
flex-direction: column;
.list {
padding-top: 1em;
display: flex;
flex-direction: column;
flex: 1;
height: 0;
.path {
display: flex;
overflow: auto;
font-size: 0.9em;
.breadItem {
cursor: pointer;
}
.breadItem:last-child {
cursor: text;
}
}
.folderList {
flex: 1;
overflow: auto;
height: 0;
margin-left: 0.5em;
.item {
display: flex;
.text {
flex: 1;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actionBar {
display: none;
width: 2em;
align-items: center;
color: red;
cursor: pointer;
}
}
.bg {
cursor: pointer;
color: rgb(24, 144, 255);
}
.bg:hover {
background: rgba(74, 74, 74, 0.3);
}
.item:hover .actionBar {
display: flex;
}
}
}
}
.action {
padding-top: 1em;
display: flex;
justify-content: space-between;
}
}
</style>

View File

@ -5,13 +5,23 @@
源码地址 源码地址
<a href="https://github.com/FleyX/bookmark" target="_blank">github.com/FleyX/bookmark</a> <a href="https://github.com/FleyX/bookmark" target="_blank">github.com/FleyX/bookmark</a>
</div> </div>
<!-- <div> <div>
当前版本{{ appVersion }}&emsp;&emsp;<a v-if="showNewVersion"
href="https://github.com/FleyX/bookmark/blob/master/DEPLOY.md">最新版本{{ latestVersion
}}</a>
</div>
<div>
使用教程 使用教程
<a href="https://github.com/FleyX/bookmark" target="_blank">点击跳转</a> <a href="https://blog.fleyx.com/blog/detail/20220329" target="_blank">点击跳转</a>
</div> --> </div>
<div>
浏览器插件
<a href="/static/bookmarkBrowserPlugin.zip" download="浏览器插件.zip"
target="_blank">最新版本{{ serverConfig.map.pluginVersion }}(注意更新)</a>
,使用详情请参考使用教程
</div>
<div>交流反馈qq群150056494,邮箱fleyx20@outlook.com</div> <div>交流反馈qq群150056494,邮箱fleyx20@outlook.com</div>
<div> <div>
统计
<a href="https://qiezi.fleyx.com" style="" target="_blank"> <a href="https://qiezi.fleyx.com" style="" target="_blank">
<div id="qieziStatisticHtmlHostPv" style="display: none"> <div id="qieziStatisticHtmlHostPv" style="display: none">
总访问次数: 总访问次数:
@ -29,17 +39,51 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
import { GLOBAL_CONFIG, SERVER_CONFIG } from "@/store/modules/globalConfig";
import httpUtil from "@/util/HttpUtil";
export default { export default {
name: "about", name: "about",
mounted() { data() {
window.qieziStatisticKey = "b74c4b571b644782a837433209827874"; return {
let script = document.createElement("script"); appVersion: "1.4", //
script.type = "text/javascript"; latestVersion: null,
script.defer = true; showNewVersion: false
script.src = "https://qiezi.fleyx.com/qiezijs/1.0/qiezi_statistic.min.js"; };
document.getElementsByTagName("head")[0].appendChild(script);
}, },
}; computed: { ...mapState(GLOBAL_CONFIG, [SERVER_CONFIG]) },
async mounted() {
//
let res = await httpUtil.http.default.get("https://s3.fleyx.com/picbed/bookmark/config.json");
console.log(res);
this.latestVersion = res.data.latestAppVersion;
this.showNewVersion = this.checkVersion(this.appVersion, this.latestVersion);
},
methods: {
checkVersion: function(version, latestVersion) {
if (version === latestVersion) {
return false;
}
let versions = version.split(".");
let latestVersions = latestVersion.split(".");
for (let i = 0; i < versions.length; i++) {
if (i >= latestVersions.length) {
return false;
}
let versionNum = parseInt(versions[i]);
let latestVersionNum = parseInt(latestVersions[i]);
if (versionNum !== latestVersionNum) {
return versionNum < latestVersionNum;
}
}
return true;
}
}
}
;
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -24,7 +24,7 @@
</div> </div>
<div class="thirdPart"> <div class="thirdPart">
<span>第三方登陆</span> <span>第三方登陆</span>
<a-tooltip v-if="serverConfig.proxyExist" title="github登陆" class="oneIcon" placement="bottom"> <a-tooltip title="github登陆" class="oneIcon" placement="bottom">
<a-icon type="github" @click="toGithub" style="font-size: 1.4em" /> <a-icon type="github" @click="toGithub" style="font-size: 1.4em" />
</a-tooltip> </a-tooltip>
</div> </div>
@ -39,39 +39,44 @@ import Header from "@/components/public/Switch.vue";
import httpUtil from "../../../util/HttpUtil.js"; import httpUtil from "../../../util/HttpUtil.js";
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
import HttpUtil from "../../../util/HttpUtil.js"; import HttpUtil from "../../../util/HttpUtil.js";
export default { export default {
name: "Login", name: "Login",
components: { components: {
Header, Header
}, },
computed: { computed: {
...mapState("globalConfig", ["serverConfig"]), ...mapState("globalConfig", ["serverConfig"])
}, },
data() { data() {
return { return {
form: { form: {
str: "", str: "",
password: "", password: "",
rememberMe: false, rememberMe: false
}, },
rules: { rules: {
str: [ str: [
{ required: true, message: "请输入用户名", trigger: "blur" }, { required: true, message: "请输入用户名", trigger: "blur" },
{ min: 1, max: 50, message: "最短1最长50", trigger: "change" }, { min: 1, max: 50, message: "最短1最长50", trigger: "change" }
], ],
password: [ password: [
{ required: true, message: "请输入密码", trigger: "blur" }, { required: true, message: "请输入密码", trigger: "blur" },
{ pattern: "^\\w{6,18}$", message: "密码为6-18位数字,字母,下划线组合", trigger: "change" }, { pattern: "^\\w{6,18}$", message: "密码为6-18位数字,字母,下划线组合", trigger: "change" }
], ]
}, },
loading: false, // loading: false, //
oauthLogining: false, //true:oauth oauthLogining: false, //true:oauth
page: null, //oauth page: null, //oauth
redirect: null
}; };
}, },
async created() { async created() {
let _this = this; let _this = this;
window.addEventListener("storage", this.storageDeal.bind(this)); window.addEventListener("storage", this.storageDeal.bind(this));
if (this.$route.query.to) {
this.redirect = atob(this.$route.query.to);
}
}, },
destroyed() { destroyed() {
window.removeEventListener("storage", this.storageDeal); window.removeEventListener("storage", this.storageDeal);
@ -85,7 +90,7 @@ export default {
this.loading = true; this.loading = true;
let token = await httpUtil.post("/user/login", null, this.form); let token = await httpUtil.post("/user/login", null, this.form);
await this.$store.dispatch("globalConfig/setToken", token); await this.$store.dispatch("globalConfig/setToken", token);
this.$router.replace("/"); this.$router.replace(this.redirect ? this.redirect : "/");
} finally { } finally {
this.loading = false; this.loading = false;
} }
@ -126,8 +131,8 @@ export default {
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, }
}, }
}; };
</script> </script>
@ -135,11 +140,13 @@ export default {
.form { .form {
margin: 0.3rem; margin: 0.3rem;
margin-bottom: 0.1rem; margin-bottom: 0.1rem;
.reset { .reset {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
} }
.thirdPart { .thirdPart {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -81,12 +81,12 @@ export default {
}; };
}, },
methods: { methods: {
submit() { async submit() {
let _this = this; let _this = this;
this.$refs.registerForm.validate(async (status) => { this.$refs.registerForm.validate(async (status) => {
if (status) { if (status) {
let res = await httpUtil.put("/user", null, _this.form); let res = await httpUtil.put("/user", null, _this.form);
this.$store.dispatch("globalConfig/setToken", res); await this.$store.dispatch("globalConfig/setToken", res);
this.$router.replace("/"); this.$router.replace("/");
} }
}); });

View File

@ -1,20 +1,21 @@
module.exports = { module.exports = {
lintOnSave: false, lintOnSave: false,
configureWebpack: { configureWebpack: {
devtool: "source-map" devtool: "source-map"
}, },
devServer: { devServer: {
proxy: { port: 4531,
"/bookmark/api": { proxy: {
//这里最好有一个 / "/bookmark/api": {
target: "http://localhost:8088", // 服务器端接口地址 //这里最好有一个 /
ws: true, //如果要代理 websockets配置这个参数 target: "http://localhost:8088", // 服务器端接口地址
// 如果是https接口需要配置这个参数 ws: true, //如果要代理 websockets配置这个参数
changeOrigin: true, //是否跨域 // 如果是https接口需要配置这个参数
pathRewrite: { changeOrigin: true, //是否跨域
"^/bookmark/api": "/bookmark/api" pathRewrite: {
} "^/bookmark/api": "/bookmark/api"
} }
} }
} }
}
}; };

7
build.sh Normal file → Executable file
View File

@ -3,8 +3,11 @@ base=$(cd "$(dirname "$0")";pwd)
echo $base echo $base
cd $base cd $base
cd 浏览器插件/bookmarkBrowserPlugin
zip -q -r ../../bookmark_front/public/static/bookmarkBrowserPlugin.zip *
cd ../../
# 前端打包 # 前端打包
docker run -it --rm --user ${UID} -v $base/bookmark_front:/opt/front node:lts-buster-slim bash -c "cd /opt/front && yarn --registry https://registry.npm.taobao.org && yarn build" docker run --rm --user ${UID} -v $base/bookmark_front:/opt/front node:16-slim bash -c "cd /opt/front && yarn --registry https://registry.npm.taobao.org && yarn build"
# 后端打包 # 后端打包
docker run -it --rm --user ${UID} -v $base/data/maven/mavenRep:/var/maven/.m2: -v $base/data/maven/settings.xml:/usr/share/maven/conf/settings.xml -v $base/bookMarkService:/code maven:3-openjdk-11-slim bash -c "cd /code && mvn clean install" docker run --rm --user ${UID} -v $base/data/maven/mavenRep:/var/maven/.m2 -v $base/data/maven/settings.xml:/usr/share/maven/conf/settings.xml -v $base/bookMarkService:/code maven:3-openjdk-11-slim bash -c "cd /code && mvn clean install"

View File

@ -14,6 +14,7 @@ services:
- ./data/mysql/my.cnf:/etc/mysql/my.cnf - ./data/mysql/my.cnf:/etc/mysql/my.cnf
- /etc/localtime:/etc/localtime - /etc/localtime:/etc/localtime
- ./data/timezone:/etc/timezone - ./data/timezone:/etc/timezone
restart: unless-stopped
environment: environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD} - MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=bookmark - MYSQL_DATABASE=bookmark
@ -25,6 +26,7 @@ services:
- /etc/localtime:/etc/localtime - /etc/localtime:/etc/localtime
- ./data/timezone:/etc/timezone - ./data/timezone:/etc/timezone
- ./data/redis:/data - ./data/redis:/data
restart: unless-stopped
networks: networks:
- bookmark - bookmark
@ -38,6 +40,7 @@ services:
- ./bookmark_front/dist:/opt/dist - ./bookmark_front/dist:/opt/dist
- ./data/nginx/nginx.conf:/etc/nginx/nginx.conf - ./data/nginx/nginx.conf:/etc/nginx/nginx.conf
- ${BOOKMARK_FILE_SAVE_PATH}/files/public:/opt/files/public - ${BOOKMARK_FILE_SAVE_PATH}/files/public:/opt/files/public
restart: unless-stopped
ports: ports:
- 8080:8080 - 8080:8080
@ -60,6 +63,7 @@ services:
- ./bookMarkService/web/target/bookmark-web-1.0-SNAPSHOT.jar:/opt/app/service.jar - ./bookMarkService/web/target/bookmark-web-1.0-SNAPSHOT.jar:/opt/app/service.jar
- ${BOOKMARK_FILE_SAVE_PATH}:/opt/files - ${BOOKMARK_FILE_SAVE_PATH}:/opt/files
working_dir: /opt/app working_dir: /opt/app
restart: unless-stopped
command: command:
- /bin/bash - /bin/bash
- -c - -c

View File

@ -1,15 +0,0 @@
{
"plugins": [
"@babel/plugin-proposal-optional-chaining"
],
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3,
"targets": {
// https://jamie.build/last-2-versions
"browsers": ["> 0.25%", "not ie 11", "not op_mini all"]
}
}]
]
}

View File

@ -1,8 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -1,33 +0,0 @@
// https://eslint.org/docs/user-guide/configuring
// File taken from https://github.com/vuejs-templates/webpack/blob/1.3.1/template/.eslintrc.js, thanks.
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
webextensions: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard',
// https://prettier.io/docs/en/index.html
'plugin:prettier/recommended'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}

View File

@ -1,4 +0,0 @@
/node_modules
/*.log
/dist
/dist-zip

View File

@ -1,5 +0,0 @@
{
"singleQuote": true,
"printWidth": 180,
"trailingComma": "es5"
}

View File

@ -1,68 +0,0 @@
{
"name": "bookmark-chrome",
"version": "1.0.0",
"description": "A Vue.js web extension",
"author": "fanxb <fanxb.tl@gmail.com>",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js,.vue src",
"prettier": "prettier \"src/**/*.{js,vue}\"",
"prettier:write": "npm run prettier -- --write",
"build": "cross-env NODE_ENV=production webpack --hide-modules",
"build:dev": "cross-env NODE_ENV=development webpack --hide-modules",
"build-zip": "node scripts/build-zip.js",
"watch": "npm run build -- --watch",
"watch:dev": "cross-env HMR=true npm run build:dev -- --watch"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
},
"dependencies": {
"axios": "^0.19.0",
"element-ui": "^2.12.0",
"vue": "^2.6.10",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"webextension-polyfill": "^0.3.1"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/runtime-corejs3": "^7.4.0",
"archiver": "^3.0.0",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.2",
"copy-webpack-plugin": "^4.5.3",
"core-js": "^3.0.1",
"cross-env": "^5.2.0",
"css-loader": "^2.1.1",
"ejs": "^2.6.1",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.3.0",
"eslint-config-standard": "^12.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.1.2",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.2",
"file-loader": "^1.1.11",
"husky": "^2.4.0",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.9.3",
"prettier": "^1.17.1",
"pretty-quick": "^1.8.0",
"sass-loader": "^7.1.0",
"vue-loader": "^15.4.2",
"vue-template-compiler": "^2.6.10",
"web-ext-types": "^2.1.0",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-extension-reloader": "^1.1.0"
}
}

View File

@ -1,53 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const DEST_DIR = path.join(__dirname, '../dist');
const DEST_ZIP_DIR = path.join(__dirname, '../dist-zip');
const extractExtensionData = () => {
const extPackageJson = require('../package.json');
return {
name: extPackageJson.name,
version: extPackageJson.version,
};
};
const makeDestZipDirIfNotExists = () => {
if (!fs.existsSync(DEST_ZIP_DIR)) {
fs.mkdirSync(DEST_ZIP_DIR);
}
};
const buildZip = (src, dist, zipFilename) => {
console.info(`Building ${zipFilename}...`);
const archive = archiver('zip', { zlib: { level: 9 } });
const stream = fs.createWriteStream(path.join(dist, zipFilename));
return new Promise((resolve, reject) => {
archive
.directory(src, false)
.on('error', err => reject(err))
.pipe(stream);
stream.on('close', () => resolve());
archive.finalize();
});
};
const main = () => {
const { name, version } = extractExtensionData();
const zipFilename = `${name}-v${version}.zip`;
makeDestZipDirIfNotExists();
buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename)
.then(() => console.info('OK'))
.catch(console.err);
};
main();

View File

@ -1,52 +0,0 @@
import httpUtil from './util/httpUtil.js';
global.browser = require('webextension-polyfill');
window.envType = 'background';
window.token = localStorage.getItem('token');
let token = null;
let globalPort = null;
chrome.extension.onConnect.addListener(port => {
console.log(port);
globalPort = port;
port.onMessage.addListener(msg => {
switch (msg.type) {
case 'sendToken':
console.log(msg);
localStorage.setItem('token', msg.data);
window.token = msg.data;
token = msg.data;
break;
default:
console.error('未知的数据', msg);
}
});
});
chrome.contextMenus.create(
{
title: '添加到书签',
onclick: (info, tab) => {
console.log(info, tab);
httpUtil.put('/bookmark', {
type: 0,
path: '',
name: tab.title,
url: tab.url,
});
},
},
err => {
console.log(err);
}
);
/**
* 构建一个标准命令
* @param {*} code code
* @param {*} data data
*/
function createMsg(code, data) {
return JSON.stringify({ code, data });
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,32 +0,0 @@
{
"name": "bookmark-chrome",
"description": "A Vue.js web extension",
"version": null,
"manifest_version": 2,
"permissions": [
"contextMenus"
],
"icons": {
"48": "icons/icon_48.png",
"128": "icons/icon_128.png"
},
"browser_action": {
"default_title": "bookmark-chrome",
"default_popup": "popup/popup.html"
},
"background": {
"scripts": [
"background.js"
]
},
"options_ui": {
"page": "options/options.html",
"chrome_style": true
},
"content_scripts": [{
"matches": ["*://*/*"],
"js": ["static/sso.js"]
}
]
}

View File

@ -1,17 +0,0 @@
<template>
<div>
<p>Hello world!这是选项页</p>
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
<style scoped>
p {
font-size: 20px;
}
</style>

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>bookmark-chrome - Options</title>
<link rel="stylesheet" href="options.css">
<% if (NODE_ENV === 'development') { %>
<!-- Load some resources only in development environment -->
<% } %>
</head>
<body>
<div id="app"></div>
<script src="options.js"></script>
</body>
</html>

View File

@ -1,10 +0,0 @@
import Vue from 'vue';
import App from './App';
global.browser = require('webextension-polyfill');
/* eslint-disable no-new */
new Vue({
el: '#app',
render: h => h(App),
});

View File

@ -1,20 +0,0 @@
<template>
<div class="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
data() {
return {};
},
};
</script>
<style>
.app {
width: 600px;
height: 580px;
}
</style>

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="popup.css">
<% if (NODE_ENV === 'development') { %>
<!-- Load some resources only in development environment -->
<% } %>
</head>
<body>
<div id="app">
</div>
<script src="/static/js/localforage.min.js"></script>
<script src="popup.js"></script>
</body>
</html>

View File

@ -1,55 +0,0 @@
import Vue from 'vue';
import App from './App';
import store from '../store';
import router from './router';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import config from '../util/config';
import axios from 'axios';
global.browser = require('webextension-polyfill');
Vue.prototype.$browser = global.browser;
Vue.use(ElementUI);
/**
* 配置axios
*/
axios.defaults.timeout = 15000;
axios.defaults.baseURL = config.baseUrl;
/**
* 请求拦截器
*/
axios.interceptors.request.use(
function(config) {
config.headers['jwt-token'] = window.token;
return config;
},
function(error) {
console.error(error);
return Promise.reject(error);
}
);
axios.interceptors.response.use(
res => {
if (res.data.code === -1) {
localStorage.removeItem('token');
window.vueInstance.$router.replace('/public/login');
} else if (res.data.code === 1) {
return res.data.data;
} else {
Promise.reject(res);
}
},
error => {
return Promise.reject(error);
}
);
/* eslint-disable no-new */
window.vueInstance = new Vue({
el: '#app',
store,
router,
render: h => h(App),
});

View File

@ -1,74 +0,0 @@
<template>
<div class="main">
<el-tree :props="props" :load="loadNode" accordion @node-click="nodeClick" lazy>
<span class="treeItem" slot-scope="{ node }">
<span>{{ node.label }}</span>
</span>
</el-tree>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'BookmarkTree',
data() {
return {
props: {
label: 'name',
children: 'children',
isLeaf: 'leaf',
},
treeObj: null,
};
},
methods: {
async loadNode(node, resolve) {
if (window.treeData == null) {
// eslint-disable-next-line no-undef
if ((window.treeData = await localforage.getItem('treeData')) == null) {
window.treeData = await axios.get('/bookmark/currentUser');
// eslint-disable-next-line no-undef
await localforage.setItem('treeData', window.treeData);
}
console.log(window.treeData);
}
console.log(node, resolve);
let list;
if (node.level === 0) {
list = window.treeData[''];
} else {
list = window.treeData[node.data.path + '.' + node.data.bookmarkId];
}
list.forEach(item => (item.leaf = item.type === 0));
console.log(list);
return resolve(list);
},
nodeClick(data) {
if (data.type === 1) {
return;
}
let url = data.url;
if (!url.startsWith('http')) {
url = 'http://' + url;
}
console.log(url);
window.open(data.url);
},
},
};
</script>
<style scoped>
.main {
width: 95%;
height: 500px;
overflow: auto;
margin: 0 auto;
}
.treeItem {
text-overflow: ellipsis;
overflow: hidden;
}
</style>

View File

@ -1,92 +0,0 @@
<template>
<div>
<el-popover placement="bottom-start" width="550" popper-class="popover" trigger="manual" :visible-arrow="false" v-model="isShow">
<div>
<div class="item" v-for="item in searchList" :key="item.bookmarkId">
<a target="_blank" :href="item.url">{{ item.name }}</a>
</div>
</div>
<el-input
ref="searchInput"
type="text"
placeholder="搜索"
v-model="searchContent"
clearable
:autofocus="true"
slot="reference"
@blur="clear"
@keyup.enter.native="searchKey"
@focus="focus"
>
<el-button slot="append" icon="el-icon-search" @click="searchKey"></el-button>
</el-input>
</el-popover>
<div class="searchResult"></div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'searchBookmark',
data() {
return {
searchContent: '',
//
isShow: false,
//
isFocus: false,
searchList: [],
timeOut: null,
};
},
watch: {
searchContent(newVal, oldVal) {
if (newVal.trim().length === 0) {
return;
}
if (newVal.trim() !== oldVal) {
if (this.timeOut != null) {
clearTimeout(this.timeOut);
}
this.searchList = [];
this.timeOut = setTimeout(async () => {
this.searchList = await axios.get(`bookmark/searchUserBookmark?content=${newVal}`);
this.isShow = this.searchList.length > 0;
this.timeOut = null;
}, 200);
}
},
},
mounted() {
this.$nextTick(() => {
this.$refs['searchInput'].focus();
});
},
methods: {
searchKey() {
window.open('https://www.baidu.com/s?ie=UTF-8&wd=' + window.encodeURIComponent(this.searchContent));
},
focus() {
this.isFocus = true;
},
clear() {
this.isFocus = false;
if (this.timeOut != null) {
clearTimeout(this.timeOut);
}
this.searchContent = '';
this.searchList = [];
this.isShow = false;
},
},
};
</script>
<style scoped>
.item {
width: 500px;
padding: 5px;
border-bottom: 1px solid black;
}
</style>

View File

@ -1,9 +0,0 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
Vue.use(VueRouter);
export default new VueRouter({
routes,
});

View File

@ -1,50 +0,0 @@
<template>
<div>
<div class="head">
<img width="30%" src="/static/img/bookmarkLogo.png" alt="icon" />
<span>{{ personInfo.username }}</span>
</div>
<!-- 书签检索 -->
<search />
</div>
</template>
<script>
import axios from 'axios';
import Search from '../components/SearchBookmark';
export default {
components: {
Search,
},
data() {
return {
personInfo: {},
};
},
created() {
window.token = localStorage.getItem('token');
if (!window.token) {
this.$router.replace('/public/login');
} else {
this.init();
}
},
methods: {
async init() {
let personInfo = await axios.get('/user/currentUserInfo');
window.personInfo = personInfo;
this.personInfo = personInfo;
},
},
};
</script>
<style lang="scss" scoped>
.head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 5% 0 5%;
}
</style>

View File

@ -1,37 +0,0 @@
<template>
<div class="main">您还未登陆授权<br />
<el-button type="primary" @click="go">前往授权页面</el-button>
</div>
</template>
<script>
import config from '../../../../util/config';
export default {
name: 'login',
data() {
return {
};
},
mounted() {
this.checkToken();
},
methods: {
go() {
window.open(config.ssoUrl);
},
// token
checkToken() {
console.log('检测token是否获取到了');
let token = localStorage.getItem('token');
if (token == null || token.length === 0) {
setTimeout(this.checkToken.bind(this), 1000);
} else {
this.$router.replace('/');
}
},
},
};
</script>
<style scoped>
</style>

View File

@ -1,13 +0,0 @@
import PageIndex from './pages/Index';
import Login from './pages/public/Login';
export default [
{
path: '/',
component: PageIndex,
},
{
path: '/public/login',
component: Login,
},
];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,33 +0,0 @@
console.log('注入了页面');
var port = chrome.extension.connect({ name: 'data' });
/**
* 接受background传来的消息
*/
port.onMessage.addListener(msg => {
console.log('收到消息:' + msg);
let obj = JSON.parse(msg);
switch (obj.code) {
case 'addBookmark':
break;
default:
console.error('未知的命令:' + obj.code);
}
});
/**
* 接收当前注入页面传来的消息
*/
window.addEventListener('message', function(event) {
if (event.data.type === undefined) {
return;
}
console.log('接受到消息', event.data);
switch (event.data.type) {
case 'sendToken':
port.postMessage(event.data);
window.token = event.data;
break;
default:
console.error('未知的事件', event);
}
});

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